diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index 24b9844..c9a9597 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -246,14 +246,13 @@ export class StreamingVideoDecoder { const hintedDurationSec = Math.max(containerDurationSec, streamDurationSec, 0); const scanEndSec = hintedDurationSec > 0 ? hintedDurationSec + 0.5 : SCAN_UNBOUNDED_FALLBACK_SEC; - let maxPacketEndUs = 0; + let maxPacketTimestampUs = 0; const scanReader = this.demuxer.read("video", 0, scanEndSec).getReader(); try { while (true) { const { done, value } = await scanReader.read(); if (done || !value) break; - const endUs = value.timestamp + (value.duration ?? 0); - if (endUs > maxPacketEndUs) maxPacketEndUs = endUs; + if (value.timestamp > maxPacketTimestampUs) maxPacketTimestampUs = value.timestamp; } } finally { try { @@ -262,7 +261,12 @@ export class StreamingVideoDecoder { /* already closed */ } } - const scannedDuration = maxPacketEndUs / 1_000_000; + // Use last frame's timestamp + one frame duration. The `duration` field on the last + // packet in a WebM container is frequently inflated by the demuxer to match the + // container's declared end, causing validateDuration to see no divergence and + // propagate a duration that the decoder can never actually reach. + const typicalFrameDurationUs = Math.ceil(1_000_000 / frameRate); + const scannedDuration = (maxPacketTimestampUs + typicalFrameDurationUs) / 1_000_000; const validatedDuration = validateDuration(mediaInfo.duration, scannedDuration); this.metadata = { diff --git a/tests/fixtures/sample-inflated-duration.webm b/tests/fixtures/sample-inflated-duration.webm new file mode 100644 index 0000000..6322840 Binary files /dev/null and b/tests/fixtures/sample-inflated-duration.webm differ