Use new audio device module in alpha and beta

This commit is contained in:
Evan Hahn 2021-09-29 13:30:42 -05:00 committed by GitHub
parent f3e07e5376
commit 64fc234490
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 372 additions and 52 deletions

View File

@ -0,0 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { makeEnumParser } from '../util/enum';
export enum AudioDeviceModule {
Default = 'Default',
WindowsAdm2 = 'WindowsAdm2',
}
export const parseAudioDeviceModule = makeEnumParser(
AudioDeviceModule,
AudioDeviceModule.Default
);

View File

@ -0,0 +1,66 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AudioDevice } from 'ringrtc';
import { AudioDeviceModule } from './audioDeviceModule';
export function findBestMatchingAudioDeviceIndex({
available,
preferred,
previousAudioDeviceModule,
currentAudioDeviceModule,
}: Readonly<{
available: ReadonlyArray<AudioDevice>;
preferred: undefined | AudioDevice;
previousAudioDeviceModule: AudioDeviceModule;
currentAudioDeviceModule: AudioDeviceModule;
}>): undefined | number {
if (!preferred) {
return available.length > 0 ? 0 : undefined;
}
if (
(currentAudioDeviceModule === AudioDeviceModule.WindowsAdm2 &&
preferred.index === 0) ||
(previousAudioDeviceModule === AudioDeviceModule.WindowsAdm2 &&
preferred.index === 1 &&
available.length >= 2)
) {
return preferred.index;
}
if (preferred.uniqueId) {
const idMatchIndex = available.findIndex(
d => d.uniqueId === preferred.uniqueId
);
if (idMatchIndex !== -1) {
return idMatchIndex;
}
}
const nameMatchIndex = available.findIndex(d => d.name === preferred.name);
if (nameMatchIndex !== -1) {
return nameMatchIndex;
}
return available.length > 0 ? 0 : undefined;
}
export function findBestMatchingCameraId(
available: ReadonlyArray<MediaDeviceInfo>,
preferred?: string
): undefined | string {
const matchingId = available.filter(d => d.deviceId === preferred);
const nonInfrared = available.filter(d => !d.label.includes('IR Camera'));
// By default, pick the first non-IR camera (but allow the user to pick the
// infrared if they so desire)
if (matchingId.length > 0) {
return matchingId[0].deviceId;
}
if (nonInfrared.length > 0) {
return nonInfrared[0].deviceId;
}
return undefined;
}

View File

