Ticket #9199: ffmpeg.pm.modified

File ffmpeg.pm.modified, 20.9 KB (added by Bedlore <bedlore@…>, 14 years ago)

modified nuvexport version to work with Debian

Line 
1#
2# $Date: 2010-07-26 22:49:28 +0200 (lun. 26 juil. 2010) $
3# $Revision: 25431 $
4# $Author: xris $
5#
6#  ffmpeg.pm
7#
8#    routines for setting up ffmpeg
9#    Maintained by Gavin Hurlbut <gjhurlbu@gmail.com>
10#
11
12#
13# Need some kind of hash to deal with the various ffmpeg command version
14# differences:
15#
16# http://svn.mplayerhq.hu/ffmpeg/trunk/libavcodec/utils.c?r1=6252&r2=6257&pathrev=6257
17#
18
19
20package export::ffmpeg;
21    use base 'export::generic';
22
23    use export::generic;
24
25    use Time::HiRes qw(usleep);
26    use POSIX;
27    use Config;
28
29    use nuv_export::shared_utils;
30    use nuv_export::cli;
31    use nuv_export::ui;
32    use mythtv::recordings;
33    use MythTV;
34
35# In case people would rather use yuvdenoise to deinterlace
36    add_arg('deint_in_yuvdenoise|deint-in-yuvdenoise!', 'Deinterlace in yuvdenoise instead of ffmpeg');
37
38# Check for ffmpeg
39    sub init_ffmpeg {
40        my $self      = shift;
41        my $audioonly = (shift or 0);
42    # Temp var
43        my $data;
44    # Make sure we have ffmpeg
45        my $ffmpeg = find_program('ffmpeg')
46            or push @{$self->{'errors'}}, 'You need ffmpeg to use this exporter.';
47    # Make sure we have yuvdenoise
48        my $yuvdenoise = find_program('yuvdenoise')
49            or push @{$self->{'errors'}}, 'You need yuvdenoise (part of mjpegtools) to use this exporter.';
50    # Check the yuvdenoise version
51#        if (!defined $self->{'denoise_vmaj'}) {
52#            $data = `cat /dev/null | yuvdenoise `;
53#            if ($data =~ m/yuvdenoise version (\d+(?:\.\d+)?)(\.\d+)?/i) {
54#                $self->{'denoise_vmaj'} = $1;
55 #               $self->{'denoise_vmin'} = ($2 or 0);
56  #          }
57   #         elsif ($data =~ m/version: mjpegtools (\d+(?:\.\d+)?)(\.\d+)?/i) {
58    #            $self->{'denoise_vmaj'} = $1;
59     #           $self->{'denoise_vmin'} = ($2 or 0);
60      #      }
61       #     else {
62        #        push @{$self->{'errors'}}, 'Unrecognizeable yuvdenoise version string.';
63         #       $self->{'noise_reduction'} = 0;
64          #      $self->{'denoise_vmaj'}    = 0;
65#                $self->{'denoise_vmin'}    = 0;
66 #           }
67        # New yuvdenoise can't deinterlace
68#            if ($self->{'denoise_vmaj'} > 1.6 || ($self->{'denoise_vmaj'} == 1.6 && $self->{'denoise_vmin'} > 2)) {
69 #               $self->{'deint_in_yuvdenoise'} = 0;
70#$self->{'deint_in_yuvdenoise'} = 1;
71  #          }
72#        }
73    # Check the ffmpeg version
74#        if (!defined $self->{'ffmpeg_vers'}) {
75 #           $data = `$ffmpeg -version `;
76  #          if ($data =~ m/ffmpeg\sversion\s(0.\d)[\-,]/si) {
77   #             $self->{'ffmpeg_vers'} = $1;
78    #        }
79#            elsif ($data =~ m/ffmpeg\sversion\sSVN-r(\d+),/si) {
80 #               $self->{'ffmpeg_vers'} = $1;
81  #          }
82   #         elsif ($data =~ m/ffmpeg\sversion\sSVN-r(\d+\.\d+)(?:\.\d+)?-.*?,/si) {
83        # Ubuntu 10.04
84        # FFmpeg version SVN-r0.5.1-4:0.5.1-1ubuntu1, Copyright <<SNIP>>
85#                $self->{'ffmpeg_vers'} = $1;
86 #           }
87            # Disabled unti I need the formatting again to detect wacky ffmpeg
88            # versions if they go back to releasing things the old way.
89            #elsif ($data =~ m/ffmpeg\sversion\s0.4.9-\d+_r(\d+)\.\w+\.at/si) {
90            #    $self->{'ffmpeg_vers'}  = 'svn';
91            #    $self->{'ffmpeg_build'} = $1;
92            #}
93            #elsif ($data =~ m/ffmpeg\sversion\s(.+?),(?:\sbuild\s(\d+))?/si) {
94            #    $self->{'ffmpeg_vers'}  = lc($1);
95            #    $self->{'ffmpeg_build'} = $2;
96            #    if ($self->{'ffmpeg_vers'} =~ /^svn-r(.+?)$/) {
97            #        $self->{'ffmpeg_vers'}  = 'svn';
98            #        $self->{'ffmpeg_build'} = $1;
99            #    }
100            #}
101#            else {
102 #               push @{$self->{'errors'}}, 'Unrecognizeable ffmpeg version string.';
103  #          }
104   #     }
105#        if ($self->{'ffmpeg_vers'} < 0.5) {
106 #           push @{$self->{'errors'}}, "This version of nuvexport requires ffmpeg 0.5.\n";
107  #      }
108    # Audio only?
109        $self->{'audioonly'} = $audioonly;
110    # Gather the supported codecs
111        $data         = `$ffmpeg -formats `;
112        my ($formats) = $data =~ /(?:^|\n\s*)File\sformats:\s*\n(.+?\n)\s*\n/s;
113        my ($codecs)  = $data =~ /(?:^|\n\s*)Codecs:\s*\n(.+?\n)\s*\n/s;
114        unless ($codecs) {
115            $data = `$ffmpeg -codecs `;
116            ($codecs)  = $data =~ /(?:^|\n\s*)Codecs:\s*\n(.+?\n)\s*\n/s;
117        }
118        if ($formats) {
119            while ($formats =~ /^\s(..)\s(\S+)\b/mg) {
120                $self->{'formats'}{$2} = $1;
121            }
122        }
123        if ($codecs) {
124            while ($codecs =~ /^\s(.{6})\s(\S+)\b/mg) {
125                $self->{'codecs'}{$2} = $1;
126            }
127        }
128    }
129
130# Returns true or false for the requested codec
131    sub can_decode {
132        my $self  = shift;
133        my $codec = shift;
134        return ($self->{'codecs'}{$codec} && $self->{'codecs'}{$codec} =~ /^D/
135                || $self->{'formats'}{$codec} && $self->{'formats'}{$codec} =~ /^D/) ? 1 : 0;
136    }
137    sub can_encode {
138        my $self  = shift;
139        my $codec = shift;
140        return ($self->{'codecs'}{$codec} && $self->{'codecs'}{$codec} =~ /^.E/
141                || $self->{'formats'}{$codec} && $self->{'formats'}{$codec} =~ /^.E/) ? 1 : 0;
142    }
143
144# ffmpeg keeps changing their parameter names... so we work around it.
145    sub param {
146        my $self  = shift;
147        my $param = lc(shift);
148        my $value = shift;
149    # No more version checks (until ffmpeg starts doing crazy releases again)
150        return param_pair('ab',             $value.'k') if ($param eq 'ab');
151        return param_pair('ac',             $value)     if ($param eq 'channels');
152        return param_pair('ar',             $value)     if ($param eq 'sample_rate');
153        return param_pair('b',              $value.'k') if ($param eq 'bit_rate');
154        return param_pair('b_qfactor',      $value)     if ($param eq 'b_quant_factor');
155        return param_pair('b_qoffset',      $value)     if ($param eq 'b_quant_offset');
156        return param_pair('bf',             $value)     if ($param eq 'max_b_frames');
157        return param_pair('bt',             $value.'k') if ($param eq 'bit_rate_tolerance');
158        return param_pair('bufsize',        $value.'k') if ($param eq 'rc_buffer_size');
159        return param_pair('bug',            $value)     if ($param eq 'bugs');
160        return param_pair('error',          $value)     if ($param eq 'error_rate');
161        return param_pair('g',              $value)     if ($param eq 'gop_size');
162        return param_pair('i_qfactor',      $value)     if ($param eq 'i_quant_factor');
163        return param_pair('i_qoffset',      $value)     if ($param eq 'i_quant_offset');
164        return param_pair('maxrate',        $value.'k') if ($param eq 'rc_max_rate');
165        return param_pair('mblmax',         $value)     if ($param eq 'mb_lmax');
166        return param_pair('mblmin',         $value)     if ($param eq 'mb_lmin');
167        return param_pair('mepc',           $value)     if ($param eq 'me_penalty_compensation');
168        return param_pair('minrate',        $value.'k') if ($param eq 'rc_min_rate');
169        return param_pair('qcomp',          $value)     if ($param eq 'qcompress');
170        return param_pair('qdiff',          $value)     if ($param eq 'max_qdiff');
171        return param_pair('qsquish',        $value)     if ($param eq 'rc_qsquish');
172        return param_pair('rc_init_cplx',   $value)     if ($param eq 'rc_initial_cplx');
173        return param_pair('skip_exp',       $value)     if ($param eq 'frame_skip_exp');
174        return param_pair('skip_factor',    $value)     if ($param eq 'frame_skip_factor');
175        return param_pair('skip_threshold', $value)     if ($param eq 'frame_skip_threshold');
176        return param_pair('threads',        $value)     if ($param eq 'thread_count');
177    # Unknown version or nothing wacky, just return the parameter
178        return param_pair($param, $value);
179    }
180
181# Load default settings
182    sub load_defaults {
183        my $self = shift;
184    # Load the parent module's settings
185        $self->SUPER::load_defaults();
186    # Not really anything to add
187    }
188
189# Gather settings from the user
190    sub gather_settings {
191        my $self = shift;
192        my $skip = shift;
193    # Gather generic settings
194        $self->SUPER::gather_settings();
195    }
196
197    sub export {
198        my $self    = shift;
199        my $episode = shift;
200        my $suffix  = (shift or '');
201        my $firstpass = (shift or 0);
202    # Init the commands
203        my $ffmpeg        = '';
204        my $mythtranscode = '';
205
206    # Generate a cutlist?
207        $self->gen_cutlist($episode);
208
209    # Set up the fifo dirs?
210        if (-e "/tmp/fifodir_$$/vidout" || -e "/tmp/fifodir_$$/audout") {
211            die "Possibly stale mythtranscode fifo's in /tmp/fifodir_$$/.\nPlease remove them before running nuvexport.\n\n";
212        }
213
214    # Here, we have to fork off a copy of mythtranscode (Do not use --fifosync with ffmpeg or it will hang)
215        my $mythtranscode_bin = find_program('mythtranscode');
216        $mythtranscode = "$NICE $mythtranscode_bin --showprogress"
217                        ." -p '$episode->{'transcoder'}'"
218                        ." -c '$episode->{'chanid'}'"
219                        ." -s '".unix_to_myth_time($episode->{'recstartts'})."'"
220                        ." -f \"/tmp/fifodir_$$/\"";
221        $mythtranscode .= ' --honorcutlist' if ($self->{'use_cutlist'});
222        $mythtranscode .= ' --fifosync'     if ($self->{'audioonly'} || $firstpass);
223
224        my $videofifo = "/tmp/fifodir_$$/vidout";
225        my $videotype = 'rawvideo -pix_fmt yuv420p';
226        my $crop_w;
227        my $crop_h;
228        my $pad_w;
229        my $pad_h;
230        my $height;
231        my $width;
232
233    # Standard encodes
234        if (!$self->{'audioonly'}) {
235        # Do noise reduction -- ffmpeg's -nr flag doesn't seem to do anything other than prevent denoise from working
236            if ($self->{'noise_reduction'}) {
237                $ffmpeg .= "$NICE ffmpeg -f rawvideo";
238                $ffmpeg .= ' -s ' . $episode->{'finfo'}{'width'} . 'x' . $episode->{'finfo'}{'height'};
239                $ffmpeg .= ' -r ' . $episode->{'finfo'}{'fps'};
240                $ffmpeg .= " -i /tmp/fifodir_$$/vidout -f yuv4mpegpipe -";
241                $ffmpeg .= ' 2> /dev/null | ';
242                $ffmpeg .= "$NICE yuvdenoise";
243                if ($self->{'denoise_vmaj'} < 1.6 || ($self->{'denoise_vmaj'} == 1.6 && $self->{'denoise_vmin'} < 3)) {
244                    $ffmpeg .= ' -r 16';
245                    if ($self->val('fast_denoise')) {
246                        $ffmpeg .= ' -f';
247                    }
248                # Deinterlace in yuvdenoise
249                    if ($self->val('deint_in_yuvdenoise') && $self->val('deinterlace')) {
250                        $ffmpeg .= " -F";
251                    }
252                }
253                $ffmpeg   .= ' 2> /dev/null | ';
254                $videofifo = '-';
255                $videotype = 'yuv4mpegpipe';
256            }
257        }
258
259    # Start the ffmpeg command
260        $ffmpeg .= "$NICE ffmpeg";
261        if ($num_cpus > 1) {
262            $ffmpeg .= ' -threads '.($num_cpus);
263        }
264        $ffmpeg .= ' -y -f '.($Config{'byteorder'} == 4321 ? 's16be' : 's16le')
265                  .' -ar ' . $episode->{'finfo'}{'audio_sample_rate'}
266                  .' -ac ' . $episode->{'finfo'}{'audio_channels'};
267        if (!$firstpass) {
268            $ffmpeg .= " -i /tmp/fifodir_$$/audout";
269        }
270        if (!$self->{'audioonly'}) {
271            $ffmpeg .= " -f $videotype"
272                      .' -s ' . $episode->{'finfo'}{'width'} . 'x' . $episode->{'finfo'}{'height'}
273                      .' -vf aspect=' . $episode->{'finfo'}{'aspect_f'}
274                      .' -r '      . $episode->{'finfo'}{'fps'}
275                      ." -i $videofifo";
276
277            $self->{'out_aspect'} ||= $episode->{'finfo'}{'aspect_f'};
278
279        # The output is actually a stretched/anamorphic aspect ratio
280        # (like 480x480 for SVCD, which is 4:3)
281            if ($self->{'aspect_stretched'}) {
282                $width  = $self->{'width'};
283                $height = $self->{'height'};
284                $pad_h  = 0;
285                $pad_w  = 0;
286            }
287        # The output will need letter/pillarboxing
288            else {
289            # We need to letterbox
290                if ($self->{'width'} / $self->{'height'} <= $self->{'out_aspect'}) {
291                    $width  = $self->{'width'};
292                    $height = $width / $episode->{'finfo'}{'aspect_f'};
293                    $height = int(($height + 2) / 4) * 4;
294                    $pad_h  = int(($self->{'height'} - $height) / 2);
295                    $pad_w  = 0;
296                }
297            # We need to pillarbox
298                else {
299                    $height = $self->{'height'};
300                    $width  = $height * $episode->{'finfo'}{'aspect_f'};
301                    $width  = int(($width + 2) / 4) * 4;
302                    $pad_w  = int(($self->{'width'} - $width) / 2);
303                    $pad_h  = 0;
304                }
305            }
306
307            $ffmpeg .= ' -vf aspect=' . $self->{'out_aspect'}
308                      .' -r '      . $self->{'out_fps'};
309
310        # Deinterlace in ffmpeg only if the user wants to
311            if ($self->val('deinterlace') && !($self->val('noise_reduction') && $self->val('deint_in_yuvdenoise'))) {
312                $ffmpeg .= ' -deinterlace';
313            }
314        # Crop
315            if ($self->{'crop'}) {
316                my $t = sprintf('%.0f', ($self->val('crop_top')    / 100) * $episode->{'finfo'}{'height'});
317                my $r = sprintf('%.0f', ($self->val('crop_right')  / 100) * $episode->{'finfo'}{'width'});
318                my $b = sprintf('%.0f', ($self->val('crop_bottom') / 100) * $episode->{'finfo'}{'height'});
319                my $l = sprintf('%.0f', ($self->val('crop_left')   / 100) * $episode->{'finfo'}{'width'});
320            # Keep the crop numbers even
321                $t-- if ($t > 0 && $t % 2);
322                $r-- if ($r > 0 && $r % 2);
323                $b-- if ($b > 0 && $b % 2);
324                $l-- if ($l > 0 && $l % 2);
325            # crop
326                $ffmpeg .= " -croptop    $t -cropright $r"
327                          ." -cropbottom $b -cropleft  $l" if ($t || $r || $b || $l);
328            }
329
330        # Letter/Pillarboxing as appropriate
331            if ($pad_h) {
332                $ffmpeg .= " -padtop $pad_h -padbottom $pad_h";
333            }
334            if ($pad_w) {
335#                $ffmpeg .= " -padleft $pad_w -padright $pad_w";
336                $ffmpeg .= " -vf pad ${width}:$height:$pad_w:$pad_w";
337            }
338            $ffmpeg .= " -s ${width}x$height";
339        }
340
341    # Add any additional settings from the child module
342        $ffmpeg .= ' '.$self->{'ffmpeg_xtra'};
343
344    # Output directory set to null means the first pass of a multipass
345        if ($firstpass || !$self->{'path'} || $self->{'path'} =~ /^\/dev\/null\b/) {
346            $ffmpeg .= ' /dev/null';
347        }
348    # Add the output filename
349        else {
350            $ffmpeg .= ' '.shell_escape($self->get_outfile($episode, $suffix));
351        }
352    # ffmpeg pids
353        my ($mythtrans_pid, $ffmpeg_pid, $mythtrans_h, $ffmpeg_h);
354
355    # Create a directory for mythtranscode's fifo's
356        mkdir("/tmp/fifodir_$$/", 0755) or die "Can't create /tmp/fifodir_$$/:  $!\n\n";
357        ($mythtrans_pid, $mythtrans_h) = fork_command("$mythtranscode ");
358        $children{$mythtrans_pid} = 'mythtranscode' if ($mythtrans_pid);
359        fifos_wait("/tmp/fifodir_$$/", $mythtrans_pid, $mythtrans_h);
360        push @tmpfiles, "/tmp/fifodir_$$", "/tmp/fifodir_$$/audout", "/tmp/fifodir_$$/vidout";
361
362    # For multipass encodes, we don't need the audio on the first pass
363        if ($self->{'audioonly'}) {
364            my ($cat_pid, $cat_h) = fork_command("cat /tmp/fifodir_$$/vidout > /dev/null");
365            $children{$cat_pid} = 'video dump' if ($cat_pid);
366        }
367        elsif ($firstpass) {
368            my ($cat_pid, $cat_h) = fork_command("cat /tmp/fifodir_$$/audout > /dev/null");
369            $children{$cat_pid} = 'audio dump' if ($cat_pid);
370        }
371    # Execute ffmpeg
372        print "Starting ffmpeg.\n" unless ($DEBUG);
373        ($ffmpeg_pid, $ffmpeg_h) = fork_command("$ffmpeg ");
374        $children{$ffmpeg_pid} = 'ffmpeg' if ($ffmpeg_pid);
375
376    # Get ready to count the frames that have been processed
377        my ($frames, $fps, $start);
378        $frames = 0;
379        $fps = 0.0;
380        $start = time();
381        if (!$episode->{'total_frames'} || $episode->{'total_frames'} < 1) {
382            $episode->{'total_frames'} = ($episode->{'last_frame'} && $episode->{'last_frame'} > 0)
383                                         ? $episode->{'last_frame'} - $episode->{'cutlist_frames'}
384                                         : 0;
385        }
386    # Keep track of any warnings
387        my $warnings    = '';
388        my $death_timer = 0;
389        my $last_death  = '';
390        # Wait for child processes to finish
391        while ((keys %children) > 0) {
392            my ($l, $pct);
393        # Show progress
394            if ($frames && $episode->{'total_frames'}) {
395                $pct = sprintf('%.2f', 100 * $frames / $episode->{'total_frames'});
396                $fps = sprintf('%.2f', ($frames * 1.0) / (time() - $start));
397            }
398            else {
399                $pct = '~';
400            }
401            unless (arg('noprogress')) {
402                print "\rprocessed:  $frames of $episode->{'total_frames'} frames at $fps fps ($pct\%, eta: ",
403                      $self->build_eta($frames, $episode->{'total_frames'}, $fps),
404                      ')  ';
405            }
406
407        # Read from the ffmpeg handle
408            while (has_data($ffmpeg_h) and $l = <$ffmpeg_h>) {
409                if ($l =~ /frame=\s*(\d+)/) {
410                    $frames = int($1);
411                    if ($episode->{'total_frames'} < $frames) {
412                        $episode->{'total_frames'} = $frames;
413                    }
414                }
415            # ffmpeg warnings?
416                elsif ($l =~ /^Un(known|supported).+?(codec|format|(?:de|en)coder)/m) {
417                    $warnings .= $l;
418                    die "\n\nffmpeg had critical errors:\n\n$warnings";
419                }
420            # Segfault?
421                elsif ($l =~ /^Segmentation\sfault/m) {
422                    $warnings .= $l;
423                    die "\n\nffmpeg had critical errors:\n\n$warnings";
424                }
425            # Most likely a misconfigured command line
426                elsif ($l =~ /^Unable\s(?:to|for)\s(?:find|parse)/m) {
427                    $warnings .= $l;
428                    die "\n\nffmpeg had critical errors:\n\n$warnings";
429                }
430            # Another error?
431                elsif ($l =~ /\b(?:Error\swhile|Invalid\svalue)\b/m) {
432                    $warnings .= $l;
433                    die "\n\nffmpeg had critical errors:\n\n$warnings";
434                }
435            # Unrecognized options
436                elsif ($l =~ /^ffmpeg:\sunrecognized\soption/m) {
437                    $warnings .= $l;
438                    die "\n\nffmpeg had critical errors:\n\n$warnings";
439                }
440            }
441        # Read from the mythtranscode handle?
442            while (has_data($mythtrans_h) and $l = <$mythtrans_h>) {
443                if ($l =~ /Processed:\s*(\d+)\s*of\s*(\d+)\s*frames\s*\((\d+)\s*seconds\)/) {
444                # ffmpeg doesn't report fame counts for audio streams, so grab it here.
445                    if ($self->{'audioonly'}) {
446                        $frames = int($1);
447                    }
448                # mythtranscode is going to be the most accurate total frame count.
449                    my $total = $2 - $episode->{'cutlist_frames'};
450                    if ($episode->{'total_frames'} < $total) {
451                        $episode->{'total_frames'} = $total;
452                    }
453                }
454            # Unrecognized options
455                elsif ($l =~ /^Couldn't\sfind\srecording/m) {
456                    $warnings .= $l;
457                    die "\n\nmythtranscode had critical errors:\n\n$warnings";
458                }
459            }
460        # Has the deathtimer been started?  Stick around for awhile, but not too long
461            if ($death_timer > 0 && time() - $death_timer > 30) {
462                $str = "\n\n$last_death died early.";
463                if ($warnings) {
464                    $str .= "See ffmpeg warnings:\n\n$warnings";
465                }
466                else {
467                    $str .= "Please use the --debug option to figure out what went wrong.\n"
468                           ."http://www.mythtv.org/wiki/index.php/Nuvexport#Debug_Mode\n\n";
469                }
470                die $str;
471            }
472        # The pid?
473            $pid = waitpid(-1, &WNOHANG);
474            if ($children{$pid}) {
475                print "\n$children{$pid} finished.\n" unless ($DEBUG);
476                $last_death  = $children{$pid};
477                $death_timer = time();
478                delete $children{$pid};
479            }
480        # Sleep for 1/10 second so we don't go too fast and annoy the cpu
481            usleep(100000);
482        }
483    # Remove the fifodir?
484        unlink "/tmp/fifodir_$$/audout", "/tmp/fifodir_$$/vidout";
485        rmdir "/tmp/fifodir_$$";
486    }
487
488
489# Return true
4901;
491
492# vim:ts=4:sw=4:ai:et:si:sts=4