Account for multi-frame Opus packets when computing packet duration (#439)

* Account for multi-frame Opus packets when computing packet duration

parseOpusTocByte only read the config field of the TOC byte and always
assumed a single frame per packet. Per RFC 6716 section 3.2, a packet may
carry 1, 2 or an arbitrary number of frames, encoded in the two low bits of
the TOC byte (plus the frame count byte for code 3), and its duration is the
frame duration times the frame count.

As a result, the Ogg muxer wrote granule positions that advanced slower than
the actual audio. Chromium's MediaRecorder packs three 20 ms frames per Opus
packet, so remuxing WebM/Opus to Ogg/Opus produced files declaring a third of
their real duration: a 5.7 s recording ended with a final granule position of
99840 (2.08 s). Decoders that trust the container then truncate the audio.

* Clean up

---------

Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
Flávio Medeiros
2026-07-18 12:05:31 +00:00
committed by GitHub
co-authored by Vanilagy
parent 1183b542ee
commit bfbd90e1d8
2 changed files with 101 additions and 3 deletions
+14 -1
View File
@@ -2207,9 +2207,22 @@ const OPUS_FRAME_DURATION_TABLE = [
export const parseOpusTocByte = (packet: Uint8Array) => {
const config = packet[0]! >> 3;
const code = packet[0]! & 0b11;
// A packet may pack more than one frame, in which case its duration is the frame duration times the number of
// frames it carries. See https://datatracker.ietf.org/doc/html/rfc6716, section 3.2.
let frameCount: number;
if (code === 0) {
frameCount = 1;
} else if (code === 1 || code === 2) {
frameCount = 2;
} else {
// Code 3: the frame count sits in the six low bits of the frame count byte
frameCount = packet[1]! & 0b111111;
}
return {
durationInSamples: OPUS_FRAME_DURATION_TABLE[config]!,
durationInSamples: OPUS_FRAME_DURATION_TABLE[config]! * frameCount,
};
};