[Recording] Reset a speaker's Opus decoder when their SSRC changes #322

Closed
opened 2026-08-25 20:38:42 +00:00 by claude-bot · 1 comment
Contributor

Severity: MEDIUM. Found in the August 2026 session lifecycle review (#319). Required companion to the wall-clock anchoring fix.

The defect

The sink keeps one discord.opus.Decoder per user id (recording.py:170), but Discord assigns a new SSRC when a user leaves and rejoins a voice channel, or when the voice server fails over or changes region. Opus is a stateful codec: feeding a new stream through a decoder carrying state from the previous one garbles the first frames after the discontinuity.

Today this is masked, because the entire absence is compressed out of the track anyway. Once gaps are filled, rejoins become common, visible, and audible — and garbled audio at exactly the moment someone rejoins is prime material for a mis-transcription.

Evidence

  • bot/questboard_bot/cogs/recording.py:168-170 — decoder keyed by user.id, created once, never reset
  • The sink keys writers by user.id too, so audio after a rejoin appends to the same file
  • Library voice_client.py:243-255 / gateway.py:176 — SSRC removal and remapping on member disconnect and reconnect
  • Library voice_client.py:71-74 — a bot channel move destroys all decoders

Proposed fix

Track the last-seen SSRC per user id. When a packet arrives with a different SSRC than the one recorded for that user, discard and recreate that user's decoder before decoding. Log the reset at INFO with the old and new SSRC so rejoin frequency is observable.

Also handle the interleaving case: the jitter-buffer tail of the old SSRC can drain after the new stream has started. With wall-clock anchoring both are placed correctly by time, so the only requirement is that the decoder used matches the SSRC of the packet being decoded — keep a decoder per (uid, ssrc) if that proves simpler than resetting.

Acceptance criteria

  • A user's decoder is reset (or per-SSRC) when their SSRC changes
  • Decoder resets are logged with old and new SSRC
  • A unit test feeds two SSRCs for one user and asserts the second stream decodes cleanly
  • Old-SSRC tail packets arriving after a rejoin do not corrupt the new stream
**Severity: MEDIUM.** Found in the August 2026 session lifecycle review (#319). Required companion to the wall-clock anchoring fix. ## The defect The sink keeps one `discord.opus.Decoder` per **user id** (`recording.py:170`), but Discord assigns a new **SSRC** when a user leaves and rejoins a voice channel, or when the voice server fails over or changes region. Opus is a stateful codec: feeding a new stream through a decoder carrying state from the previous one garbles the first frames after the discontinuity. Today this is masked, because the entire absence is compressed out of the track anyway. Once gaps are filled, rejoins become common, visible, and audible — and garbled audio at exactly the moment someone rejoins is prime material for a mis-transcription. ## Evidence - `bot/questboard_bot/cogs/recording.py:168-170` — decoder keyed by `user.id`, created once, never reset - The sink keys writers by `user.id` too, so audio after a rejoin appends to the same file - Library `voice_client.py:243-255` / `gateway.py:176` — SSRC removal and remapping on member disconnect and reconnect - Library `voice_client.py:71-74` — a bot channel move destroys all decoders ## Proposed fix Track the last-seen SSRC per user id. When a packet arrives with a different SSRC than the one recorded for that user, discard and recreate that user's decoder before decoding. Log the reset at INFO with the old and new SSRC so rejoin frequency is observable. Also handle the interleaving case: the jitter-buffer tail of the *old* SSRC can drain after the new stream has started. With wall-clock anchoring both are placed correctly by time, so the only requirement is that the decoder used matches the SSRC of the packet being decoded — keep a decoder per `(uid, ssrc)` if that proves simpler than resetting. ## Acceptance criteria - [ ] A user's decoder is reset (or per-SSRC) when their SSRC changes - [ ] Decoder resets are logged with old and new SSRC - [ ] A unit test feeds two SSRCs for one user and asserts the second stream decodes cleanly - [ ] Old-SSRC tail packets arriving after a rejoin do not corrupt the new stream
Author
Contributor

Shipped — closing as part of a v3.11.5 bookkeeping sweep.

Evidence at bot/questboard_bot/cogs/recording.py:261, in the decode path:

through the old decoder garbles its first frames (#322)

The decoder is reset when a speaker's SSRC changes, so a mid-session renegotiation no longer feeds Opus frames to a decoder holding the previous stream's state.

Shipped — closing as part of a v3.11.5 bookkeeping sweep. Evidence at `bot/questboard_bot/cogs/recording.py:261`, in the decode path: > through the old decoder garbles its first frames (#322) The decoder is reset when a speaker's SSRC changes, so a mid-session renegotiation no longer feeds Opus frames to a decoder holding the previous stream's state.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#322
No description provided.