diff --git a/src/livekit/useLivekit.ts b/src/livekit/useLivekit.ts index b7032645..24e0ca29 100644 --- a/src/livekit/useLivekit.ts +++ b/src/livekit/useLivekit.ts @@ -323,16 +323,18 @@ export function useLivekit( useEffect(() => { // Sync the requested devices with LiveKit's devices - if ( - room !== undefined && - connectionState === ConnectionState.Connected && - !controlledAudioDevices - ) { + if (room !== undefined && connectionState === ConnectionState.Connected) { const syncDevice = ( kind: MediaDeviceKind, selected$: Observable, ): Subscription => selected$.subscribe((device) => { + logger.info( + "[LivekitRoom] syncDevice room.getActiveDevice(kind) !== d.id :", + room.getActiveDevice(kind), + " !== ", + device?.id, + ); if ( device !== undefined && room.getActiveDevice(kind) !== device.id @@ -347,7 +349,9 @@ export function useLivekit( const subscriptions = [ syncDevice("audioinput", devices.audioInput.selected$), - syncDevice("audiooutput", devices.audioOutput.selected$), + !controlledAudioDevices + ? syncDevice("audiooutput", devices.audioOutput.selected$) + : undefined, syncDevice("videoinput", devices.videoInput.selected$), // Restart the audio input track whenever we detect that the active media // device has changed to refer to a different hardware device. We do this @@ -387,7 +391,7 @@ export function useLivekit( ]; return (): void => { - for (const s of subscriptions) s.unsubscribe(); + for (const s of subscriptions) s?.unsubscribe(); }; } }, [room, devices, connectionState, controlledAudioDevices]); diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 16ba171a..1f4b8e82 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -86,6 +86,14 @@ export function useMuteStates(isJoined: boolean): MuteStates { const audio = useMuteState(devices.audioInput, () => { return Config.get().media_devices.enable_audio && !skipLobby && !isJoined; }); + useEffect(() => { + // If audio is enabled, we need to request the device names again, + // because iOS will not be able to switch to the correct device after un-muting. + // This is one of the main changes that makes iOS work with bluetooth audio devices. + if (audio.enabled) { + devices.requestDeviceNames(); + } + }, [audio.enabled, devices]); const isEarpiece = useIsEarpiece(); const video = useMuteState( devices.videoInput, diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index e9b86d99..bd2a7607 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -1326,7 +1326,7 @@ export class CallViewModel extends ViewModel { (available, selected) => { const selectionType = selected && available.get(selected.id)?.type; - // If we are in any output mode other than spaeker switch to speaker. + // If we are in any output mode other than speaker switch to speaker. const newSelectionType: "earpiece" | "speaker" = selectionType === "speaker" ? "earpiece" : "speaker"; const newSelection = [...available].find( diff --git a/src/state/MediaDevices.ts b/src/state/MediaDevices.ts index ef60dd24..3a09bf91 100644 --- a/src/state/MediaDevices.ts +++ b/src/state/MediaDevices.ts @@ -258,6 +258,15 @@ class AudioOutput class ControlledAudioOutput implements MediaDevice { + // We need to subscribe to the raw devices so that the OS does update the input + // back to what it was before. otherwise we will switch back to the default + // whenever we allocate a new stream. + public readonly availableRaw$ = availableRawDevices$( + "audiooutput", + this.usingNames$, + this.scope, + ); + public readonly available$ = this.scope.behavior( combineLatest( [controlledAvailableOutputDevices$.pipe(startWith([])), iosDeviceMenu$], @@ -309,14 +318,17 @@ class ControlledAudioOutput ), ); - public constructor(private readonly scope: ObservableScope) { + public constructor( + private readonly usingNames$: Behavior, + private readonly scope: ObservableScope, + ) { this.selected$.subscribe((device) => { // Let the hosting application know which output device has been selected. // This information is probably only of interest if the earpiece mode has // been selected - for example, Element X iOS listens to this to determine // whether it should enable the proximity sensor. if (device !== undefined) { - logger.info("[controlled-output] setAudioDeviceSelect called:", device); + logger.info("[controlled-output] onAudioDeviceSelect called:", device); window.controls.onAudioDeviceSelect?.(device.id); // Also invoke the deprecated callback for backward compatibility window.controls.onOutputDeviceSelect?.(device.id); @@ -325,6 +337,9 @@ class ControlledAudioOutput this.available$.subscribe((available) => { logger.info("[controlled-output] available devices:", available); }); + this.availableRaw$.subscribe((availableRaw) => { + logger.info("[controlled-output] available raw devices:", availableRaw); + }); } } @@ -385,7 +400,7 @@ export class MediaDevices { AudioOutputDeviceLabel, SelectedAudioOutputDevice > = getUrlParams().controlledAudioDevices - ? new ControlledAudioOutput(this.scope) + ? new ControlledAudioOutput(this.usingNames$, this.scope) : new AudioOutput(this.usingNames$, this.scope); public readonly videoInput: MediaDevice =