Skip to content

fix(watch): survive an audio decoder error, resample, and re-anchor on resubscribe#2189

Closed
fperex wants to merge 2 commits into
moq-dev:devfrom
fperex:split/watch-audio-decode
Closed

fix(watch): survive an audio decoder error, resample, and re-anchor on resubscribe#2189
fperex wants to merge 2 commits into
moq-dev:devfrom
fperex:split/watch-audio-decode

Conversation

@fperex

@fperex fperex commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Split out of #2163.

Stacked on #2182 (the hang container consumer uses isStreamAbort). The first commit belongs to that PR; review the second commit only. The diff collapses once #2182 merges and I rebase.

Audio had the same fatal-decoder problem as video (#2187): an AudioDecoder that errored never decoded again, so the stream went silent until a reload. It restarts in place on a budget now, with the same config-supersession check.

Plus three fixes for crackle and drift, all of which I hit on Safari but which are not Safari-specific:

  • Resample when the rates differ. If the catalog rate and the AudioContext rate disagree, samples were written into a ring built at the wrong rate, which garbles playback outright.
  • Snap timestamps onto the frame cadence. A jittery per-frame timestamp makes the timestamp-indexed ring zero-fill or overwrite a sample every single frame, which is audible as a constant crackle.
  • Re-anchor the discontinuity counter on resubscribe. Audio.Decoder is built once and its #discontinuity outlives the subscription, but the container Consumer's rewind counter is per-consumer and restarts at zero. So after any rewind had bumped the decoder's count, the next resubscribe delivered 0, which looked like a rewind and reset the Sync reference shared with video, desyncing audio from video for no reason.

Also resumes the AudioContext on a user gesture rather than assuming it starts in running.

Public API changes: @moq/watch gains AudioBuffer.wait(timestamp, signal?) (additive).

Test plan: bun test js/watch/src/audio js/hang/src/container: includes a test pinning that a fresh/resubscribed Consumer starts its rewind counter at zero (the bug above), plus the timestamp-snapping math. just js check green.

fperex added 2 commits July 11, 2026 22:14
Every run loop logged any error from a stream as a warning, so a normal
unsubscribe, a publisher handover or a peer closing the session produced
scary warnings on every viewer that stopped watching.

isStreamAbort() decides on the application code the relay encodes
(WebTransportError.streamErrorCode natively, or the trailing number in
qmux's RESET_STREAM/STOP_SENDING and Connection closed messages on the
WebSocket fallback). Coded faults are a denylist, so an unknown code is
treated as routine teardown while a client-actionable fault (auth,
not-found, protocol, unroutable) still surfaces at warn.

Call sites in the lite/ietf publishers, the subscriber and the connection
run loop now pick their log level from it. The bandwidth probe streams no
longer swallow genuine faults at debug.
…n resubscribe

Audio had the same fatal-decoder problem as video: an AudioDecoder that errored
never decoded again, so the stream went silent until a reload. Restart it in
place on a budget, with the same config-supersession check.

Also fix three sources of crackle and drift:

- Resample when the catalog rate and the AudioContext rate differ, instead of
  writing samples into a ring built at the wrong rate.
- Snap timestamps onto the frame cadence so a jittery per-frame timestamp does
  not zero-fill or overwrite a sample every frame.
- Re-anchor the discontinuity counter on resubscribe. Audio.Decoder is built
  once and its #discontinuity outlives the subscription, but the container
  Consumer's rewind counter is per-consumer and restarts at zero, so after any
  rewind the next resubscribe delivered 0, looked like a rewind, and reset the
  Sync reference shared with video.

Resume the AudioContext on a user gesture rather than assuming it starts running.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @fperex, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@kixelated

Copy link
Copy Markdown
Collaborator

dio had the same fatal-decoder problem as video (#2187): an AudioDecoder that errored never decoded again, so the stream went silent until a reload. It restarts in place on a budget now, with the same config-supersession check.

How did this happen? Blindly retrying is the wrong fix, please try to reproduce the original bug instead.

@kixelated kixelated closed this Jul 12, 2026
@fperex fperex deleted the split/watch-audio-decode branch July 12, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants