Garage uenoB このページをアンテナに追加 RSSフィード

2007-04-16

[] MP4Boxとdecoding delay 04:39  MP4Boxとdecoding delayを含むブックマーク

最近,MEncoderとかx264とかfaacとかMP4Boxとかを使って手持ちのDVDとかをH.264+AACなmp4ファイルにエンコードしたりとかしてます.

エンコード作業は初めてなので何も分からない状態から始めたのですが,コーデックのことやオプションの使い方についてはWeb上に秀逸な資料がいろいろあったおかげで(特にageha was hereは参考になりました),いろいろ試行錯誤しながらもそれなりな出力を得られるようになってきました.ただ,リップシンクがどうのこうのとかいうんですかね?なんか音と映像がずれてるんですよ.2〜3コマ分くらい映像が遅れて来る.

まあ,いつも使っているMPlayerでずれるのは仕方がないとして,気になるのはQuickTime+avc1decoderの場合.この組み合わせならば,QuickTime様の厳格なる時間管理のおかげで原理上デコードに遅延があったとしても表示が遅延することはないはずなのに,何故か映像が遅れてくる.しかも頭から再生すると真っ白な画面から始まって最初の数コマはカクカクしてるし.なにこれ.

どうもこういう症状はdelay frameとかいうのが紛れてると起こるらしいのですが,今回はmencoderにrawvideoを吐かせてtailで冒頭の緑一色のサンプルを除去した上でx264に流し込む方法を取ったので, そんなモノが紛れ込む隙は原理的にありません.あ,イメージとしてはこんな感じね*1

mencoder dvd:// -ovc raw -of rawvideo | tail -c +バイト数 | x264 > video.264
MP4Box -add video.264 -add audio.aac output.mp4

実際に出力をhexdumpして確認しても余計なモノは入っていません.きっと原因は他にあるに違いない,ってことで,MovieVideoChartやらQTCoffeeやらで遊びつつ,規格書読みながらmp4ファイルやらのhexdumpとにらめっこしていると,ひとつのことに気がつきました.

Edit Box入ってねーじゃん.

mp4ファイルの中にedtsが入ってねーじゃん.このままだと composition time = presentation time になるから,decoding delay が presentation に影響しちゃって,何も表示するモノがない空白の時間が先頭に入ってしまうではないですか.悪いのはmp4ファイルで,QuickTimeはそれを忠実に再現していたわけですな.冒頭の真っ白の画面は何も表示するモノがない間QuickTimeが苦し紛れに出していたものだと思われます.詳しい説明はきっとあとで書く

ちゅーわけでEdit Boxを入れると冒頭の白画面やら遅延やらは解消できるはずです.MP4Boxの場合,Edit Boxはdelayオプションを手で指定することで挿入することができるんですが*2,これくらい自動でやってもらわないと困るよね,ってことでad-hocにパッチ

