Fix device selection persistence bug

This commit is contained in:
Miriam Zimmerman 2024-10-07 16:32:31 -04:00 committed by GitHub
parent bad065859c
commit a3b972f6e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 157 additions and 67 deletions

View file

@ -3,20 +3,27 @@
import type { AudioDevice } from '@signalapp/ringrtc'; import type { AudioDevice } from '@signalapp/ringrtc';
export function findBestMatchingAudioDeviceIndex({ export function findBestMatchingAudioDeviceIndex(
{
available, available,
preferred, preferred,
}: Readonly<{ }: Readonly<{
available: ReadonlyArray<AudioDevice>; available: ReadonlyArray<AudioDevice>;
preferred: undefined | AudioDevice; preferred: undefined | AudioDevice;
}>): undefined | number { }>,
isWindows: boolean
): undefined | number {
if (!preferred) { if (!preferred) {
return available.length > 0 ? 0 : undefined; return available.length > 0 ? 0 : undefined;
} }
// On Linux and Mac, the default device is at index 0.
// On Windows, there are two default devices, as presented by RingRTC:
// * The default communications device (for voice calls, at index 0)
// * the default device (for, e.g., media, at index 1)
if ( if (
preferred.index === 0 || preferred.index === 0 ||
(preferred.index === 1 && available.length >= 2) (isWindows && preferred.index === 1 && available.length >= 2)
) { ) {
return preferred.index; return preferred.index;
} }

View file

@ -156,6 +156,7 @@ import { sendCallLinkUpdateSync } from '../util/sendCallLinkUpdateSync';
import { createIdenticon } from '../util/createIdenticon'; import { createIdenticon } from '../util/createIdenticon';
import { getColorForCallLink } from '../util/getColorForCallLink'; import { getColorForCallLink } from '../util/getColorForCallLink';
import { getUseRingrtcAdm } from '../util/ringrtc/ringrtcAdm'; import { getUseRingrtcAdm } from '../util/ringrtc/ringrtcAdm';
import OS from '../util/os/osMain';
const { wasGroupCallRingPreviouslyCanceled } = DataReader; const { wasGroupCallRingPreviouslyCanceled } = DataReader;
const { const {
@ -2330,20 +2331,26 @@ export class CallingClass {
await this.getAvailableIODevices(); await this.getAvailableIODevices();
const preferredMicrophone = window.Events.getPreferredAudioInputDevice(); const preferredMicrophone = window.Events.getPreferredAudioInputDevice();
const selectedMicIndex = findBestMatchingAudioDeviceIndex({ const selectedMicIndex = findBestMatchingAudioDeviceIndex(
{
available: availableMicrophones, available: availableMicrophones,
preferred: preferredMicrophone, preferred: preferredMicrophone,
}); },
OS.isWindows()
);
const selectedMicrophone = const selectedMicrophone =
selectedMicIndex !== undefined selectedMicIndex !== undefined
? availableMicrophones[selectedMicIndex] ? availableMicrophones[selectedMicIndex]
: undefined; : undefined;
const preferredSpeaker = window.Events.getPreferredAudioOutputDevice(); const preferredSpeaker = window.Events.getPreferredAudioOutputDevice();
const selectedSpeakerIndex = findBestMatchingAudioDeviceIndex({ const selectedSpeakerIndex = findBestMatchingAudioDeviceIndex(
{
available: availableSpeakers, available: availableSpeakers,
preferred: preferredSpeaker, preferred: preferredSpeaker,
}); },
OS.isWindows()
);
const selectedSpeaker = const selectedSpeaker =
selectedSpeakerIndex !== undefined selectedSpeakerIndex !== undefined
? availableSpeakers[selectedSpeakerIndex] ? availableSpeakers[selectedSpeakerIndex]

View file

@ -14,10 +14,13 @@ describe('"find best matching device" helpers', () => {
{ name: 'Big Microphone', index: 1, uniqueId: 'abc123' }, { name: 'Big Microphone', index: 1, uniqueId: 'abc123' },
].forEach(preferred => { ].forEach(preferred => {
assert.isUndefined( assert.isUndefined(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [], available: [],
preferred, preferred,
}) },
false
)
); );
}); });
}); });
@ -26,14 +29,17 @@ describe('"find best matching device" helpers', () => {
const itReturnsTheFirstAvailableDeviceIfNoneIsPreferred = () => { const itReturnsTheFirstAvailableDeviceIfNoneIsPreferred = () => {
it('returns the first available device if none is preferred', () => { it('returns the first available device if none is preferred', () => {
assert.strictEqual( assert.strictEqual(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [ available: [
{ name: 'A', index: 123, uniqueId: 'device-A' }, { name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' }, { name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' }, { name: 'C', index: 789, uniqueId: 'device-C' },
], ],
preferred: undefined, preferred: undefined,
}), },
false
),
0 0
); );
}); });
@ -41,28 +47,34 @@ describe('"find best matching device" helpers', () => {
const testUniqueIdMatch = () => { const testUniqueIdMatch = () => {
assert.strictEqual( assert.strictEqual(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [ available: [
{ name: 'A', index: 123, uniqueId: 'device-A' }, { name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' }, { name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' }, { name: 'C', index: 789, uniqueId: 'device-C' },
], ],
preferred: { name: 'Ignored', index: 99, uniqueId: 'device-C' }, preferred: { name: 'Ignored', index: 99, uniqueId: 'device-C' },
}), },
false
),
2 2
); );
}; };
const testNameMatch = () => { const testNameMatch = () => {
assert.strictEqual( assert.strictEqual(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [ available: [
{ name: 'A', index: 123, uniqueId: 'device-A' }, { name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' }, { name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' }, { name: 'C', index: 789, uniqueId: 'device-C' },
], ],
preferred: { name: 'C', index: 99, uniqueId: 'ignored' }, preferred: { name: 'C', index: 99, uniqueId: 'ignored' },
}), },
false
),
2 2
); );
}; };
@ -71,14 +83,17 @@ describe('"find best matching device" helpers', () => {
() => { () => {
it('returns the first available device if the preferred device is not found', () => { it('returns the first available device if the preferred device is not found', () => {
assert.strictEqual( assert.strictEqual(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [ available: [
{ name: 'A', index: 123, uniqueId: 'device-A' }, { name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' }, { name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' }, { name: 'C', index: 789, uniqueId: 'device-C' },
], ],
preferred: { name: 'X', index: 123, uniqueId: 'Y' }, preferred: { name: 'X', index: 123, uniqueId: 'Y' },
}), },
false
),
0 0
); );
}); });
@ -89,28 +104,64 @@ describe('"find best matching device" helpers', () => {
itReturnsTheFirstAvailableDeviceIfNoneIsPreferred(); itReturnsTheFirstAvailableDeviceIfNoneIsPreferred();
[0, 1].forEach(index => { it('returns 0 if that was the previous preferred index (and a device is available)', () => {
it(`returns ${index} if that was the previous preferred index (and a device is available)`, () => {
assert.strictEqual( assert.strictEqual(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [ available: [
{ name: 'A', index: 123, uniqueId: 'device-A' }, { name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' }, { name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' }, { name: 'C', index: 789, uniqueId: 'device-C' },
], ],
preferred: { name: 'C', index, uniqueId: 'device-C' }, preferred: { name: 'C', index: 0, uniqueId: 'device-C' },
}), },
index false
),
0
); );
}); });
it('(windows) returns 1 if that was the previous preferred index (and a device is available)', () => {
assert.strictEqual(
findBestMatchingAudioDeviceIndex(
{
available: [
{ name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' },
],
preferred: { name: 'C', index: 1, uniqueId: 'device-C' },
},
true
),
1
);
});
it('(non-windows) returns 2 if the preferred device was at index 1 and is now at index 2', () => {
assert.strictEqual(
findBestMatchingAudioDeviceIndex(
{
available: [
{ name: 'A', index: 123, uniqueId: 'device-A' },
{ name: 'B', index: 456, uniqueId: 'device-B' },
{ name: 'C', index: 789, uniqueId: 'device-C' },
],
preferred: { name: 'C', index: 1, uniqueId: 'device-C' },
},
false
),
2
);
}); });
it("returns 0 if the previous preferred index was 1 but there's only 1 audio device", () => { it("returns 0 if the previous preferred index was 1 but there's only 1 audio device", () => {
assert.strictEqual( assert.strictEqual(
findBestMatchingAudioDeviceIndex({ findBestMatchingAudioDeviceIndex(
{
available: [{ name: 'A', index: 123, uniqueId: 'device-A' }], available: [{ name: 'A', index: 123, uniqueId: 'device-A' }],
preferred: { name: 'C', index: 1, uniqueId: 'device-C' }, preferred: { name: 'C', index: 1, uniqueId: 'device-C' },
}), },
false
),
0 0
); );
}); });

