🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

fix(audio): give remote tracks a consumer again, so calls are not silent - #42

Merged
ralyodio merged 1 commit into
masterfrom
fix/remote-audio-silence
Aug 9, 2026
Merged

fix(audio): give remote tracks a consumer again, so calls are not silent#42
ralyodio merged 1 commit into
masterfrom
fix/remote-audio-silence

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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 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 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:

  • host hears viewers — useWebRTCHostAPI / useWebRTCHostSFUAPI ontrack
  • viewer hears host — VideoViewer

The 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:

  • The shared AudioContext now 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.
  • amplifyRemoteAudio logs 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 a play() that returns no promise does not throw.
  • desktop 51/51 suites (508 tests), web 62/62 suites (593 tests), typecheck and lint clean, both apps build.

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

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>
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

159 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 48 | LOW: 106

Severity Rule Location
HIGH secret-generic-credential apps/livekit/fly.toml:12
HIGH manifest-typosquat apps/mobile/package.json:43
HIGH secret-generic-credential apps/turn/fly.toml:11
HIGH secret-generic-credential docs/API.md:648
HIGH secret-generic-credential docs/API.md:654
MEDIUM insecure-temp-file apps/desktop/src/main/streaming/index.test.ts:64
MEDIUM sql-template-interpolation apps/desktop/src/renderer/components/capture/CameraBubble.tsx:129
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:370
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:393
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:402
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:438
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:191
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:165
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:128
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM manifest-install-lifecycle-script package.json:54
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:57
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:72
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:88
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:108
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:128
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:151
MEDIUM sql-template-interpolation scripts/lib/package-managers/apt.ts:55
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:208
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:313
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:310
MEDIUM sql-template-interpolation scripts/lib/package-managers/aur.ts:341
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:341
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:264
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:288
MEDIUM sql-template-interpolation scripts/lib/package-managers/gentoo.ts:274
MEDIUM sql-template-interpolation scripts/lib/package-managers/homebrew.ts:152
MEDIUM sql-template-interpolation scripts/lib/package-managers/nix.ts:225
MEDIUM sql-template-interpolation scripts/lib/package-managers/rpm.ts:55
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:201
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:261
MEDIUM sql-template-interpolation scripts/lib/package-managers/scoop.ts:145
MEDIUM js-shell-exec-interpolation scripts/release.mjs:145
MEDIUM js-shell-exec-interpolation scripts/release.mjs:146
MEDIUM js-shell-exec-interpolation scripts/release.mjs:147
MEDIUM sql-template-interpolation scripts/supabase-update-emails.ts:101

…and 109 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit ca3711e into master Aug 9, 2026
13 checks passed
@ralyodio
ralyodio deleted the fix/remote-audio-silence branch August 9, 2026 15:36
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.

1 participant