Skip to content

Commit 95624d6

Browse files
authored
Merge pull request #1943 from danog/fix-mp4-stsc-quadratic
MP4: parse the sample-to-chunk table in linear time
2 parents a9fecea + 30c8f73 commit 95624d6

3 files changed

Lines changed: 54 additions & 6 deletions

File tree

‎src/API.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ final class API extends AbstractAPI
5454
*
5555
* @var string
5656
*/
57-
public const RELEASE = '8.15.3';
57+
public const RELEASE = '8.15.4';
5858
/**
5959
* We're not logged in.
6060
*

‎src/Mp4.php‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -650,12 +650,15 @@ private function parseSampleTables(int $trackId, array $tables, array &$progress
650650
$sample = 0;
651651
$time = 0;
652652
$chunkCount = \count($chunkOffsets);
653+
// The entries of the sample-to-chunk table are sorted by first chunk: walk them along with the chunks, rescanning
654+
// the table for each chunk is quadratic (minutes of blocked event loop for a file with a long table).
655+
$perChunk = 0;
656+
$stscIndex = 0;
657+
$stscCount = \count($stsc);
653658
foreach ($chunkOffsets as $chunkIndex => $chunkOffset) {
654-
$perChunk = 0;
655-
foreach ($stsc as [$firstChunk, $samplesPerChunk]) {
656-
if ($firstChunk <= $chunkIndex + 1) {
657-
$perChunk = $samplesPerChunk;
658-
}
659+
while ($stscIndex < $stscCount && $stsc[$stscIndex][0] <= $chunkIndex + 1) {
660+
$perChunk = $stsc[$stscIndex][1];
661+
$stscIndex++;
659662
}
660663
$offset = $chunkOffset;
661664
for ($i = 0; $i < $perChunk && $sample < $sampleCount; $i++, $sample++) {

‎tests/danog/MadelineProto/Mp4Test.php‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)