Skip to content

feat(realtime): add playRemoteAudio connect option to gate audio playback#147

Open
nagar-decart wants to merge 3 commits into
mainfrom
fix/optional-remote-audio-playback
Open

feat(realtime): add playRemoteAudio connect option to gate audio playback#147
nagar-decart wants to merge 3 commits into
mainfrom
fix/optional-remote-audio-playback

Conversation

@nagar-decart
Copy link
Copy Markdown
Contributor

@nagar-decart nagar-decart commented May 29, 2026

Summary

  • Add opt-in playRemoteAudio: boolean to both client.realtime.connect() and client.realtime.subscribe(). Default false.
  • When false, audio tracks the model emits are dropped on the client: no playback element is attached, and the track is not added to the stream passed to onRemoteStream.
  • When true, current behavior is preserved (auto-attach + included in remoteStream).
  • Video tracks are untouched.

Why

livekit-client's track.attach() creates a hidden, un-muted <audio> element per subscribed audio track and plays it. That element is independent of any <video muted> an app wires up via onRemoteStream — muting the app's video element does not silence the audio playback.

Apps that previously assumed "no audio output unless I unmute my own <video>" started hearing unexpected playback when the upstream service began publishing audio tracks (e.g. for models that echo input audio or generate speech). The right gate is in the SDK: only the SDK knows the difference between "an audio track was delivered" and "the app actually wants to play it." Apps that do want audio opt in.

Behavior matrix

playRemoteAudio unset / false playRemoteAudio: true
track.attach() for audio skipped called (default LK behavior)
Audio in MediaStream from onRemoteStream omitted included
Video handling unchanged unchanged

Default rationale

Default is false — audio is an explicit opt-in. Defaulting to true would be a silent breaking change for apps that already exist: they'd start hearing audio they never asked for and that they tried to silence with <video muted>. False matches the implicit "audio is off unless I ask for it" behavior most existing apps already rely on.

Test plan

  • Added unit tests in tests/realtime.unit.test.ts:
    • drops remote audio tracks by default — no attach, not added to remoteStream (and verifies video still flows through in the same session)
    • plays remote audio tracks when playRemoteAudio is true
  • pnpm test — 208/208 pass
  • pnpm typecheck — clean
  • pnpm build — clean

Notes / follow-ups

  • The audio track is still subscribed by livekit-client and consumes bandwidth even when playRemoteAudio: false. A future optimization could unsubscribe at the publication level (publication.setSubscribed(false) on RoomEvent.TrackPublished) to also save bandwidth, but that's a larger change and orthogonal to silencing.
  • The same gate is applied to both realtime.connect() (publisher) and realtime.subscribe() (viewer) so audio behavior is consistent across both API surfaces.

🤖 Generated with Claude Code


Note

Low Risk
Client-side media gating with safe default false; no auth or server contract changes beyond optional connect/subscribe options and documented behavior.

Overview
Adds an opt-in playRemoteAudio flag (default false) on client.realtime.connect() and subscribe(), threaded through StreamSession into MediaChannel (and the subscribe LiveKit handler).

When playRemoteAudio is unset or false, subscribed remote audio from the inference server is ignored: track.attach() is not called and the audio MediaStreamTrack is not added to the stream delivered via onRemoteStream, so LiveKit’s hidden playback element does not play model/mic echo audio. true restores prior behavior (attach + audio in the remote stream). Video handling is unchanged.

Unit tests cover default-off audio dropping vs video still flowing, and playRemoteAudio: true enabling audio attach and stream inclusion.

Reviewed by Cursor Bugbot for commit c89a1c1. Bugbot is set up for automated code reviews on this repo. Configure here.

…back

livekit-client's track.attach() auto-creates a hidden <audio> element for
every subscribed audio track and plays it — independent of any <video muted>
element the app wires up to onRemoteStream. After API#1764 enabled V2V
audio passthrough server-side, apps started hearing their own mic echoed
back even though they kept the same <video muted> playback element that
previously silenced everything.

Add an opt-in `playRemoteAudio: boolean` (default `false`). When false, the
TrackSubscribed handler skips both track.attach() and the addTrack into
remoteStream for audio tracks, so neither the SDK nor the app can
accidentally route remote audio to a speaker. Apps that want server-emitted
audio (avatars, V2V echo, voice-driven sessions) opt in with `true`.

Default is false to restore the prior implicit "no audio output" behavior
that apps relied on before the server-side passthrough flip — turning it on
silently would be a breaking change for every existing app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@decartai/sdk@147

commit: c89a1c1

nagar-decart and others added 2 commits May 29, 2026 13:58
The publisher-side gate added in the parent commit only covers
realtime.connect(). realtime.subscribe() has the same unconditional
track.attach() on subscribed audio — viewer clients hit the same
surprise audio playback when the inference server publishes audio.

Mirror the gate exactly: SubscribeOptions gets `playRemoteAudio?: boolean`
(default false); the TrackSubscribed handler skips attach + remoteStream
addTrack for audio when unset/false. Same semantics, same default, same
opt-in story across both API surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop internal-only context from the comments — server-side PR
numbers, internal model family names, the internal product name for
the upstream service, and livekit-client implementation details.
Describe only what the option does from a consumer's standpoint:
when set to false the track is dropped (no playback element attached,
not added to remoteStream); set true when the model emits audio you
want heard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nagar-decart nagar-decart force-pushed the fix/optional-remote-audio-playback branch from f849128 to c89a1c1 Compare May 29, 2026 11:03
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