From 85d6beb4f21c6bb83dc105cbe423ce6ef6c212a1 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Mon, 15 Apr 2024 17:04:42 +0200 Subject: [PATCH 1/3] fix(media): refactor preferences functions - fix registering only last device when populate with several - fix selecting last available device in list when join (instead of last used | first available) - move console.log from function for testing purposes - add types, enum, refactor functions - drop 'promote' option as unused (we register promoted device in another place) Signed-off-by: Maksim Sukharev --- src/services/mediaDevicePreferences.ts | 72 ++++++++++++++----------- src/utils/webrtc/MediaDevicesManager.js | 44 ++++++++------- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/services/mediaDevicePreferences.ts b/src/services/mediaDevicePreferences.ts index 6693e7948b..04a33dc667 100644 --- a/src/services/mediaDevicePreferences.ts +++ b/src/services/mediaDevicePreferences.ts @@ -20,33 +20,49 @@ * */ +type InputId = string | undefined | null +type InputListUpdated = MediaDeviceInfo[] | null +type InputLists = { + newAudioInputList: InputListUpdated, + newVideoInputList: InputListUpdated, +} +type Attributes = { + devices: MediaDeviceInfo[], + audioInputId: InputId, + videoInputId: InputId, +} + +enum DeviceKind { + AudioInput = 'audioinput', + VideoInput = 'videoinput', + AudioOutput = 'audiooutput', +} + /** * List all registered devices in order of their preferences * Show whether device is currently unplugged or selected, if information is available * - * @param devices list of available devices - * @param audioInputId id of currently selected audio input - * @param videoInputId id of currently selected video input + * @param attributes MediaDeviceManager attributes * @param audioInputList list of registered audio devices in order of preference * @param videoInputList list of registered video devices in order of preference + * @return {string} preference list in readable format */ -function listMediaDevices(devices: MediaDeviceInfo[], audioInputId: string, videoInputId: string, audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]): void { - const availableDevices = devices.map(device => device.deviceId).filter(id => id !== 'default') +function listMediaDevices(attributes: Attributes, audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]): string { + const availableDevices = attributes.devices.map(device => device.deviceId) const getDeviceString = (device: MediaDeviceInfo, index: number) => { const isUnplugged = !availableDevices.includes(device.deviceId) ? ' (unplugged)' : '' const isSelected = () => { - if (device.kind === 'audioinput') { - return device.deviceId === audioInputId ? ' (selected)' : '' - } else if (device.kind === 'videoinput') { - return device.deviceId === videoInputId ? ' (selected)' : '' + if (device.kind === DeviceKind.AudioInput) { + return device.deviceId === attributes.audioInputId ? ' (selected)' : '' + } else if (device.kind === DeviceKind.VideoInput) { + return device.deviceId === attributes.videoInputId ? ' (selected)' : '' } } return ` ${index + 1}. ${device.label} | ${device.deviceId}` + isUnplugged + isSelected() } - // eslint-disable-next-line no-console - console.log(`Media devices: + return (`Media devices: Audio input: ${audioInputList.map(getDeviceString).join('\n')} @@ -75,20 +91,11 @@ function getFirstAvailableMediaDevice(devices: MediaDeviceInfo[], inputList: Med * * @param device device * @param devicesList list of registered devices in order of preference - * @param promote whether device should be promoted (to be used in updateMediaDevicesPreferences) - * @return updated devices list + * @return {MediaDeviceInfo[]} updated devices list */ -function registerNewMediaDevice(device: MediaDeviceInfo, devicesList: MediaDeviceInfo[], promote: boolean = false): MediaDeviceInfo[] { - const newDevicesList = devicesList.slice() +function registerNewMediaDevice(device: MediaDeviceInfo, devicesList: MediaDeviceInfo[]): MediaDeviceInfo[] { console.debug('Registering new device:', device) - - if (promote) { - newDevicesList.unshift(device) - } else { - newDevicesList.push(device) - } - - return newDevicesList + return [...devicesList, device] } /** @@ -105,9 +112,9 @@ function registerNewMediaDevice(device: MediaDeviceInfo, devicesList: MediaDevic * @param devices list of available devices * @param inputList list of registered audio/video devices in order of preference * @param inputId id of currently selected input - * @return updated devices list (null, if it has not been changed) + * @return {InputListUpdated} updated devices list (null, if it has not been changed) */ -function promoteMediaDevice(devices: MediaDeviceInfo[], inputList: MediaDeviceInfo[], inputId: string | null) { +function promoteMediaDevice(devices: MediaDeviceInfo[], inputList: MediaDeviceInfo[], inputId: InputId): InputListUpdated { const newInputList = inputList.slice() // Get the index of the first plugged device @@ -149,22 +156,22 @@ function promoteMediaDevice(devices: MediaDeviceInfo[], inputList: MediaDeviceIn * @param devices list of available devices * @param audioInputList list of registered audio devices in order of preference * @param videoInputList list of registered video devices in order of preference - * @return object with updated devices lists (null, if they have not been changed) + * @return {InputLists} object with updated devices lists (null, if they have not been changed) */ -function populateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]) { +function populateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]): InputLists { let newAudioInputList = null let newVideoInputList = null for (const device of devices) { - if (device.kind === 'audioinput') { + if (device.deviceId && device.kind === DeviceKind.AudioInput) { // Add to the list of known devices if (device.deviceId !== 'default' && !audioInputList.some(input => input.deviceId === device.deviceId)) { - newAudioInputList = registerNewMediaDevice(device, audioInputList) + newAudioInputList = registerNewMediaDevice(device, newAudioInputList ?? audioInputList) } - } else if (device.kind === 'videoinput') { + } else if (device.deviceId && device.kind === DeviceKind.VideoInput) { // Add to the list of known devices if (device.deviceId !== 'default' && !videoInputList.some(input => input.deviceId === device.deviceId)) { - newVideoInputList = registerNewMediaDevice(device, videoInputList) + newVideoInputList = registerNewMediaDevice(device, newVideoInputList ?? videoInputList) } } } @@ -186,8 +193,9 @@ function populateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputL * @param videoInputId id of currently selected video input * @param audioInputList list of registered audio devices in order of preference * @param videoInputList list of registered video devices in order of preference + * @return {InputLists} object with updated devices lists (null, if they have not been changed) */ -function updateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputId: string, videoInputId: string, audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]) { +function updateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputId: InputId, videoInputId: InputId, audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]): InputLists { return { newAudioInputList: promoteMediaDevice(devices, audioInputList, audioInputId), newVideoInputList: promoteMediaDevice(devices, videoInputList, videoInputId), diff --git a/src/utils/webrtc/MediaDevicesManager.js b/src/utils/webrtc/MediaDevicesManager.js index 5bbc20fdb8..f5ad527afc 100644 --- a/src/utils/webrtc/MediaDevicesManager.js +++ b/src/utils/webrtc/MediaDevicesManager.js @@ -210,16 +210,24 @@ MediaDevicesManager.prototype = { if (this.attributes.audioInputId === undefined) { if (BrowserStorage.getItem('audioInputId')) { // Couldn't find device by id - console.debug('Could not find previous audio device, falling back to default/first device in the list', BrowserStorage.getItem('audioInputId'), this.attributes.devices) + console.debug(`Could not find previous audio device (${BrowserStorage.getItem('audioInputId')}), falling back to first available device\n`, listMediaDevices( + this.attributes, + this._preferenceAudioInputList, + this._preferenceVideoInputList, + )) } - this.attributes.audioInputId = getFirstAvailableMediaDevice(devices, this._preferenceAudioInputList, this._fallbackAudioInputId) ?? 'default' + this.attributes.audioInputId = getFirstAvailableMediaDevice(devices, this._preferenceAudioInputList, 'default') } if (this.attributes.videoInputId === undefined) { if (BrowserStorage.getItem('videoInputId')) { // Couldn't find device by id, try the label - console.debug('Could not find previous video device, falling back to default/first device in the list', BrowserStorage.getItem('videoInputId'), this.attributes.devices) + console.debug(`Could not find previous video device (${BrowserStorage.getItem('videoInputId')}), falling back to first available device\n`, listMediaDevices( + this.attributes, + this._preferenceAudioInputList, + this._preferenceVideoInputList, + )) } - this.attributes.videoInputId = getFirstAvailableMediaDevice(devices, this._preferenceVideoInputList, this._fallbackVideoInputId) ?? 'default' + this.attributes.videoInputId = getFirstAvailableMediaDevice(devices, this._preferenceVideoInputList, 'default') } // Trigger change events after all the devices are processed to @@ -282,15 +290,17 @@ MediaDevicesManager.prototype = { * Show whether device is currently unplugged or selected, if information is available */ listDevices() { - navigator.mediaDevices.enumerateDevices().then(devices => { - listMediaDevices( - devices, - this.attributes.audioInputId, - this.attributes.videoInputId, - this._preferenceAudioInputList, - this._preferenceVideoInputList, - ) - }) + if (this.attributes.devices.length) { + console.info(listMediaDevices(this.attributes, this._preferenceAudioInputList, this._preferenceVideoInputList)) + } else { + navigator.mediaDevices.enumerateDevices().then(devices => { + console.info(listMediaDevices( + { devices, audioInputId: this.attributes.audioInputId, videoInputId: this.attributes.videoInputId }, + this._preferenceAudioInputList, + this._preferenceVideoInputList, + )) + }) + } }, _removeDevice(removedDevice) { @@ -363,20 +373,18 @@ MediaDevicesManager.prototype = { // Always refresh the known device with the latest values. this._knownDevices[addedDevice.kind + '-' + addedDevice.deviceId] = addedDevice - // Restore previously selected device if it becomes available again. + // Restore previously selected device (based on preferences list) if it becomes available again. // Additionally, set first available device as fallback, and override // any fallback previously set if the default device is added. if (addedDevice.kind === 'audioinput') { - if (BrowserStorage.getItem('audioInputId') === addedDevice.deviceId - || getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceAudioInputList, this._fallbackAudioInputId)) { + if (getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceAudioInputList) === addedDevice.deviceId) { this.attributes.audioInputId = addedDevice.deviceId } if (!this._fallbackAudioInputId || addedDevice.deviceId === 'default') { this._fallbackAudioInputId = addedDevice.deviceId } } else if (addedDevice.kind === 'videoinput') { - if (BrowserStorage.getItem('videoInputId') === addedDevice.deviceId - || getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceVideoInputList, this._fallbackVideoInputId)) { + if (getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceVideoInputList) === addedDevice.deviceId) { this.attributes.videoInputId = addedDevice.deviceId } if (!this._fallbackVideoInputId || addedDevice.deviceId === 'default') { From 41c6aa83c4c0d1815d5fb45f524c97664a6c638e Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Mon, 15 Apr 2024 17:11:35 +0200 Subject: [PATCH 2/3] fix(media): keep default devices in preferences list, remove fallback id logic Signed-off-by: Maksim Sukharev --- .../__tests__/mediaDevicePreferences.spec.js | 239 ++++++++++++++++++ src/services/mediaDevicePreferences.ts | 65 +++-- src/utils/webrtc/MediaDevicesManager.js | 77 ++---- 3 files changed, 285 insertions(+), 96 deletions(-) create mode 100644 src/services/__tests__/mediaDevicePreferences.spec.js diff --git a/src/services/__tests__/mediaDevicePreferences.spec.js b/src/services/__tests__/mediaDevicePreferences.spec.js new file mode 100644 index 0000000000..323c32ca7f --- /dev/null +++ b/src/services/__tests__/mediaDevicePreferences.spec.js @@ -0,0 +1,239 @@ +import { + getFirstAvailableMediaDevice, + listMediaDevices, + populateMediaDevicesPreferences, + updateMediaDevicesPreferences, +} from '../mediaDevicePreferences.ts' + +describe('mediaDevicePreferences', () => { + afterEach(() => { + // cleaning up the mess left behind the previous test + jest.clearAllMocks() + }) + + // navigator.enumerateDevices() will list 'default' capture device first + const audioInputDeviceDefault = { deviceId: 'default', groupId: 'def1234567890', kind: 'audioinput', label: 'Default' } + const audioInputDeviceA = { deviceId: 'da1234567890', groupId: 'ga1234567890', kind: 'audioinput', label: 'Audio Input Device A' } + const audioInputDeviceB = { deviceId: 'db1234567890', groupId: 'gb1234567890', kind: 'audioinput', label: 'Audio Input Device B' } + + const videoInputDeviceDefault = { deviceId: 'default', groupId: 'def4567890123', kind: 'videoinput', label: 'Default' } + const videoInputDeviceA = { deviceId: 'da4567890123', groupId: 'ga4567890123', kind: 'videoinput', label: 'Video Input Device A' } + const videoInputDeviceB = { deviceId: 'db4567890123', groupId: 'gb4567890123', kind: 'videoinput', label: 'Video Input Device B' } + + const audioOutputDeviceDefault = { deviceId: 'default', groupId: 'def7890123456', kind: 'audiooutput', label: 'Default' } + const audioOutputDeviceA = { deviceId: 'da7890123456', groupId: 'ga7890123456', kind: 'audiooutput', label: 'Audio Output Device A' } + const audioOutputDeviceB = { deviceId: 'db7890123456', groupId: 'gb7890123456', kind: 'audiooutput', label: 'Audio Output Device B' } + + const allDevices = [audioInputDeviceDefault, audioInputDeviceA, audioInputDeviceB, + videoInputDeviceDefault, videoInputDeviceA, videoInputDeviceB, + audioOutputDeviceDefault, audioOutputDeviceA, audioOutputDeviceB] + const audioInputPreferenceList = [audioInputDeviceDefault, audioInputDeviceA, audioInputDeviceB] + const videoInputPreferenceList = [videoInputDeviceDefault, videoInputDeviceA, videoInputDeviceB] + + describe('listMediaDevices', () => { + it('list all input devices from preference lists', () => { + const attributes = { + devices: allDevices, + audioInputId: undefined, + videoInputId: undefined, + } + const output = listMediaDevices( + attributes, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert: should show all registered devices, apart from default / outputs + const inputDevices = allDevices.filter(device => device.kind !== 'audiooutput' && device.deviceId !== 'default') + inputDevices.forEach(device => { + expect(output).toContain(device.deviceId) + }) + }) + + it('show selected devices from preference lists', () => { + const attributes = { + devices: allDevices, + audioInputId: audioInputDeviceA.deviceId, + videoInputId: videoInputDeviceA.deviceId, + } + const output = listMediaDevices( + attributes, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert: should show a label next to selected registered devices + const selectedDeviceIds = [audioInputDeviceA.deviceId, videoInputDeviceA.deviceId] + selectedDeviceIds.forEach(deviceId => { + expect(output).toContain(deviceId + ' (selected)') + }) + }) + + it('show unplugged devices from preference lists', () => { + const unpluggedDeviceIds = [audioInputDeviceA.deviceId, videoInputDeviceA.deviceId] + const attributes = { + devices: allDevices.filter(device => !unpluggedDeviceIds.includes(device.deviceId)), + audioInputId: undefined, + videoInputId: undefined, + } + const output = listMediaDevices( + attributes, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert: should show a label next to unplugged registered devices + unpluggedDeviceIds.forEach(deviceId => { + expect(output).toContain(deviceId + ' (unplugged)') + }) + }) + }) + + describe('getFirstAvailableMediaDevice', () => { + it('returns id of first available device from preference list', () => { + const output = getFirstAvailableMediaDevice( + allDevices, + audioInputPreferenceList, + ) + + // Assert: should return default id + expect(output).toBe('default') + }) + + it('returns id of first available device from preference list (default device is unavailable)', () => { + const output = getFirstAvailableMediaDevice( + allDevices.filter(device => device.deviceId !== 'default'), + audioInputPreferenceList, + ) + + // Assert: should return id of device A + expect(output).toBe(audioInputPreferenceList[1].deviceId) + }) + + it('returns undefined id if there is no available devices from preference list', () => { + const output = getFirstAvailableMediaDevice( + allDevices.filter(device => device.kind !== 'audioinput'), + audioInputPreferenceList, + ) + + // Assert: should return provided fallback id + expect(output).not.toBeDefined() + }) + }) + + describe('populateMediaDevicesPreferences', () => { + beforeEach(() => { + console.debug = jest.fn() + }) + + it('returns preference lists with all available devices', () => { + const output = populateMediaDevicesPreferences( + allDevices, + [], + [], + ) + + // Assert: should contain all available devices, apart from default / outputs + expect(output).toMatchObject({ newAudioInputList: audioInputPreferenceList, newVideoInputList: videoInputPreferenceList }) + }) + + it('returns null if preference lists were not updated', () => { + const output = populateMediaDevicesPreferences( + allDevices, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert + expect(output).toMatchObject({ newAudioInputList: null, newVideoInputList: null }) + }) + }) + + describe('updateMediaDevicesPreferences', () => { + it('returns null if preference lists were not updated (no id or default id provided)', () => { + const ids = [null, undefined, 'default'] + const getOutput = (id) => updateMediaDevicesPreferences( + allDevices, + id, + id, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert + ids.forEach(id => { + expect(getOutput(id)).toMatchObject({ newAudioInputList: null, newVideoInputList: null }) + }) + }) + + it('returns updated preference lists (device A id provided)', () => { + const attributes = { + devices: allDevices, + audioInputId: audioInputDeviceA.deviceId, + videoInputId: videoInputDeviceA.deviceId, + } + const output = updateMediaDevicesPreferences( + attributes, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert: should put device A on top of default device + expect(output).toMatchObject({ + newAudioInputList: [audioInputDeviceA, audioInputDeviceDefault, audioInputDeviceB], + newVideoInputList: [videoInputDeviceA, videoInputDeviceDefault, videoInputDeviceB], + }) + }) + + it('returns null if preference lists were not updated (device A id provided but not available)', () => { + const attributes = { + devices: allDevices.filter(device => !['da1234567890', 'da4567890123'].includes(device.deviceId)), + audioInputId: audioInputDeviceA.deviceId, + videoInputId: videoInputDeviceA.deviceId, + } + const output = updateMediaDevicesPreferences( + attributes, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert + expect(output).toMatchObject({ newAudioInputList: null, newVideoInputList: null }) + }) + + it('returns null if preference lists were not updated (all devices are not available)', () => { + const attributes = { + devices: allDevices.filter(device => !['audioinput', 'videoinput'].includes(device.kind)), + audioInputId: audioInputDeviceA.deviceId, + videoInputId: videoInputDeviceA.deviceId, + } + const output = updateMediaDevicesPreferences( + attributes, + audioInputPreferenceList, + videoInputPreferenceList, + ) + + // Assert + expect(output).toMatchObject({ newAudioInputList: null, newVideoInputList: null }) + }) + + it('returns updated preference lists (device B id provided, but not registered, default device and device A not available)', () => { + const attributes = { + devices: allDevices.filter(device => !['default', 'da1234567890', 'da4567890123'].includes(device.deviceId)), + audioInputId: audioInputDeviceB.deviceId, + videoInputId: videoInputDeviceB.deviceId, + } + const output = updateMediaDevicesPreferences( + attributes, + [audioInputDeviceDefault, audioInputDeviceA], + [videoInputDeviceDefault, videoInputDeviceA], + ) + + // Assert: should put device C on top of device B, but not the device A + expect(output).toMatchObject({ + newAudioInputList: [audioInputDeviceDefault, audioInputDeviceA, audioInputDeviceB], + newVideoInputList: [videoInputDeviceDefault, videoInputDeviceA, videoInputDeviceB], + }) + }) + }) +}) diff --git a/src/services/mediaDevicePreferences.ts b/src/services/mediaDevicePreferences.ts index 04a33dc667..88c8e56dab 100644 --- a/src/services/mediaDevicePreferences.ts +++ b/src/services/mediaDevicePreferences.ts @@ -74,16 +74,14 @@ ${videoInputList.map(getDeviceString).join('\n')} /** * Get the first available device from the preference list. * - * Returns id of device from the list / provided fallback id / 'default' id + * Returns id of device from the list * * @param devices list of available devices * @param inputList list of registered audio/video devices in order of preference - * @param [fallbackId] id of currently selected input - * @return first available (plugged) device id or fallback + * @return {string|undefined} first available (plugged) device id */ -function getFirstAvailableMediaDevice(devices: MediaDeviceInfo[], inputList: MediaDeviceInfo[], fallbackId?: string): string | undefined { - const availableDevices = devices.map(device => device.deviceId).filter(id => id !== 'default') - return inputList.find(device => availableDevices.includes(device.deviceId))?.deviceId ?? fallbackId +function getFirstAvailableMediaDevice(devices: MediaDeviceInfo[], inputList: MediaDeviceInfo[]): string | undefined { + return inputList.find(device => devices.some(d => d.kind === device.kind && d.deviceId === device.deviceId))?.deviceId } /** @@ -109,43 +107,44 @@ function registerNewMediaDevice(device: MediaDeviceInfo, devicesList: MediaDevic * * Returns changed preference lists for audio / video devices (null, if it hasn't been changed) * + * @param kind kind of device * @param devices list of available devices * @param inputList list of registered audio/video devices in order of preference * @param inputId id of currently selected input * @return {InputListUpdated} updated devices list (null, if it has not been changed) */ -function promoteMediaDevice(devices: MediaDeviceInfo[], inputList: MediaDeviceInfo[], inputId: InputId): InputListUpdated { - const newInputList = inputList.slice() +function promoteMediaDevice(kind: DeviceKind, devices: MediaDeviceInfo[], inputList: MediaDeviceInfo[], inputId: InputId): InputListUpdated { + if (!inputId) { + return null + } // Get the index of the first plugged device - const availableDevices = devices.map(device => device.deviceId).filter(id => id !== 'default') - const firstPluggedIndex = newInputList.findIndex(device => availableDevices.includes(device.deviceId)) - const insertPosition = firstPluggedIndex === -1 ? newInputList.length : firstPluggedIndex + const availableDevices = devices.filter(device => device.kind === kind) + const deviceToPromote = availableDevices.find(device => device.deviceId === inputId) + if (!deviceToPromote) { + return null + } + + const firstPluggedIndex = inputList.findIndex(device => availableDevices.some(d => d.deviceId === device.deviceId)) + const insertPosition = firstPluggedIndex === -1 ? inputList.length : firstPluggedIndex // Get the index of the currently selected device - const currentDevicePosition = newInputList.findIndex(device => device.deviceId === inputId) + const currentDevicePosition = inputList.findIndex(device => device.deviceId === inputId) if (currentDevicePosition === insertPosition) { // preferences list is unchanged return null } - let deviceToPromote = null - if (currentDevicePosition === -1 && inputId !== 'default' && inputId !== null) { - // If device was not registered in preferences list, get it from devices list - deviceToPromote = devices.find(device => device.deviceId === inputId) - } else if (currentDevicePosition > 0) { - // Otherwise extract it from preferences list - deviceToPromote = newInputList.splice(currentDevicePosition, 1)[0] - } + const newInputList = inputList.slice() - if (deviceToPromote) { - // Put the device at the new position - newInputList.splice(insertPosition, 0, deviceToPromote) - return newInputList - } else { - return null + if (currentDevicePosition > 0) { + // Extract promoted device it from preferences list + newInputList.splice(currentDevicePosition, 1) } + + newInputList.splice(insertPosition, 0, deviceToPromote) + return newInputList } /** @@ -165,12 +164,12 @@ function populateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputL for (const device of devices) { if (device.deviceId && device.kind === DeviceKind.AudioInput) { // Add to the list of known devices - if (device.deviceId !== 'default' && !audioInputList.some(input => input.deviceId === device.deviceId)) { + if (!audioInputList.some(input => input.deviceId === device.deviceId)) { newAudioInputList = registerNewMediaDevice(device, newAudioInputList ?? audioInputList) } } else if (device.deviceId && device.kind === DeviceKind.VideoInput) { // Add to the list of known devices - if (device.deviceId !== 'default' && !videoInputList.some(input => input.deviceId === device.deviceId)) { + if (!videoInputList.some(input => input.deviceId === device.deviceId)) { newVideoInputList = registerNewMediaDevice(device, newVideoInputList ?? videoInputList) } } @@ -188,17 +187,15 @@ function populateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputL * * Returns changed preference lists for audio / video devices (null, if it hasn't been changed) * - * @param devices list of available devices - * @param audioInputId id of currently selected audio input - * @param videoInputId id of currently selected video input + * @param attributes MediaDeviceManager attributes * @param audioInputList list of registered audio devices in order of preference * @param videoInputList list of registered video devices in order of preference * @return {InputLists} object with updated devices lists (null, if they have not been changed) */ -function updateMediaDevicesPreferences(devices: MediaDeviceInfo[], audioInputId: InputId, videoInputId: InputId, audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]): InputLists { +function updateMediaDevicesPreferences(attributes: Attributes, audioInputList: MediaDeviceInfo[], videoInputList: MediaDeviceInfo[]): InputLists { return { - newAudioInputList: promoteMediaDevice(devices, audioInputList, audioInputId), - newVideoInputList: promoteMediaDevice(devices, videoInputList, videoInputId), + newAudioInputList: promoteMediaDevice(DeviceKind.AudioInput, attributes.devices, audioInputList, attributes.audioInputId), + newVideoInputList: promoteMediaDevice(DeviceKind.VideoInput, attributes.devices, videoInputList, attributes.videoInputId), } } diff --git a/src/utils/webrtc/MediaDevicesManager.js b/src/utils/webrtc/MediaDevicesManager.js index f5ad527afc..ef3b3917b2 100644 --- a/src/utils/webrtc/MediaDevicesManager.js +++ b/src/utils/webrtc/MediaDevicesManager.js @@ -94,9 +94,6 @@ export default function MediaDevicesManager() { this._knownDevices = {} - this._fallbackAudioInputId = undefined - this._fallbackVideoInputId = undefined - const audioInputPreferences = BrowserStorage.getItem('audioInputPreferences') this._preferenceAudioInputList = audioInputPreferences !== null ? JSON.parse(audioInputPreferences) : [] @@ -191,6 +188,8 @@ MediaDevicesManager.prototype = { this._pendingEnumerateDevicesPromise = navigator.mediaDevices.enumerateDevices().then(devices => { const previousAudioInputId = this.attributes.audioInputId const previousVideoInputId = this.attributes.videoInputId + const previousFirstAvailableAudioInputId = getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceAudioInputList) + const previousFirstAvailableVideoInputId = getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceVideoInputList) const removedDevices = this.attributes.devices.filter(oldDevice => !devices.find(device => oldDevice.deviceId === device.deviceId && oldDevice.kind === device.kind)) const updatedDevices = devices.filter(device => this.attributes.devices.find(oldDevice => device.deviceId === oldDevice.deviceId && device.kind === oldDevice.kind)) @@ -206,28 +205,15 @@ MediaDevicesManager.prototype = { this._addDevice(addedDevice) }) - // Fallback in case we didn't find the previously picked device - if (this.attributes.audioInputId === undefined) { - if (BrowserStorage.getItem('audioInputId')) { - // Couldn't find device by id - console.debug(`Could not find previous audio device (${BrowserStorage.getItem('audioInputId')}), falling back to first available device\n`, listMediaDevices( - this.attributes, - this._preferenceAudioInputList, - this._preferenceVideoInputList, - )) - } - this.attributes.audioInputId = getFirstAvailableMediaDevice(devices, this._preferenceAudioInputList, 'default') + // Selecting preferred device in case it was removed/unplugged, or it is a first initialization after reload, + // or we add/plug preferred device and overwriting automatic selection + if (this.attributes.audioInputId === undefined || this.attributes.audioInputId === previousFirstAvailableAudioInputId) { + this.attributes.audioInputId = getFirstAvailableMediaDevice(devices, this._preferenceAudioInputList) + console.debug(listMediaDevices(this.attributes, this._preferenceAudioInputList, this._preferenceVideoInputList)) } - if (this.attributes.videoInputId === undefined) { - if (BrowserStorage.getItem('videoInputId')) { - // Couldn't find device by id, try the label - console.debug(`Could not find previous video device (${BrowserStorage.getItem('videoInputId')}), falling back to first available device\n`, listMediaDevices( - this.attributes, - this._preferenceAudioInputList, - this._preferenceVideoInputList, - )) - } - this.attributes.videoInputId = getFirstAvailableMediaDevice(devices, this._preferenceVideoInputList, 'default') + if (this.attributes.videoInputId === undefined || this.attributes.videoInputId === previousFirstAvailableVideoInputId) { + this.attributes.videoInputId = getFirstAvailableMediaDevice(devices, this._preferenceVideoInputList) + console.debug(listMediaDevices(this.attributes, this._preferenceAudioInputList, this._preferenceVideoInputList)) } // Trigger change events after all the devices are processed to @@ -268,9 +254,7 @@ MediaDevicesManager.prototype = { updatePreferences() { const { newAudioInputList, newVideoInputList } = updateMediaDevicesPreferences( - this.attributes.devices, - this.attributes.audioInputId, - this.attributes.videoInputId, + this.attributes, this._preferenceAudioInputList, this._preferenceVideoInputList, ) @@ -308,21 +292,10 @@ MediaDevicesManager.prototype = { if (removedDeviceIndex >= 0) { this.attributes.devices.splice(removedDeviceIndex, 1) } - - if (removedDevice.kind === 'audioinput') { - if (this._fallbackAudioInputId === removedDevice.deviceId) { - this._fallbackAudioInputId = getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceAudioInputList, undefined) - } - if (this.attributes.audioInputId === removedDevice.deviceId) { - this.attributes.audioInputId = getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceAudioInputList, this._fallbackAudioInputId) - } - } else if (removedDevice.kind === 'videoinput') { - if (this._fallbackVideoInputId === removedDevice.deviceId) { - this._fallbackVideoInputId = getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceVideoInputList, undefined) - } - if (this.attributes.videoInputId === removedDevice.deviceId) { - this.attributes.videoInputId = getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceVideoInputList, this._fallbackVideoInputId) - } + if (removedDevice.kind === 'audioinput' && removedDevice.deviceId === this.attributes.audioInputId) { + this.attributes.audioInputId = undefined + } else if (removedDevice.kind === 'videoinput' && removedDevice.deviceId === this.attributes.videoInputId) { + this.attributes.videoInputId = undefined } }, @@ -372,26 +345,6 @@ MediaDevicesManager.prototype = { // Always refresh the known device with the latest values. this._knownDevices[addedDevice.kind + '-' + addedDevice.deviceId] = addedDevice - - // Restore previously selected device (based on preferences list) if it becomes available again. - // Additionally, set first available device as fallback, and override - // any fallback previously set if the default device is added. - if (addedDevice.kind === 'audioinput') { - if (getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceAudioInputList) === addedDevice.deviceId) { - this.attributes.audioInputId = addedDevice.deviceId - } - if (!this._fallbackAudioInputId || addedDevice.deviceId === 'default') { - this._fallbackAudioInputId = addedDevice.deviceId - } - } else if (addedDevice.kind === 'videoinput') { - if (getFirstAvailableMediaDevice(this.attributes.devices, this._preferenceVideoInputList) === addedDevice.deviceId) { - this.attributes.videoInputId = addedDevice.deviceId - } - if (!this._fallbackVideoInputId || addedDevice.deviceId === 'default') { - this._fallbackVideoInputId = addedDevice.deviceId - } - } - this.attributes.devices.push(addedDevice) }, From 595558e4f9566717dddd6b6486cc1db296a2a8a2 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Mon, 15 Apr 2024 17:12:04 +0200 Subject: [PATCH 3/3] fix(media): add test coverage Signed-off-by: Maksim Sukharev --- .../__tests__/mediaDevicePreferences.spec.js | 85 ++++--------------- 1 file changed, 18 insertions(+), 67 deletions(-) diff --git a/src/services/__tests__/mediaDevicePreferences.spec.js b/src/services/__tests__/mediaDevicePreferences.spec.js index 323c32ca7f..9c0afd9eb2 100644 --- a/src/services/__tests__/mediaDevicePreferences.spec.js +++ b/src/services/__tests__/mediaDevicePreferences.spec.js @@ -32,16 +32,8 @@ describe('mediaDevicePreferences', () => { describe('listMediaDevices', () => { it('list all input devices from preference lists', () => { - const attributes = { - devices: allDevices, - audioInputId: undefined, - videoInputId: undefined, - } - const output = listMediaDevices( - attributes, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const attributes = { devices: allDevices, audioInputId: undefined, videoInputId: undefined } + const output = listMediaDevices(attributes, audioInputPreferenceList, videoInputPreferenceList) // Assert: should show all registered devices, apart from default / outputs const inputDevices = allDevices.filter(device => device.kind !== 'audiooutput' && device.deviceId !== 'default') @@ -51,16 +43,8 @@ describe('mediaDevicePreferences', () => { }) it('show selected devices from preference lists', () => { - const attributes = { - devices: allDevices, - audioInputId: audioInputDeviceA.deviceId, - videoInputId: videoInputDeviceA.deviceId, - } - const output = listMediaDevices( - attributes, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const attributes = { devices: allDevices, audioInputId: audioInputDeviceA.deviceId, videoInputId: videoInputDeviceA.deviceId } + const output = listMediaDevices(attributes, audioInputPreferenceList, videoInputPreferenceList) // Assert: should show a label next to selected registered devices const selectedDeviceIds = [audioInputDeviceA.deviceId, videoInputDeviceA.deviceId] @@ -76,11 +60,7 @@ describe('mediaDevicePreferences', () => { audioInputId: undefined, videoInputId: undefined, } - const output = listMediaDevices( - attributes, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const output = listMediaDevices(attributes, audioInputPreferenceList, videoInputPreferenceList) // Assert: should show a label next to unplugged registered devices unpluggedDeviceIds.forEach(deviceId => { @@ -91,10 +71,7 @@ describe('mediaDevicePreferences', () => { describe('getFirstAvailableMediaDevice', () => { it('returns id of first available device from preference list', () => { - const output = getFirstAvailableMediaDevice( - allDevices, - audioInputPreferenceList, - ) + const output = getFirstAvailableMediaDevice(allDevices, audioInputPreferenceList) // Assert: should return default id expect(output).toBe('default') @@ -110,7 +87,7 @@ describe('mediaDevicePreferences', () => { expect(output).toBe(audioInputPreferenceList[1].deviceId) }) - it('returns undefined id if there is no available devices from preference list', () => { + it('returns undefined if there is no available devices from preference list', () => { const output = getFirstAvailableMediaDevice( allDevices.filter(device => device.kind !== 'audioinput'), audioInputPreferenceList, @@ -127,22 +104,14 @@ describe('mediaDevicePreferences', () => { }) it('returns preference lists with all available devices', () => { - const output = populateMediaDevicesPreferences( - allDevices, - [], - [], - ) + const output = populateMediaDevicesPreferences(allDevices, [], []) // Assert: should contain all available devices, apart from default / outputs expect(output).toMatchObject({ newAudioInputList: audioInputPreferenceList, newVideoInputList: videoInputPreferenceList }) }) it('returns null if preference lists were not updated', () => { - const output = populateMediaDevicesPreferences( - allDevices, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const output = populateMediaDevicesPreferences(allDevices, audioInputPreferenceList, videoInputPreferenceList) // Assert expect(output).toMatchObject({ newAudioInputList: null, newVideoInputList: null }) @@ -152,13 +121,11 @@ describe('mediaDevicePreferences', () => { describe('updateMediaDevicesPreferences', () => { it('returns null if preference lists were not updated (no id or default id provided)', () => { const ids = [null, undefined, 'default'] - const getOutput = (id) => updateMediaDevicesPreferences( - allDevices, - id, - id, - audioInputPreferenceList, - videoInputPreferenceList, - ) + + const getOutput = (id) => { + const attributes = { devices: allDevices, audioInputId: id, videoInputId: id } + return updateMediaDevicesPreferences(attributes, audioInputPreferenceList, videoInputPreferenceList) + } // Assert ids.forEach(id => { @@ -167,16 +134,8 @@ describe('mediaDevicePreferences', () => { }) it('returns updated preference lists (device A id provided)', () => { - const attributes = { - devices: allDevices, - audioInputId: audioInputDeviceA.deviceId, - videoInputId: videoInputDeviceA.deviceId, - } - const output = updateMediaDevicesPreferences( - attributes, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const attributes = { devices: allDevices, audioInputId: audioInputDeviceA.deviceId, videoInputId: videoInputDeviceA.deviceId } + const output = updateMediaDevicesPreferences(attributes, audioInputPreferenceList, videoInputPreferenceList) // Assert: should put device A on top of default device expect(output).toMatchObject({ @@ -191,11 +150,7 @@ describe('mediaDevicePreferences', () => { audioInputId: audioInputDeviceA.deviceId, videoInputId: videoInputDeviceA.deviceId, } - const output = updateMediaDevicesPreferences( - attributes, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const output = updateMediaDevicesPreferences(attributes, audioInputPreferenceList, videoInputPreferenceList) // Assert expect(output).toMatchObject({ newAudioInputList: null, newVideoInputList: null }) @@ -207,11 +162,7 @@ describe('mediaDevicePreferences', () => { audioInputId: audioInputDeviceA.deviceId, videoInputId: videoInputDeviceA.deviceId, } - const output = updateMediaDevicesPreferences( - attributes, - audioInputPreferenceList, - videoInputPreferenceList, - ) + const output = updateMediaDevicesPreferences(attributes, audioInputPreferenceList, videoInputPreferenceList) // Assert expect(output).toMatchObject({ newAudioInputList: null, newVideoInputList: null })