From 02b08e036b029012eac58d7b0e376bf802db7b3c Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:07:17 +0100 Subject: [PATCH] Fix Matroska files where no audio packets are key packets (fixes #192) --- dev/demux.html | 4 ++-- src/matroska/matroska-demuxer.ts | 10 +++++++++- src/muxer.ts | 22 +++++++++++----------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/dev/demux.html b/dev/demux.html index 8c5310b..c0c8346 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -14,8 +14,8 @@ source: new Mediabunny.BlobSource(file), }); - const videoTrack = await input.getPrimaryVideoTrack(); - const sink = new Mediabunny.EncodedPacketSink(videoTrack); + const audioTrack = await input.getPrimaryAudioTrack(); + const sink = new Mediabunny.EncodedPacketSink(audioTrack); for await (const packet of sink.packets()) { console.log(packet.timestamp, packet.duration, packet.type) diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 32880d5..9af38db 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -1374,9 +1374,17 @@ export class MatroskaDemuxer extends Demuxer { const relativeTimestamp = readI16Be(slice); const flags = readU8(slice); - const isKeyFrame = !!(flags & 0x80); const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later + let isKeyFrame = !!(flags & 0x80); + if (trackData.track.info?.type === 'audio' && trackData.track.info.codec) { + // Some files don't mark their audio packets as key packets (I'm looking at you, Firefox). But, we + // can fix this in most cases: if we recognize the codec of the track, then we know every packet is + // necessarily a key packet, no matter what the container says. + // https://github.com/Vanilagy/mediabunny/issues/192 + isKeyFrame = true; + } + const blockData = readBytes(slice, size - (slice.filePos - dataStartPos)); const hasDecodingInstructions = trackData.track.decodingInstructions.length > 0; diff --git a/src/muxer.ts b/src/muxer.ts index b23356f..de2a36e 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -46,21 +46,21 @@ export abstract class Muxer { private trackTimestampInfo = new WeakMap(); - protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyFrame: boolean) { + protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) { timestampInSeconds += track.source._timestampOffset; let timestampInfo = this.trackTimestampInfo.get(track); if (!timestampInfo) { - if (!isKeyFrame) { - throw new Error('First frame must be a key frame.'); + if (!isKeyPacket) { + throw new Error('First packet must be a key packet.'); } timestampInfo = { maxTimestamp: timestampInSeconds, - maxTimestampBeforeLastKeyFrame: timestampInSeconds, + maxTimestampBeforeLastKeyPacket: timestampInSeconds, }; this.trackTimestampInfo.set(track, timestampInfo); } @@ -69,15 +69,15 @@ export abstract class Muxer { throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } - if (isKeyFrame) { - timestampInfo.maxTimestampBeforeLastKeyFrame = timestampInfo.maxTimestamp; + if (isKeyPacket) { + timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp; } - if (timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyFrame) { + if (timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket) { throw new Error( - `Timestamps cannot be smaller than the highest timestamp of the previous GOP (a GOP begins with a key` - + ` frame and ends right before the next key frame). Got ${timestampInSeconds}s, but highest timestamp` - + ` is ${timestampInfo.maxTimestampBeforeLastKeyFrame}s.`, + `Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a key` + + ` packet and ends right before the next key packet). Got ${timestampInSeconds}s, but largest` + + ` timestamp is ${timestampInfo.maxTimestampBeforeLastKeyPacket}s.`, ); }