Skip to content

Add Safari speaker selection support - #1635

Open
fkwp wants to merge 5 commits into
livekit:mainfrom
fkwp:fkwp/isSafariSpeakerSelectionSupported
Open

fkwp wants to merge 5 commits into
livekit:mainfrom
fkwp:fkwp/isSafariSpeakerSelectionSupported

Conversation

@fkwp

@fkwp fkwp commented Aug 27, 2025

Copy link
Copy Markdown

Support iOS 26 Speaker Selection API

Apple added HTMLMediaElement.setSinkId() in iOS/iPadOS 26 and Safari 26. However, calling it directly on <audio> elements backed by WebRTC remote tracks is silently ignored. It seems iOS renders that audio through the WebRTC engine's internal pipeline (→ AVAudioSession), bypassing the AVPlayer pipeline that setSinkId controls.

This PR adds a shared WebAudio relay element per AudioContext: all remote tracks mix into one MediaStreamAudioDestinationNode, played via a single <audio> element on which setSinkId is called once during the user gesture in switchActiveDevice. iOS grants the permission once for the lifetime of that element, so participants joining later route correctly without their own user gesture.

RTCPeerConnection remote track A ─┐
RTCPeerConnection remote track B ─┼─→ shared MediaStreamDestinationNode ─→ shared ← setSinkId() here
RTCPeerConnection remote track C ─┘
(each via AudioContext + GainNode)

Changes

  • isSafariSpeakerSelectionSupported() — detects iOS/Safari 26+
  • supportsSetSinkId() — returns true for iOS 26+ (skips feature-detection that misfires before DOM attachment)
  • getOrCreateSharedRelay() / disposeSharedRelay() — lazy AudioContext-scoped relay setup
  • RemoteAudioTrack.connectWebAudio routes the gain node to the shared destination when AudioContext.setSinkId is unavailable
  • Room.switchActiveDevice calls setSinkId on the shared relay for the iOS 26 path; per-participant setAudioOutput path retained for Chrome (echo cancellation) and other
    browsers
  • selectDefaultDevices / handleDeviceChange no longer block audiooutput auto-switching on iOS 26
  • supportsSetSinkId exported alongside isSafariSpeakerSelectionSupported

fixes #1568

My testing stack is https://github.com/livekit-examples/meet as reference app and meanwhile the following stack is working on my end:

@changeset-bot

changeset-bot Bot commented Aug 27, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9110173

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@CLAassistant

CLAassistant commented Aug 27, 2025

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@fkwp fkwp changed the title Add Safari speaker selection supported Add Safari speaker selection support Aug 27, 2025

@lukasIO lukasIO left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

I believe the speaker selection API comes with some more caveats (explicit user permission must be granted for output selection if no microphone is acquired yet).

I haven't tested this, but probably needs its own selection path via MediaDevices.selectAudioOutput ?

Comment thread src/room/utils.ts Outdated
return (
(browser?.name === 'Safari' && compareVersions(browser.version, '26') >= 0) ||
(browser?.os === 'iOS' && !!browser?.osVersion && compareVersions(browser.osVersion, '26') >= 0) ||
(browser?.os === 'macOS' && !!browser?.osVersion && compareVersions(browser.osVersion, '26') >= 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check is not connected to checking against Safari, I believe?

@fkwp
fkwp force-pushed the fkwp/isSafariSpeakerSelectionSupported branch 3 times, most recently from 13aa615 to d2eab63 Compare May 12, 2026 21:49
@fkwp
fkwp force-pushed the fkwp/isSafariSpeakerSelectionSupported branch from f779352 to 2b09b51 Compare May 15, 2026 09:19
@1egoman

1egoman commented May 18, 2026

Copy link
Copy Markdown
Contributor

@fkwp If it's helpful, a few months back I spent a bit of time attempting to further adapt your changes to get ios speaker selection to work. I wasn't able to get it all the way there (and the effort has since been deprioritized), but maybe some of this code could be useful in your effort here: https://github.com/livekit/client-sdk-js/tree/ios-speaker-selection

@fkwp

fkwp commented May 18, 2026

Copy link
Copy Markdown
Author

@fkwp If it's helpful, a months back I spent a bit of time attempting to further adapt your changes to get ios speaker selection to work. I wasn't able to get it all the way there (and the effort has since been deprioritized), but maybe some of this code could be useful in your effort here: https://github.com/livekit/client-sdk-js/tree/ios-speaker-selection

@1egoman nice Ill definitely will look into this. I used https://github.com/livekit-examples/meet as reference app and meanwhile the following stack is working on my end (given you also publish audio):

@1egoman

1egoman commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Per comment left in #1836, some more notes from when I attempted to dig into this on my own, in case they would be helpful to anybody else attempting to introduce speaker selection support:


I was able to get the api working on my test iphone 11, but was NOT able to get the api to work when I integrated it within livekit-client.

Importantly, if I add a button into the demo app which on tap reaches into the DOM, gets the active audio element playing remote participant audio, and manually calls setSinkId on it, this works and works every single time without fail. So the APIs definitely exist on my test iphone.

A few things that might be part of the problem with integrating into livekit-client:

  • setSinkId has a requirement in that it must be triggered by a user gesture. Right now the setSinkId implementation in livekit-client isn't always (at least based off of my minimal testing) triggered in a user gesture path in the demo. I made some changes in my WIP branch (added an "apply" button next to the output device selector select element as onChange doesn't count as a user gesture on ios apparently) and was I think able to fix this but it needs some more thorough testing.

  • I have a suspicion there's some sort of race condition going on. I can do the same exact sequence of events (ie, connect, switch from "default speaker" -> "receiver" -> "speaker") and get different results each time. There's something going on that makes this non deterministic and I (or a LLM) couldn't figure it out on my own.

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.

Add support for iOS26 Speaker Selection API

4 participants