View file

@ -15,6 +15,7 @@ import {
import { awaitObject } from '../../util/awaitObject'; import { awaitObject } from '../../util/awaitObject';
import { DurationInSeconds } from '../../util/durations'; import { DurationInSeconds } from '../../util/durations';
import { createSetting, createCallback } from '../../util/preload'; import { createSetting, createCallback } from '../../util/preload';
import { findBestMatchingAudioDeviceIndex } from '../../calling/findBestMatchingDevice';
function doneRendering() { function doneRendering() {
ipcRenderer.send('settings-done-rendering'); ipcRenderer.send('settings-done-rendering');
@ -239,6 +240,30 @@ async function renderPreferences() {
const preferredSystemLocales = const preferredSystemLocales =
MinimalSignalContext.getPreferredSystemLocales(); MinimalSignalContext.getPreferredSystemLocales();
const selectedMicIndex = findBestMatchingAudioDeviceIndex(
{
available: availableMicrophones,
preferred: selectedMicrophone,
},
OS.isWindows()
);
const recomputedSelectedMicrophone =
selectedMicIndex !== undefined
? availableMicrophones[selectedMicIndex]
: undefined;
const selectedSpeakerIndex = findBestMatchingAudioDeviceIndex(
{
available: availableSpeakers,
preferred: selectedSpeaker,
},
OS.isWindows()
);
const recomputedSelectedSpeaker =
selectedSpeakerIndex !== undefined
? availableSpeakers[selectedSpeakerIndex]
: undefined;
const props = { const props = {
// Settings // Settings
availableCameras, availableCameras,
@ -279,8 +304,8 @@ async function renderPreferences() {
preferredSystemLocales, preferredSystemLocales,
resolvedLocale, resolvedLocale,
selectedCamera, selectedCamera,
selectedMicrophone, selectedMicrophone: recomputedSelectedMicrophone,
selectedSpeaker, selectedSpeaker: recomputedSelectedSpeaker,
sentMediaQualitySetting, sentMediaQualitySetting,
themeSetting, themeSetting,
universalExpireTimer: DurationInSeconds.fromSeconds(universalExpireTimer), universalExpireTimer: DurationInSeconds.fromSeconds(universalExpireTimer),