@ -53,8 +53,17 @@ import {
PresentedSource,
ProcessGroupCallRingRequestResult,
} from '../types/Calling';
import {
AudioDeviceModule,
parseAudioDeviceModule,
} from '../calling/audioDeviceModule';
import {
findBestMatchingAudioDeviceIndex,
findBestMatchingCameraId,
} from '../calling/findBestMatchingDevice';
import { LocalizerType } from '../types/Util';
import { UUID } from '../types/UUID';
import * as OS from '../OS';
import { ConversationModel } from '../models/conversations';
import * as Bytes from '../Bytes';
import { uuidToBytes, bytesToUuid } from '../Crypto';
@ -63,6 +72,7 @@ import { getOwn } from '../util/getOwn';
import { isNormalNumber } from '../util/isNormalNumber';
import * as durations from '../util/durations';
import { handleMessageSend } from '../util/handleMessageSend';
import { isAlpha, isBeta } from '../util/version';
import {
fetchMembershipProof,
getMembershipList,
@ -212,6 +222,10 @@ export class CallingClass {
private lastMediaDeviceSettings?: MediaDeviceSettings;
private previousAudioDeviceModule?: AudioDeviceModule;
private currentAudioDeviceModule?: AudioDeviceModule;
private deviceReselectionTimer?: NodeJS.Timeout;
private callsByConversation: { [conversationId: string]: Call | GroupCall };
@ -237,6 +251,24 @@ export class CallingClass {
this.sfuUrl = sfuUrl;
this.previousAudioDeviceModule = parseAudioDeviceModule(
window.storage.get('previousAudioDeviceModule')
);
this.currentAudioDeviceModule =
OS.isWindows() &&
(isAlpha(window.getVersion()) || isBeta(window.getVersion()))
? AudioDeviceModule.WindowsAdm2
: AudioDeviceModule.Default;
window.storage.put(
'previousAudioDeviceModule',
this.currentAudioDeviceModule
);
RingRTC.setConfig({
use_new_audio_device_module:
this.currentAudioDeviceModule === AudioDeviceModule.WindowsAdm2,
});
RingRTC.handleOutgoingSignaling = this.handleOutgoingSignaling.bind(this);
RingRTC.handleIncomingCall = this.handleIncomingCall.bind(this);
RingRTC.handleAutoEndedIncomingCallRequest = this.handleAutoEndedIncomingCallRequest.bind(
@ -1263,6 +1295,13 @@ export class CallingClass {
}
async getMediaDeviceSettings(): Promise<MediaDeviceSettings> {
const { previousAudioDeviceModule, currentAudioDeviceModule } = this;
if (!previousAudioDeviceModule || !currentAudioDeviceModule) {
throw new Error(
'Calling#getMediaDeviceSettings cannot be called before audio device settings are set'
);
}
const {
availableCameras,
availableMicrophones,
@ -1270,27 +1309,31 @@ export class CallingClass {
} = await this.getAvailableIODevices();
const preferredMicrophone = window.Events.getPreferredAudioInputDevice();
const selectedMicIndex = this.findBestMatchingDeviceIndex(
availableMicrophones,
preferredMicrophone
);
const selectedMicIndex = findBestMatchingAudioDeviceIndex({
available: availableMicrophones,
preferred: preferredMicrophone,
previousAudioDeviceModule,
currentAudioDeviceModule,
});
const selectedMicrophone =
selectedMicIndex !== undefined
? availableMicrophones[selectedMicIndex]
: undefined;
const preferredSpeaker = window.Events.getPreferredAudioOutputDevice();
const selectedSpeakerIndex = this.findBestMatchingDeviceIndex(
availableSpeakers,
preferredSpeaker
);
const selectedSpeakerIndex = findBestMatchingAudioDeviceIndex({
available: availableSpeakers,
preferred: preferredSpeaker,
previousAudioDeviceModule,
currentAudioDeviceModule,
});
const selectedSpeaker =
selectedSpeakerIndex !== undefined
? availableSpeakers[selectedSpeakerIndex]
: undefined;
const preferredCamera = window.Events.getPreferredVideoInputDevice();
const selectedCamera = this.findBestMatchingCamera(
const selectedCamera = findBestMatchingCameraId(
availableCameras,
preferredCamera
);
@ -1305,49 +1348,6 @@ export class CallingClass {
};
}
findBestMatchingDeviceIndex(
available: Array<AudioDevice>,
preferred: AudioDevice | undefined
): number | undefined {
if (preferred) {
// Match by uniqueId first, if available
if (preferred.uniqueId) {
const matchIndex = available.findIndex(
d => d.uniqueId === preferred.uniqueId
);
if (matchIndex !== -1) {
return matchIndex;
}
}
// Match by name second
const matchingNames = available.filter(d => d.name === preferred.name);
if (matchingNames.length > 0) {
return matchingNames[0].index;
}
}
// Nothing matches or no preference; take the first device if there are any
return available.length > 0 ? 0 : undefined;
}
findBestMatchingCamera(
available: Array<MediaDeviceInfo>,
preferred?: string
): string | undefined {
const matchingId = available.filter(d => d.deviceId === preferred);
const nonInfrared = available.filter(d => !d.label.includes('IR Camera'));
// By default, pick the first non-IR camera (but allow the user to pick the
// infrared if they so desire)
if (matchingId.length > 0) {
return matchingId[0].deviceId;
}
if (nonInfrared.length > 0) {
return nonInfrared[0].deviceId;
}
return undefined;
}
setPreferredMicrophone(device: AudioDevice): void {
log.info('MediaDevice: setPreferredMicrophone', device);
window.Events.setPreferredAudioInputDevice(device);

View File

@ -0,0 +1,237 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { AudioDeviceModule } from '../../calling/audioDeviceModule';
import { findBestMatchingAudioDeviceIndex } from '../../calling/findBestMatchingDevice';
describe('"find best matching device" helpers', () => {
describe('findBestMatchingAudioDeviceIndex', () => {
type AdmOptionsType = Readonly<{
previousAudioDeviceModule: AudioDeviceModule;
currentAudioDeviceModule: AudioDeviceModule;
}>;
const itReturnsUndefinedIfNoDevicesAreAvailable = (
admOptions: AdmOptionsType
) => {
it('returns undefined if no devices are available', () => {
[
undefined,
{ name: 'Big Microphone', index: 1, uniqueId: 'abc123' },
].forEach(preferred => {
assert.isUndefined(
findBestMatchingAudioDeviceIndex({
available: [],
preferred,
...admOptions,
})
);
});
});
};
const itReturnsTheFirstAvailableDeviceIfNoneIsPreferred = (
admOptions: AdmOptionsType
) => {
it('returns the first available device if none is preferred', () => {
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: undefined,
...admOptions,
}),
0
);
});
};
const testUniqueIdMatch = (admOptions: AdmOptionsType) => {
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: 'Ignored', index: 99, uniqueId: 'device-C' },
...admOptions,
}),
2
);
};
const testNameMatch = (admOptions: AdmOptionsType) => {
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: 99, uniqueId: 'ignored' },
...admOptions,
}),
2
);
};
const itReturnsTheFirstAvailableDeviceIfThePreferredDeviceIsNotFound = (
admOptions: AdmOptionsType
) => {
it('returns the first available device if the preferred device is not found', () => {
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: 'X', index: 123, uniqueId: 'Y' },
...admOptions,
}),
0
);
});
};
describe('with default audio device module', () => {
const admOptions = {
previousAudioDeviceModule: AudioDeviceModule.Default,
currentAudioDeviceModule: AudioDeviceModule.Default,
};
itReturnsUndefinedIfNoDevicesAreAvailable(admOptions);
itReturnsTheFirstAvailableDeviceIfNoneIsPreferred(admOptions);
it('returns a unique ID match if it exists', () => {
testUniqueIdMatch(admOptions);
});
it('returns a name match if it exists', () => {
testNameMatch(admOptions);
});
itReturnsTheFirstAvailableDeviceIfThePreferredDeviceIsNotFound(
admOptions
);
});
describe('when going from the default to Windows ADM2', () => {
const admOptions = {
previousAudioDeviceModule: AudioDeviceModule.Default,
currentAudioDeviceModule: AudioDeviceModule.WindowsAdm2,
};
itReturnsUndefinedIfNoDevicesAreAvailable(admOptions);
itReturnsTheFirstAvailableDeviceIfNoneIsPreferred(admOptions);
it('returns 0 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' },
],
preferred: { name: 'B', index: 0, uniqueId: 'device-B' },
...admOptions,
}),
0
);
});
it('returns a unique ID match if it exists and the preferred index is not 0', () => {
testUniqueIdMatch(admOptions);
});
it('returns a name match if it exists and the preferred index is not 0', () => {
testNameMatch(admOptions);
});
itReturnsTheFirstAvailableDeviceIfThePreferredDeviceIsNotFound(
admOptions
);
});
describe('when going "backwards" from Windows ADM2 to the default', () => {
const admOptions = {
previousAudioDeviceModule: AudioDeviceModule.WindowsAdm2,
currentAudioDeviceModule: AudioDeviceModule.Default,
};
itReturnsUndefinedIfNoDevicesAreAvailable(admOptions);
itReturnsTheFirstAvailableDeviceIfNoneIsPreferred(admOptions);
it('returns a unique ID match if it exists', () => {
testUniqueIdMatch(admOptions);
});
it('returns a name match if it exists', () => {
testNameMatch(admOptions);
});
itReturnsTheFirstAvailableDeviceIfThePreferredDeviceIsNotFound(
admOptions
);
});
describe('with Windows ADM2', () => {
const admOptions = {
previousAudioDeviceModule: AudioDeviceModule.WindowsAdm2,
currentAudioDeviceModule: AudioDeviceModule.WindowsAdm2,
};
itReturnsUndefinedIfNoDevicesAreAvailable(admOptions);
itReturnsTheFirstAvailableDeviceIfNoneIsPreferred(admOptions);
[0, 1].forEach(index => {
it(`returns ${index} 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, uniqueId: 'device-C' },
...admOptions,
}),
index
);
});
});
it("returns 0 if the previous preferred index was 1 but there's only 1 audio device", () => {
assert.strictEqual(
findBestMatchingAudioDeviceIndex({
available: [{ name: 'A', index: 123, uniqueId: 'device-A' }],
preferred: { name: 'C', index: 1, uniqueId: 'device-C' },
...admOptions,
}),
0
);
});
it('returns a unique ID match if it exists and the preferred index is not 0 or 1', () => {
testUniqueIdMatch(admOptions);
});
it('returns a name match if it exists and the preferred index is not 0 or 1', () => {
testNameMatch(admOptions);
});
itReturnsTheFirstAvailableDeviceIfThePreferredDeviceIsNotFound(
admOptions
);
});
});
});

View File

@ -6,6 +6,7 @@ import type {
CustomColorsItemType,
DefaultConversationColorType,
} from './Colors';
import type { AudioDeviceModule } from '../calling/audioDeviceModule';
import type { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
import type { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
import type { RetryItemType } from '../util/retryPlaceholders';
@ -113,6 +114,7 @@ export type StorageAccessType = {
'preferred-video-input-device': string;
'preferred-audio-input-device': AudioDevice;
'preferred-audio-output-device': AudioDevice;
previousAudioDeviceModule: AudioDeviceModule;
remoteConfig: RemoteConfigType;
unidentifiedDeliveryIndicators: boolean;
groupCredentials: Array<GroupCredentialType>;

View File

@ -26,6 +26,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
'preferred-audio-input-device',
'preferred-audio-output-device',
'preferredReactionEmoji',
'previousAudioDeviceModule',
'skinTone',
'zoomFactor',
];