@@ -108,4 +108,49 @@ public function testEveryFrameIsDemuxedWithItsPresentationTimestamp(array $flags
108108 @unlink ($ file );
109109 }
110110 }
111+
112+ /**
113+ * Interleaved audio and video make a sample-to-chunk table about as long as the chunk list: it used to be rescanned
114+ * for each chunk, blocking the event loop for minutes before the first frame of a film.
115+ */
116+ public function testLongSampleToChunkTableIsParsedInLinearTime (): void
117+ {
118+ exec ('ffmpeg -hide_banner -encoders 2>/dev/null ' , $ encoders , $ code );
119+ if ($ code !== 0 || !str_contains (implode ("\n" , $ encoders ), 'libx264 ' )) {
120+ self ::markTestSkipped ('ffmpeg with libx264 is needed ' );
121+ }
122+ $ file = tempnam (sys_get_temp_dir (), 'mp4 ' ).'.mp4 ' ;
123+ try {
124+ exec (implode (' ' , array_map (escapeshellarg (...), [
125+ 'ffmpeg ' , '-hide_banner ' , '-loglevel ' , 'error ' , '-y ' ,
126+ '-f ' , 'lavfi ' , '-i ' , 'testsrc2=size=64x48:rate=25 ' , '-f ' , 'lavfi ' , '-i ' , 'sine=sample_rate=48000 ' ,
127+ '-t ' , '1800 ' , '-c:v ' , 'libx264 ' , '-preset ' , 'ultrafast ' , '-g ' , '250 ' , '-pix_fmt ' , 'yuv420p ' ,
128+ '-c:a ' , 'aac ' , '-b:a ' , '16k ' , '-movflags ' , '+faststart ' , $ file ,
129+ ])), $ output , $ code );
130+ self ::assertSame (0 , $ code , implode ("\n" , $ output ));
131+ $ data = (string ) file_get_contents ($ file );
132+ $ pos = strpos ($ data , 'stsc ' );
133+ $ entries = 0 ;
134+ for (; $ pos !== false ; $ pos = strpos ($ data , 'stsc ' , $ pos + 1 )) {
135+ $ entries = max ($ entries , unpack ('N ' , $ data , $ pos + 8 )[1 ]);
136+ }
137+ self ::assertGreaterThan (10000 , $ entries , 'The sample-to-chunk table is too short to tell ' );
138+
139+ $ start = hrtime (true );
140+ $ mp4 = new Mp4 (new ReadableBuffer ($ data ));
141+ $ video = 0 ;
142+ foreach ($ mp4 ->frames as $ frame ) {
143+ if ($ video === 0 ) {
144+ // Quadratic, this took about 20 seconds.
145+ self ::assertLessThan (2_000_000_000 , hrtime (true ) - $ start , 'The sample tables took too long to parse ' );
146+ }
147+ if ($ frame ['codec ' ] === 'V_MPEG4/ISO/AVC ' ) {
148+ $ video ++;
149+ }
150+ }
151+ self ::assertSame (1800 * 25 , $ video );
152+ } finally {
153+ @unlink ($ file );
154+ }
155+ }
111156}
0 commit comments