RESOLVED FIXED320087
iOS Safari: `setSinkId()` returns success but audio output doesn't actually switch on single-track WebRTC MediaStream. Works correctly with dual-track streams.
https://bugs.webkit.org/show_bug.cgi?id=320087
Summary iOS Safari: `setSinkId()` returns success but audio output doesn't actually s...
ernst.zhu
Reported 2026-07-23 02:37:07 PDT
# iOS Safari WebRTC Speaker Switch Bug Report ## Bug Summary `setSinkId()` returns success but audio output doesn't actually switch on single-track WebRTC MediaStream. Works correctly with dual-track streams. ## Reproducible Test Case Test page: `https://okmimi521.github.io/Media/ios_webrtc_speaker_test/` ## Detailed Description ### Problem On iOS Safari, when an `HTMLAudioElement` receives a remote MediaStream via WebRTC containing only ONE audio track, calling `setSinkId()` returns a resolved Promise (appears successful) but the audio output device does NOT actually switch. However, with TWO or more audio tracks in the same WebRTC stream, `setSinkId()` works correctly and audio actually routes to the selected device. ### Technical Details **Failing Scenario (Silent Failure):** ```javascript // Single track from WebRTC remotePeer.ontrack = (event) => { const stream = event.streams[0]; // MediaStream with 1 audio track audioElement.srcObject = stream; // Promise resolves but audio doesn't switch: await audioElement.setSinkId(deviceId); // ✅ Promise resolves console.log(audioElement.sinkId); // Shows new deviceId // ❌ But audio still plays from old device! }; ``` **Working Scenario:** ```javascript // Dual track from WebRTC remotePeer.ontrack = (event) => { const stream = event.streams[0]; // MediaStream with 2 audio tracks audioElement.srcObject = stream; // Promise resolves AND audio actually switches: await audioElement.setSinkId(deviceId); // ✅ Promise resolves console.log(audioElement.sinkId); // Shows new deviceId // ✅ Audio correctly plays from new device! }; ``` ### Key Observations 1. **Silent failure**: `setSinkId()` Promise resolves successfully, `audioElement.sinkId` property updates to the new deviceId, but the actual audio routing does NOT change 2. **Only affects WebRTC streams**: Local MediaStreams from `getUserMedia()` or file-based streams work fine with single track 3. **No console errors**: The bug is completely silent - no exceptions, no warnings ### Steps to Reproduce 1. Open test page on iOS Safari: `https://localhost/ios_webrtc_speaker_test/index.html` 2. Click **"Send SINGLE Track"** button - Creates a WebRTC peer connection - Loads one audio file and creates MediaStream with 1 track - Transmits via `RTCPeerConnection.addTrack()` - Receiver gets stream via `ontrack` event 3. Wait for connection to establish 4. Click **"🎤 Get Microphone Permission"** (optional, for full device labels) 5. Click **"Refresh Speakers"** 6. Select a different speaker from dropdown 7. **Observe**: - Console shows: `✓ setSinkId() SUCCEEDED` - `audioElement.sinkId` shows the new deviceId - **BUT audio still plays from the original device** (no actual device switch) - Put device near ear to verify audio is still from earpiece/speaker/headphones 8. Click **"Send DUAL Track"** button - Same process but with 2 audio tracks 9. Refresh speakers and select different device 10. **Observe**: - Console shows: `✓ setSinkId() SUCCEEDED` - `audioElement.sinkId` shows the new deviceId - **Audio actually switches to the selected device** ✓ ### Expected Result `setSinkId()` should successfully change the audio output device regardless of the number of audio tracks in the WebRTC MediaStream. Both the Promise resolution and the actual audio routing should succeed. ### Actual Result - **Single track WebRTC stream**: - ✅ `setSinkId()` Promise resolves - ✅ `audioElement.sinkId` property updates - ❌ Audio continues playing from the ORIGINAL device (no actual device switch) - **Dual track WebRTC stream**: - ✅ `setSinkId()` Promise resolves - ✅ `audioElement.sinkId` property updates - ✅ Audio correctly plays from the NEW device ### Environment - **Device**: iPhone 15 - **iOS Version**: iOS 26.5 - **WebRTC**: Standard RTCPeerConnection API ### Impact - **Severity**: High (Silent Failure - No Error Indication) - **Affected Users**: Any iOS Safari users in WebRTC applications trying to switch audio output devices (e.g., video conferencing apps, VoIP apps) - **User Experience**: Users THINK they switched devices (UI shows success) but audio doesn't actually change. This is worse than an error because users don't know something is wrong. - **Real-world scenario**: User tries to switch from earpiece to speakerphone during a WebRTC call, sees "success", but audio still plays from earpiece. Very confusing UX. ### Related APIs - `HTMLMediaElement.setSinkId()` - `RTCPeerConnection` - `MediaStream` - `MediaStreamTrack` ### Console Output **No errors are thrown!** This is a silent bug: ```javascript // Single track - appears successful but doesn't work await audioElement.setSinkId('speaker-device-id'); console.log(audioElement.sinkId); // Output: "speaker-device-id" ✅ // But audio still plays from earpiece ❌ // Dual track - actually works await audioElement.setSinkId('speaker-device-id'); console.log(audioElement.sinkId); // Output: "speaker-device-id" ✅ // Audio correctly plays from speaker ✅ ``` **Testing Method**: 1. Switch from earpiece to speaker 2. Put iPhone near your ear 3. If you can still hear clearly = bug present (audio didn't switch) 4. If audio is faint/distant = working correctly (audio switched to speaker) ### Additional Testing Tested with: - ✅ File → MediaStream → Audio element (single track): **Works** - ✅ getUserMedia() → Audio element (single track): **Works** - ❌ WebRTC → Audio element (single track): **Fails** - ✅ WebRTC → Audio element (dual track): **Works** This isolates the issue to WebRTC MediaStreams with single track only. --- ## For Apple Engineers ### Suspected Root Cause Likely an issue in WebKit's audio routing logic that assumes WebRTC streams should have multiple tracks, or a bug in the iOS audio session management when handling single-track remote streams. ### Suggested Investigation Areas 1. `WebCore/Modules/webaudio/MediaStreamAudioDestinationNode` 2. `Source/WebCore/platform/audio/ios/AudioDestinationIOS` 3. `RTCAudioSession` handling in WebKit 4. Audio routing logic in `HTMLMediaElement::setSinkId()` for remote tracks ### Test Automation This bug should be reproducible in WebKit layout tests with: - Create local RTCPeerConnection loopback - Single track vs dual track scenarios - Call setSinkId() on receiver side - Assert device change succeeds --- **Date**: 2026-07-23 **Reported by**: Zoom **Contact**: ernst.zhu@zoom.us
Attachments
ernst.zhu
Comment 1 2026-07-23 02:38:01 PDT
open
Alexey Proskuryakov
Comment 2 2026-07-23 09:14:00 PDT
*** Bug 320086 has been marked as a duplicate of this bug. ***
Radar WebKit Bug Importer
Comment 3 2026-07-24 10:59:10 PDT
youenn fablet
Comment 4 2026-07-29 00:32:02 PDT
EWS
Comment 5 2026-07-29 04:20:45 PDT
Committed 318149@main (bdaffe35a57e): <https://commits.webkit.org/318149@main> Reviewed commits have been landed. Closing PR #70428 and removing active labels.
ernst.zhu
Comment 6 2026-07-29 19:36:35 PDT
Hi everyone. I noticed that this bug has been fixed. Could you please let me know in which version of Safari it will be released? Thanks!!!
Note You need to log in before you can comment on or make changes to this bug.