File Coverage

blib/lib/Linux/DVB/DVBT/Ffmpeg.pm
Criterion Covered Total %
statement 136 279 48.7
branch 65 168 38.6
condition 21 34 61.7
subroutine 10 15 66.6
pod 5 5 100.0
total 237 501 47.3


line stmt bran cond sub pod time code
1             package Linux::DVB::DVBT::Ffmpeg ;
2              
3             =head1 NAME
4              
5             Linux::DVB::DVBT::Ffmpeg - Helper module for transcoding recorded streams
6              
7             =head1 SYNOPSIS
8              
9             use Linux::DVB::DVBT::Ffmpeg ;
10            
11              
12             =head1 DESCRIPTION
13              
14             Module provides a set of useful routines used for transcoding the recorded .ts transport stream file
15             into mpeg/mp4 etc. Use of these routines is entirely optional and does not form part of the base
16             DVBT functionality.
17              
18             Currently supported file formats are:
19              
20             =over 4
21              
22             =item B<.mpeg> - mpeg2 video / audio / subtitles
23              
24             =item B<.mp4> - mpeg4 video / audio
25              
26             The mp4 settings are configured to ensure that the file is compatible with playback on the PS3
27             (server by Twonky media server).
28              
29             Note: if you use this then be prepared for a long wait! On my system, a 2 hour film can take 13 hours to transcode.
30              
31             =item B<.m2v> - mpeg2 video
32              
33             =item B<.mp2> - mpeg2 audio
34              
35             =item B<.mp3> - mpeg3 audio
36              
37             You may notice ffmpeg reports the error:
38              
39             lame: output buffer too small
40            
41             Apparently (accoriding to the ffmpeg developers) this is perfectly fine. For further details see (http://howto-pages.org/ffmpeg/#basicaudio).
42              
43             =back
44              
45             Obviously, ffmpeg must be installed on your machine to run these functions.
46              
47             =head1 SUPPORT
48              
49             I don't intend to support every possible option in ffmpeg! These routines are provided as being helpful, but you
50             can ignore them if you don't like them.
51              
52             Helpful suggestions/requests may result in my adding functionality, but I don't promise anything!
53              
54             =cut
55              
56              
57             #============================================================================================
58             # USES
59             #============================================================================================
60 10     10   37 use strict ;
  10         12  
  10         248  
61 10     10   36 use File::Basename ;
  10         12  
  10         626  
62 10     10   37 use Data::Dumper ;
  10         13  
  10         3637  
63              
64             #============================================================================================
65             # GLOBALS
66             #============================================================================================
67              
68             our $VERSION = '2.10' ;
69             our $DEBUG = 0 ;
70             our $DEBUG_FFMPEG = 0 ;
71              
72             # margin on video length checks (in seconds) - allow for 3 minutes of padding
73             our $DURATION_MARGIN = 180 ;
74              
75             # Niceness level
76             our $NICE = 19 ;
77              
78             ## mpeg4
79              
80             # const
81             our $vidbitrate='1050kb' ;
82             our $bittolerance='200kb' ;
83             our $audbitrate='128kb' ;
84             our $audchannels=2 ;
85             our $threads=2 ;
86              
87             my $me_method_opt="-me_method" ;
88              
89             my $common_start = "-vcodec libx264 ".
90             "-b $vidbitrate ".
91             "-flags +loop ".
92             "-cmp +chroma ".
93             "-partitions +parti4x4+partp8x8+partb8x8" ;
94             my $common_end = "-bf 3 ".
95             "-b_strategy 1 ".
96             "-threads $threads ".
97             "-level 31 ".
98             "-coder 1 ".
99             "-me_range 16 ".
100             "-g 250 ".
101             "-keyint_min 25 ".
102             "-sc_threshold 40 ".
103             "-i_qfactor 0.71 ".
104             "-bt $bittolerance ".
105             "-rc_eq 'blurCplx^(1-qComp)' ".
106             "-qcomp 0.6 ".
107             "-qmin 10 ".
108             "-qmax 51 ".
109             "-qdiff 4 ".
110             "-aspect 16:9 ".
111             "-y " ;
112             my $pass1_codec = "-an" ;
113             my $pass2_codec = "-acodec libfaac ".
114             "-ac $audchannels ".
115             "-ab $audbitrate ".
116             "-async 1 ".
117             "-f mp4 " ;
118             my $pass1_options = "$me_method_opt epzs ".
119             "-subq 1 ".
120             "-trellis 0 ".
121             "-refs 1" ;
122             my $pass2_options = "$me_method_opt umh ".
123             "-subq 5 ".
124             "-trellis 1 ".
125             "-refs 5 " .
126             "-scodec copy ";
127              
128              
129              
130             ## This is how to transcode the source into the various supported formats
131             our %COMMANDS = (
132              
133             'ts' =>
134             'ffmpeg -i "$src" -vcodec copy -acodec copy -scodec copy -async 1 -y "$dest.$ext"',
135              
136             'mpeg' => [
137             # First try
138             'ffmpeg -i "$src" -vcodec copy -acodec copy -scodec copy -async 1 -y "$dest.$ext"',
139            
140             # Alternate
141             'ffmpeg -i "$src" -vcodec mpeg2video -sameq -acodec copy -scodec copy -async 1 -y "$dest.$ext"',
142             ],
143            
144             'm2v' => [
145             # First try
146             'ffmpeg -i "$src" -vcodec copy -f mpeg2video -y "$dest.$ext"',
147            
148             # Alternate
149             'ffmpeg -i "$src" -vcodec mpeg2video -sameq -f mpeg2video -y "$dest.$ext"',
150             ],
151              
152             'mp2' =>
153             'ffmpeg -i "$src" -acodec copy -f mp2 -y "$dest.$ext"',
154              
155             'mp3' =>
156             'ffmpeg -i "$src" -f mp3 -y "$dest.$ext"',
157              
158             'mp4' => [
159             [
160             # 1st pass
161             'ffmpeg -i "$src" ' . "$pass1_codec -pass 1 $common_start $pass1_options $common_end" . ' -y "$temp.$ext"',
162            
163             # 2nd pass
164             'ffmpeg -i "$src" ' . "$pass2_codec -pass 2 $common_start $pass2_options $common_end" . '$title_opt -y "$dest.$ext"',
165             ],
166             ],
167             ) ;
168              
169              
170             ## Order in which to process streams
171             my @STREAM_ORDER = qw/video audio subtitle/ ;
172              
173             ## Map the requested audio/video/subtitle streams into a file format
174             #
175             # file format (extension), output spec regexp, supported audio channels (0, 1, 2+)
176             #
177             # List is ordered so that preferred file formats are earlier in the list
178             # End of lis contain least preferred options.
179             #
180             our @FORMATS ;
181              
182             # map file extension into format
183             my %FORMATS ;
184              
185             # Aliases
186             my %ALIASES ;
187              
188             BEGIN {
189              
190              
191              
192             ## Map the requested audio/video/subtitle streams into a file format
193             #
194             # file format (extension), output spec regexp, supported audio channels (0, 1, 2+)
195             #
196             # List is ordered so that preferred file formats are earlier in the list
197             # End of lis contain least preferred options.
198             #
199 10     10   63 @FORMATS = (
200             # default
201             ['mpeg', 'va+s*'], # normal video
202            
203             ['m2v', 'v'],
204             ['mp2', 'a'],
205             ['mp3', 'a'],
206            
207             # catch-alls:
208             ['mpeg', 'a+s*'], # mpeg can also be a container for just multiple audio etc
209             ['mpeg', 'vs*'],
210              
211             ['mp4', 'va+s*'],
212             ['mp4', 'a+s*'],
213             ['mp4', 'vs*'],
214              
215             ['ts', 'va+s*'], # normal video
216             ['ts', 'a+s*'], # mpeg can also be a container for just multiple audio etc
217             ['ts', 'vs*'],
218             ['ts', '.+'], # must contain at least 1 stream
219             ) ;
220              
221             # make a HASH keyed on the file format (extension), where the value is an array of the possible
222             # output regexps
223 10         23 foreach (@FORMATS)
224             {
225 130         126 my ($ext, $regexp) = @$_ ;
226 130   100     297 $FORMATS{$ext} ||= [] ;
227 130         85 push @{$FORMATS{$ext}}, $regexp ;
  130         172  
228             }
229            
230             # Create alias list
231             %ALIASES = (
232 10         38 'mpg' => 'mpeg',
233             'mpeg2' => 'mpeg',
234             'mpeg4' => 'mp4',
235             'TS' => 'ts',
236             ) ;
237 10         20 foreach (@FORMATS)
238             {
239 130         103 my ($ext, $regexp) = @$_ ;
240 130         25815 $ALIASES{$ext} = $ext ;
241             }
242            
243             }
244              
245              
246             #============================================================================================
247              
248             =head2 Functions
249              
250             =over 4
251              
252             =cut
253              
254              
255              
256             #-----------------------------------------------------------------------------
257              
258             =item B
259              
260             Transcode the recorded transport stream file into the required format, as specified by $multiplex_info_href.
261              
262             (Called by L).
263              
264             Multiplex info HASH ref is of the form:
265              
266             {
267             'pids' => [
268             {
269             'pid' => Stream PID
270             'pidtype' => pid type (video, audio, subtitle)
271             },
272             ...
273             ],
274             'errors' => [],
275             'warnings' => [],
276             'lines' => [],
277              
278             'destfile' => final written file name (set by this function)
279             }
280              
281             $written_files_href is an optional HASH that may be provided by the calling routine to track which files have been written.
282             Since the file type (extension) can be adjusted by the routine to match it's supported formats/codecs, there is the chance that
283             a file may be accidently over written. The tracking HASH ensures that, if a file was previously written with the same filename,
284             then the new file is written with a unique filename.
285              
286             The 'errors' and 'warnings' ARRAY refs are filled with any error/warning messages as the routine is run. Also, the 'lines'
287             ARRAY ref is filled with the ffmpeg output.
288              
289             If $destfile is undefined, then the routine just checks that $srcfile duration is as expected; setting the errors array if not.
290              
291             When $destfile is defined, it's file type is checked to ensure that the number of streams (pids) and the stream types are supported
292             for this file. If not, then the next preferred file type is used and the destination file adjusted accordingly. The final written
293             destination filename is written into the HASH as 'destfile'.
294              
295             The routine returns 0 on success; non-zero for any error.
296              
297             =cut
298              
299             sub ts_transcode
300             {
301 0     0 1 0 my ($src, $destfile, $multiplex_info_href, $written_files_href) = @_ ;
302 0         0 my $error = 0 ;
303              
304 0 0       0 print STDERR "ts_transcode($src, $destfile)\n" if $DEBUG ;
305            
306             ## errors, warnings, and output lines are stored in the HASH
307 0   0     0 my $errors_aref = ($multiplex_info_href->{'errors'} ||= []) ;
308 0   0     0 my $warnings_aref = ($multiplex_info_href->{'warnings'} ||= []) ;
309 0   0     0 my $lines_aref = ($multiplex_info_href->{'lines'} ||= []) ;
310            
311             ## if src not specified, then destination is a ts file - just check it's length
312 0 0       0 if (! $src)
313             {
314             #### Check the destination file duration
315 0 0       0 if (! -s "$destfile")
316             {
317 0         0 $error = "final file \"$destfile\" zero length" ;
318 0         0 push @$errors_aref, $error ;
319 0         0 return $error ;
320             }
321 0         0 my $file_duration = video_duration("$destfile") ;
322 0 0       0 if ($file_duration < $multiplex_info_href->{'duration'} - $DURATION_MARGIN)
323             {
324 0         0 $error = "Duration of final \"$destfile\" ($file_duration secs) not as expected ($multiplex_info_href->{'duration'} secs)" ;
325 0         0 push @$errors_aref, $error ;
326 0         0 return $error ;
327             }
328             }
329             else
330             {
331             #### Check the source file duration
332 0 0       0 if (! -s "$src")
333             {
334 0         0 $error = "source file \"$src\" zero length" ;
335 0         0 push @$errors_aref, $error ;
336 0         0 return $error ;
337             }
338 0         0 my $file_duration = video_duration("$src") ;
339 0 0       0 if ($file_duration < $multiplex_info_href->{'duration'} - $DURATION_MARGIN)
340             {
341 0         0 my $warn = "Duration of source \"$src\" ($file_duration secs) not as expected ($multiplex_info_href->{'duration'} secs)" ;
342 0         0 push @$warnings_aref, $warn ;
343             }
344             }
345            
346             ## Save source filename
347 0         0 $multiplex_info_href->{'srcfile'} = $src ;
348              
349              
350            
351             #### Select the dest file format
352 0 0       0 if ($src)
353             {
354             # turn the pid types into a valid output spec
355             # e.g. vaa for 2 audio + 1 video
356 0         0 my $out_spec = _pids_out_spec(@{$multiplex_info_href->{'pids'}}) ;
  0         0  
357            
358             # Ensure the file format is correct
359 0         0 $error = _sanitise_options(\$destfile, \$out_spec, $errors_aref, $warnings_aref) ;
360 0 0       0 return $error if $error ;
361            
362            
363             # check specified filename
364 0         0 my ($name, $path, $ext) = fileparse($destfile, '\..*') ;
365 0 0       0 $ext = substr $ext, 1 if $ext ;
366 0         0 my $dest = "$path$name" ;
367            
368             # check to see if we've already written this filename
369 0 0       0 if (exists($written_files_href->{"$dest.$ext"}))
370             {
371             # have to amend the filename so we don't overwrite
372 0         0 my $num=1 ;
373 0         0 while (exists($written_files_href->{"$dest$num.$ext"}))
374             {
375 0         0 ++$num ;
376             }
377            
378             # report the change
379 0         0 push @$warnings_aref, "Filename \"$dest.$ext\" was modified to \"$dest$num.$ext\" because a previously written file has the same name" ;
380            
381             # change
382 0         0 $dest .= $num ;
383             }
384            
385             # track filenames
386 0         0 $written_files_href->{"$dest.$ext"} = 1 ;
387            
388             # return written filename & extension
389 0         0 $multiplex_info_href->{'destfile'} = "$dest.$ext" ;
390 0         0 $multiplex_info_href->{'destext'} = ".$ext" ;
391            
392            
393             # make sure the extension is in a form we understand
394 0         0 my $aliased_ext = $ALIASES{$ext} ;
395            
396 0 0       0 print STDERR " + dest=$dest ext=$ext\n" if $DEBUG ;
397            
398 0 0       0 print STDERR "COMMANDS list for $aliased_ext =" . Data::Dumper->Dump([$COMMANDS{$aliased_ext}]) if $DEBUG >= 5 ;
399            
400             ## Run ffmpeg
401 0         0 my $cmds_ref = $COMMANDS{$aliased_ext} ;
402 0 0       0 my @cmds = ref($cmds_ref) ? @$cmds_ref : ($cmds_ref) ;
403            
404             # create extra variables for variable replacement
405 0         0 my $temp = "${path}temp$$.$name" ; # used for mp4
406 0         0 my $title_opt = "" ; # used for mp4
407 0 0       0 if ($multiplex_info_href->{'title'})
408             {
409 0         0 $title_opt = "-metadata title=\"$multiplex_info_href->{'title'}\" " ;
410             }
411            
412             # run through alternatives
413 0         0 for (my $idx=0; $idx < scalar(@cmds); ++$idx)
414             {
415 0         0 my $cmd = $cmds[$idx] ;
416 0         0 my $rc=0 ;
417 0         0 my $pass_str ;
418            
419             # if this is an array, then it's a multi-pass algorithm
420 0 0       0 my @passes = ref($cmd) ? @$cmd : ($cmd) ;
421 0         0 foreach my $pass (@passes)
422             {
423            
424 0 0       0 print STDERR "PASS: $pass\n" if $DEBUG ;
425 0         0 ($pass_str = $pass) =~ s/\\/\\\\/g ;
426 0         0 $pass_str =~ s/\"/\\\"/g ;
427            
428 0 0       0 print STDERR "PASS STR: $pass_str\n" if $DEBUG ;
429            
430             # expand all variables
431 0         0 my $args ;
432 0         0 eval "\$args = \"$pass_str\";" ;
433            
434 0 0       0 print STDERR "ARGS: $args\n" if $DEBUG ;
435            
436             # run the ffmpeg command
437 0         0 my @lines ;
438 0         0 $rc = run_transcoder($args, \@lines) ;
439            
440             # save results
441 0         0 push @$lines_aref, @lines ;
442            
443 0 0       0 print STDERR "RC = $rc\n" if $DEBUG ;
444            
445             # stop if failed (non-zero exit status)
446 0 0       0 last if $rc ;
447             }
448            
449             # failed?
450 0         0 my $pass_failed ;
451 0 0       0 if ($rc)
452             {
453 0         0 $pass_failed = "ffmpeg command failed (status = $rc)" ;
454             }
455             else
456             {
457             # check video duration
458 0 0       0 if (! -s "$dest.$ext")
459             {
460 0         0 $pass_failed = "destination file \"$dest.$ext\" zero length" ;
461             }
462             else
463             {
464 0         0 my $file_duration = video_duration("$dest.$ext") ;
465 0 0       0 if ($file_duration < $multiplex_info_href->{'duration'} - $DURATION_MARGIN)
466             {
467 0         0 $pass_failed = "Duration of \"$dest.$ext\" ($file_duration secs) not as expected ($multiplex_info_href->{'duration'} secs)" ;
468             }
469             else
470             {
471             # all's well so stop
472 0         0 last ;
473             }
474             }
475             }
476            
477 0 0       0 if ($pass_failed)
478             {
479             # can we try again
480 0 0       0 if ( ($idx+1) < scalar(@cmds))
481             {
482 0         0 push @$warnings_aref, "$pass_failed, trying alternate command" ;
483             }
484             else
485             {
486 0         0 $error = $pass_failed ;
487 0         0 push @$errors_aref, $error ;
488 0         0 return $error ;
489             }
490             }
491             }
492            
493             # delete any temp files
494 0         0 for my $tempfile (glob("${path}temp*"))
495             {
496 0         0 unlink $tempfile ;
497             }
498             }
499            
500 0         0 return $error ;
501             }
502              
503              
504              
505              
506             #-----------------------------------------------------------------------------
507              
508             =item B
509              
510             Processes the destination file, the requested output spec (if any), and the language spec (if any)
511             to ensure they are set to a valid combination of settings which will result in a supported file format
512             being written. Adjusts/sets the values accordingly.
513              
514             If defined, the output spec (and language) always has precedence over the specified file format and the file format
515             (i.e. extension) will be adjusted to match the required output.
516              
517             This is used when scheduling recording of multiple channels in the multiplex. This routine ensures that the correct
518             streams are added to the recording.
519              
520             =cut
521              
522             # Want Input Out
523             # ext lang out record out ext
524             # av '' '' av av .mpeg
525             # av .mpeg '' '' av av .mpeg (=ext)
526             # a+v .mpeg 'a a' '' a+v a+v .mpeg (=ext)
527             # avs .mpeg '' 'avs' avs =out .mpeg (=ext)
528             # a+vs .mpeg 'a a' 'avs' a+vs =out .mpeg (=ext)
529              
530             # a '' 'a' a =out .mp2
531             # a 'a' 'a' a =out .mp2
532             # a .mp2 '' '' a a .mp2 (=ext)
533             # a .mp3 '' '' a a .mp3 (=ext)
534             # a .mp3 'a' '' a a .mp3 (=ext)
535              
536             # v .m2v '' '' v v .m2v (=ext)
537             # v '' 'v' v =out .m2v (=ext)
538              
539             sub sanitise_options
540             {
541 53     53 1 107202 my ($destfile_ref, $out_ref, $lang_ref, $errors_aref, $warnings_aref) = @_ ;
542 53         91 my $error = 0 ;
543            
544             # language spec is optional
545 53         100 my $lang = "" ;
546 53   50     135 $lang_ref ||= \$lang ;
547            
548 53   50     87 $$destfile_ref ||= "" ;
549 53   100     135 $$lang_ref ||= "" ;
550 53         49 my $orig_lang = $$lang_ref ;
551            
552             # merge together the output spec & language spec and ensure output spec is in the correct form
553 53         119 ($$out_ref, $$lang_ref) = _normalise_output_spec($$out_ref, $$lang_ref) ;
554 53 50       87 print STDERR "sanitise_options($$destfile_ref, $$out_ref, $$lang_ref)\n" if $DEBUG ;
555            
556             # check specified filename
557 53         1168 my ($name, $path, $ext) = fileparse($$destfile_ref, '\..*') ;
558 53 100       129 $ext = substr $ext, 1 if $ext ;
559 53         92 my $dest = "$path$name" ;
560              
561 53 50       81 print STDERR " + dest=$dest ext=$ext\n" if $DEBUG ;
562              
563             # check output spec
564 53 100       96 if (!$$out_ref)
565             {
566             ## no output spec defined
567              
568             # get default output spec
569 19         31 my $default_ext = $FORMATS[0][0] ;
570 19 50       26 print STDERR "format regexp 2 out - Default...\n" if $DEBUG ;
571 19         38 my $default_out = _format_regexp2out($FORMATS[0][1], $$lang_ref) ;
572              
573 19 50       25 print STDERR "No out specified: default out=$default_out default ext=$default_ext\n" if $DEBUG ;
574              
575             # check file format
576 19 100       28 if (!$ext)
577             {
578 2 50       3 print STDERR " + No ext specified: using default out=$default_out default ext=$default_ext\n" if $DEBUG ;
579             # no file format, set it based on default output spec
580 2         3 $$out_ref = $default_out ;
581 2         4 $ext = $default_ext ;
582             }
583             else
584             {
585             # file format specified, see if we support it
586 17 100       30 if (exists($ALIASES{$ext}))
587             {
588             # make sure the extension is in a form we understand
589 16         20 my $aliased_ext = $ALIASES{$ext} ;
590            
591 16 50       22 print STDERR "format regexp 2 out - preferred for $ext ...\n" if $DEBUG ;
592             # convert the first (preferred) regexp into output spec
593 16         32 ($$out_ref, $$lang_ref) = _format_regexp2out($FORMATS{$aliased_ext}[0], $$lang_ref) ;
594              
595 16 50       35 print STDERR " + ext specified ($ext): using regexp $FORMATS{$aliased_ext}[0] out=$$out_ref ext=$aliased_ext\n" if $DEBUG ;
596             }
597             else
598             {
599 1 50       3 print STDERR " + non-supported ext specified: using default out=$default_out default ext=$default_ext\n" if $DEBUG ;
600              
601             # report warning
602 1         4 push @$warnings_aref, "File type \"$ext\" is not supported, changing to \"$default_ext\"" ;
603            
604             # non-supoported file format, set to defaults
605 1         2 $$out_ref = $default_out ;
606 1         1 $ext = $default_ext ;
607             }
608             }
609             }
610              
611             # check for language spec being dropped due to output spec
612 53 100 100     158 if (($orig_lang) && ($$lang_ref ne $orig_lang))
613             {
614             # report warning
615 6         22 push @$warnings_aref, "Language spec \"$orig_lang\" is being ignored because of the specified required output (no audio)" ;
616             }
617            
618             ## Do the rest of the processing now that we've handled the language spec
619 53         97 $error = _sanitise_options($destfile_ref, $out_ref, $errors_aref, $warnings_aref) ;
620              
621 53         114 return $error ;
622             }
623              
624              
625             #-----------------------------------------------------------------------------
626              
627             =item B
628              
629             Uses ffmpeg to determine the video file duration. Returns the duration in seconds.
630              
631             =cut
632              
633             sub video_duration
634             {
635 0     0 1 0 my ($file) = @_ ;
636            
637 0         0 my %info = video_info($file) ;
638 0         0 return $info{'duration'} ;
639             }
640              
641             #-----------------------------------------------------------------------------
642              
643             =item B
644              
645             Uses ffmpeg to determine the video file contents. Returns a HASH containing:
646              
647             'input' => Input number
648             'duration' => video duration in seconds
649             'pids' => {
650             $pid => {
651             'input' => input number that this pid is part of
652             'stream' => stream number
653             'lang' => audio language
654             'pidtype' => pid type (video, audio, subtitle)
655             }
656             }
657              
658             =cut
659              
660             sub video_info
661             {
662 0     0 1 0 my ($file) = @_ ;
663            
664 0         0 my @lines ;
665 0         0 run_transcoder("ffmpeg -i '$file'", \@lines) ;
666            
667 0         0 my %info = (
668             'input' => undef,
669             'duration' => 0,
670             'pids' => {},
671             ) ;
672 0         0 foreach my $line (reverse @lines)
673             {
674             # Input #0, mpegts, from 'bbc1-bbc2.ts':
675             # Duration: 00:00:27.18, start: 15213.487800, bitrate: 4049 kb/s
676             # Stream #0.0[0x259]: Audio: mp2, 48000 Hz, 2 channels, s16, 256 kb/s
677             # Stream #0.1[0x258]: Video: mpeg2video, yuv420p, 720x576 [PAR 64:45 DAR 16:9], 15000 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc
678             # Stream #0.2[0x262]: Video: mpeg2video, yuv420p, 720x576 [PAR 64:45 DAR 16:9], 15000 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc
679             # Stream #0.3[0x263]: Audio: mp2, 48000 Hz, 2 channels, s16, 256 kb/s
680             # At least one output file must be specified
681            
682             # Stream #0.0[0x258]: Video: mpeg2video, yuv420p, 720x576 [PAR 64:45 DAR 16:9], 15000 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc
683             # Stream #0.1[0x259](eng): Audio: mp2, 48000 Hz, 2 channels, s16, 256 kb/s
684             # Stream #0.2[0x25a](eng): Audio: mp2, 48000 Hz, 1 channels, s16, 64 kb/s
685             # Stream #0.3[0x25d](eng): Subtitle: dvbsub
686              
687             # Stream #0.1[0x259](eng): Audio: mp2, 48000 Hz, 2 channels, s16, 256 kb/s
688 0 0       0 if ($line =~ /Stream #(\d+)\.(\d+)\[(0x[\da-f]+)\]\((\S+)\): (\S+): /i)
    0          
    0          
    0          
689             {
690 0         0 my ($input, $stream, $lang, $pid, $type) = ($1, $2, $3, hex($4), lc $5) ;
691 0         0 $info{'pids'}{$pid} = {
692             'input' => $input,
693             'stream' => $stream,
694             'lang' => $lang,
695             'pidtype' => $type,
696             } ;
697             }
698             # Stream #0.0[0x258]: Video: mpeg2video, yuv420p, 720x576 [PAR 64:45 DAR 16:9], 15000 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc
699             elsif ($line =~ /Stream #(\d+)\.(\d+)\[(0x[\da-f]+)\]: (\S+): /i)
700             {
701 0         0 my ($input, $stream, $pid, $type) = ($1, $2, hex($3), lc $4) ;
702 0         0 $info{'pids'}{$pid} = {
703             'input' => $input,
704             'stream' => $stream,
705             'pidtype' => $type,
706             } ;
707             }
708             # Duration: 00:00:27.18, start: 15213.487800, bitrate: 4049 kb/s
709             elsif ($line =~ /Duration: (\d+):(\d+):(\d+).(\d+)/i)
710             {
711 0         0 my ($hour, $min, $sec, $ms) = ($1, $2, $3, $4) ;
712 0         0 $sec += $min*60 + $hour*60*60;
713 0         0 $info{'duration'} = $sec ;
714             }
715             # Input #0, mpegts, from 'bbc1-bbc2.ts':
716             elsif ($line =~ /Input #(\d+),/i)
717             {
718 0         0 $info{'input'} = $1 ;
719 0         0 last ;
720             }
721             }
722            
723 0         0 return %info ;
724             }
725              
726             #-----------------------------------------------------------------------------
727              
728             =item B
729              
730             Run the transcoder command with the provided arguments. If the $lines_aref ARRAY ref is supplied,
731             then the output lines from ffmpeg are returned in that array (one entry per line).
732              
733             Returns the exit status from ffmpeg.
734              
735             =cut
736              
737             sub run_transcoder
738             {
739 0     0 1 0 my ($args, $lines_aref) = @_ ;
740              
741 0   0     0 $lines_aref ||= [] ;
742              
743             # get command name
744 0         0 my $transcoder = "" ;
745 0         0 ($transcoder = $args) =~ s/^\s*(\S+).*/$1/ ;
746              
747             # set niceness
748 0         0 my $nice = "" ;
749 0 0       0 if ($NICE)
750             {
751 0         0 $nice = "nice -n $NICE" ;
752             }
753             # run ffmpeg
754             # my $cmd = "$nice ffmpeg $args" ;
755 0         0 my $cmd = "$nice $args" ;
756 0         0 @$lines_aref = `$cmd 2>&1 ; echo RC=$?` ;
757            
758             # strip newlines
759 0         0 foreach (@$lines_aref)
760             {
761 0         0 chomp $_ ;
762            
763             # Strip out the intermediate processing output
764 0         0 $_ =~ s/^.*\r//g ;
765            
766             # prepend with command name
767 0         0 $_ = "[$transcoder] $_" ;
768             }
769              
770             # Add command to start
771 0         0 unshift @$lines_aref , $cmd ;
772            
773             # get status
774 0         0 my $rc=-1 ;
775 0 0       0 if ($lines_aref->[-1] =~ m/RC=(\d+)/)
776             {
777 0         0 $rc = $1 ;
778             }
779              
780 0 0       0 if ($DEBUG_FFMPEG)
781             {
782 0         0 print STDERR "-------------------------------------------\n" ;
783 0         0 foreach (@$lines_aref)
784             {
785 0         0 print STDERR "$_\n" ;
786             }
787 0         0 print STDERR "STATUS = $rc\n" ;
788 0         0 print STDERR "-------------------------------------------\n" ;
789             }
790            
791 0         0 return $rc ;
792             }
793              
794             # ============================================================================================
795             # PRIVATE
796             # ============================================================================================
797              
798             # --------------------------------------------------------------------------------------------
799             # Internal routine that expects the output spec to already have been created/normalised into
800             # a valid spec (including any language options)
801             sub _sanitise_options
802             {
803 53     53   59 my ($destfile_ref, $output_spec_ref, $errors_aref, $warnings_aref) = @_ ;
804 53         44 my $error = 0 ;
805            
806 53   50     85 $$destfile_ref ||= "" ;
807 53   50     91 $errors_aref ||= [] ;
808 53   50     66 $warnings_aref ||= [] ;
809            
810 53 50       75 print STDERR "_sanitise_options($$destfile_ref, $$output_spec_ref)\n" if $DEBUG ;
811            
812             # check specified filename
813 53         804 my ($name, $path, $ext) = fileparse($$destfile_ref, '\..*') ;
814 53 100       106 $ext = substr $ext, 1 if $ext ;
815 53         50 my $dest = "$path$name" ;
816              
817 53 50       74 print STDERR " + dest=$dest ext=$ext\n" if $DEBUG ;
818              
819             # check output spec
820 53 50       83 if ($$output_spec_ref)
821             {
822             ## output spec defined
823            
824             # use it to check the file format
825 53         82 my @supported_types = _output_formats($$output_spec_ref) ;
826              
827 53 50       85 print STDERR "out specified: supported types=" . Data::Dumper->Dump([\@supported_types]) if $DEBUG ;
828            
829             # check file format
830 53 100       75 if (!$ext)
831             {
832             # no file format, set it based on output spec
833 7 50       15 if (@supported_types)
834             {
835             # use first supported type
836 7         11 $ext = $supported_types[0] ;
837             }
838             }
839             else
840             {
841             # file format specified, check it matches supported types
842 46         62 my %valid_types = map { $_ => 1} @supported_types ;
  207         350  
843 46 100 100     250 if (exists($ALIASES{$ext}) && exists($valid_types{ $ALIASES{$ext} }))
844             {
845             # ok to use
846             }
847             else
848             {
849             # file format does not match requested output, use default for the requested output
850 14         21 my $old_ext = $ext ;
851 14         14 $ext = undef ;
852 14 50       32 if (@supported_types)
853             {
854             # use first supported type
855 14         19 my $new_ext = $supported_types[0] ;
856            
857             # report warning
858 14         39 push @$warnings_aref, "File type \"$old_ext\" does not match requested output, changing to \"$new_ext\"" ;
859            
860             # change
861 14         40 $ext = $new_ext ;
862             }
863             }
864             }
865             }
866              
867              
868             ## If we get here and either the output spec or the file format are not defined, then there's been an error
869 53 50       96 if (!$$output_spec_ref)
    50          
870             {
871 0         0 $error = "Unable to determine the correct recording type for the specified output file format" ;
872             }
873             elsif (!$ext)
874             {
875 0         0 $error = "Unable to determine the correct recording type for the specified output file format" ;
876             }
877             else
878             {
879             ## ok to finish off the output file
880 53         85 $$destfile_ref = "$path$name.$ext" ;
881             }
882            
883 53 50       71 if ($error)
884             {
885 0         0 push @$errors_aref, $error ;
886             }
887            
888 53         73 return $error ;
889             }
890              
891              
892             # --------------------------------------------------------------------------------------------
893             # Ensure the output spec is in the correct form & has the stream types in the correct order
894             sub _normalise_output_spec
895             {
896 88     88   104 my ($out, $lang) = @_ ;
897            
898 88   100     157 $out ||= "" ;
899 88   100     167 $lang ||= "" ;
900            
901 88         77 my $out_spec = "" ;
902              
903 88 50       139 print STDERR "_normalise_output_spec(out=\"$out\", lang=\"$lang\")\n" if $DEBUG ;
904            
905 88 100       131 if ($out)
906             {
907             # number of audio channels (-1 because, if audio is specified in the output, that already adds 1)
908 69         106 my $num_audio = _audio_chan_count($lang) ;
909 69 100       122 $num_audio-- if $num_audio >= 1 ;
910              
911 69 50       87 print STDERR " + num_audio=$num_audio\n" if $DEBUG ;
912            
913             # look at each stream type in the correct order
914 69         94 foreach my $type (@STREAM_ORDER)
915             {
916             # add stream type code (a=audio, v=video etc) to spec if this type
917             # is in the specified spec
918 207         233 my $type_ch = substr $type, 0, 1 ;
919 207 100       1529 if ($out =~ /$type_ch/)
920             {
921 120         104 $out_spec .= $type_ch ;
922              
923 120 50       164 print STDERR " + + add $type : $type_ch\n" if $DEBUG ;
924            
925 120 100       222 if ($type eq 'audio')
926             {
927             ## add in the language audio channels (-1 )
928 58         75 $out_spec .= 'a'x$num_audio ;
929              
930 58 50       132 print STDERR " + + add lang : ".'a'x$num_audio."\n" if $DEBUG ;
931             }
932             }
933             else
934             {
935             # this type is not in the output spec
936 87 100       201 if ($type eq 'audio')
937             {
938             ## no audio specified, so kill off the language spec
939 11         19 $lang = "" ;
940              
941 11 50       27 print STDERR " + + no audio : removing any language spec\n" if $DEBUG ;
942             }
943             }
944             }
945             }
946              
947 88 50       425 print STDERR "final out_spec=\"$out_spec\"\n" if $DEBUG ;
948              
949 88         212 return ($out_spec, $lang) ;
950             }
951              
952              
953             # --------------------------------------------------------------------------------------------
954             # Convert the output spec into a list of supported types
955             sub _output_formats
956             {
957 53     53   57 my ($out) = @_ ;
958            
959             ## Get list of valid file types
960 53 50       74 print STDERR "checking out $out...\n" if $DEBUG ;
961 53         38 my @valid_types ;
962 53         67 foreach my $fmt_aref (@FORMATS)
963             {
964 689         821 my ($valid_fmt, $regexp) = @$fmt_aref ;
965            
966 689 50       913 print STDERR " + check $out : $valid_fmt => $regexp\n" if $DEBUG ;
967 689 100       5280 if ($out =~ m/^$regexp$/)
968             {
969 241 50       317 print STDERR " + + match\n" if $DEBUG ;
970             # save format with the command information
971             # $valid_types{$valid_fmt} = $COMMANDS{$valid_fmt} ;
972 241         420 push @valid_types, $valid_fmt ;
973             }
974             }
975 53         144 return @valid_types ;
976             }
977              
978             # --------------------------------------------------------------------------------------------
979             # Convert the regular expression string into an output spec string. For example, converts:
980             # v+a+s*
981             # into
982             # va
983             #
984             # Also amends the spec with any language specifier
985             #
986             sub _format_regexp2out
987             {
988 35     35   41 my ($regexp, $lang) = @_ ;
989            
990 35         34 my $out = $regexp ;
991              
992 35 50       49 print STDERR "_format_regexp2out($regexp)\n" if $DEBUG ;
993            
994 35         67 $out =~ s/\+//g ;
995 35 50       50 print STDERR " + remove + : $out\n" if $DEBUG ;
996 35         64 $out =~ s/(.)\*//g ;
997 35 50       49 print STDERR " + remove X* : $out\n" if $DEBUG ;
998 35         32 $out =~ s/\*//g ;
999 35 50       44 print STDERR " + remove * : $out\n" if $DEBUG ;
1000              
1001             # normalise
1002 35         45 ($out, $lang) = _normalise_output_spec($out, $lang) ;
1003              
1004 35 50       53 print STDERR " + final out : $out\n" if $DEBUG ;
1005            
1006 35 100       85 return wantarray ? ($out, $lang) : $out ;
1007             }
1008              
1009             # --------------------------------------------------------------------------------------------
1010             # Return the number of audio channels requested by the language spec
1011             #
1012             sub _audio_chan_count
1013             {
1014 69     69   68 my ($language_spec) = @_ ;
1015 69         63 my $num_audio = 0 ;
1016            
1017             # process language spec
1018 69   100     133 $language_spec ||= "" ;
1019 69 100       89 if ($language_spec)
1020             {
1021             # appends to default audio chan
1022 38 100       126 if ($language_spec =~ s/\+//g)
1023             {
1024 17         20 ++$num_audio ;
1025             }
1026            
1027             # work through the language spec
1028 38         151 my @lang = split /[\s,]+/, $language_spec ;
1029 38         56 $num_audio += scalar(@lang) ;
1030             }
1031              
1032 69         88 return $num_audio ;
1033             }
1034              
1035             # --------------------------------------------------------------------------------------------
1036             # Convert the recorded pids into a valid output spec
1037             #
1038             sub _pids_out_spec
1039             {
1040 0     0     my (@pids) = @_ ;
1041              
1042 0 0         print STDERR "_pids_out_spec()\n" if $DEBUG>=10 ;
1043            
1044             ## turn the pid types into a format string of the form:
1045             ## e.g. aav for 2 audio + 1 video
1046 0           my $out_spec = "" ;
1047 0           foreach my $type (@STREAM_ORDER)
1048             {
1049 0           foreach my $pid_href (@pids)
1050             {
1051 0 0         print STDERR " + check pid $pid_href->{'pid'} type=$pid_href->{'type'} against $type..\n" if $DEBUG>=10 ;
1052 0 0         if ($pid_href->{'pidtype'} eq $type)
1053             {
1054 0           $out_spec .= substr($type, 0, 1) ;
1055 0 0         print STDERR " + + added outspec=\"$out_spec\"\n" if $DEBUG>=10 ;
1056             }
1057             }
1058             }
1059 0           return $out_spec ;
1060             }
1061              
1062              
1063             # ============================================================================================
1064             # END OF PACKAGE
1065              
1066             =back
1067              
1068             =cut
1069              
1070             1;
1071