Say where background effects run slowly

- Where the browser has no MediaStreamTrackProcessor, effects draw each
  frame through a canvas: slower, and the first build stalls the page.
  They stay offered and choosable there, with a notice under the grid
  that says so and describes the group.
- Decided in the view model, beside whether effects can run at all, so
  the lobby and the call give the same answer.
- The notice stays within the menu's width however long the sentence,
  which the story checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-10-08 14:35:23 +02:00
co-authored by Claude Opus 5.5
parent db57962c50
commit 00402b54b5
6 changed files with 77 additions and 5 deletions
+1
View File
@@ -26,6 +26,7 @@
"blur": "Blur", "blur": "Blur",
"none": "None", "none": "None",
"numbered": "Background {{n}}", "numbered": "Background {{n}}",
"slow": "Background effects run slowly on this platform, which may cause your video to stutter.",
"unavailable": "Background effects are not supported on this platform." "unavailable": "Background effects are not supported on this platform."
}, },
"call_ended_view": { "call_ended_view": {
+4 -2
View File
@@ -86,7 +86,7 @@ export interface FooterState {
/** Every effect on offer, in the order they are shown. */ /** Every effect on offer, in the order they are shown. */
backgroundEffects: BackgroundEffectChoice[]; backgroundEffects: BackgroundEffectChoice[];
/** What the user is told about effects here, if anything. */ /** What the user is told about effects here, if anything. */
backgroundEffectNotice: "unavailable" | undefined; backgroundEffectNotice: "unavailable" | "slow" | undefined;
showFooter: boolean; showFooter: boolean;
/* This is needed for WindowMode = "flat" */ /* This is needed for WindowMode = "flat" */
@@ -243,7 +243,9 @@ export const CallFooter: FC<FooterProps> = ({
backgroundEffectNotice={ backgroundEffectNotice={
backgroundEffectNotice === "unavailable" backgroundEffectNotice === "unavailable"
? t("background_effects.unavailable") ? t("background_effects.unavailable")
: undefined : backgroundEffectNotice === "slow"
? t("background_effects.slow")
: undefined
} }
/>, />,
); );
@@ -31,8 +31,10 @@ vi.mock("../Platform", () => ({
// The SDK's own check needs WebGL and canvas APIs jsdom does not have. // The SDK's own check needs WebGL and canvas APIs jsdom does not have.
const sdkSupportMock = vi.hoisted(() => vi.fn(() => false)); const sdkSupportMock = vi.hoisted(() => vi.fn(() => false));
const modernRouteMock = vi.hoisted(() => vi.fn(() => true));
vi.mock("@livekit/track-processors", () => ({ vi.mock("@livekit/track-processors", () => ({
supportsBackgroundProcessors: (): boolean => sdkSupportMock(), supportsBackgroundProcessors: (): boolean => sdkSupportMock(),
supportsModernBackgroundProcessors: (): boolean => modernRouteMock(),
})); }));
const outputSelectionMock = vi.hoisted(() => vi.fn(() => true)); const outputSelectionMock = vi.hoisted(() => vi.fn(() => true));
@@ -280,6 +282,16 @@ describe("createCallFooterViewModel", () => {
expect(backgroundEffectSetting.getValue()).toBe("none"); expect(backgroundEffectSetting.getValue()).toBe("none");
}); });
it("says they run slowly where only the slower route exists", () => {
sdkSupportMock.mockReturnValue(true);
modernRouteMock.mockReturnValue(false);
const slow = lobbyOn("desktop");
expect(slow.backgroundEffectNotice$.value).toBe("slow");
expect(slow.selectBackgroundEffect$.value).toBeDefined();
modernRouteMock.mockReturnValue(true);
expect(lobbyOn("desktop").backgroundEffectNotice$.value).toBeUndefined();
});
it("offers every effect in order", () => { it("offers every effect in order", () => {
sdkSupportMock.mockReturnValue(true); sdkSupportMock.mockReturnValue(true);
expect(lobbyOn("desktop").backgroundEffects$.value).toEqual([ expect(lobbyOn("desktop").backgroundEffects$.value).toEqual([
+10 -2
View File
@@ -8,7 +8,10 @@ Please see LICENSE in the repository root for full details.
import { combineLatest, map, type Observable, switchMap } from "rxjs"; import { combineLatest, map, type Observable, switchMap } from "rxjs";
import { supportsAudioOutputSelection } from "livekit-client"; import { supportsAudioOutputSelection } from "livekit-client";
import { supportsBackgroundProcessors } from "../livekit/backgroundProcessing"; import {
supportsBackgroundProcessors,
usesFallbackProcessing,
} from "../livekit/backgroundProcessing";
import { import {
parseEffect, parseEffect,
serializeEffect, serializeEffect,
@@ -101,6 +104,7 @@ function buildDeviceBehaviors(
); );
const supported = supportsBackgroundProcessors(); const supported = supportsBackgroundProcessors();
const slow = usesFallbackProcessing();
const offered$ = disableSwitcher$.pipe( const offered$ = disableSwitcher$.pipe(
map((switcherDisabled) => !switcherDisabled && supported), map((switcherDisabled) => !switcherDisabled && supported),
); );
@@ -147,7 +151,11 @@ function buildDeviceBehaviors(
), ),
backgroundEffects$: constant(backgroundEffectChoices()), backgroundEffects$: constant(backgroundEffectChoices()),
backgroundEffectNotice$: scope.behavior( backgroundEffectNotice$: scope.behavior(
offered$.pipe(map((offered) => (offered ? undefined : "unavailable"))), offered$.pipe(
map((offered) =>
!offered ? "unavailable" : slow ? "slow" : undefined,
),
),
), ),
}; };
} }
@@ -268,6 +268,43 @@ export const BackgroundEffectsUnavailable: Story = {
}, },
}; };
/** Where only the slower route exists: offered and choosable, with the cost said. */
export const BackgroundEffectsSlowInThisBrowser: Story = {
args: {
...VideoUnmute.args,
backgroundEffectNotice:
"Background effects run slowly on this platform, which may cause your video to stutter.",
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "Camera" }));
const menu = within(document.body);
const section = await menu.findByRole("group", {
name: "Background effects",
});
await expect(section).toHaveAccessibleDescription(
"Background effects run slowly on this platform, which may cause your video to stutter.",
);
for (const tile of within(section).getAllByRole("menuitemradio"))
await expect(tile).not.toHaveAttribute("aria-disabled");
await userEvent.click(
within(section).getByRole("menuitemradio", { name: "Background 1" }),
);
await expect(args.onSelectBackgroundEffect).toHaveBeenCalledWith(
"image:arc",
);
const notice = document.body.querySelector(`.${styles.notice}`)!;
const frame = document.body
.querySelector("[role='menu']")!
.getBoundingClientRect();
await expect(frame.width).toBeLessThanOrEqual(296);
await expect(notice.getBoundingClientRect().right).toBeLessThanOrEqual(
frame.right,
);
},
};
/** In a short call the effects scroll into view with the list. */ /** In a short call the effects scroll into view with the list. */
export const BackgroundEffectsScrollWhenTheyDoNotFit: Story = { export const BackgroundEffectsScrollWhenTheyDoNotFit: Story = {
args: { args: {
+13 -1
View File
@@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk } from "@livekit/track-processors"; import {
supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk,
supportsModernBackgroundProcessors,
} from "@livekit/track-processors";
import { platform } from "../Platform"; import { platform } from "../Platform";
@@ -13,3 +16,12 @@ import { platform } from "../Platform";
export function supportsBackgroundProcessors(): boolean { export function supportsBackgroundProcessors(): boolean {
return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop"; return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop";
} }
/**
* Whether effects, where they run at all, draw every frame through a canvas:
* the slower route, which costs frame rate and stalls the page while the
* first one is built.
*/
export function usesFallbackProcessing(): boolean {
return !supportsModernBackgroundProcessors();
}