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) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-09-28 17:38:27 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b297dbafa2
commit 9ce5cf0d8e
5 changed files with 88 additions and 12 deletions
+58 -4
View File
@@ -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<typeof createLobbyFooterViewModel> {
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);
}
});
});
});
+2 -1
View File
@@ -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";
+11 -1
View File
@@ -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<BackgroundOptions>;
@@ -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();
+1 -6
View File
@@ -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<Props> = ({ children }) => {
const [effects, setEffects] = useState<BackgroundEffects | null>(null);
useEffect(() => {
+16
View File
@@ -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();
}