Use new audio device module in alpha and beta
This commit is contained in:
parent
f3e07e5376
commit
64fc234490
6 changed files with 372 additions and 52 deletions
14
ts/calling/audioDeviceModule.ts
Normal file
14
ts/calling/audioDeviceModule.ts
Normal 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
|
||||
);
|
66
ts/calling/findBestMatchingDevice.ts
Normal file
66
ts/calling/findBestMatchingDevice.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
|
|
237
ts/test-both/calling/findBestMatchingDevice_test.ts
Normal file
237
ts/test-both/calling/findBestMatchingDevice_test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
|
@ -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>;
|
||||
|
|
|
@ -26,6 +26,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
|
|||
'preferred-audio-input-device',
|
||||
'preferred-audio-output-device',
|
||||
'preferredReactionEmoji',
|
||||
'previousAudioDeviceModule',
|
||||
'skinTone',
|
||||
'zoomFactor',
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue