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   61 use strict ;
  10         19  
  10         429  
61 10     10   63 use File::Basename ;
  10         22  
  10         908  
62 10     10   59 use Data::Dumper ;
  10         20  
  10         6289  
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   106 @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         39 foreach (@FORMATS)
224             {
225 130         180 my ($ext, $regexp) = @$_ ;
226 130   100     529 $FORMATS{$ext} ||= [] ;
227 130         132 push @{$FORMATS{$ext}}, $regexp ;
  130         351  
228             }
229            
230             # Create alias list
231             %ALIASES = (
232 10         62 'mpg' => 'mpeg',
233             'mpeg2' => 'mpeg',
234             'mpeg4' => 'mp4',
235             'TS' => 'ts',
236             ) ;
237 10         29 foreach (@FORMATS)
238             {
239 130         152 my ($ext, $regexp) = @$_ ;
240 130         60253 $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 180209 my ($destfile_ref, $out_ref, $lang_ref, $errors_aref, $warnings_aref) = @_ ;
542 53         99 my $error = 0 ;
543            
544             # language spec is optional
545 53         125 my $lang = "" ;
546 53   50     161 $lang_ref ||= \$lang ;
547            
548 53   50     153 $$destfile_ref ||= "" ;
549 53   100     188 $$lang_ref ||= "" ;
550 53         83 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         142 ($$out_ref, $$lang_ref) = _normalise_output_spec($$out_ref, $$lang_ref) ;
554 53 50       122 print STDERR "sanitise_options($$destfile_ref, $$out_ref, $$lang_ref)\n" if $DEBUG ;
555            
556             # check specified filename
557 53         1550 my ($name, $path, $ext) = fileparse($$destfile_ref, '\..*') ;
558 53 100       156 $ext = substr $ext, 1 if $ext ;
559 53         82 my $dest = "$path$name" ;
560              
561 53 50       100 print STDERR " + dest=$dest ext=$ext\n" if $DEBUG ;
562              
563             # check output spec
564 53 100       125 if (!$$out_ref)
565             {
566             ## no output spec defined
567              
568             # get default output spec
569 19         54 my $default_ext = $FORMATS[0][0] ;
570 19 50       39 print STDERR "format regexp 2 out - Default...\n" if $DEBUG ;
571 19         63 my $default_out = _format_regexp2out($FORMATS[0][1], $$lang_ref) ;
572              
573 19 50       47 print STDERR "No out specified: default out=$default_out default ext=$default_ext\n" if $DEBUG ;
574              
575             # check file format
576 19 100       35 if (!$ext)
577             {
578 2 50       5 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         5 $ext = $default_ext ;
582             }
583             else
584             {
585             # file format specified, see if we support it
586 17 100       61 if (exists($ALIASES{$ext}))
587             {
588             # make sure the extension is in a form we understand
589 16         36 my $aliased_ext = $ALIASES{$ext} ;
590            
591 16 50       35 print STDERR "format regexp 2 out - preferred for $ext ...\n" if $DEBUG ;
592             # convert the first (preferred) regexp into output spec
593 16         66 ($$out_ref, $$lang_ref) = _format_regexp2out($FORMATS{$aliased_ext}[0], $$lang_ref) ;
594              
595 16 50       55 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         3 $ext = $default_ext ;
607             }
608             }
609             }
610              
611             # check for language spec being dropped due to output spec
612 53 100 100     229 if (($orig_lang) && ($$lang_ref ne $orig_lang))
613             {
614             # report warning
615 6         18 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         127 $error = _sanitise_options($destfile_ref, $out_ref, $errors_aref, $warnings_aref) ;
620              
621 53         154 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   82 my ($destfile_ref, $output_spec_ref, $errors_aref, $warnings_aref) = @_ ;
804 53         61 my $error = 0 ;
805            
806 53   50     112 $$destfile_ref ||= "" ;
807 53   50     101 $errors_aref ||= [] ;
808 53   50     103 $warnings_aref ||= [] ;
809            
810 53 50       95 print STDERR "_sanitise_options($$destfile_ref, $$output_spec_ref)\n" if $DEBUG ;
811            
812             # check specified filename
813 53         1164 my ($name, $path, $ext) = fileparse($$destfile_ref, '\..*') ;
814 53 100       137 $ext = substr $ext, 1 if $ext ;
815 53         76 my $dest = "$path$name" ;
816              
817 53 50       95 print STDERR " + dest=$dest ext=$ext\n" if $DEBUG ;
818              
819             # check output spec
820 53 50       112 if ($$output_spec_ref)
821             {
822             ## output spec defined
823            
824             # use it to check the file format
825 53         114 my @supported_types = _output_formats($$output_spec_ref) ;
826              
827 53 50       114 print STDERR "out specified: supported types=" . Data::Dumper->Dump([\@supported_types]) if $DEBUG ;
828            
829             # check file format
830 53 100       95 if (!$ext)
831             {
832             # no file format, set it based on output spec
833 7 50       22 if (@supported_types)
834             {
835             # use first supported type
836 7         16 $ext = $supported_types[0] ;
837             }
838             }
839             else
840             {
841             # file format specified, check it matches supported types
842 46         73 my %valid_types = map { $_ => 1} @supported_types ;
  207         476  
843 46 100 100     315 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         22 my $old_ext = $ext ;
851 14         17 $ext = undef ;
852 14 50       28 if (@supported_types)
853             {
854             # use first supported type
855 14         16 my $new_ext = $supported_types[0] ;
856            
857             # report warning
858 14         32 push @$warnings_aref, "File type \"$old_ext\" does not match requested output, changing to \"$new_ext\"" ;
859            
860             # change
861 14         38 $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       136 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         114 $$destfile_ref = "$path$name.$ext" ;
881             }
882            
883 53 50       107 if ($error)
884             {
885 0         0 push @$errors_aref, $error ;
886             }
887            
888 53         111 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   147 my ($out, $lang) = @_ ;
897            
898 88   100     223 $out ||= "" ;
899 88   100     235 $lang ||= "" ;
900            
901 88         117 my $out_spec = "" ;
902              
903 88 50       222 print STDERR "_normalise_output_spec(out=\"$out\", lang=\"$lang\")\n" if $DEBUG ;
904            
905 88 100       170 if ($out)
906             {
907             # number of audio channels (-1 because, if audio is specified in the output, that already adds 1)
908 69         182 my $num_audio = _audio_chan_count($lang) ;
909 69 100       161 $num_audio-- if $num_audio >= 1 ;
910              
911 69 50       127 print STDERR " + num_audio=$num_audio\n" if $DEBUG ;
912            
913             # look at each stream type in the correct order
914 69         186 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         415 my $type_ch = substr $type, 0, 1 ;
919 207 100       2305 if ($out =~ /$type_ch/)
920             {
921 120         141 $out_spec .= $type_ch ;
922              
923 120 50       260 print STDERR " + + add $type : $type_ch\n" if $DEBUG ;
924            
925 120 100       325 if ($type eq 'audio')
926             {
927             ## add in the language audio channels (-1 )
928 58         106 $out_spec .= 'a'x$num_audio ;
929              
930 58 50       198 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       857 if ($type eq 'audio')
937             {
938             ## no audio specified, so kill off the language spec
939 11         14 $lang = "" ;
940              
941 11 50       39 print STDERR " + + no audio : removing any language spec\n" if $DEBUG ;
942             }
943             }
944             }
945             }
946              
947 88 50       185 print STDERR "final out_spec=\"$out_spec\"\n" if $DEBUG ;
948              
949 88         301 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   79 my ($out) = @_ ;
958            
959             ## Get list of valid file types
960 53 50       110 print STDERR "checking out $out...\n" if $DEBUG ;
961 53         70 my @valid_types ;
962 53         117 foreach my $fmt_aref (@FORMATS)
963             {
964 689         1030 my ($valid_fmt, $regexp) = @$fmt_aref ;
965            
966 689 50       1087 print STDERR " + check $out : $valid_fmt => $regexp\n" if $DEBUG ;
967 689 100       7657 if ($out =~ m/^$regexp$/)
968             {
969 241 50       411 print STDERR " + + match\n" if $DEBUG ;
970             # save format with the command information
971             # $valid_types{$valid_fmt} = $COMMANDS{$valid_fmt} ;
972 241         671 push @valid_types, $valid_fmt ;
973             }
974             }
975 53         196 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   64 my ($regexp, $lang) = @_ ;
989            
990 35         54 my $out = $regexp ;
991              
992 35 50       84 print STDERR "_format_regexp2out($regexp)\n" if $DEBUG ;
993            
994 35         112 $out =~ s/\+//g ;
995 35 50       76 print STDERR " + remove + : $out\n" if $DEBUG ;
996 35         106 $out =~ s/(.)\*//g ;
997 35 50       125 print STDERR " + remove X* : $out\n" if $DEBUG ;
998 35         59 $out =~ s/\*//g ;
999 35 50       122 print STDERR " + remove * : $out\n" if $DEBUG ;
1000              
1001             # normalise
1002 35         69 ($out, $lang) = _normalise_output_spec($out, $lang) ;
1003              
1004 35 50       84 print STDERR " + final out : $out\n" if $DEBUG ;
1005            
1006 35 100       117 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   104 my ($language_spec) = @_ ;
1015 69         93 my $num_audio = 0 ;
1016            
1017             # process language spec
1018 69   100     227 $language_spec ||= "" ;
1019 69 100       141 if ($language_spec)
1020             {
1021             # appends to default audio chan
1022 38 100       154 if ($language_spec =~ s/\+//g)
1023             {
1024 17         26 ++$num_audio ;
1025             }
1026            
1027             # work through the language spec
1028 38         190 my @lang = split /[\s,]+/, $language_spec ;
1029 38         87 $num_audio += scalar(@lang) ;
1030             }
1031              
1032 69         124 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