fix(audio): give remote tracks a consumer again, so calls are not silent - #42
Merged
Conversation
Nobody could hear anybody, in either direction, since v0.9.49. That release put a realtime gain stage in front of playback so a quiet talker could be made louder than an element's 1.0 ceiling. It also made Web Audio the *only* thing attached to the remote track: the element now plays the graph's output, and nothing at all holds the raw track. A track arriving over a peer connection is only pulled while something consumes it, and in Chromium a MediaStreamAudioSourceNode does not count. The receiver stayed parked, the source node read silence, and the graph faithfully amplified it. Video was unaffected, which is why this looked like a microphone problem rather than a playback one. The raw track gets a hidden element again. It is muted and stays muted -- an audible second copy of every participant is exactly the double- playback echo removed in v0.9.48 -- and muting also sidesteps autoplay policy, which never blocks a silent element. The element is held by the returned handle rather than left to float, since a collectable element would take the consumer away again, and it is released on dispose. The shared AudioContext now also retries resume() on the next pointer or key event. Participants are wired up once, when they join, so a context that was gesture-locked at that moment had nothing to un-suspend it later -- the same silence by a different route. jsdom implements neither play() nor pause(), so the test setup supplies them; the same stubs keep the existing playback tests from logging a jsdomError. Calls to play() are wrapped rather than chained, because the method predates its own promise and still returns undefined on some stacks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan159 finding(s) HIGH/CRITICAL: 5 | MEDIUM: 48 | LOW: 106
…and 109 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nobody can hear anybody, in either direction, since v0.9.49.
What broke
v0.9.49 put a realtime gain stage in front of playback (
amplifyRemoteAudio: source → gain → limiter → destination stream → element) so a quiet talker could be pushed past an element's 1.0 ceiling. In doing so it made Web Audio the only thing attached to the remote track — the element now plays the graph's output, and nothing holds the raw track any more.A track arriving over a peer connection is only pulled while something consumes it, and in Chromium a
MediaStreamAudioSourceNodedoes not count. The receiver stayed parked, the source node read silence, and the graph faithfully amplified it. Video was unaffected, which is why this presented as a microphone problem rather than a playback one.Both directions go through the same helper, which is why it is silent both ways:
useWebRTCHostAPI/useWebRTCHostSFUAPIontrackVideoViewerThe fix
The raw track gets a hidden element again. It is muted and stays muted — an audible second copy of every participant is exactly the double-playback echo removed in v0.9.48 — and muting also sidesteps autoplay policy, which never blocks a silent element. The element is held by the returned handle rather than left to float, since a collectable element would take the consumer away again, and it is released on
dispose().The change is inside
amplifyRemoteAudio, so every call site is covered at once: desktop host, desktop viewer, web host, web viewer, and both SFU paths. Mobile is untouched — react-native-webrtc does not use this module.Two smaller things in the same area:
AudioContextnow retriesresume()on the next pointer or key event. Participants are wired up once, when they join, so a context that was gesture-locked at that moment had nothing to un-suspend it later — the same silence by a different route.amplifyRemoteAudiologs track and context state on attach. Silent playback is otherwise hard to tell apart from nobody talking.Testing
remoteAudioGain.test.ts: 13 tests, 4 new — the element is attached to the raw track, it is muted, it is released on dispose, and aplay()that returns no promise does not throw.Not verified in a live call — this environment has no display and no GTK libraries, so neither Electron nor Chromium will start here. Worth a two-party smoke test before release.
🤖 Generated with Claude Code