mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Make sure that selecting a device that was previously resolved from the default selection actually pins it
This commit is contained in:
@@ -5,7 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
test,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import {
|
||||
ConnectionState as LivekitConnectionState,
|
||||
LocalParticipant,
|
||||
@@ -186,18 +195,36 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("Publisher device sync", () => {
|
||||
it("does not pin a device for the virtual browser default input", async () => {
|
||||
const selected$ = new BehaviorSubject<
|
||||
{ id: string; hardwareDeviceChange$: typeof NEVER } | undefined
|
||||
>({ id: "", hardwareDeviceChange$: NEVER });
|
||||
type SelectedInput =
|
||||
| { id: string; hardwareDeviceChange$: typeof NEVER }
|
||||
| undefined;
|
||||
|
||||
function selectedInput(id: string): SelectedInput {
|
||||
return { id, hardwareDeviceChange$: NEVER };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Publisher whose LiveKit room starts out capturing from
|
||||
* capturedDeviceId (undefined meaning no deviceId constraint, i.e. the
|
||||
* browser default) and reports activeDeviceId as its active audio input.
|
||||
*/
|
||||
function createDeviceSyncPublisher({
|
||||
selected$,
|
||||
capturedDeviceId,
|
||||
activeDeviceId,
|
||||
}: {
|
||||
selected$: BehaviorSubject<SelectedInput>;
|
||||
capturedDeviceId?: string;
|
||||
activeDeviceId: string;
|
||||
}): { publisher: Publisher; switchActiveDevice: Mock } {
|
||||
const switchActiveDevice = vi.fn().mockResolvedValue(true);
|
||||
const livekitRoom = mockLivekitRoom({
|
||||
localParticipant,
|
||||
state: LivekitConnectionState.Connected,
|
||||
// ConnectionFactory leaves the deviceId out for the browser default
|
||||
options: { audioCaptureDefaults: {} },
|
||||
options: { audioCaptureDefaults: { deviceId: capturedDeviceId } },
|
||||
switchActiveDevice,
|
||||
getActiveDevice: () => "hardware-id-of-whatever-the-browser-chose",
|
||||
getActiveDevice: () => activeDeviceId,
|
||||
} as unknown as Partial<LivekitRoom>);
|
||||
|
||||
const publisher = new Publisher(
|
||||
@@ -210,17 +237,27 @@ describe("Publisher device sync", () => {
|
||||
logger,
|
||||
);
|
||||
|
||||
return { publisher, switchActiveDevice };
|
||||
}
|
||||
|
||||
it("does not pin a device for the virtual browser default input", async () => {
|
||||
const selected$ = new BehaviorSubject<SelectedInput>(selectedInput(""));
|
||||
const { publisher, switchActiveDevice } = createDeviceSyncPublisher({
|
||||
selected$,
|
||||
activeDeviceId: "hardware-id-of-whatever-the-browser-chose",
|
||||
});
|
||||
|
||||
// Already capturing from the browser default: nothing to switch, even
|
||||
// though LiveKit reports the physical device it ended up with.
|
||||
expect(switchActiveDevice).not.toHaveBeenCalled();
|
||||
|
||||
selected$.next({ id: "headset", hardwareDeviceChange$: NEVER });
|
||||
selected$.next(selectedInput("headset"));
|
||||
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
||||
"audioinput",
|
||||
"headset",
|
||||
);
|
||||
|
||||
selected$.next({ id: "", hardwareDeviceChange$: NEVER });
|
||||
selected$.next(selectedInput(""));
|
||||
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
||||
"audioinput",
|
||||
"default",
|
||||
@@ -230,6 +267,47 @@ describe("Publisher device sync", () => {
|
||||
|
||||
await publisher.destroy();
|
||||
});
|
||||
|
||||
it("pins the device the browser default resolved to when picked explicitly", async () => {
|
||||
const selected$ = new BehaviorSubject<SelectedInput>(selectedInput(""));
|
||||
const { publisher, switchActiveDevice } = createDeviceSyncPublisher({
|
||||
selected$,
|
||||
// The browser default happens to capture from the built-in microphone
|
||||
activeDeviceId: "built-in-mic",
|
||||
});
|
||||
|
||||
expect(switchActiveDevice).not.toHaveBeenCalled();
|
||||
|
||||
// Picking that same microphone has to replace the loose constraint with a
|
||||
// pin, or the capture would keep following the OS default
|
||||
selected$.next(selectedInput("built-in-mic"));
|
||||
expect(switchActiveDevice).toHaveBeenCalledTimes(1);
|
||||
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
||||
"audioinput",
|
||||
"built-in-mic",
|
||||
);
|
||||
|
||||
// Once pinned, re-emitting the same selection is a no-op again
|
||||
selected$.next(selectedInput("built-in-mic"));
|
||||
expect(switchActiveDevice).toHaveBeenCalledTimes(1);
|
||||
|
||||
await publisher.destroy();
|
||||
});
|
||||
|
||||
it("does not switch when the room already captures from the selected device", async () => {
|
||||
const selected$ = new BehaviorSubject<SelectedInput>(
|
||||
selectedInput("headset"),
|
||||
);
|
||||
const { publisher, switchActiveDevice } = createDeviceSyncPublisher({
|
||||
selected$,
|
||||
capturedDeviceId: "headset",
|
||||
activeDeviceId: "headset",
|
||||
});
|
||||
|
||||
expect(switchActiveDevice).not.toHaveBeenCalled();
|
||||
|
||||
await publisher.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Publisher", () => {
|
||||
|
||||
@@ -376,9 +376,20 @@ export class Publisher {
|
||||
);
|
||||
if (device === undefined) return;
|
||||
const browserDefaultInput = device.id === "" && kind !== "audiooutput";
|
||||
// While we capture from the browser default, LiveKit reports the
|
||||
// physical device it resolved to, which can be any of the devices the
|
||||
// user could also pick explicitly. getActiveDevice therefore can't
|
||||
// tell an explicit pin apart from the default resolving to the same
|
||||
// device, and only requestedId can: without this, picking the device
|
||||
// the default happens to use would leave the loose constraint in
|
||||
// place, and the capture would keep following the OS default.
|
||||
const capturingBrowserDefault = requestedId === "";
|
||||
if (browserDefaultInput) {
|
||||
if (requestedId === "") return;
|
||||
} else if (lkRoom.getActiveDevice(kind) === device.id) {
|
||||
if (capturingBrowserDefault) return;
|
||||
} else if (
|
||||
!capturingBrowserDefault &&
|
||||
lkRoom.getActiveDevice(kind) === device.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
requestedId = device.id;
|
||||
|
||||
Reference in New Issue
Block a user