diff --git a/e2e/tests/start_call.spec.ts b/e2e/tests/start_call.spec.ts index 7fbe33349..a25def0d6 100644 --- a/e2e/tests/start_call.spec.ts +++ b/e2e/tests/start_call.spec.ts @@ -246,10 +246,10 @@ test.describe('setting audio input device', () => { await page.locator('#calls-widget-audio-input-button').click(); await expect(page.locator('#calls-widget-audio-inputs-menu')).toBeVisible(); - let currentAudioInputDevice = await page.evaluate(() => { + const currentAudioInputDeviceID = await page.evaluate(() => { return window.callsClient.currentInputAudioDevice?.deviceId; }); - if (currentAudioInputDevice) { + if (currentAudioInputDeviceID) { test.fail(); return; } @@ -257,10 +257,10 @@ test.describe('setting audio input device', () => { await page.locator('#calls-widget-audio-inputs-menu button:has-text("Fake Audio Input 1")').click(); await expect(page.locator('#calls-widget-audio-inputs-menu')).toBeHidden(); - currentAudioInputDevice = await page.evaluate(() => { - return window.callsClient.currentAudioInputDevice?.deviceId; + const currentAudioInputDevice = await page.evaluate(() => { + return window.callsClient.currentAudioInputDevice; }); - if (!currentAudioInputDevice) { + if (currentAudioInputDevice.label !== 'Fake Audio Input 1') { test.fail(); return; } @@ -272,7 +272,7 @@ test.describe('setting audio input device', () => { const currentAudioInputDevice2 = await page.evaluate(() => { return window.callsClient.currentAudioInputDevice?.deviceId; }); - if (currentAudioInputDevice2 !== currentAudioInputDevice) { + if (currentAudioInputDevice2 !== currentAudioInputDevice.deviceId) { test.fail(); return; } @@ -280,10 +280,10 @@ test.describe('setting audio input device', () => { await devPage.leaveCall(); await page.reload(); - const deviceID = await page.evaluate(() => { - return window.localStorage.getItem('calls_default_audio_input'); + const device = await page.evaluate(() => { + return JSON.parse(window.localStorage.getItem('calls_default_audio_input')!); }); - if (!deviceID) { + if (!device || !device.deviceId || !device.label) { test.fail(); } }); @@ -316,10 +316,10 @@ test.describe('setting audio output device', () => { await page.locator('#calls-widget-audio-output-button').click(); await expect(page.locator('#calls-widget-audio-outputs-menu')).toBeVisible(); - let currentAudioOutputDevice = await page.evaluate(() => { + const currentAudioOutputDeviceID = await page.evaluate(() => { return window.callsClient.currentAudioOutputDevice?.deviceId; }); - if (currentAudioOutputDevice) { + if (currentAudioOutputDeviceID) { test.fail(); return; } @@ -327,10 +327,10 @@ test.describe('setting audio output device', () => { await page.locator('#calls-widget-audio-outputs-menu button:has-text("Fake Audio Output 1")').click(); await expect(page.locator('#calls-widget-audio-outputs-menu')).toBeHidden(); - currentAudioOutputDevice = await page.evaluate(() => { - return window.callsClient.currentAudioOutputDevice?.deviceId; + const currentAudioOutputDevice = await page.evaluate(() => { + return window.callsClient.currentAudioOutputDevice; }); - if (!currentAudioOutputDevice) { + if (currentAudioOutputDevice.label !== 'Fake Audio Output 1') { test.fail(); return; } @@ -342,7 +342,7 @@ test.describe('setting audio output device', () => { const currentAudioOutputDevice2 = await page.evaluate(() => { return window.callsClient.currentAudioOutputDevice?.deviceId; }); - if (currentAudioOutputDevice2 !== currentAudioOutputDevice) { + if (currentAudioOutputDevice2 !== currentAudioOutputDevice.deviceId) { test.fail(); return; } @@ -350,10 +350,10 @@ test.describe('setting audio output device', () => { await devPage.leaveCall(); await page.reload(); - const deviceID = await page.evaluate(() => { - return window.localStorage.getItem('calls_default_audio_output'); + const device = await page.evaluate(() => { + return JSON.parse(window.localStorage.getItem('calls_default_audio_output')!); }); - if (!deviceID) { + if (!device || !device.deviceId || !device.label) { test.fail(); } }); diff --git a/webapp/src/client.ts b/webapp/src/client.ts index 3d181b952..06e038978 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -107,17 +107,51 @@ export default class CallsClient extends EventEmitter { }; } - const defaultInputID = window.localStorage.getItem(STORAGE_CALLS_DEFAULT_AUDIO_INPUT_KEY); - const defaultOutputID = window.localStorage.getItem(STORAGE_CALLS_DEFAULT_AUDIO_OUTPUT_KEY); - if (defaultInputID && !this.currentAudioInputDevice) { - const devices = this.audioDevices.inputs.filter((dev) => { - return dev.deviceId === defaultInputID; + let defaultInputDevice: {deviceId: string; label?: string} = { + deviceId: '', + }; + const defaultAudioInputData = window.localStorage.getItem(STORAGE_CALLS_DEFAULT_AUDIO_INPUT_KEY); + if (defaultAudioInputData) { + try { + defaultInputDevice = JSON.parse(defaultAudioInputData); + } catch { + // Backwards compatibility case when we used to store the device id directly (before MM-63274). + defaultInputDevice = { + deviceId: defaultAudioInputData, + }; + } + } + + let defaultOutputDevice: {deviceId: string; label?: string} = { + deviceId: '', + }; + const defaultAudioOutputData = window.localStorage.getItem(STORAGE_CALLS_DEFAULT_AUDIO_OUTPUT_KEY); + if (defaultAudioOutputData) { + try { + defaultOutputDevice = JSON.parse(defaultAudioOutputData); + } catch { + // Backwards compatibility case when we used to store the device id directly (before MM-63274). + defaultOutputDevice = { + deviceId: defaultAudioOutputData, + }; + } + } + + if (defaultInputDevice.deviceId && !this.currentAudioInputDevice) { + let devices = this.audioDevices.inputs.filter((dev) => { + return dev.deviceId === defaultInputDevice.deviceId || dev.label === defaultInputDevice.label; }); + if (devices.length > 1) { + // If there are multiple devices with the same label, we select the default device by ID. + logInfo('multiple audio input devices found with the same label, checking by id', devices); + devices = devices.filter((dev) => dev.deviceId === defaultInputDevice.deviceId); + } + if (devices && devices.length === 1) { logDebug(`found default audio input device to use: ${devices[0].label}`); audioOptions.deviceId = { - exact: defaultInputID, + exact: devices[0].deviceId, }; this.currentAudioInputDevice = devices[0]; } else { @@ -126,11 +160,17 @@ export default class CallsClient extends EventEmitter { } } - if (defaultOutputID) { - const devices = this.audioDevices.outputs.filter((dev) => { - return dev.deviceId === defaultOutputID; + if (defaultOutputDevice.deviceId) { + let devices = this.audioDevices.outputs.filter((dev) => { + return dev.deviceId === defaultOutputDevice.deviceId || dev.label === defaultOutputDevice.label; }); + if (devices.length > 1) { + // If there are multiple devices with the same label, we select the default device by ID. + logInfo('multiple audio output devices found with the same label, checking by id', devices); + devices = devices.filter((dev) => dev.deviceId === defaultInputDevice.deviceId); + } + if (devices && devices.length === 1) { logDebug(`found default audio output device to use: ${devices[0].label}`); this.currentAudioOutputDevice = devices[0]; @@ -423,7 +463,7 @@ export default class CallsClient extends EventEmitter { return; } - window.localStorage.setItem(STORAGE_CALLS_DEFAULT_AUDIO_INPUT_KEY, device.deviceId); + window.localStorage.setItem(STORAGE_CALLS_DEFAULT_AUDIO_INPUT_KEY, JSON.stringify(device)); this.currentAudioInputDevice = device; // We emit this event so it's easier to keep state in sync between widget and pop out. @@ -473,7 +513,7 @@ export default class CallsClient extends EventEmitter { if (!this.peer) { return; } - window.localStorage.setItem(STORAGE_CALLS_DEFAULT_AUDIO_OUTPUT_KEY, device.deviceId); + window.localStorage.setItem(STORAGE_CALLS_DEFAULT_AUDIO_OUTPUT_KEY, JSON.stringify(device)); this.currentAudioOutputDevice = device; // We emit this event so it's easier to keep state in sync between widget and pop out. diff --git a/webapp/src/components/user_settings/audio_devices_settings_section.tsx b/webapp/src/components/user_settings/audio_devices_settings_section.tsx index 7582b24e9..e18c25be2 100644 --- a/webapp/src/components/user_settings/audio_devices_settings_section.tsx +++ b/webapp/src/components/user_settings/audio_devices_settings_section.tsx @@ -4,6 +4,10 @@ import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; import ReactSelect from 'react-select'; +import { + STORAGE_CALLS_DEFAULT_AUDIO_INPUT_KEY, + STORAGE_CALLS_DEFAULT_AUDIO_OUTPUT_KEY, +} from 'src/constants'; import {logErr} from 'src/log'; import styled from 'styled-components'; @@ -41,19 +45,39 @@ const AudioDevicesSelection = forwardRef { + return dev.deviceId === defaultDevice.deviceId || dev.label === defaultDevice.label; + }); + if (selected.length > 1) { + // If there are multiple devices with the same label, we select the default device by ID. + selected = selected.filter((dev) => dev.deviceId === defaultDevice.deviceId); + } + if (selected.length > 0) { + return { + label: selected[0].label, + value: selected[0].deviceId, + }; + } + return { label: devices[0]?.label ?? '', value: devices[0]?.deviceId ?? '', @@ -122,10 +146,17 @@ export default function AudioDevicesSettingsSection() { const handleSave = () => { if (audioInputsRef.current) { - window.localStorage.setItem('calls_default_audio_input', audioInputsRef.current.getOption().value); + window.localStorage.setItem(STORAGE_CALLS_DEFAULT_AUDIO_INPUT_KEY, JSON.stringify({ + deviceId: audioInputsRef.current.getOption().value, + label: audioInputsRef.current.getOption().label, + })); } if (audioOutputsRef.current) { - window.localStorage.setItem('calls_default_audio_output', audioOutputsRef.current.getOption().value); + window.localStorage.setItem(STORAGE_CALLS_DEFAULT_AUDIO_OUTPUT_KEY, JSON.stringify({ + deviceId: audioOutputsRef.current.getOption().value, + label: audioOutputsRef.current.getOption().label, + }, + )); } setActive(false); };