VFRなmp4の作り方(3)

24fpsと30fpsが混合したmp4を作る自分的手順(自動化編)

timecode v1対応のnhml→VFR変換スクリプトを作ってみた。
これの場合、timecode v2対応のものに比べて、30000/1001とか24000/1001など、より正確なフレームレートが再現できる。

使い方:
 mp4box -nhml 1 cfr.mp4
 (VFR変換スクリプト) -i mkv-timecodesfile.txt -n cfr_track1.nhml -o vfr_track1.nhml
 mp4box -add vfr_track1.nhml -new vfr_track1.mp4
 mp4box -add vfr_track1.mp4 -add cfr.aac -new vfr.mp4

ここにexe版も用意してみました。

#!/usr/bin/perl
# url - http://d.hatena.ne.jp/zmi/

use Getopt::Std;
use Math::BigInt;

getopts('i:n:o:');

@timescales = parseTimecode($opt_i);

for($frame = 0; $frame < @timescales; $frame++){
    $factors{$timescales[$frame]}++;
}

$lcmTimescale = Math::BigInt::blcm(keys %factors);

print "[Timecode info]\n";
print "  Total frames($frame), LCM Timescale($lcmTimescale)";
print ", Factors(" . join('/',sort keys %factors) . ")\n";

# generate CTS from timecode
@cts = toCTS(\@timescales, $lcmTimescale);

# parse nhml
open SRC_NHML, "<$opt_n" or die $!;
open TC_NHML, ">$opt_o" or die $!;

while(<SRC_NHML>){
    next unless /<NHNTSample.*DTS="(\d+)".*\/>/;
    $dts[$count] = $1;
    break if $count++ < 2;
}
seek(SRC_NHML, 0, 0);

$timeBase = $dts[1] - $dts[0];
print "[NHML info]\n";
print "  TimeBase($timeBase)\n";

$dtsFrame = 0;
while(<SRC_NHML>){
    s/timeScale="\d+"/timeScale="$lcmTimescale"/ if(/<NHNTStream.*?>/);

    ((print TC_NHML), next) unless /<NHNTSample.*\/>/;

    next unless $dtsFrame < @cts;

    /CTSOffset="(\d+)"/;
    $ctsOffset = ($1 or 0);

    $ctsFrame = $dtsFrame + int($ctsOffset / $timeBase + 0.5);

    if($dtsFrame == 0){
	$delayFrame = $ctsFrame;
	$delayTC =  $cts[$delayFrame];
	print "[Delay]\n";
	print "  Frames($delayFrame), Timecode($delayTC)\n";
    }

    if($dtsFrame < $delayFrame){
	$dts = $cts[$dtsFrame];
	$ctsIndex = $ctsFrame;
	$cts = $cts[$ctsIndex];
    }else{
	$dts = $cts[$dtsFrame - $delayFrame] + $delayTC;
	$ctsIndex = $ctsFrame - $delayFrame;
	$cts = $cts[$ctsIndex] + $delayTC;
    }
    $ctsOffset = $cts - $dts;

    s/DTS="\d+"/DTS="$dts"/;
    s/CTSOffset="\d+"/CTSOffset="$ctsOffset"/;
    s/(dataLength="\d+")/$1 CTSOffset="$ctsOffset"/ if $ctsOffset == 0; # hack for mp4box's bug
    print TC_NHML;

    $dtsFrame++;
}

close SRC_NHML;
close TC_NHML;

print "Completed.\n";
exit 0;

sub toCTS{
    my $ref = shift @_;
    my @timescales = @$ref;
    my $lcmTimescale = shift @_;
    my @cts, $frame, $duration;
    
    $cts[0] = 0;
    for($frame = 1; $frame < @timescales; $frame++){
	$duration = ($lcmTimescale / $timescales[$frame -1]) * 1001;
	$cts[$frame] = $cts[$frame -1] + $duration;
    }
    return @cts;
}


sub toTimescale{
    my $fps = shift @_;
    my $timescale = int($fps * 1.001 + 0.5) * 1000;
    return $timescale;
}

sub parseTimecode{
    my $timecodeFile = shift @_;

    my @timescale;
    my $tcfv;
    my $assume;
    my $begin,$end,$last,$fps;
    my $frame = 0;

    open TC, "<$timecodeFile" or die $!;
    
    while(<TC>){
	($tcfv = $1, next)
	    if /^\# timecode format v([1-2])/;
	
	($last = $1, next)
	    if /^\# TDecimate Mode.*Last Frame = (\d+)/;
	
	next if /^\#/;
	
	($assume = $1, next)
	    if /^Assume ([0-9.]+)/;
	
	next unless /(\d+),(\d+),([\d.]+)/;
	
	($begin, $end, $fps) = ($1, $2, $3);
	
	for($frame; $frame < $begin; $frame++){
	    $timescale[$frame] = toTimescale($assume);
	}
	
	for($frame = $begin; $frame <= $end; $frame++){
	    $timescale[$frame] = toTimescale($fps);
	}
    }
    for(;$frame <= $last; $frame++){
	$timescale[$frame] = toTimescale($assume);
    }
    close TC;
    
    defined $tcfv or die "Only timecode format v1 is supported.";
    return @timescale;
}