From 4aec5c34f3db6e550f7bbf6d1b94fe5a16bc5e7a Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:10:14 +0200 Subject: [PATCH] Firefox audio output issues fix (#1510) --------- Signed-off-by: Timo K --- src/livekit/MediaDevicesContext.tsx | 41 +++++++++++++++++++---------- src/room/GroupCallView.tsx | 3 ++- src/settings/SettingsModal.tsx | 4 ++- src/settings/useSetting.ts | 8 ++++-- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index b109d3b9..38e30214 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -28,6 +28,7 @@ import { createMediaDeviceObserver } from "@livekit/components-core"; import { Observable } from "rxjs"; import { + isFirefox, useAudioInput, useAudioOutput, useVideoInput, @@ -65,7 +66,8 @@ function useObservableState( function useMediaDevice( kind: MediaDeviceKind, fallbackDevice: string | undefined, - usingNames: boolean + usingNames: boolean, + alwaysDefault: boolean = false ): MediaDevice { // Make sure we don't needlessly reset to a device observer without names, // once permissions are already given @@ -86,18 +88,19 @@ function useMediaDevice( const available = useObservableState(deviceObserver, []); const [selectedId, select] = useState(fallbackDevice); - return useMemo( - () => ({ + return useMemo(() => { + const devId = available.some((d) => d.deviceId === selectedId) + ? selectedId + : available.some((d) => d.deviceId === fallbackDevice) + ? fallbackDevice + : available.at(0)?.deviceId; + + return { available, - selectedId: available.some((d) => d.deviceId === selectedId) - ? selectedId - : available.some((d) => d.deviceId === fallbackDevice) - ? fallbackDevice - : available.at(0)?.deviceId, + selectedId: alwaysDefault ? undefined : devId, select, - }), - [available, selectedId, fallbackDevice, select] - ); + }; + }, [available, selectedId, fallbackDevice, select, alwaysDefault]); } const deviceStub: MediaDevice = { @@ -120,10 +123,17 @@ interface Props { } export const MediaDevicesProvider: FC = ({ children }) => { - // Counts the number of callers currently using device names + // Counts the number of callers currently using device names. const [numCallersUsingNames, setNumCallersUsingNames] = useState(0); const usingNames = numCallersUsingNames > 0; + // Use output device names for output devices on all platforms except FF. + const useOutputNames = usingNames && !isFirefox(); + + // Setting the audio device to sth. else than 'undefined' breaks echo-cancellation + // and even can introduce multiple different output devices for one call. + const alwaysUseDefaultAudio = isFirefox(); + const [audioInputSetting, setAudioInputSetting] = useAudioInput(); const [audioOutputSetting, setAudioOutputSetting] = useAudioOutput(); const [videoInputSetting, setVideoInputSetting] = useVideoInput(); @@ -136,7 +146,8 @@ export const MediaDevicesProvider: FC = ({ children }) => { const audioOutput = useMediaDevice( "audiooutput", audioOutputSetting, - usingNames + useOutputNames, + alwaysUseDefaultAudio ); const videoInput = useMediaDevice( "videoinput", @@ -150,7 +161,9 @@ export const MediaDevicesProvider: FC = ({ children }) => { }, [setAudioInputSetting, audioInput.selectedId]); useEffect(() => { - if (audioOutput.selectedId !== undefined) + // Skip setting state for ff output. Redundent since it is set to always return 'undefined' + // but makes it clear while debugging that this is not happening on FF. + perf ;) + if (audioOutput.selectedId !== undefined && !isFirefox()) setAudioOutputSetting(audioOutput.selectedId); }, [setAudioOutputSetting, audioOutput.selectedId]); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 21a29b92..a9db1bb7 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -224,7 +224,8 @@ export function GroupCallView({ leaveRTCSession(rtcSession); if (widget) { - // we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked. + // we need to wait until the callEnded event is tracked on posthog. + // Otherwise the iFrame gets killed before the callEnded event got tracked. await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms widget.api.setAlwaysOnScreen(false); PosthogAnalytics.instance.logout(); diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index a0e14ee9..5f01e4ce 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -35,6 +35,7 @@ import { useDeveloperSettingsTab, useShowConnectionStats, useEnableE2EE, + isFirefox, } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; @@ -130,7 +131,8 @@ export const SettingsModal = (props: Props) => { } > {generateDeviceSelection(devices.audioInput, t("Microphone"))} - {generateDeviceSelection(devices.audioOutput, t("Speaker"))} + {!isFirefox() && + generateDeviceSelection(devices.audioOutput, t("Speaker"))} ); diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 53253116..1ab9781d 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -58,8 +58,12 @@ export const getSetting = (name: string, defaultValue: T): T => { export const setSetting = (name: string, newValue: T) => setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue)); -const canEnableSpatialAudio = () => { +export const isFirefox = () => { const { userAgent } = navigator; + return userAgent.includes("Firefox"); +}; + +const canEnableSpatialAudio = () => { // Spatial audio means routing audio through audio contexts. On Chrome, // this bypasses the AEC processor and so breaks echo cancellation. // We only allow spatial audio to be enabled on Firefox which we know @@ -69,7 +73,7 @@ const canEnableSpatialAudio = () => { // widely enough, we can allow spatial audio everywhere. It's currently in a // chrome flag, so we could enable this in Electron if we enabled the chrome flag // in the Electron wrapper. - return userAgent.includes("Firefox"); + return isFirefox(); }; export const useSpatialAudio = (): DisableableSetting => {