mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Hold the chosen effect across calls, and forget one that can't run
- A remembered effect this browser can't run is cleared rather than kept, as the menu already showed no effect in force there. - A camera turned off and on again with an effect chosen gets the same pipeline as its track arrives, neither rebuilt nor switched again. - End to end, on Chromium and Firefox: the effect chosen before a call is still chosen, and worn, in the next call after leaving; and a second tab still shuts the first down with the existing "Opened in another tab" error while an effect runs. - Switching camera and sharing a screen are checked by hand: they need two cameras and a screen, which the fake devices don't give yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,69 @@ test.describe("background effects section", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("the chosen effect", () => {
|
||||||
|
test("chosen effect survives leaving and rejoining", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
await page.goto("/");
|
||||||
|
await SpaHelpers.createCall(page, "Returning user", "Effect remembered");
|
||||||
|
const { camera, picture } = await chooseAndWear(page, "Background 1");
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
await page.getByTestId("incall_leave").click();
|
||||||
|
await expect(page.getByRole("heading")).toContainText(
|
||||||
|
"your call has ended",
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
await page.getByTestId("home_callName").fill("Effect remembered again");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Camera", exact: true }).click();
|
||||||
|
const tile = page
|
||||||
|
.getByRole("group", { name: "Background effects" })
|
||||||
|
.getByRole("menuitemradio", { name: "Background 1" });
|
||||||
|
await expect(tile).toHaveAttribute("aria-checked", "true");
|
||||||
|
const preview = page.locator("video").first();
|
||||||
|
await expect
|
||||||
|
.poll(async () => distance(await averageColour(preview), picture), {
|
||||||
|
timeout: 60_000,
|
||||||
|
})
|
||||||
|
.toBeLessThan(distance(camera, picture) / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("second session still shows the existing error", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
await page.goto("/");
|
||||||
|
await SpaHelpers.createCall(page, "Two tabs", "Effect in two tabs");
|
||||||
|
await chooseAndWear(page, "Background 1");
|
||||||
|
|
||||||
|
const second = await page.context().newPage();
|
||||||
|
await second.goto("/");
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Opened in another tab" }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Chooses an effect in the lobby, and waits until the preview wears it. */
|
||||||
|
async function chooseAndWear(
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
): Promise<{ camera: Colour; picture: Colour }> {
|
||||||
|
const preview = page.locator("video").first();
|
||||||
|
const camera = await cameraColour(preview);
|
||||||
|
await page.getByRole("button", { name: "Camera", exact: true }).click();
|
||||||
|
const tile = page.getByRole("menuitemradio", { name });
|
||||||
|
await tile.click();
|
||||||
|
const picture = await averageColour(tile.locator("img"));
|
||||||
|
await expect
|
||||||
|
.poll(async () => distance(await averageColour(preview), picture), {
|
||||||
|
timeout: 60_000,
|
||||||
|
})
|
||||||
|
.toBeLessThan(distance(camera, picture) / 2);
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
return { camera, picture };
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("joining with a background chosen", () => {
|
test.describe("joining with a background chosen", () => {
|
||||||
test("publishes no frame of the room", async ({ browser }) => {
|
test("publishes no frame of the room", async ({ browser }) => {
|
||||||
// Two first builds of the pipeline, one of them held back.
|
// Two first builds of the pipeline, one of them held back.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { backgroundEffect } from "../settings/settings";
|
|||||||
import { constant } from "../state/Behavior";
|
import { constant } from "../state/Behavior";
|
||||||
import { flushPromises, testScope } from "../utils/test";
|
import { flushPromises, testScope } from "../utils/test";
|
||||||
|
|
||||||
|
const sdkSupportMock = vi.hoisted(() => vi.fn(() => true));
|
||||||
const pipelines = vi.hoisted(() => ({
|
const pipelines = vi.hoisted(() => ({
|
||||||
built: 0,
|
built: 0,
|
||||||
destroyed: 0,
|
destroyed: 0,
|
||||||
@@ -48,7 +49,7 @@ vi.mock("@livekit/track-processors", () => ({
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
supportsBackgroundProcessors: (): boolean => true,
|
supportsBackgroundProcessors: (): boolean => sdkSupportMock(),
|
||||||
}));
|
}));
|
||||||
const platformMock = vi.hoisted(() => ({ platform: "desktop" }));
|
const platformMock = vi.hoisted(() => ({ platform: "desktop" }));
|
||||||
vi.mock("../Platform", () => platformMock);
|
vi.mock("../Platform", () => platformMock);
|
||||||
@@ -181,6 +182,7 @@ describe("BackgroundEffectsProvider", () => {
|
|||||||
pipelines.destroyed = 0;
|
pipelines.destroyed = 0;
|
||||||
pipelines.switches = [];
|
pipelines.switches = [];
|
||||||
platformMock.platform = "desktop";
|
platformMock.platform = "desktop";
|
||||||
|
sdkSupportMock.mockReturnValue(true);
|
||||||
backgroundEffect.setValue("none");
|
backgroundEffect.setValue("none");
|
||||||
});
|
});
|
||||||
afterEach(() => backgroundEffect.setValue("none"));
|
afterEach(() => backgroundEffect.setValue("none"));
|
||||||
@@ -199,6 +201,28 @@ describe("BackgroundEffectsProvider", () => {
|
|||||||
expect(track.stopProcessor).not.toHaveBeenCalled();
|
expect(track.stopProcessor).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("camera on with an effect chosen publishes nothing unprocessed", async () => {
|
||||||
|
const view = render(surfaces(cameraTrack()));
|
||||||
|
await blur(true);
|
||||||
|
const pipeline = latest().processor;
|
||||||
|
|
||||||
|
// Camera off, then on again: a new track, given the pipeline as it
|
||||||
|
// arrives, and the pipeline neither rebuilt nor switched again.
|
||||||
|
await act(async () => {
|
||||||
|
view.rerender(surfaces(null));
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
const again = cameraTrack();
|
||||||
|
await act(async () => {
|
||||||
|
view.rerender(surfaces(again));
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
expect(again.setProcessor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(again.setProcessor).toHaveBeenCalledWith(pipeline);
|
||||||
|
expect(pipelines.built).toBe(1);
|
||||||
|
expect(pipelines.switches).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("runs on a phone whose browser can run it", async () => {
|
it("runs on a phone whose browser can run it", async () => {
|
||||||
platformMock.platform = "ios";
|
platformMock.platform = "ios";
|
||||||
const track = cameraTrack();
|
const track = cameraTrack();
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export function createAppBackgroundEffects(
|
|||||||
return createBackgroundEffects(scope, {
|
return createBackgroundEffects(scope, {
|
||||||
supported: supportsBackgroundProcessors(),
|
supported: supportsBackgroundProcessors(),
|
||||||
effect$: backgroundEffectSetting.value$,
|
effect$: backgroundEffectSetting.value$,
|
||||||
|
setEffect: backgroundEffectSetting.setValue,
|
||||||
pipeline: new OneStepPipeline(transformer, "background-effect"),
|
pipeline: new OneStepPipeline(transformer, "background-effect"),
|
||||||
transformer,
|
transformer,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ describe("the pipeline's state", () => {
|
|||||||
const effects = createBackgroundEffects(testScope(), {
|
const effects = createBackgroundEffects(testScope(), {
|
||||||
supported: true,
|
supported: true,
|
||||||
effect$: behavior(effect, { n: "none", b: "blur" }),
|
effect$: behavior(effect, { n: "none", b: "blur" }),
|
||||||
|
setEffect: vi.fn(),
|
||||||
pipeline: fakePipeline().pipeline,
|
pipeline: fakePipeline().pipeline,
|
||||||
transformer,
|
transformer,
|
||||||
});
|
});
|
||||||
@@ -94,6 +95,7 @@ describe("the pipeline's state", () => {
|
|||||||
|
|
||||||
describe("background effects", () => {
|
describe("background effects", () => {
|
||||||
let effect$: BehaviorSubject<string>;
|
let effect$: BehaviorSubject<string>;
|
||||||
|
let setEffect: (raw: string) => void;
|
||||||
let fake: ReturnType<typeof fakePipeline>;
|
let fake: ReturnType<typeof fakePipeline>;
|
||||||
|
|
||||||
function build(
|
function build(
|
||||||
@@ -102,6 +104,7 @@ describe("background effects", () => {
|
|||||||
return createBackgroundEffects(testScope(), {
|
return createBackgroundEffects(testScope(), {
|
||||||
supported: true,
|
supported: true,
|
||||||
effect$,
|
effect$,
|
||||||
|
setEffect,
|
||||||
pipeline: fake.pipeline,
|
pipeline: fake.pipeline,
|
||||||
transformer: { onFirstFrame: undefined },
|
transformer: { onFirstFrame: undefined },
|
||||||
...options,
|
...options,
|
||||||
@@ -116,6 +119,7 @@ describe("background effects", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
effect$ = new BehaviorSubject("none");
|
effect$ = new BehaviorSubject("none");
|
||||||
|
setEffect = vi.fn((raw: string) => effect$.next(raw));
|
||||||
fake = fakePipeline();
|
fake = fakePipeline();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,4 +173,11 @@ describe("background effects", () => {
|
|||||||
{ mode: "disabled" },
|
{ mode: "disabled" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears a remembered effect when it cannot be honoured", () => {
|
||||||
|
effect$.next(`image:${shippedBackgrounds[0].id}`);
|
||||||
|
const effects = build({ supported: false });
|
||||||
|
expect(effect$.value).toBe("none");
|
||||||
|
expect(effects.state$.value.processor).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { type BackgroundEffectsState } from "../livekit/BackgroundEffectsContext
|
|||||||
import {
|
import {
|
||||||
type BackgroundEffect,
|
type BackgroundEffect,
|
||||||
blurRadius,
|
blurRadius,
|
||||||
|
type EffectId,
|
||||||
imagePathFor,
|
imagePathFor,
|
||||||
parseEffect,
|
parseEffect,
|
||||||
} from "../livekit/backgroundEffects";
|
} from "../livekit/backgroundEffects";
|
||||||
@@ -34,6 +35,8 @@ export interface BackgroundEffectsOptions {
|
|||||||
supported: boolean;
|
supported: boolean;
|
||||||
/** The effect chosen, as the setting stores it. */
|
/** The effect chosen, as the setting stores it. */
|
||||||
effect$: Behavior<string>;
|
effect$: Behavior<string>;
|
||||||
|
/** Stores a choice, to forget one that can't be honoured. */
|
||||||
|
setEffect: (id: EffectId) => void;
|
||||||
/** The background processor pipeline to be switched between effects. */
|
/** The background processor pipeline to be switched between effects. */
|
||||||
pipeline: BackgroundProcessorWrapper;
|
pipeline: BackgroundProcessorWrapper;
|
||||||
/** Tells, once, that a frame carrying an effect has been drawn. */
|
/** Tells, once, that a frame carrying an effect has been drawn. */
|
||||||
@@ -48,7 +51,13 @@ export interface BackgroundEffects {
|
|||||||
/** Switches the pipeline as the choice changes, for as long as the scope lasts. */
|
/** Switches the pipeline as the choice changes, for as long as the scope lasts. */
|
||||||
export function createBackgroundEffects(
|
export function createBackgroundEffects(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
{ supported, effect$, pipeline, transformer }: BackgroundEffectsOptions,
|
{
|
||||||
|
supported,
|
||||||
|
effect$,
|
||||||
|
setEffect,
|
||||||
|
pipeline,
|
||||||
|
transformer,
|
||||||
|
}: BackgroundEffectsOptions,
|
||||||
): BackgroundEffects {
|
): BackgroundEffects {
|
||||||
const choice$ = effect$.pipe(map(parseEffect));
|
const choice$ = effect$.pipe(map(parseEffect));
|
||||||
const wanted$ = choice$.pipe(
|
const wanted$ = choice$.pipe(
|
||||||
@@ -56,6 +65,11 @@ export function createBackgroundEffects(
|
|||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
choice$.pipe(scope.bind()).subscribe((effect) => {
|
||||||
|
if (effect.kind === "none") return;
|
||||||
|
if (!supported) setEffect("none");
|
||||||
|
});
|
||||||
|
|
||||||
const drewAFrame$ = scope.behavior(
|
const drewAFrame$ = scope.behavior(
|
||||||
new Observable<boolean>((subscriber) => {
|
new Observable<boolean>((subscriber) => {
|
||||||
subscriber.next(false);
|
subscriber.next(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user