diff options
Diffstat (limited to 'abs/core-testing/nuvexport/MP4.pm')
-rwxr-xr-x | abs/core-testing/nuvexport/MP4.pm | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/abs/core-testing/nuvexport/MP4.pm b/abs/core-testing/nuvexport/MP4.pm new file mode 100755 index 0000000..e13d44b --- /dev/null +++ b/abs/core-testing/nuvexport/MP4.pm @@ -0,0 +1,316 @@ +#!/usr/bin/perl -w +# +# ffmpeg-based MP4 (iPod) video module for nuvexport. +# +# Many thanks to cartman in #ffmpeg, and for the instructions at +# http://rob.opendot.cl/index.php?active=3&subactive=1 +# http://videotranscoding.wikispaces.com/EncodeForIPodorPSP +# +# @url $URL: svn+ssh://xris@svn.mythtv.org/var/lib/svn/trunk/mythextras/nuvexport/export/ffmpeg/MP4.pm $ +# @date $Date: 2008-02-16 20:54:43 -0800 (Sat, 16 Feb 2008) $ +# @version $Revision: 16110 $ +# @author $Author: xris $ +# @copyright Silicon Mechanics +# + +package export::ffmpeg::MP4; + use base 'export::ffmpeg'; + +# Load the myth and nuv utilities, and make sure we're connected to the database + use nuv_export::shared_utils; + use nuv_export::cli; + use nuv_export::ui; + use mythtv::recordings; + +# Load the following extra parameters from the commandline + add_arg('quantisation|q=i', 'Quantisation'); + add_arg('a_bitrate|a=i', 'Audio bitrate'); + add_arg('v_bitrate|v=i', 'Video bitrate'); + add_arg('multipass!', 'Enable two-pass encoding.'); + add_arg('mp4_codec=s', 'Video codec to use for MP4/iPod video (mpeg4 or h264).'); + add_arg('mp4_fps=s', 'Framerate to use: auto, 25, 23.97, 29.97.'); + add_arg('ipod!', 'Produce ipod-compatible output.'); + + sub new { + my $class = shift; + my $self = { + 'cli' => qr/\b(?:mp4|ipod)\b/i, + 'name' => 'Export to MP4 (iPod)', + 'enabled' => 1, + 'errors' => [], + 'defaults' => {}, + }; + bless($self, $class); + + # Initialize the default parameters + $self->load_defaults(); + + # Verify any commandline or config file options + die "Audio bitrate must be > 0\n" unless (!defined $self->val('a_bitrate') || $self->{'a_bitrate'} > 0); + die "Video bitrate must be > 0\n" unless (!defined $self->val('v_bitrate') || $self->{'v_bitrate'} > 0); + die "Width must be > 0\n" unless (!defined $self->val('width') || $self->{'width'} =~ /^\s*\D/ || $self->{'width'} > 0); + die "Height must be > 0\n" unless (!defined $self->val('height') || $self->{'height'} =~ /^\s*\D/ || $self->{'height'} > 0); + + # VBR, multipass, etc. + if ($self->val('multipass')) { + $self->{'vbr'} = 1; + } + elsif ($self->val('quantisation')) { + die "Quantisation must be a number between 1 and 31 (lower means better quality).\n" if ($self->{'quantisation'} < 1 || $self->{'quantisation'} > 31); + $self->{'vbr'} = 1; + } + + # Initialize and check for ffmpeg + $self->init_ffmpeg(); + + # Can we even encode mp4? + if (!$self->can_encode('mp4')) { + push @{$self->{'errors'}}, "Your ffmpeg installation doesn't support encoding to mp4 file formats."; + } + if (!$self->can_encode('aac') && !$self->can_encode('libfaac')) { + push @{$self->{'errors'}}, "Your ffmpeg installation doesn't support encoding to aac audio."; + } + if (!$self->can_encode('mpeg4') && !$self->can_encode('h264') && !$self->can_encode('libx264')) { + push @{$self->{'errors'}}, "Your ffmpeg installation doesn't support encoding to either mpeg4 or h264 video."; + } + # Any errors? disable this function + $self->{'enabled'} = 0 if ($self->{'errors'} && @{$self->{'errors'}} > 0); + # Return + return $self; + } + +# Load default settings + sub load_defaults { + my $self = shift; + # Load the parent module's settings + $self->SUPER::load_defaults(); + # Default settings + $self->{'defaults'}{'v_bitrate'} = 384; + $self->{'defaults'}{'a_bitrate'} = 64; + $self->{'defaults'}{'width'} = 320; + $self->{'defaults'}{'mp4_codec'} = 'mpeg4'; + # Verify commandline options + if ($self->val('mp4_codec') !~ /^(?:mpeg4|h264)$/i) { + die "mp4_codec must be either mpeg4 or h264.\n"; + } + $self->{'mp4_codec'} =~ tr/A-Z/a-z/; + + } + +# Gather settings from the user + sub gather_settings { + my $self = shift; + # Load the parent module's settings + $self->SUPER::gather_settings(); + # Audio Bitrate + $self->{'a_bitrate'} = query_text('Audio bitrate?', + 'int', + $self->val('a_bitrate')); + # Video options + if (!$is_cli) { + # iPod compatibility mode? + $self->{'ipod'} = query_text('Enable iPod compatibility?', + 'yesno', + $self->val('ipod')); + # Video codec + if ($self->{'ffmpeg_vers'} eq 'svn') { + while (1) { + my $codec = query_text('Video codec (mpeg4 or h264)?', + 'string', + $self->{'mp4_codec'}); + if ($codec =~ /^m/) { + $self->{'mp4_codec'} = 'mpeg4'; + last; + } + elsif ($codec =~ /^h/) { + $self->{'mp4_codec'} = 'h264'; + last; + } + print "Please choose either mpeg4 or h264\n"; + } + } + else { + $self->{'mp4_codec'} = 'mpeg4'; + print "Using the mpeg4 codec (h.264 mp4/ipod encoding requires the svn version of ffmpeg.)\n"; + } + # Video bitrate options + $self->{'vbr'} = query_text('Variable bitrate video?', + 'yesno', + $self->val('vbr')); + if ($self->{'vbr'}) { + $self->{'multipass'} = query_text('Multi-pass (slower, but better quality)?', + 'yesno', + $self->val('multipass')); + if (!$self->{'multipass'}) { + while (1) { + my $quantisation = query_text('VBR quality/quantisation (1-31)?', + 'float', + $self->val('quantisation')); + if ($quantisation < 1) { + print "Too low; please choose a number between 1 and 31.\n"; + } + elsif ($quantisation > 31) { + print "Too high; please choose a number between 1 and 31\n"; + } + else { + $self->{'quantisation'} = $quantisation; + last; + } + } + } + } else { + $self->{'multipass'} = 0; + } + # Ask the user what video bitrate he/she wants + $self->{'v_bitrate'} = query_text('Video bitrate?', + 'int', + $self->val('v_bitrate')); + } + # Complain about h264 + if ($self->{'mp4_codec'} eq 'h264' && $self->{'ffmpeg_vers'} ne 'svn') { + die "h.264 mp4/ipod encoding requires the svn version of ffmpeg.\n"; + } + # Loop, in case we need to verify ipod compatibility + while (1) { + # Query the resolution + $self->query_resolution(); + # Warn about ipod resolution + if ($self->val('ipod') && ($self->{'height'} > 480 || $self->{'width'} > 640)) { + my $note = "WARNING: Video larger than 640x480 will not play on an iPod.\n"; + die $note if ($is_cli); + print $note; + next; + } + # Done looping + last; + } + } + + sub export { + my $self = shift; + my $episode = shift; + # Make sure this is set to anamorphic mode + $self->{'aspect_stretched'} = 1; + # Framerate + my $standard = ($episode->{'finfo'}{'fps'} =~ /^2(?:5|4\.9)/) ? 'PAL' : 'NTSC'; + if ($standard eq 'PAL') { + $self->{'out_fps'} = 25; + } + elsif ($self->val('mp4_fps') =~ /^23/) { + $self->{'out_fps'} = 23.97; + } + elsif ($self->val('mp4_fps') =~ /^29/) { + $self->{'out_fps'} = 29.97; + } + else { + $self->{'out_fps'} = ($self->{'width'} > 320 || $self->{'height'} > 288) ? 29.97 : 23.97; + } + # Embed the title + $safe_title = $episode->{'title'}; + if ($episode->{'subtitle'} ne 'Untitled') { + $safe_title .= ' - '.$episode->{'subtitle'}; + } + my $safe_title = shell_escape($safe_title); + # Codec name changes between ffmpeg versions + my $codec = $self->{'mp4_codec'}; + if ($codec eq 'h264' && $self->can_encode('libx264')) { + $codec = 'libx264'; + } + # Build the common ffmpeg string + my $ffmpeg_xtra = ' -vcodec '.$codec + .$self->param('bit_rate', $self->{'v_bitrate'}) + ;### ." -title $safe_title"; + # Options required for the codecs separately + if ($self->{'mp4_codec'} eq 'h264') { + $ffmpeg_xtra .= ' -level 30' + ### .' -loop 1' + .' -g 250 -keyint_min 25' + .' -sc_threshold 40' + ### .' -rc_eq \'blurCplx^(1-qComp)\'' + .$self->param('bit_rate_tolerance', $self->{'v_bitrate'}) + .$self->param('rc_max_rate', 1500 - $self->{'a_bitrate'}) + .$self->param('rc_buffer_size', 2000) + .$self->param('i_quant_factor', 0.71428572) + .$self->param('b_quant_factor', 0.76923078) + .$self->param('max_b_frames', 0) + ### .' -me umh' # this will eventually be me_method, but not all ffmpeg versions support it yet + ; + } + else { + $ffmpeg_xtra .= ' -flags +mv4' ## +trell+loop' + ### .' -aic 1' + .' -mbd 1' + .' -cmp 2 -subcmp 2' + ; + } + # Some shared options + if ($self->{'multipass'} || $self->{'vbr'}) { + $ffmpeg_xtra .= $self->param('qcompress', 0.6) + .$self->param('qmax', 51) + .$self->param('max_qdiff', 4) + ; + } + # Dual pass? + if ($self->{'multipass'}) { + # Apparently, the -passlogfile option doesn't work for h264, so we need + # to be aware of other processes that might be working in this directory + if ($self->{'mp4_codec'} eq 'h264' && (-e 'x264_2pass.log.temp' || -e 'x264_2pass.log')) { + die "ffmpeg does not allow us to specify the name of the multi-pass log\n" + ."file, and x264_2pass.log exists in this directory already. Please\n" + ."wait for the other process to finish, or remove the stale file.\n"; + } + # Add all possible temporary files to the list + push @tmpfiles, 'x264_2pass.log', + 'x264_2pass.log.temp', + 'ffmpeg2pass-0.log'; + # Build the ffmpeg string + print "First pass...\n"; + $self->{'ffmpeg_xtra'} = ' -pass 1' + .$ffmpeg_xtra + .' -f mp4'; + if ($self->{'mp4_codec'} eq 'h264') { + $self->{'ffmpeg_xtra'} .= ' -refs 1 -subq 1' + .' -trellis 0' + ; + } + $self->SUPER::export($episode, '', 1); + # Second Pass + print "Final pass...\n"; + $ffmpeg_xtra = ' -pass 2 ' + .$ffmpeg_xtra; + } + # Single Pass + else { + if ($self->{'vbr'}) { + $ffmpeg_xtra .= ' -qmin '.$self->{'quantisation'}; + } + } + # Single/final pass options + if ($self->{'mp4_codec'} eq 'h264') { + $ffmpeg_xtra .= ' -refs '.($self->val('ipod') ? 2 : 7) + .' -subq 7' + .' -partitions parti4x4+parti8x8+partp4x4+partp8x8+partb8x8' + .' -flags2 +bpyramid+wpred+mixed_refs+8x8dct' ##+brdo' + .' -me_range 21' + .' -trellis 2' + .' -chromaoffset 1' + ### .' -slice 2' + ### .' -cmp 1' + # These should match the defaults: + .' -deblockalpha 0 -deblockbeta 0' + ; + } + # Audio codec name changes between ffmpeg versions + my $acodec = $self->can_encode('libfaac') ? 'libfaac' : 'aac'; + # Don't forget the audio, etc. + $self->{'ffmpeg_xtra'} = $ffmpeg_xtra + ." -acodec $acodec -ar 48000 -async 1" + .$self->param('ab', $self->{'a_bitrate'}); + # Execute the (final pass) encode + $self->SUPER::export($episode, '.mp4'); + } + +1; #return true + +# vim:ts=4:sw=4:ai:et:si:sts=4 + |