Reset tracked request value on failure

This commit is contained in:
Johannes Marbach
2026-09-16 16:23:55 +02:00
parent dda549ffd2
commit 11ef319536
2 changed files with 45 additions and 4 deletions
@@ -212,12 +212,13 @@ describe("Publisher device sync", () => {
selected$, selected$,
capturedDeviceId, capturedDeviceId,
activeDeviceId, activeDeviceId,
switchActiveDevice = vi.fn().mockResolvedValue(true),
}: { }: {
selected$: BehaviorSubject<SelectedInput>; selected$: BehaviorSubject<SelectedInput>;
capturedDeviceId?: string; capturedDeviceId?: string;
activeDeviceId: string; activeDeviceId: string;
switchActiveDevice?: Mock;
}): { publisher: Publisher; switchActiveDevice: Mock } { }): { publisher: Publisher; switchActiveDevice: Mock } {
const switchActiveDevice = vi.fn().mockResolvedValue(true);
const livekitRoom = mockLivekitRoom({ const livekitRoom = mockLivekitRoom({
localParticipant, localParticipant,
state: LivekitConnectionState.Connected, state: LivekitConnectionState.Connected,
@@ -294,6 +295,41 @@ describe("Publisher device sync", () => {
await publisher.destroy(); await publisher.destroy();
}); });
it("retries the same selection after a failed switch", async () => {
const selected$ = new BehaviorSubject<SelectedInput>(selectedInput(""));
const switchActiveDevice = vi
.fn()
.mockRejectedValueOnce(new Error("could not open the device"))
.mockResolvedValue(true);
const { publisher } = createDeviceSyncPublisher({
selected$,
// Pinned to the headset, so moving to the browser default is a switch
capturedDeviceId: "headset",
activeDeviceId: "headset",
switchActiveDevice,
});
expect(switchActiveDevice).toHaveBeenCalledTimes(1);
await flushPromises();
// The failed switch left us on the headset, so selecting the browser
// default again has to try once more rather than count as already done
selected$.next(selectedInput(""));
expect(switchActiveDevice).toHaveBeenCalledTimes(2);
expect(switchActiveDevice).toHaveBeenLastCalledWith(
"audioinput",
"default",
false,
);
await flushPromises();
// This time it worked, so there is nothing left to do
selected$.next(selectedInput(""));
expect(switchActiveDevice).toHaveBeenCalledTimes(2);
await publisher.destroy();
});
it("does not switch when the room already captures from the selected device", async () => { it("does not switch when the room already captures from the selected device", async () => {
const selected$ = new BehaviorSubject<SelectedInput>( const selected$ = new BehaviorSubject<SelectedInput>(
selectedInput("headset"), selectedInput("headset"),
@@ -392,6 +392,7 @@ export class Publisher {
) { ) {
return; return;
} }
const previousRequestedId = requestedId;
requestedId = device.id; requestedId = device.id;
// For the browser default input, ask for "default" as a non-exact // For the browser default input, ask for "default" as a non-exact
// constraint: browsers that have no such device ignore it and capture // constraint: browsers that have no such device ignore it and capture
@@ -399,9 +400,13 @@ export class Publisher {
(browserDefaultInput (browserDefaultInput
? lkRoom.switchActiveDevice(kind, "default", false) ? lkRoom.switchActiveDevice(kind, "default", false)
: lkRoom.switchActiveDevice(kind, device.id) : lkRoom.switchActiveDevice(kind, device.id)
).catch((e: Error) => ).catch((e: Error) => {
this.logger.error(`Failed to sync ${kind} device with LiveKit`, e), // LiveKit keeps capturing from the previous device, so record that
); // again to leave the next selection of this device something to do.
// A selection made while the switch was in flight wins, though.
if (requestedId === device.id) requestedId = previousRequestedId;
this.logger.error(`Failed to sync ${kind} device with LiveKit`, e);
});
}); });
}; };