Skip to content

Valid code-3 CBR (VBR=0) multi-frame packets rejected — VBR bit ignored in code-3 parsing #6

Description

@ekhodzitsky

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions