mirror of
https://github.com/element-hq/element-call.git
synced 2026-10-09 20:37:38 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
+9
-10
@@ -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<ElementCallProps> = ({
|
||||
// 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<MediaDevices | null>(null);
|
||||
const [vm, setVm] = useState<AppViewModel | null>(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<ElementCallProps> = ({
|
||||
// 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<ElementCallProps> = ({
|
||||
<Decoration>
|
||||
<TooltipProvider>
|
||||
<ClientProvider client={client}>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<BackgroundEffectsProvider>
|
||||
<MediaDevicesContext value={vm.mediaDevices}>
|
||||
<BackgroundEffectsProvider
|
||||
effects={vm.backgroundEffects}
|
||||
>
|
||||
<CallView
|
||||
client={client}
|
||||
rtcSession={rtcSession}
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ export const App: FC<Props> = ({ vm, widget }) => {
|
||||
loaded && clientReady ? (
|
||||
<ClientProvider client={widgetClient}>
|
||||
<MediaDevicesContext value={vm.mediaDevices}>
|
||||
<BackgroundEffectsProvider>
|
||||
<BackgroundEffectsProvider effects={vm.backgroundEffects}>
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={(error) => <ErrorPage error={error} />}
|
||||
>
|
||||
|
||||
@@ -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<typeof createElement> => createElement(Surfaces, { tracks });
|
||||
@@ -173,6 +176,7 @@ describe("BackgroundEffectsProvider", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
seen = [];
|
||||
effects = undefined;
|
||||
pipelines.built = 0;
|
||||
pipelines.destroyed = 0;
|
||||
pipelines.switches = [];
|
||||
|
||||
@@ -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<Props> = ({ children }) => {
|
||||
const [effects, setEffects] = useState<BackgroundEffects | null>(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 (
|
||||
<BackgroundEffectsContext value={effects}>
|
||||
{children}
|
||||
</BackgroundEffectsContext>
|
||||
);
|
||||
};
|
||||
export const BackgroundEffectsProvider: FC<Props> = ({ effects, children }) => (
|
||||
<BackgroundEffectsContext value={effects}>
|
||||
{children}
|
||||
</BackgroundEffectsContext>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
<HostBridgeProvider value={hostBridge}>
|
||||
<TooltipProvider>
|
||||
<MediaDevicesContext value={mockMediaDevices({})}>
|
||||
<BackgroundEffectsProvider>
|
||||
<BackgroundEffectsProvider
|
||||
effects={createAppBackgroundEffects(testScope())}
|
||||
>
|
||||
{options.withErrorBoundary ? (
|
||||
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()}>
|
||||
{callView}
|
||||
|
||||
@@ -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(
|
||||
<BrowserRouter>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<BackgroundEffectsProvider>
|
||||
<BackgroundEffectsProvider
|
||||
effects={createAppBackgroundEffects(testScope())}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
||||
<ActiveCall
|
||||
@@ -292,7 +298,9 @@ describe("ActiveCall", () => {
|
||||
<BrowserRouter>
|
||||
<RootElementProvider value={root}>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<BackgroundEffectsProvider>
|
||||
<BackgroundEffectsProvider
|
||||
effects={createAppBackgroundEffects(testScope())}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
||||
<ActiveCall
|
||||
|
||||
@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type AudioOutputOptions, MediaDevices } from "./MediaDevices";
|
||||
import { createAppBackgroundEffects } from "../livekit/BackgroundEffectsContext";
|
||||
import { type ObservableScope } from "./ObservableScope";
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ export class AppViewModel {
|
||||
this.scope,
|
||||
this.audioOutputOptions,
|
||||
);
|
||||
public readonly backgroundEffects = createAppBackgroundEffects(this.scope);
|
||||
|
||||
// TODO: Move more application logic here. The CallViewModel, at the very
|
||||
// least, ought to be accessible from this object.
|
||||
|
||||
Reference in New Issue
Block a user