codepad
[
create a new paste
]
login
|
about
Language:
C
C++
D
Haskell
Lua
OCaml
PHP
Perl
Plain Text
Python
Ruby
Scheme
Tcl
#!/usr/bin/perl # This script uses a MythTV database to transcode recorded TV programs to # MPEG-4 video using the H.264 and AAC codecs, cutting the video at certain # commercial points selected by the user within MythTV's frontend or by an # automatic commercial flagger, complete with SRT subtitles extracted from # MythTV's embedded VBI closed-caption data and iTunes-style metadata about # the program, if it can be found. # In short, this script calls a bunch of programs to convert a file like # 1041_20100523000000.mpg into a file like # /usr/video/Program Name/Program Name - Episode Name.mp4, including captions, # file tags, and with commercials clipped. # Requirements: # - FFmpeg # - x264 (ffmpeg will be called with -vcodec libx264) # - neroAacEnc (http://www.nero.com/enu/technologies-aac-codec.html) # - MP4Box (http://gpac.sourceforge.net) # - ccextractor (http://ccextractor.sourceforge.net) # - AtomicParsley (http://atomicparsley.sourceforge.net - the tarball labelled # 'debian' contains a generic Linux binary) # Usage: <script> %CHANID% %STARTTIME% # Script adapted from a script by Christopher Neufeld, released GPLv2. # May not work correctly on recordings that span the autumn time change. use strict; use warnings; use POSIX "mkfifo"; use Cwd; use DBI; use File::Copy; # Make sure the executables mentioned above can be found in # one of these directories. $ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin'; # Enter MythTV database information below. Below are the default values. my $mysql_host = "localhost"; my $mysql_db = "mythconverg"; my $mysql_user = "mythtv"; my $mysql_passwd = "mythtv"; # This directory will be used to store intermediate files while encoding, and # should be large enough to hold at least the H.264-encoded version of a # reasonably sized recording without any commercial cuts. # Around 4-8GB free should work well, but 2GB might be okay. my $tmp_dir = "/tmp"; # This directory will store the generated MP4 files. Subdirectories will be # created for each series. my $final_dir = "/usr/video"; # When cutting commercials at the beginning or end of a video, sometimes # MythTV's automatic commercial flagger will mark the cut-point a few seconds # ahead or behind the actual start or end point of the video. Therefore, any # clips at the beginning or end of the video that are shorter than this # threshold (in seconds) will be ignored. my $thresh = 5; # Average bitrate of the H.264 video, in kb/s. my $vid_br = 1000; # Average quality of the AAC audio, from 0 to 1. my $audio_q = 0.3; # Whether to make the resulting files iPod Touch 3G-compatible. my $iPod = 1; # ISO 639-2 three-letter language code to use for audio and subtitle tracks. my $lang_code = "eng"; sub short_time_to_long { my $st = shift; my $t = ""; $t .= substr($st, 0, 4) . "-" . substr($st, 4, 2) . "-" . substr($st, 6, 2); $t .= " "; $t .= substr($st, 8, 2) . ":" . substr($st, 10, 2) . ":" . substr($st, 12, 2); return $t; } sub long_time_to_short { my $lt = shift; $lt =~ tr/ :-//d; return $lt; } sub seconds_to_time { my $seconds = shift; my $hours = int($seconds / 3600); $seconds -= $hours * 3600; my $minutes = int($seconds / 60); $seconds -= $minutes * 60; return sprintf "%.2d:%.2d:%.2d", $hours, $minutes, $seconds; } sub seconds_to_time_frac { my $seconds = shift; my $hours = int($seconds / 3600); $seconds -= $hours * 3600; my $minutes = int($seconds / 60); $seconds -= $minutes * 60; my $split = $seconds - int($seconds); my $s = ""; if($split != 0) { $s = sprintf "%.4f", $split; $s =~ s/^[^.]*\./\./; } return sprintf "%.2d:%.2d:%.2d$s", $hours, $minutes, $seconds; } sub add_trail_slash { my $str = shift; if($$str !~ m/\/$/) { $$str .= "/"; } } sub get_path { my ($channel, $long_time) = @_; my $path; my $db = DBI->connect("DBI:mysql:database=$mysql_db;host=$mysql_host", $mysql_user, $mysql_passwd); die "Could not connect to database." unless defined $db; my $query = $db->prepare("select endtime, basename, storagegroup from " . "recorded where chanid = ? and starttime = ?;"); $query->execute($channel, $long_time); my ($endtime, $basename, $storagegroup) = $query->fetchrow_array(); die "Unable to locate recording basename for channel $channel " . "and time $long_time." unless defined $basename; $query = $db->prepare("select dirname from storagegroup " . "where groupname = ?;"); $query->execute($storagegroup); while(my @dir = $query->fetchrow_array()) { if(-e $dir[0] . $basename) { $path = $dir[0] . $basename; } } die "Unable to find recording." unless defined $path; return $path; } sub get_video_param { my $video = shift; my ($fps, $duration, $res); open my $pipe, "ffmpeg -i $video 2>&1 |" or die "Unable to determine FPS / duration."; foreach my $line (<$pipe>) { if($line =~ m/([0-9]*\.?[0-9]*) fps/) { $fps = $1; } elsif($line =~ m/Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),/) { $duration = $4 / 100 + $3 + $2 * 60 + $1 * 3600; } if($line =~ m/Video:.* ([0-9]+)x([0-9]+) \[/) { $res = [$1, $2]; } } close $pipe; die "Unable to determine FPS of video." unless defined $fps; die "Unable to determine duration of video." unless defined $duration; die "Unable to determine resolution of video." unless defined $res; return ($fps, $duration, $res); } sub get_cutlist { my ($channel, $time) = @_; my @cutlist; print "*** Obtaining cut list ***\n"; open my $pipe, "mythcommflag --getcutlist -c $channel -s $time 2>/dev/null |" or die "Unable to obtain cut list."; foreach my $line (<$pipe>) { if($line =~ m/^Cutlist: /) { my @afterCut = split / /, $line; if(@afterCut > 1 and $afterCut[1] ne "\n") { @cutlist = split /,/, $afterCut[1]; } } } close $pipe; return @cutlist; } sub get_cut { my $cut = shift; my @spl = split /-/, $cut; return ($spl[0], $spl[1]); } sub extract_captions { my ($start, $end, $delay, $subs, $fps, $files) = @_; my $start_s = seconds_to_time($start / $fps); my $end_s = seconds_to_time($end / $fps); my $delay_s = int($delay / $fps * 1000); my $blank = 1; print "*** Extracting captions to $files->{'srt'} ***\n"; system "ccextractor -o $files->{'fifo'} -startat $start_s -endat $end_s " . "-delay -$delay_s -utf8 --no_progress_bar $files->{'video'} " . "> /dev/null 2>&1 &"; open my $pipe, '<', $files->{'fifo'}; open my $file, '>>', $files->{'srt'}; foreach my $line (<$pipe>) { if($blank == 1) { print $file ++$$subs . "\n"; $blank = 0; } else { if($line =~ m/^\s*$/) { $blank = 1; } print $file $line; } } close $pipe; close $file; } sub adjust_res { my $res = shift; my $fmt = "mp4"; my $prof = "high"; my $size = ""; if($iPod) { my $aspect = $res->[0] * 1.0 / $res->[1]; if($aspect > (640.0 / 480.0)) { my $hres = int(640.0 / $aspect + 0.5); if($hres % 2 == 1) { $hres += 1; } my $pad = (480 - $hres) / 2; $size = "-s 640x" . $hres . " -padtop $pad -padbottom $pad"; } else { my $vres = int(480.0 * $aspect + 0.5); if($vres % 2 == 1) { $vres += 1; } my $pad = (640 - $vres) / 2; $size = "-s " . $vres . "x480 -padleft $pad -padright $pad"; } $fmt = "ipod"; $prof = "ipod640"; } return ($fmt, $prof, $size); } sub encode_video { my ($start, $end, $fps, $res, $suffix, $files) = @_; my $start_s = $start / $fps; my $dur_s = $end / $fps - $start_s; my $file = $files->{'h264'} . $suffix; my $cwd = getcwd(); unlink $file if -e $file; chdir $files->{'dir'}; my ($fmt, $prof, $size) = adjust_res($res); print "*** Encoding video to $files->{'dir'} - first pass ***\n"; system "ffmpeg -y -i $files->{'video'} -ss $start_s -t $dur_s -pass 1 " . "-an -vcodec libx264 -vb " . $vid_br . "k -bt " . $vid_br . "k " . "-vpre slow_firstpass -vpre $prof $size -threads 0 -f h264 " . "/dev/null > /dev/null 2>&1"; print "*** Encoding video to $file - second pass ***\n"; system "ffmpeg -y -i $files->{'video'} -ss $start_s -t $dur_s -pass 2 " . "-an -vcodec libx264 -vb " . $vid_br . "k -bt " . $vid_br . "k " . "-vpre slow -vpre $prof $size -threads 0 -f $fmt $file " . "> /dev/null 2>&1"; die "Could not encode video." unless -e $file and ! -z $file; chdir $cwd; } sub encode_audio { my ($start, $end, $fps, $suffix, $files) = @_; my $start_s = $start / $fps; my $dur_s = $end / $fps - $start_s; my $file = $files->{'aac'} . $suffix; unlink $file if -e $file; print "*** Encoding audio to $file ***\n"; system "ffmpeg -y -i $files->{'video'} -ss $start_s -t $dur_s -vn " . "-acodec pcm_s16le -f wav - 2>/dev/null | " . "neroAacEnc -ignorelength -q $audio_q -if - -of $file " . "> /dev/null 2>&1"; die "Could not encode audio." unless -e $file and ! -z $file; } sub segment { my ($start, $end, $delay, $seg, $subs, $fps, $res, $files) = @_; my $suffix = "-" . $$seg++; my $start_s = seconds_to_time($start / $fps); my $end_s = seconds_to_time($end / $fps); print "*** Segment $$seg: [$start_s - $end_s] ***\n"; extract_captions($start, $end, $delay, $subs, $fps, $files); encode_audio($start, $end, $fps, $suffix, $files); encode_video($start, $end, $fps, $res, $suffix, $files); } sub clean_tmp { my ($files, $fifo) = @_; my @log2pass = ("ffmpeg2pass-0.log", "x264_2pass.log", "x264_2pass.log.mbtree"); print "*** Removing temporary files ***\n"; unlink $files->{'srt'} if -e $files->{'srt'}; if($fifo) { mkfifo $files->{'fifo'}, 0700 unless -p $files->{'fifo'}; } else { unlink $files->{'fifo'} if -e $files->{'fifo'}; } for my $file (@log2pass) { $file = $files->{'dir'} . $file; unlink $file if -e $file; } } sub cut_and_transcode { my ($duration, $fps, $r, $cutlist, $files) = @_; my $pos = 0; my $delay = 0; my $seg = 0; my $subs = 0; clean_tmp($files, 1); for my $cut (@$cutlist) { my ($start, $end) = get_cut($cut); if($start > $thresh * $fps && $start > $pos) { segment($pos, $start, $delay, \$seg, \$subs, $fps, $r, $files); } $delay += $end - $start; $pos = $end; } if($pos < ($duration - $thresh) * $fps) { segment($pos, $duration * $fps, $delay, \$seg, \$subs, $fps, $r, $files); } unlink $files->{'fifo'} if -p $files->{'fifo'}; return $seg; } sub remux { my ($seg, $files) = @_; print "*** Remuxing to $files->{'mp4'} ***\n"; unlink $files->{'mp4'} if -e $files->{'mp4'}; unlink $files->{'mp4tmp'} if -e $files->{'mp4tmp'}; my $tmp = $files->{'mp4'}; $tmp =~ s/\/[^\/]*$//; for(my $s = 0; $s < $seg; $s++) { my $h264 = $files->{'h264'} . "-" . $s; my $aac = $files->{'aac'} . "-" . $s; if($s == 0) { system "MP4Box -tmp $tmp -add $h264 -add $aac $files->{'mp4'}" . " > /dev/null 2>&1"; die "Could not remux video." unless -e $files->{'mp4'} and ! -z $files->{'mp4'}; } else { system "MP4Box -tmp $tmp -add $h264 -add $aac $files->{'mp4tmp'}" . " > /dev/null 2>&1"; die "Could not remux video." unless -e $files->{'mp4tmp'} and ! -z $files->{'mp4tmp'}; system "MP4Box -tmp $tmp -cat $files->{'mp4tmp'} $files->{'mp4'}" . " > /dev/null 2>&1"; unlink $files->{'mp4tmp'}; } unlink $h264; unlink $aac; } system "MP4Box -tmp $tmp -add $files->{'srt'} $files->{'mp4'}" . " > /dev/null 2>&1"; system "MP4Box -tmp $tmp -lang $lang_code $files->{'mp4'}" . " > /dev/null 2>&1"; unlink $files->{'srt'}; } sub get_metadata { my ($channel, $long_time) = @_; my ($path, @credits); my $count = 0; my $db = DBI->connect("DBI:mysql:database=$mysql_db;host=$mysql_host", $mysql_user, $mysql_passwd); die "Could not connect to database." unless defined $db; my $query = $db->prepare("select title, subtitle, description, category," . "originalairdate, syndicatedepisodenumber from " . "recordedprogram where chanid = ? and " . "starttime = ?;"); $query->execute($channel, $long_time); my $hash = $query->fetchrow_hashref(); die "Unable to obtain metadata for channel $channel and time $long_time." unless defined $hash; $query = $db->prepare("select rating from recordedrating " . "where chanid = ? and starttime = ?;"); $query->execute($channel, $long_time); $hash->{'rating'} = $query->fetchrow_array(); $query = $db->prepare("select name from channel where chanid = ?;"); $query->execute($channel); $hash->{'channel'} = $query->fetchrow_array(); $query = $db->prepare("select name, role from recordedcredits, people " . "where chanid = ? and starttime = ? and " . "people.person = recordedcredits.person order by " . "role, substring_index(name, ' ', -1) asc;"); $query->execute($channel, $long_time); while(my @person = $query->fetchrow_array()) { my $role = lc $person[1]; $role =~ s/_/ /g; push @credits, [$person[0], $role]; } $hash->{'credits'} = \@credits; return $hash; } sub print_metadata { my $meta = shift; print "*** Printing file metadata ***\n"; for my $key (keys %$meta) { if($key ne "credits") { my $val = $meta->{$key}; print "$key: $val\n"; } } my $credits = $meta->{'credits'}; if(@$credits == 0) { print "No credits\n"; } else { print "Credits:\n"; for my $credit (@$credits) { print "$credit->[0] ($credit->[1])\n"; } } } sub get_versions { my ($ffmpeg, $x264, $nero, $pipe); open $pipe, "ffmpeg -version 2> /dev/null |"; foreach my $line (<$pipe>) { if($line =~ m/^(FFmpeg.*)$/) { $ffmpeg = $1; } } close $pipe; open $pipe, "x264 --version 2> /dev/null |"; foreach my $line (<$pipe>) { if($line =~ m/^(x264.*)$/) { $x264 = $1; } } close $pipe; open $pipe, "neroAacEnc 2>&1 |"; foreach my $line (<$pipe>) { if($line =~ m/Package version:\s*([0-9.]+)/) { $nero = "Nero AAC Encoder $1"; } } close $pipe; return "$ffmpeg, $x264, $nero"; } sub xml_credits_section { my ($people, $key, $xml) = @_; if(@$people > 0) { $$xml .= " <key>$key</key>\n"; $$xml .= ' <array>' . "\n"; for my $member (@$people) { $$xml .= ' <dict>' . "\n"; $$xml .= ' <key>name</key>' . "\n"; $$xml .= " <string>$member</string>\n"; $$xml .= ' </dict>' . "\n"; } $$xml .= ' </array>' . "\n"; } } sub xml_credits { my $meta = shift; my $credits = $meta->{'credits'}; my $xml; if(@$credits == 0) { return $xml; } my (@cast, @directors, @producers); for my $credit (@$credits) { if($credit->[1] eq 'actor' or $credit->[1] eq 'host') { push @cast, $credit->[0]; } elsif($credit->[1] eq 'director') { push @directors, $credit->[0]; } elsif($credit->[1] eq 'executive producer') { push @producers, $credit->[0]; } } for my $credit (@$credits) { if($credit->[1] eq 'guest star') { push @cast, $credit->[0]; } elsif($credit->[1] eq 'producer') { push @producers, $credit->[0]; } } for my $credit (@$credits) { if($credit->[1] eq 'writer') { push @producers, $credit->[0]; } } if(@cast == 0 and @directors == 0 and @producers == 0) { return $xml; } $xml .= '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; $xml .= '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"' . ' "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' . "\n"; $xml .= '<plist version="1.0">'. "\n"; $xml .= ' <dict>' . "\n"; xml_credits_section(\@cast, 'cast', \$xml); xml_credits_section(\@directors, 'directors', \$xml); xml_credits_section(\@producers, 'producers', \$xml); $xml .= ' </dict>' . "\n"; $xml .= '</plist>' . "\n\n"; $xml =~ s/\"/\\\"/g; return $xml; } sub write_metadata { my ($long_time, $meta, $files) = @_; my $str = "AtomicParsley $files->{'mp4'}"; my $versions = get_versions(); my $file = $files->{'mp4'}; $file =~ s/\.mp4$//; my $final; if(defined $meta->{'title'} and defined $meta->{'subtitle'}) { my $title_dir = $files->{'finaldir'} . $meta->{'title'}; mkdir $title_dir unless -d $title_dir; $final = $title_dir . "/" . $meta->{'title'} . " - " . $meta->{'subtitle'} . ".mp4"; } else { $final = $files->{'mp4'}; } my @tmp = glob "$file-temp-*.mp4"; for my $f (@tmp) { unlink $f; } print "*** Adding metadata to $final ***\n"; my $xml = xml_credits($meta); if(defined $meta->{'title'}) { my $title = $meta->{'title'}; $str .= " --artist \"$title\" --album \"$title\"" . " --albumArtist \"$title\" --TVShowName \"$title\""; } if(defined $meta->{'subtitle'}) { $str .= " --title \"$meta->{'subtitle'}\""; } if(defined $meta->{'category'}) { $str .= " --genre \"$meta->{'category'}\""; } if(defined $meta->{'originalairdate'}) { $str .= " --year $meta->{'originalairdate'}"; } if(defined $meta->{'description'}) { $str .= " --description \"$meta->{'description'}\""; } if(defined $meta->{'channel'}) { $str .= " --TVNetwork \"$meta->{'channel'}\""; } if(defined $meta->{'syndicatedepisodenumber'}) { $str .= " --TVEpisode \"$meta->{'syndicatedepisodenumber'}\""; } if(defined $meta->{'rating'}) { $str .= " --contentRating \"$meta->{'rating'}\""; } if(-e $files->{'video'} . ".png") { my $art = $files->{'video'} . ".png"; $str .= " --artwork $art"; } $long_time =~ s/ /T/; $long_time .= "Z"; $str .= " --stik \"TV Show\""; $str .= " --purchaseDate $long_time"; $str .= " --encodingTool \"$versions\""; $str .= " --grouping \"MythTV Recording\""; if(defined $xml) { $str .= " --rDNSatom \"$xml\" name=iTunMOVI domain=com.apple.iTunes"; } system $str . " > /dev/null 2>&1"; @tmp = glob "$file-temp-*.mp4"; if(@tmp == 1) { move $tmp[0], $final; unlink $files->{'mp4'}; } else { die "Could not add metadata tags."; } } sub generate_edl { my ($fps, $cutlist, $files) = @_; open my $file, '>', $files->{'edl'} or die "Could not write to EDL file."; for my $cut (@$cutlist) { my ($start, $end) = get_cut($cut); $start /= $fps; $end /= $fps; print $file "$start $end 0\n"; } close $file; } die "Usage: $0 <channel> <start-time>" if @ARGV != 2; add_trail_slash(\$tmp_dir); add_trail_slash(\$final_dir); my $channel = $ARGV[0]; my $time = $ARGV[1]; my $long_time = short_time_to_long($time); my $video = get_path($channel, $long_time); my ($fps, $duration, $res) = get_video_param($video); my $base = $tmp_dir . $channel . "_" . $time; my $fifo = $base . ".tmp"; my $srt = $base . ".srt"; my $edl = $base . ".edl"; my $h264 = $base . ".h264"; my $aac = $base . ".aac"; my $xml = $base . ".xml"; my $mp4 = $video; $mp4 =~ s/\.[^.]*$//; $mp4 .= ".mp4"; my $mp4tmp = $mp4 . "-tmp"; my %files = (video => $video, fifo => $fifo, srt => $srt, edl => $edl, h264 => $h264, aac => $aac, xml => $xml, mp4 => $mp4, mp4tmp => $mp4tmp, dir => $tmp_dir, finaldir => $final_dir); print "*** Transcoding $video ***\n"; my @cutlist = get_cutlist($channel, $time); my $seg = cut_and_transcode($duration, $fps, $res, \@cutlist, \%files); remux($seg, \%files); my $meta = get_metadata($channel, $time); print_metadata($meta); write_metadata($long_time, $meta, \%files); clean_tmp(\%files, 0);
Private
[
?
]
Run code