/* Copyright 2025-2026 Element Creations Ltd. 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, test, vi } from "vitest"; import { BehaviorSubject } from "rxjs"; import { logger } from "matrix-js-sdk/lib/logger"; import { MuteStates, MuteState } from "./MuteStates"; import { type AudioOutputDeviceLabel, type DeviceLabel, type MediaDevice, type SelectedAudioOutputDevice, type SelectedDevice, } from "./MediaDevices"; import { constant } from "./Behavior"; import { ObservableScope } from "./ObservableScope"; import { flushPromises, mockMediaDevices } from "../utils/test"; const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); vi.mock("../UrlParams", () => ({ getUrlParams })); let testScope: ObservableScope; beforeEach(() => { testScope = new ObservableScope(); }); afterEach(() => { testScope.end(); }); describe("MuteState", () => { test("should automatically mute if force mute is set", async () => { const forceMute$ = new BehaviorSubject(false); const deviceStub = { available$: constant( new Map([ ["fbac11", { type: "name", name: "HD Camera" }], ]), ), selected$: constant({ id: "fbac11" }), select(): void {}, } as unknown as MediaDevice; const muteState = new MuteState(testScope, deviceStub, true, forceMute$); let lastEnabled: boolean = false; muteState.enabled$.subscribe((enabled) => { lastEnabled = enabled; }); let setEnabled: ((enabled: boolean) => void) | null = null; muteState.setEnabled$.subscribe((setter) => { setEnabled = setter; }); await flushPromises(); setEnabled!(true); await flushPromises(); expect(lastEnabled).toBe(true); // Now force mute forceMute$.next(true); await flushPromises(); // Should automatically mute expect(lastEnabled).toBe(false); // Try to unmute can not work expect(setEnabled).toBeNull(); // Disable force mute forceMute$.next(false); await flushPromises(); // TODO I'd expect it to go back to previous state (enabled) // but actually it goes back to the initial state from construction (disabled) // Should go back to previous state (enabled) // Skip for now // expect(lastEnabled).toBe(true); // But yet it can be unmuted now expect(setEnabled).not.toBeNull(); setEnabled!(true); await flushPromises(); expect(lastEnabled).toBe(true); }); }); describe("MuteStates", () => { function aAudioOutputDevices(): MediaDevice< AudioOutputDeviceLabel, SelectedAudioOutputDevice > { const selected$ = new BehaviorSubject< SelectedAudioOutputDevice | undefined >({ id: "default", virtualEarpiece: false, }); return { available$: constant( new Map([ ["default", { type: "speaker" }], ["0000", { type: "speaker" }], ["1111", { type: "earpiece" }], ["222", { type: "name", name: "Bluetooth Speaker" }], ]), ), selected$, select(id: string): void { if (!this.available$.getValue().has(id)) { logger.warn(`Attempted to select unknown device id: ${id}`); return; } selected$.next({ id, /** For test purposes we ignore this */ virtualEarpiece: false, }); }, }; } function aVideoInput(): MediaDevice { const selected$ = new BehaviorSubject( undefined, ); return { available$: constant( new Map([ ["0000", { type: "name", name: "HD Camera" }], ["1111", { type: "name", name: "WebCam Pro" }], ]), ), selected$, select(id: string): void { if (!this.available$.getValue().has(id)) { logger.warn(`Attempted to select unknown device id: ${id}`); return; } selected$.next({ id }); }, }; } test("should mute camera when in earpiece mode", async () => { const audioOutputDevice = aAudioOutputDevices(); const mediaDevices = mockMediaDevices({ audioOutput: audioOutputDevice, videoInput: aVideoInput(), // other devices are not relevant for this test }); const muteStates = new MuteStates(testScope, mediaDevices, { audioEnabled: false, videoEnabled: false, }); let latestSyncedState: boolean | null = null; muteStates.video.setHandler(async (enabled: boolean): Promise => { logger.info(`Video mute state set to: ${enabled}`); latestSyncedState = enabled; return Promise.resolve(enabled); }); let lastVideoEnabled: boolean = false; muteStates.video.enabled$.subscribe((enabled) => { lastVideoEnabled = enabled; }); expect(muteStates.video.setEnabled$.value).toBeDefined(); muteStates.video.setEnabled$.value?.(true); await flushPromises(); expect(lastVideoEnabled).toBe(true); // Select earpiece audio output audioOutputDevice.select("1111"); await flushPromises(); // Video should be automatically muted expect(lastVideoEnabled).toBe(false); expect(latestSyncedState).toBe(false); // Try to switch to speaker audioOutputDevice.select("0000"); await flushPromises(); // TODO I'd expect it to go back to previous state (enabled)?? // But maybe not? If you move the phone away from your ear you may not want it // to automatically enable video? expect(lastVideoEnabled).toBe(false); // But yet it can be unmuted now expect(muteStates.video.setEnabled$.value).toBeDefined(); muteStates.video.setEnabled$.value?.(true); await flushPromises(); expect(lastVideoEnabled).toBe(true); }); });