From 9ce5cf0d8e38359852facfade47ccc771e8ae650 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 24 Sep 2026 12:38:31 +0200 Subject: [PATCH] Ask one question about whether background effects can run - The menu asked the browser; the pipeline asked the browser and for a desktop. On a phone whose browser can run them, blur was offered and then refused: the toggle moved and the video did not change. - Both now ask one function, and it asks the browser alone. The platform was the wrong measure: a phone on the fast route held 99% of its frame rate and was refused, a desktop on the slow one held 73% and was allowed. - The in-call device switcher is still withheld on a phone; that is the device menu's own rule and is unchanged. - The provider tests run as a phone, and fail with the desktop test back. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/components/CallFooterViewModel.test.ts | 62 ++++++++++++++++++++-- src/components/CallFooterViewModel.tsx | 3 +- src/livekit/TrackProcessorContext.test.ts | 12 ++++- src/livekit/TrackProcessorContext.tsx | 7 +-- src/livekit/backgroundProcessing.ts | 16 ++++++ 5 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 src/livekit/backgroundProcessing.ts diff --git a/src/components/CallFooterViewModel.test.ts b/src/components/CallFooterViewModel.test.ts index 0c02a2ba7..450482ceb 100644 --- a/src/components/CallFooterViewModel.test.ts +++ b/src/components/CallFooterViewModel.test.ts @@ -14,7 +14,10 @@ import type { CallViewModel } from "../state/CallViewModel/CallViewModel"; import type { Alignment, 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")); @@ -24,10 +27,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)); @@ -222,4 +225,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 them on a phone whose browser can run them", () => { + sdkSupportMock.mockReturnValue(true); + expect(lobbyOn("ios").toggleBlur$.value).toBeDefined(); + }); + + 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..83a58b053 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 "../livekit/backgroundProcessing"; + import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { type MenuOptions } from "./MediaMuteAndSwitchButton"; import { type MediaDevices } from "../state/MediaDevices"; diff --git a/src/livekit/TrackProcessorContext.test.ts b/src/livekit/TrackProcessorContext.test.ts index c873276e0..0ba37c402 100644 --- a/src/livekit/TrackProcessorContext.test.ts +++ b/src/livekit/TrackProcessorContext.test.ts @@ -48,10 +48,11 @@ vi.mock("@livekit/track-processors", () => ({ }), supportsBackgroundProcessors: (): boolean => true, })); +// A phone: the pipeline must not ask. +vi.mock("../Platform", () => ({ platform: "ios" })); vi.mock("./BackgroundEffectTransformer", () => ({ BackgroundEffectTransformer: vi.fn(), })); -vi.mock("../Platform", () => ({ platform: "desktop" })); const processor = {} as ProcessorWrapper; @@ -193,6 +194,15 @@ describe("ProcessorProvider", () => { expect(track.stopProcessor).not.toHaveBeenCalled(); }); + it("runs on a phone whose browser can run it", async () => { + const track = cameraTrack(); + render(surfaces(track)); + await blur(true); + + expect(latest().supported).toBe(true); + expect(track.setProcessor).toHaveBeenCalledWith(latest().processor); + }); + it("preview and call share one pipeline", async () => { const preview = cameraTrack(); const call = cameraTrack(); diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index 7ee7cf562..a10da91fb 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details. import { type ProcessorWrapper, - supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk, type BackgroundOptions, } from "@livekit/track-processors"; import { @@ -26,11 +25,11 @@ import { combineLatest } from "rxjs"; import { backgroundBlur as backgroundBlurSettings } from "../settings/settings"; import { BackgroundEffectTransformer } from "./BackgroundEffectTransformer"; import { OneStepPipeline } from "./OneStepPipeline"; +import { supportsBackgroundProcessors } from "./backgroundProcessing"; import { type Behavior } from "../state/Behavior"; import { ObservableScope } from "../state/ObservableScope"; import { BackgroundEffects } from "../state/BackgroundEffects"; import { useBehavior } from "../useBehavior"; -import { platform } from "../Platform"; //TODO-MULTI-SFU: This is not yet fully there. // it is a combination of exposing observable and react hooks. @@ -122,10 +121,6 @@ interface Props { children: JSX.Element; } -function supportsBackgroundProcessors(): boolean { - return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop"; -} - export const ProcessorProvider: FC = ({ children }) => { const [effects, setEffects] = useState(null); useEffect(() => { diff --git a/src/livekit/backgroundProcessing.ts b/src/livekit/backgroundProcessing.ts new file mode 100644 index 000000000..3dc32e5ec --- /dev/null +++ b/src/livekit/backgroundProcessing.ts @@ -0,0 +1,16 @@ +/* +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"; + +/** + * Whether this browser can run background effects: the one answer for the + * controls and the pipeline, so nothing is offered that the pipeline refuses. + */ +export function supportsBackgroundProcessors(): boolean { + return supportsBackgroundProcessorsLivekitSdk(); +}