feat(realtime): add playRemoteAudio connect option to gate audio playback#147
Open
nagar-decart wants to merge 3 commits into
Open
feat(realtime): add playRemoteAudio connect option to gate audio playback#147nagar-decart wants to merge 3 commits into
nagar-decart wants to merge 3 commits into
Conversation
…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>
commit: |
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>
f849128 to
c89a1c1
Compare
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.
Summary
playRemoteAudio: booleanto bothclient.realtime.connect()andclient.realtime.subscribe(). Defaultfalse.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 toonRemoteStream.true, current behavior is preserved (auto-attach + included inremoteStream).Why
livekit-client'strack.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 viaonRemoteStream— 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
playRemoteAudiounset /falseplayRemoteAudio: truetrack.attach()for audioMediaStreamfromonRemoteStreamDefault rationale
Default is
false— audio is an explicit opt-in. Defaulting totruewould 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
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 truepnpm test— 208/208 passpnpm typecheck— cleanpnpm build— cleanNotes / follow-ups
playRemoteAudio: false. A future optimization could unsubscribe at the publication level (publication.setSubscribed(false)onRoomEvent.TrackPublished) to also save bandwidth, but that's a larger change and orthogonal to silencing.realtime.connect()(publisher) andrealtime.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
playRemoteAudioflag (defaultfalse) onclient.realtime.connect()andsubscribe(), threaded throughStreamSessionintoMediaChannel(and the subscribe LiveKit handler).When
playRemoteAudiois unset orfalse, subscribed remote audio from the inference server is ignored:track.attach()is not called and the audioMediaStreamTrackis not added to the stream delivered viaonRemoteStream, so LiveKit’s hidden playback element does not play model/mic echo audio.truerestores 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: trueenabling 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.