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.
|
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 {
|
import {
|
||||||
ConnectionState as LivekitConnectionState,
|
ConnectionState as LivekitConnectionState,
|
||||||
LocalParticipant,
|
LocalParticipant,
|
||||||
@@ -186,18 +195,36 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Publisher device sync", () => {
|
describe("Publisher device sync", () => {
|
||||||
it("does not pin a device for the virtual browser default input", async () => {
|
type SelectedInput =
|
||||||
const selected$ = new BehaviorSubject<
|
| { id: string; hardwareDeviceChange$: typeof NEVER }
|
||||||
{ id: string; hardwareDeviceChange$: typeof NEVER } | undefined
|
| undefined;
|
||||||
>({ id: "", hardwareDeviceChange$: NEVER });
|
|
||||||
|
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 switchActiveDevice = vi.fn().mockResolvedValue(true);
|
||||||
const livekitRoom = mockLivekitRoom({
|
const livekitRoom = mockLivekitRoom({
|
||||||
localParticipant,
|
localParticipant,
|
||||||
state: LivekitConnectionState.Connected,
|
state: LivekitConnectionState.Connected,
|
||||||
// ConnectionFactory leaves the deviceId out for the browser default
|
// ConnectionFactory leaves the deviceId out for the browser default
|
||||||
options: { audioCaptureDefaults: {} },
|
options: { audioCaptureDefaults: { deviceId: capturedDeviceId } },
|
||||||
switchActiveDevice,
|
switchActiveDevice,
|
||||||
getActiveDevice: () => "hardware-id-of-whatever-the-browser-chose",
|
getActiveDevice: () => activeDeviceId,
|
||||||
} as unknown as Partial<LivekitRoom>);
|
} as unknown as Partial<LivekitRoom>);
|
||||||
|
|
||||||
const publisher = new Publisher(
|
const publisher = new Publisher(
|
||||||
@@ -210,17 +237,27 @@ describe("Publisher device sync", () => {
|
|||||||
logger,
|
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
|
// Already capturing from the browser default: nothing to switch, even
|
||||||
// though LiveKit reports the physical device it ended up with.
|
// though LiveKit reports the physical device it ended up with.
|
||||||
expect(switchActiveDevice).not.toHaveBeenCalled();
|
expect(switchActiveDevice).not.toHaveBeenCalled();
|
||||||
|
|
||||||
selected$.next({ id: "headset", hardwareDeviceChange$: NEVER });
|
selected$.next(selectedInput("headset"));
|
||||||
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
||||||
"audioinput",
|
"audioinput",
|
||||||
"headset",
|
"headset",
|
||||||
);
|
);
|
||||||
|
|
||||||
selected$.next({ id: "", hardwareDeviceChange$: NEVER });
|
selected$.next(selectedInput(""));
|
||||||
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
expect(switchActiveDevice).toHaveBeenLastCalledWith(
|
||||||
"audioinput",
|
"audioinput",
|
||||||
"default",
|
"default",
|
||||||
@@ -230,6 +267,47 @@ describe("Publisher device sync", () => {
|
|||||||
|
|
||||||
await publisher.destroy();
|
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", () => {
|
describe("Publisher", () => {
|
||||||
|
|||||||
@@ -376,9 +376,20 @@ export class Publisher {
|
|||||||
);
|
);
|
||||||
if (device === undefined) return;
|
if (device === undefined) return;
|
||||||
const browserDefaultInput = device.id === "" && kind !== "audiooutput";
|
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 (browserDefaultInput) {
|
||||||
if (requestedId === "") return;
|
if (capturingBrowserDefault) return;
|
||||||
} else if (lkRoom.getActiveDevice(kind) === device.id) {
|
} else if (
|
||||||
|
!capturingBrowserDefault &&
|
||||||
|
lkRoom.getActiveDevice(kind) === device.id
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requestedId = device.id;
|
requestedId = device.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user