diff --git a/examples/demo/demo.ts b/examples/demo/demo.ts index 3659f65e3d..944dba7d64 100644 --- a/examples/demo/demo.ts +++ b/examples/demo/demo.ts @@ -46,7 +46,12 @@ import { import PTWorker from '../../src/packetTrailer/worker/packetTrailer.worker?worker'; import type { DataTrackFrame } from '../../src/room/data-track/frame'; import { TrackEvent } from '../../src/room/events'; -import { isSVCCodec, sleep, supportsH265 } from '../../src/room/utils'; +import { + isSVCCodec, + isSafariSpeakerSelectionSupported, + sleep, + supportsH265, +} from '../../src/room/utils'; setLogLevel(LogLevel.debug); @@ -182,6 +187,9 @@ const appActions = { audioOutput: { deviceId: audioOutputId, }, + // Route remote audio through AudioContext on iOS 26 so the shared relay element + // handles setSinkId — without this, iOS silently ignores setSinkId on WebRTC tracks. + webAudioMix: isSafariSpeakerSelectionSupported(), publishDefaults: { simulcast, videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360], diff --git a/src/index.ts b/src/index.ts index d54746e7f1..5974ba5d88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,12 +53,14 @@ import { isLocalTrack, isRemoteParticipant, isRemoteTrack, + isSafariSpeakerSelectionSupported, isVideoCodec, isVideoTrack, supportsAV1, supportsAdaptiveStream, supportsAudioOutputSelection, supportsDynacast, + supportsSetSinkId, supportsVP9, } from './room/utils'; import { getBrowser } from './utils/browserParser'; @@ -130,12 +132,14 @@ export { getEmptyVideoStreamTrack, getLogger, isBrowserSupported, + isSafariSpeakerSelectionSupported, setLogExtension, setLogLevel, supportsAV1, supportsAdaptiveStream, supportsAudioOutputSelection, supportsDynacast, + supportsSetSinkId, supportsVP9, Mutex, isAudioCodec, diff --git a/src/room/Room.ts b/src/room/Room.ts index 4c1b47f713..c241776286 100644 --- a/src/room/Room.ts +++ b/src/room/Room.ts @@ -103,10 +103,12 @@ import { import { Future, createDummyVideoStreamTrack, + disposeSharedRelay, extractChatMessage, extractTranscriptionSegments, getDisconnectReasonFromConnectionError, getEmptyAudioStreamTrack, + getOrCreateSharedRelay, isBrowserSupported, isCloud, isLocalAudioTrack, @@ -114,6 +116,7 @@ import { isReactNative, isRemotePub, isSafariBased, + isSafariSpeakerSelectionSupported, isWeb, numberToBigInt, sleep, @@ -1446,14 +1449,28 @@ class Room extends (EventEmitter as new () => TypedEmitter) if (success && isMuted) shouldTriggerImmediateDeviceChange = true; } else if (kind === 'audiooutput') { shouldTriggerImmediateDeviceChange = true; - if ( - (!supportsSetSinkId() && !this.options.webAudioMix) || - (this.options.webAudioMix && this.audioContext && !('setSinkId' in this.audioContext)) - ) { + // True when we can route output via AudioContext.setSinkId directly, e.g., + // Chrome / Edge / Firefox + webAudioMix : true (use AudioContext.setSinkId) + // Safari macOS (any version) : false (AudioContext.setSinkId not implemented) + // Safari iOS (any version) : false (AudioContext.setSinkId not implemented) + // any browser without webAudioMix : false + const audioContextHasSinkId = + this.options.webAudioMix && !!this.audioContext && 'setSinkId' in this.audioContext; + // True when we route output via the shared relay