Skip to content

fix(publish): stop audio capture on mute, publishing silence in its place#2186

Closed
fperex wants to merge 1 commit into
moq-dev:devfrom
fperex:split/publish-mute
Closed

fix(publish): stop audio capture on mute, publishing silence in its place#2186
fperex wants to merge 1 commit into
moq-dev:devfrom
fperex:split/publish-mute

Conversation

@fperex

@fperex fperex commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Split out of #2163.

Muting used to tear the whole audio pipeline down. It stopped the mic track, closed the AudioContext, the worklet and the AudioEncoder, closed the audio track producer, and cascaded the catalog to drop the audio section entirely. Two consequences: every watcher had to rebuild its audio subtree (AudioContext + worklet + ring buffer) on every mute and unmute, and the resulting head-of-line contention on the shared WebSocket froze the video for a moment. Muting your mic should not stutter your video for everyone watching.

Mute now gates the audio input, not the pipeline. Per source:

  • A microphone track is stopped outright, so the OS releases the device and the recording indicator goes off. That is the entire point of a mute, and the previous soft-mute-by-gain approach (which I tried first) fails it: the mic stays live and the indicator stays lit while the UI claims you are muted.
  • Screen and file audio is detached from the encoder instead, because stopping a share track would kill the share itself and force a re-prompt.

A silent ConstantSourceNode keeps the worklet pulled while no input is attached, so the encoder keeps publishing true silence on the same clock and the catalog never changes shape. Selecting a source while muted never touches the microphone at all; unmuting acquires it.

Also scopes the four source-proxy effects to the child effect rather than the root, so switching source tears its proxies down in order instead of leaking a root-scoped writer over the next source.

Public API changes: none. muted keeps its type and name; its behavior changes as described (documented in the README and /doc).

Test plan: just js check green. The behavior was proven end-to-end with an ad hoc headless-Chrome run (fake media device via CDP; not a committed test, the repo has no browser-test harness) against the fork's integrated branch, whose js/ tree is byte-identical to these split PRs combined:

  • selecting a source while muted performs zero getUserMedia calls;
  • every mute ends the mic track (readyState: "ended");
  • the audio catalog stays present and shape-identical across mute cycles;
  • exactly one AudioContext spans the whole cycle, proving the pipeline survives the mute.

…lace

Muting used to tear the whole audio pipeline down: it stopped the mic track,
closed the AudioContext, worklet and AudioEncoder, closed the audio track
producer, and cascaded the catalog to drop the audio section entirely. The
watcher then had to rebuild its audio subtree (AudioContext, worklet, ring
buffer) on every mute and unmute, and the resulting head-of-line contention on
the shared WebSocket froze video for a moment.

Mute now gates the audio INPUT, not the pipeline. Per source: a microphone
track is stopped outright, so the OS releases the device and the recording
indicator goes off (that is the point of a mute); screen and file audio is
detached from the encoder instead, since stopping a share track would kill the
share and force a re-prompt. A silent ConstantSourceNode keeps the worklet
pulled, so the encoder keeps publishing true silence on the same clock and the
catalog never changes shape. Selecting a source while muted never touches the
microphone at all; unmuting acquires it.

Also scope the four source-proxy effects to the child effect rather than the
root, so switching source tears its proxies down in order instead of leaking a
root-scoped writer over the next source.

@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

Why make this change? Why publish 128Kb/s or whatever instead of nothing...? DTX will reduce it but yeah.

Two consequences: every watcher had to rebuild its audio subtree (AudioContext + worklet + ring buffer) on every mute and unmute, and the resulting head-of-line contention on the shared WebSocket froze the video for a moment. Muting your mic should not stutter your video for everyone watching.

Fix that then if reinitializing the audio context is expensive. We can cache it.

@kixelated kixelated closed this Jul 12, 2026
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