--- gpac/applications/mp4box/fileimport.c	27 Feb 2007 16:18:08 -0000	1.53
+++ gpac/applications/mp4box/fileimport.c	17 Apr 2007 07:27:03 -0000
@@ -130,6 +130,7 @@
 {
 	u32 track_id, i, timescale, track;
 	s32 par_d, par_n, prog_id, delay;
+	Bool do_ctdelay;
 	Bool do_audio, do_video, do_all;
 	const char *szLan;
 	GF_Err e;
@@ -147,6 +148,7 @@
 
 	szLan = NULL;
 	delay = 0;
+	do_ctdelay = 0;
 	par_d = par_n = -2;
 	/*use ':' as separator, but beware DOS paths...*/
 	ext = strchr(szName, ':');
@@ -161,6 +163,7 @@
 
 		/*all extensions for track-based importing*/
 		if (!strnicmp(ext+1, "lang=", 5)) szLan = GetLanguageCode(ext+6);
+		else if (!stricmp(ext+1, "delay=ct")) do_ctdelay = 1;
 		else if (!strnicmp(ext+1, "delay=", 6)) delay = atoi(ext+7);
 		else if (!strnicmp(ext+1, "fps=", 4)) {
 			if (!strcmp(ext+5, "auto")) force_fps = 10000.0;
@@ -252,6 +255,21 @@
 		timescale = gf_isom_get_timescale(dest);
 		for (i=o_count; i<count; i++) {
 			if (szLan) gf_isom_set_media_language(import.dest, i+1, (char *) szLan);
+			if (do_ctdelay) {
+				GF_ISOSample *samp;
+				u64 tk_dur;
+				samp = gf_isom_get_sample_info(import.dest, i+1, 1, NULL, NULL);
+				if (!samp) {
+					fprintf(stdout, "Track ID %d ctdelay failed\n", i+1);
+					goto exit;
+				}
+				if (samp->CTS_Offset > 0) {
+					tk_dur = gf_isom_get_track_duration(import.dest, i+1);
+					gf_isom_remove_edit_segments(import.dest, i+1);
+					gf_isom_append_edit_segment(import.dest, i+1, tk_dur, samp->CTS_Offset, GF_ISOM_EDIT_NORMAL);
+				}
+				gf_isom_sample_del(&samp);
+			}
 			if (delay) {
 				u64 tk_dur;
 				gf_isom_remove_edit_segments(import.dest, i+1);
@@ -300,6 +318,21 @@
 			timescale = gf_isom_get_timescale(dest);
 			track = gf_isom_get_track_by_id(import.dest, import.final_trackID);
 			if (szLan) gf_isom_set_media_language(import.dest, track, (char *) szLan);
+			if (do_ctdelay) {
+				GF_ISOSample *samp;
+				u64 tk_dur;
+				samp = gf_isom_get_sample_info(import.dest, track, 1, NULL, NULL);
+				if (!samp) {
+					fprintf(stdout, "Track ID %d ctdelay failed\n", track);
+					goto exit;
+				}
+				if (samp->CTS_Offset > 0) {
+					tk_dur = gf_isom_get_track_duration(import.dest, track);
+					gf_isom_remove_edit_segments(import.dest, track);
+					gf_isom_append_edit_segment(import.dest, track, tk_dur, samp->CTS_Offset, GF_ISOM_EDIT_NORMAL);
+				}
+				gf_isom_sample_del(&samp);
+			}
 			if (delay) {
 				u64 tk_dur;
 				gf_isom_remove_edit_segments(import.dest, track);

MP4Boxにdelay=ctというオプションを追加してます.decoding delayを自動で得て,それを打ち消すためのedtsボックスを挿入します.-delayと同時に指定した場合は-delayを優先します.使い方はこんな感じ.

MP4Box -add video.mp4:delay=ct -add audio.mp4 output.mp4

[] MPlayerとEdit Box付きmp4ファイル 04:39  MPlayerとEdit Box付きmp4ファイルを含むブックマーク

このようにしてEdit Boxを入れたmp4ファイルをMPlayerで再生すると,libmpdemuxでは正しくdemuxできず映像が乱れます.これはlibmpdemuxがcttsボックスを無視してcomposition timeを計算していないからです.ちゅーわけでパッチ

--- mplayer/libmpdemux/demux_mov.c	(revision 23007)
+++ mplayer/libmpdemux/demux_mov.c	(working copy)
@@ -66,6 +66,7 @@
 
 typedef struct {
     unsigned int pts; // duration
+    unsigned int cts;  // composition time
     unsigned int size;
     off_t pos;
 } mov_sample_t;
@@ -89,6 +90,11 @@
 } mov_durmap_t;
 
 typedef struct {
+    unsigned int num;
+    unsigned int offset;
+} mov_ctoffmap_t;
+
+typedef struct {
     unsigned int dur;
     unsigned int pos;
     int speed;
@@ -139,6 +145,8 @@
     mov_chunkmap_t* chunkmap;
     int durmap_size;
     mov_durmap_t* durmap;
+    int ctoffmap_size;
+    mov_ctoffmap_t* ctoffmap;
     int keyframes_size;
     unsigned int* keyframes;
     int editlist_size;
@@ -242,6 +250,17 @@
 	}
     }
     
+    // calc composition time:
+    for(i=0;j<trak->samples_size;j++)
+       trak->samples[j].cts = trak->samples[j].pts;
+    s=0;
+    for(j=0;j<trak->ctoffmap_size;j++){
+	for(i=0;i<trak->ctoffmap[j].num;i++){
+	    trak->samples[s].cts+=trak->ctoffmap[j].offset;
+	    ++s;
+	}
+    }
+
     // calc sample offsets
     s=0;
     for(j=0;j<trak->chunks_size;j++){
@@ -272,7 +291,7 @@
 	    }
 	    // find start sample
 	    for(;sample<trak->samples_size;sample++){
-		if(pts<=trak->samples[sample].pts) break;
+		if(pts<=trak->samples[sample].cts) break;
 	    }
 	    el->start_sample=sample;
 	    el->pts_offset=((long long)e_pts*(long long)trak->timescale)/(long long)timescale-trak->samples[sample].pts;
@@ -280,7 +299,7 @@
 	    e_pts+=el->dur;
 	    // find end sample
 	    for(;sample<trak->samples_size;sample++){
-		if(pts<trak->samples[sample].pts) break;
+		if(pts<trak->samples[sample].cts) break;
 	    }
 	    el->frames=sample-el->start_sample;
 	    frame+=el->frames;
@@ -539,6 +558,7 @@
       free(track->chunks);
       free(track->chunkmap);
       free(track->durmap);
+      free(track->ctoffmap);
       free(track->keyframes);
       free(track->editlist);
       free(track->desc);
@@ -1740,6 +1760,23 @@
                pts, trak->length);
       break;
     }
+    case MOV_FOURCC('c','t','t','s'): {
+      int temp = stream_read_dword(demuxer->stream);
+      int len = stream_read_dword(demuxer->stream);
+      int i;
+      unsigned int pts = 0;
+      mp_msg(MSGT_DEMUX, MSGL_V,
+             "MOV: %*sComposition time offset table! (%d blocks)\n", level, "",
+             len);
+      trak->ctoffmap = calloc(len, sizeof(mov_ctoffmap_t));
+      trak->ctoffmap_size = len;
+      for (i = 0; i < len; i++) {
+        trak->ctoffmap[i].num = stream_read_dword(demuxer->stream);
+        trak->ctoffmap[i].offset = stream_read_dword(demuxer->stream);
+        pts += trak->durmap[i].num * trak->durmap[i].dur;
+      }
+      break;
+    }
     case MOV_FOURCC('s','t','s','c'): {
       int temp = stream_read_dword(demuxer->stream);
       int len = stream_read_dword(demuxer->stream);

[][] 狂ったmp4ファイルをMovieVideoChartで開く 04:39  狂ったmp4ファイルをMovieVideoChartで開くを含むブックマーク

ついでに.今回のように表示するモノがない時間が存在するようなmp4ファイルをMovieVideoChartで開くと最上段の表示が狂いますが(何も表示されなかったりSample Numberがバグってたり),これを無理矢理表示させるようにするad-hocなパッチ

--- ChartView.c.orig	2006-07-11 06:37:58.000000000 +0900
+++ ChartView.c	2007-04-17 03:26:43.000000000 +0900
@@ -1113,10 +1113,12 @@
 	float minimumTime,
 	float maximumTime )
 {
+	OSErr err;
 	Track videoTrack = data->videoTrack;
 	Media videoMedia = GetTrackMedia( videoTrack );
 	TimeScale trackTimeScale = GetMovieTimeScale( GetTrackMovie( videoTrack ) );
 	TimeValue trackSampleTime, trackSampleDuration;
+	TimeValue nextTrackSampleTime;
 	HIThemeTextInfo labelTextInfo = { 0, kThemeStateActive, kThemeSystemFont, kHIThemeTextHorizontalFlushLeft, kHIThemeTextVerticalFlushTop, kHIThemeTextBoxOptionStronglyVertical, kHIThemeTextTruncationNone, 0, false };
 	float maskWidth;
 	CGImageRef maskImage = NULL;
@@ -1124,12 +1126,19 @@
 	CGContextSaveGState( c );
 	
 	// Walk the track, looking for frame times in range.
+	trackSampleTime = minimumTime * trackTimeScale;
+	do {
 	GetTrackNextInterestingTime( videoTrack, 
 								 nextTimeMediaSample | nextTimeEdgeOK,
-								 minimumTime * trackTimeScale,
+								 trackSampleTime,
 								 fixed1,
-								 &trackSampleTime,
+								 &nextTrackSampleTime,
 								 &trackSampleDuration );
+	err = GetMoviesError();
+	if (err == noErr && nextTrackSampleTime >= 0 && trackSampleDuration > 0) break;
+	trackSampleTime++;
+	} while( trackSampleTime < maximumTime * trackTimeScale );
+
 	while( ( trackSampleTime >= 0 ) && ( trackSampleDuration > 0 ) && ( trackSampleTime < maximumTime * trackTimeScale ) ) {
 		float showX, showWidth;
 		CGRect maskRect;
@@ -1140,6 +1149,7 @@
 		TimeValue64 displayTime = TrackTimeToMediaDisplayTime( trackSampleTime, videoTrack );
 		SInt64 sampleNumber = 0;
 		MediaDisplayTimeToSampleNum( videoMedia, displayTime, &sampleNumber, NULL, NULL );
+		err = GetMoviesError();
 		CFStringRef labelString;
 		
 		CGContextSaveGState( c );
@@ -1158,10 +1168,14 @@
 			CGContextClipToMask( c, maskRect, maskImage );
 			
 			// Get the media sample description index.
+			if (err == noErr) {
 			SampleNumToMediaDecodeTime( videoMedia, sampleNumber, &sampleDecodeTime, NULL );
+			err =
 			GetMediaSample2( videoMedia, NULL, 0, NULL, sampleDecodeTime,
 							 NULL, NULL, NULL, NULL, &mediaSampleDescIndex, 1, NULL, NULL );
+			}
 			
+			if (err == noErr) {
 			// Draw the frame thumbnail.
 			if( showWidth <= 10 )
 				onlyIfCached = true;
@@ -1171,9 +1185,13 @@
 				HIViewDrawCGImage( c, &thumbnailRect, frameThumbnail ); // like CGContextDrawImage, but handles flip
 				CFRelease( frameThumbnail );
 			}
+			}
 
 			// Write the sample number.
+			if (err == noErr)
 			labelString = CFStringCreateWithFormat( NULL, NULL, CFSTR("%lld"), (long long)sampleNumber );
+			else
+			labelString = CFStringCreateWithFormat( NULL, NULL, CFSTR("Err:%d"), (int)err );
 			if( labelString ) {
 				CGRect labelRect = CGRectMake( showX, data->rowRects.sampleNumberTrackRect.origin.y, kLabelWidth, data->rowRects.sampleNumberTrackRect.size.height );
 				HIThemeDrawTextBox( labelString, &labelRect, &labelTextInfo, c, kHIThemeOrientationNormal );
@@ -1184,12 +1202,18 @@
 		CGContextRestoreGState( c );
 		
 		// Find the next frame.
+ 		while( trackSampleTime < maximumTime * trackTimeScale ) {
 		GetTrackNextInterestingTime( videoTrack, 
 									 nextTimeMediaSample,
 									 trackSampleTime,
 									 fixed1,
-									 &trackSampleTime,
+									 &nextTrackSampleTime,
 									 &trackSampleDuration );
+ 		err = GetMoviesError();
+ 		if (err == noErr && nextTrackSampleTime >= 0 && trackSampleDuration > 0) break;
+ 		trackSampleTime++;
+ 		}
+ 		trackSampleTime = nextTrackSampleTime;
 	}
 	
 	CGContextRestoreGState( c );

ちゅーかいくらサンプルだからってエラーチェックくらいしようよ…

*1:こいつら標準出力にメッセージを吐きやがるんで単純にパイプで繋げないのにはやられた

*2delayオプションって期待する動作をしませんよね?ソースコード見てもこれがまともに動くとは思えないのですが.

トラックバック - http://d.hatena.ne.jp/uenoB/20070416