From a0aaa8e09a82fe6236ffbbb2bcd56fed45d19ab3 Mon Sep 17 00:00:00 2001 From: fkwp Date: Wed, 4 Nov 2026 12:42:12 +0100 Subject: [PATCH] Ask one question about whether background effects can run The controls and the pipeline each asked the SDK on their own whether background processing is supported, so one could offer what the other refused. One function answers both, and it asks the browser, kept to desktops until the menu says what the effects cost elsewhere. Ported from spec/background-effects/2-pipeline (#4282): f78e9c05 and 87638325, the tests on the lobby and call footers as they are. Co-Authored-By: Claude Fable 5.1 --- src/components/CallFooterViewModel.test.ts | 62 ++++++++++++++++++++-- src/components/CallFooterViewModel.tsx | 3 +- src/tracks/BackgroundEffectsContext.tsx | 11 +--- src/tracks/backgroundProcessing.ts | 15 ++++++ 4 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 src/tracks/backgroundProcessing.ts 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"; +}