diff --git a/src/components/CallFooterViewModel.test.ts b/src/components/CallFooterViewModel.test.ts index ed0ffe8ed..6f353e540 100644 --- a/src/components/CallFooterViewModel.test.ts +++ b/src/components/CallFooterViewModel.test.ts @@ -13,7 +13,10 @@ import type { CallViewModel } from "../state/CallViewModel/CallViewModel"; import type { Layout } from "../state/layout-types"; import type { SpotlightTileViewModel } from "../state/TileViewModel"; import type { DeviceLabel } from "../state/MediaDevices"; -import { createCallFooterViewModel } from "./CallFooterViewModel"; +import { + createCallFooterViewModel, + createLobbyFooterViewModel, +} from "./CallFooterViewModel"; import { HeaderStyle } from "../UrlParams"; const platformMock = vi.hoisted(() => vi.fn(() => "desktop")); @@ -23,10 +26,10 @@ vi.mock("../Platform", () => ({ }, })); -// Prevent supportsBackgroundProcessors from throwing in jsdom – it is not -// exercised by these tests (only used in `videoToggles`, not `videoOptions`). +// The SDK's own check needs WebGL and canvas APIs jsdom does not have. +const sdkSupportMock = vi.hoisted(() => vi.fn(() => false)); vi.mock("@livekit/track-processors", () => ({ - supportsBackgroundProcessors: (): boolean => false, + supportsBackgroundProcessors: (): boolean => sdkSupportMock(), })); const outputSelectionMock = vi.hoisted(() => vi.fn(() => true)); @@ -217,4 +220,55 @@ describe("createCallFooterViewModel", () => { ]); }); }); + + describe("background effects", () => { + // The lobby, because in a call the whole device switcher is already + // withheld on a phone, whatever the verdict says. + function lobbyOn( + platform: string, + ): ReturnType { + platformMock.mockReturnValue(platform); + return createLobbyFooterViewModel( + testScope(), + mockMuteStates(), + twoMicsAndOneCamMediaDevices, + /* openSettings */ undefined, + /* hangup */ undefined, + /* showLogo */ false, + ); + } + + it("offers nothing on a phone, where the pipeline would refuse them", () => { + sdkSupportMock.mockReturnValue(true); + expect(lobbyOn("ios").toggleBlur$.value).toBeUndefined(); + }); + + it("offers them where the pipeline will honour them", () => { + sdkSupportMock.mockReturnValue(true); + expect(lobbyOn("desktop").toggleBlur$.value).toBeDefined(); + }); + + it("offers nothing where the browser itself cannot run them", () => { + sdkSupportMock.mockReturnValue(false); + expect(lobbyOn("desktop").toggleBlur$.value).toBeUndefined(); + }); + + it("availability is the same before and during a call", () => { + for (const supported of [true, false]) { + sdkSupportMock.mockReturnValue(supported); + const lobby = lobbyOn("desktop"); + const inCall = createCallFooterViewModel( + testScope(), + buildMinimalCallViewModel(gridLayout), + mockMuteStates(), + twoMicsAndOneCamMediaDevices, + /* reactionIdentifier */ undefined, + { showControls: true, header: HeaderStyle.Standard }, + ); + const offeredInLobby = lobby.toggleBlur$.value !== undefined; + expect(inCall.toggleBlur$.value !== undefined).toBe(offeredInLobby); + expect(offeredInLobby).toBe(supported); + } + }); + }); }); diff --git a/src/components/CallFooterViewModel.tsx b/src/components/CallFooterViewModel.tsx index 50efb42f9..9a110c370 100644 --- a/src/components/CallFooterViewModel.tsx +++ b/src/components/CallFooterViewModel.tsx @@ -6,9 +6,10 @@ Please see LICENSE in the repository root for full details. */ import { combineLatest, map, type Observable, switchMap } from "rxjs"; -import { supportsBackgroundProcessors } from "@livekit/track-processors"; import { supportsAudioOutputSelection } from "livekit-client"; +import { supportsBackgroundProcessors } from "../tracks/backgroundProcessing"; + import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { type MenuOptions } from "./MediaMuteAndSwitchButton"; import { type MediaDevices } from "../state/MediaDevices"; diff --git a/src/tracks/BackgroundEffectsContext.tsx b/src/tracks/BackgroundEffectsContext.tsx index f1b384e0a..c5ae575fd 100644 --- a/src/tracks/BackgroundEffectsContext.tsx +++ b/src/tracks/BackgroundEffectsContext.tsx @@ -5,17 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { - BackgroundProcessorWrapper, - supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk, -} from "@livekit/track-processors"; +import { BackgroundProcessorWrapper } from "@livekit/track-processors"; import { createContext, type FC, type JSX, use } from "react"; import { backgroundBlur as backgroundBlurSettings } from "../settings/settings"; import { BackgroundEffectTransformer } from "./BackgroundEffectTransformer"; +import { supportsBackgroundProcessors } from "./backgroundProcessing"; import { type Behavior } from "../state/Behavior"; import { type ObservableScope } from "../state/ObservableScope"; -import { platform } from "../Platform"; import { type BackgroundEffects, type BackgroundEffectsState, @@ -40,10 +37,6 @@ export function useBackgroundEffectsState$(): Behavior { return effects.state$; } -function supportsBackgroundProcessors(): boolean { - return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop"; -} - /** * The app's one pipeline, shared by every camera track it opens. Its builds * and teardowns take their turns through the SDK, which runs one operation diff --git a/src/tracks/backgroundProcessing.ts b/src/tracks/backgroundProcessing.ts new file mode 100644 index 000000000..8b6168dad --- /dev/null +++ b/src/tracks/backgroundProcessing.ts @@ -0,0 +1,15 @@ +/* +Copyright 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 { supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk } from "@livekit/track-processors"; + +import { platform } from "../Platform"; + +/** One answer for the controls and the pipeline, so neither offers what the other refuses. */ +export function supportsBackgroundProcessors(): boolean { + return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop"; +}