Valid code-3 CBR (VBR=0) multi-frame packets rejected — VBR bit ignored in code-3 parsing
Summary
OpusDecoder::decode rejects valid Opus packets that use frame-packing code 3 with VBR disabled (V=0, constant bitrate): the code-3 parser treats every non-padding code-3 packet as self-delimiting (V=1) and never checks the V bit. Any CBR multi-frame packet produced by libopus (e.g. opus_encode with 40/60 ms frames in CELT mode, or opus_repacketizer output) fails with Err("Code 3: frame length exceeds packet").
Environment
- opus-rs 0.1.23 (crates.io tarball, sha256
1cc978697f3ada0ad98b323b9d85e85c6b08d478830148636c19b6d5f2ac9b85, verified against the sparse index; used as a path dep with identical content)
- rustc 1.96.0, macOS aarch64 (Apple Silicon), release build
- ffmpeg 8.1.1 with libopus 1.6.1
Minimal reproduction
The packet below is verbatim from libopus output (ffmpeg -c:a libopus -ar 16000 -b:a 24k -frame_duration 60 switches to CELT for some frames and packs 3 × 20 ms frames as code 3, V=0). Per RFC 6716 §3.2.2 (code 3, V=0): after the count byte there are no length fields; the remaining bytes are split equally into M frames.
use opus_rs::OpusDecoder;
fn main() {
// TOC 0xbb: CELT, 20 ms frames, code 3. Count byte 0x03: V=0 (CBR), M=3.
// Remaining 6 bytes = 3 frames x 2 bytes, no length fields.
let pkt: [u8; 8] = [0xbb, 0x03, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe];
let mut dec = OpusDecoder::new(48000, 1).unwrap();
let mut pcm = vec![0.0f32; 2880]; // 3 x 20 ms @ 48 kHz
match dec.decode(&pkt, 2880, &mut pcm) {
Ok(n) => println!("decoded {n} samples (expected)"),
Err(e) => println!("Err: {e} (BUG: valid packet rejected)"),
}
}
Fresh run (2026-07-19, release):
Err: Code 3: frame length exceeds packet (BUG: valid packet rejected)
The parser reads the first frame byte 0xff, sees the two-byte-length marker (bit 7), computes a frame length of 0x7FFE = 32766, and bails out — but those bytes are frame payload, not lengths, because V=0.
A full real-world stream (repro0203_framedur60_16k_mono_24k.ogg, produced by the ffmpeg command above) behaves the same way: every CELT packet in it is code-3 CBR and every one is rejected, so the stream cannot be decoded at all. libopus decodes it without errors.
Expected vs actual
- Expected: code 3 with V=0 splits
input[2..] (after optional padding) into M equal frames and decodes them (RFC 6716 §3.2.2: "V=0: ... the remaining bytes are split into M equal chunks").
- Actual:
Err("Code 3: frame length exceeds packet") on valid packets.
Root cause
src/lib.rs in 0.1.23, code-3 branch of OpusDecoder::decode (~line 890): after reading the count byte it only checks the padding flag (0x40) and then unconditionally parses the "self-delimiting format" (per-frame length prefixes) — the V bit (0x80) is never read:
let count_byte = input[1];
let n_frames = (count_byte & 0x3F) as usize;
...
let padding_flag = (count_byte & 0x40) != 0;
if padding_flag { ... } else {
// Self-delimiting format: <- taken for ALL V=0 packets
...
}
The V=0 CBR case (equal-length split, no per-frame lengths) is missing entirely.
Impact
Any multi-frame CBR Opus stream is undecodable: libopus emits code-3 CBR packets whenever frame_duration is 40/60 ms in CELT mode and from opus_repacketizer-based remuxing (common in WebRTC recording pipelines). Streams fail cleanly (Err, no panic), but completely — no audio is recovered.
Reproduction assets
Fully self-contained: the exact 8-byte triggering packet is embedded in the snippet above. The full stream it was captured from is regenerable with the ffmpeg command shown (-frame_duration 60); the captured packet is also kept on file as repro-03.zip if wanted.
Valid code-3 CBR (VBR=0) multi-frame packets rejected — VBR bit ignored in code-3 parsing
Summary
OpusDecoder::decoderejects valid Opus packets that use frame-packing code 3 with VBR disabled (V=0, constant bitrate): the code-3 parser treats every non-padding code-3 packet as self-delimiting (V=1) and never checks the V bit. Any CBR multi-frame packet produced by libopus (e.g.opus_encodewith 40/60 ms frames in CELT mode, oropus_repacketizeroutput) fails withErr("Code 3: frame length exceeds packet").Environment
1cc978697f3ada0ad98b323b9d85e85c6b08d478830148636c19b6d5f2ac9b85, verified against the sparse index; used as a path dep with identical content)Minimal reproduction
The packet below is verbatim from libopus output (
ffmpeg -c:a libopus -ar 16000 -b:a 24k -frame_duration 60switches to CELT for some frames and packs 3 × 20 ms frames as code 3, V=0). Per RFC 6716 §3.2.2 (code 3, V=0): after the count byte there are no length fields; the remaining bytes are split equally into M frames.Fresh run (2026-07-19, release):
The parser reads the first frame byte
0xff, sees the two-byte-length marker (bit 7), computes a frame length of 0x7FFE = 32766, and bails out — but those bytes are frame payload, not lengths, because V=0.A full real-world stream (
repro0203_framedur60_16k_mono_24k.ogg, produced by the ffmpeg command above) behaves the same way: every CELT packet in it is code-3 CBR and every one is rejected, so the stream cannot be decoded at all. libopus decodes it without errors.Expected vs actual
input[2..](after optional padding) into M equal frames and decodes them (RFC 6716 §3.2.2: "V=0: ... the remaining bytes are split into M equal chunks").Err("Code 3: frame length exceeds packet")on valid packets.Root cause
src/lib.rsin 0.1.23, code-3 branch ofOpusDecoder::decode(~line 890): after reading the count byte it only checks the padding flag (0x40) and then unconditionally parses the "self-delimiting format" (per-frame length prefixes) — the V bit (0x80) is never read:The V=0 CBR case (equal-length split, no per-frame lengths) is missing entirely.
Impact
Any multi-frame CBR Opus stream is undecodable: libopus emits code-3 CBR packets whenever
frame_durationis 40/60 ms in CELT mode and fromopus_repacketizer-based remuxing (common in WebRTC recording pipelines). Streams fail cleanly (Err, no panic), but completely — no audio is recovered.Reproduction assets
Fully self-contained: the exact 8-byte triggering packet is embedded in the snippet above. The full stream it was captured from is regenerable with the ffmpeg command shown (
-frame_duration 60); the captured packet is also kept on file asrepro-03.zipif wanted.