From 5ca9cedf30eac2a3ed93b16b2dfa62481579114a Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 8 Oct 2026 14:34:45 +0200 Subject: [PATCH] Create the background effects with the app's view model - AppViewModel creates them, next to its media devices, as review asked; BackgroundEffectsProvider takes them as a prop rather than building them in an effect. - The component build, which has no AppViewModel of its own, now makes one in the effect that made its media devices, so both builds get their state the same way. Co-Authored-By: Claude Opus 5.5 (1M context) --- component/index.tsx | 19 ++++--- src/App.tsx | 2 +- src/livekit/BackgroundEffectsContext.test.ts | 14 +++-- src/livekit/BackgroundEffectsContext.tsx | 55 ++++++++------------ src/room/CallView.test.tsx | 10 +++- src/room/InCallView.test.tsx | 14 +++-- src/state/AppViewModel.ts | 2 + 7 files changed, 62 insertions(+), 54 deletions(-) diff --git a/component/index.tsx b/component/index.tsx index d49d87d53..e57482abe 100644 --- a/component/index.tsx +++ b/component/index.tsx @@ -72,7 +72,7 @@ import { useUrlParams, } from "../src/UrlParams"; import { MediaDevicesContext } from "../src/MediaDevicesContext"; -import { MediaDevices } from "../src/state/MediaDevices"; +import { AppViewModel } from "../src/state/AppViewModel"; import { ObservableScope } from "../src/state/ObservableScope"; import { BackgroundEffectsProvider } from "../src/livekit/BackgroundEffectsContext"; import { Config } from "../src/config/Config"; @@ -192,14 +192,12 @@ export const ElementCall: FC = ({ // observers running for the rest of the page's life. Null until then, which // is one render. const { controlledAudioDevices, callIntent } = params; - const [mediaDevices, setMediaDevices] = useState(null); + const [vm, setVm] = useState(null); useEffect(() => { const scope = new ObservableScope(); - setMediaDevices( - new MediaDevices(scope, { controlledAudioDevices, callIntent }), - ); + setVm(new AppViewModel(scope, { controlledAudioDevices, callIntent })); return (): void => { - setMediaDevices(null); + setVm(null); scope.end(); }; }, [controlledAudioDevices, callIntent]); @@ -220,8 +218,7 @@ export const ElementCall: FC = ({ // is told that Element Call has loaded, as the widget tells its client once // its own initialisation is over. Once per mount, however often the pieces // are later swapped out. - const ready = - container !== null && rtcSession !== null && mediaDevices !== null; + const ready = container !== null && rtcSession !== null && vm !== null; const announcedLoaded = useRef(false); useEffect(() => { if (!ready || announcedLoaded.current) return; @@ -251,8 +248,10 @@ export const ElementCall: FC = ({ - - + + = ({ vm, widget }) => { loaded && clientReady ? ( - + } > diff --git a/src/livekit/BackgroundEffectsContext.test.ts b/src/livekit/BackgroundEffectsContext.test.ts index 409b54afd..4fd4b67b9 100644 --- a/src/livekit/BackgroundEffectsContext.test.ts +++ b/src/livekit/BackgroundEffectsContext.test.ts @@ -21,7 +21,9 @@ import { syncBackgroundEffects, useBackgroundEffects, useSyncBackgroundEffects, + createAppBackgroundEffects, } from "./BackgroundEffectsContext"; +import { type BackgroundEffects } from "../state/BackgroundEffects"; import { backgroundBlur } from "../settings/settings"; import { constant } from "../state/Behavior"; import { flushPromises, testScope } from "../utils/test"; @@ -150,16 +152,17 @@ describe("BackgroundEffectsProvider", () => { }; // One component for every render, so a rerender updates the tree rather than // mounting a second provider with a pipeline of its own. + let effects: BackgroundEffects | undefined; const Surfaces: FC<{ tracks: (LocalVideoTrack | null)[] }> = ({ tracks }) => - createElement( - BackgroundEffectsProvider, - null, - createElement( + createElement(BackgroundEffectsProvider, { + // Made on first render, once a test has set up the platform. + effects: (effects ??= createAppBackgroundEffects(testScope())), + children: createElement( "div", null, ...tracks.map((track, key) => createElement(Surface, { key, track })), ), - ); + }); const surfaces = ( ...tracks: (LocalVideoTrack | null)[] ): ReturnType => createElement(Surfaces, { tracks }); @@ -173,6 +176,7 @@ describe("BackgroundEffectsProvider", () => { beforeEach(() => { seen = []; + effects = undefined; pipelines.built = 0; pipelines.destroyed = 0; pipelines.switches = []; diff --git a/src/livekit/BackgroundEffectsContext.tsx b/src/livekit/BackgroundEffectsContext.tsx index b9a0a4b98..05c8d4f20 100644 --- a/src/livekit/BackgroundEffectsContext.tsx +++ b/src/livekit/BackgroundEffectsContext.tsx @@ -10,14 +10,7 @@ import { type ProcessorWrapper, type BackgroundOptions, } from "@livekit/track-processors"; -import { - createContext, - type FC, - type JSX, - use, - useEffect, - useState, -} from "react"; +import { createContext, type FC, type JSX, use, useEffect } from "react"; import { type LocalVideoTrack } from "livekit-client"; import { logger } from "matrix-js-sdk/lib/logger"; import { combineLatest } from "rxjs"; @@ -27,7 +20,7 @@ import { BackgroundEffectTransformer } from "./BackgroundEffectTransformer"; import { OneStepPipeline } from "./OneStepPipeline"; import { supportsBackgroundProcessors } from "./backgroundProcessing"; import { type Behavior } from "../state/Behavior"; -import { ObservableScope } from "../state/ObservableScope"; +import { type ObservableScope } from "../state/ObservableScope"; import { type BackgroundEffects, createBackgroundEffects, @@ -118,31 +111,27 @@ export const useSyncBackgroundEffects = ( }, [processor, videoTrack]); }; +/** The app's one pipeline, shared by every camera track it opens. */ +export function createAppBackgroundEffects( + scope: ObservableScope, +): BackgroundEffects { + return createBackgroundEffects(scope, { + supported: supportsBackgroundProcessors(), + blur$: backgroundBlurSettings.value$, + pipeline: new OneStepPipeline( + new BackgroundEffectTransformer({ backgroundDisabled: true }), + "background-effect", + ), + }); +} + interface Props { + effects: BackgroundEffects; children: JSX.Element; } -export const BackgroundEffectsProvider: FC = ({ children }) => { - const [effects, setEffects] = useState(null); - useEffect(() => { - const scope = new ObservableScope(); - setEffects( - createBackgroundEffects(scope, { - supported: supportsBackgroundProcessors(), - blur$: backgroundBlurSettings.value$, - pipeline: new OneStepPipeline( - new BackgroundEffectTransformer({ backgroundDisabled: true }), - "background-effect", - ), - }), - ); - return (): void => scope.end(); - }, []); - - if (effects === null) return null; - return ( - - {children} - - ); -}; +export const BackgroundEffectsProvider: FC = ({ effects, children }) => ( + + {children} + +); diff --git a/src/room/CallView.test.tsx b/src/room/CallView.test.tsx index 1968222db..a2253609e 100644 --- a/src/room/CallView.test.tsx +++ b/src/room/CallView.test.tsx @@ -50,6 +50,7 @@ import { mockMediaDevices, mockRtcMembership, MockRTCSession, + testScope, } from "../utils/test"; import { CallView } from "./CallView"; import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary"; @@ -61,7 +62,10 @@ import { } from "../HostBridge"; import { type JoinCallData } from "../widget"; import { MatrixRTCTransportMissingError } from "../utils/errors"; -import { BackgroundEffectsProvider } from "../livekit/BackgroundEffectsContext"; +import { + BackgroundEffectsProvider, + createAppBackgroundEffects, +} from "../livekit/BackgroundEffectsContext"; import { MediaDevicesContext } from "../MediaDevicesContext"; import { constant } from "../state/Behavior"; @@ -193,7 +197,9 @@ function createCallView( - + {options.withErrorBoundary ? ( {callView} diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index 542880afd..c46164d96 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -31,6 +31,7 @@ import { mockRemoteParticipant, mockRtcMembership, type MockRTCSession, + testScope, } from "../utils/test"; import { E2eeType } from "../e2ee/e2eeType"; import { @@ -49,7 +50,10 @@ import { MediaDevicesContext } from "../MediaDevicesContext"; import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices"; import { AppBar } from "../AppBar"; import { type MatrixInfo } from "./VideoPreview"; -import { BackgroundEffectsProvider } from "../livekit/BackgroundEffectsContext"; +import { + BackgroundEffectsProvider, + createAppBackgroundEffects, +} from "../livekit/BackgroundEffectsContext"; import { initializeWidget } from "../widget"; import { RootElementProvider } from "../RootElementContext"; @@ -242,7 +246,9 @@ describe("ActiveCall", () => { const { findByTestId } = render( - + { - +