mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +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,
|
useUrlParams,
|
||||||
} from "../src/UrlParams";
|
} from "../src/UrlParams";
|
||||||
import { MediaDevicesContext } from "../src/MediaDevicesContext";
|
import { MediaDevicesContext } from "../src/MediaDevicesContext";
|
||||||
import { MediaDevices } from "../src/state/MediaDevices";
|
import { AppViewModel } from "../src/state/AppViewModel";
|
||||||
import { ObservableScope } from "../src/state/ObservableScope";
|
import { ObservableScope } from "../src/state/ObservableScope";
|
||||||
import { BackgroundEffectsProvider } from "../src/livekit/BackgroundEffectsContext";
|
import { BackgroundEffectsProvider } from "../src/livekit/BackgroundEffectsContext";
|
||||||
import { Config } from "../src/config/Config";
|
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
|
// observers running for the rest of the page's life. Null until then, which
|
||||||
// is one render.
|
// is one render.
|
||||||
const { controlledAudioDevices, callIntent } = params;
|
const { controlledAudioDevices, callIntent } = params;
|
||||||
const [mediaDevices, setMediaDevices] = useState<MediaDevices | null>(null);
|
const [vm, setVm] = useState<AppViewModel | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
setMediaDevices(
|
setVm(new AppViewModel(scope, { controlledAudioDevices, callIntent }));
|
||||||
new MediaDevices(scope, { controlledAudioDevices, callIntent }),
|
|
||||||
);
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
setMediaDevices(null);
|
setVm(null);
|
||||||
scope.end();
|
scope.end();
|
||||||
};
|
};
|
||||||
}, [controlledAudioDevices, callIntent]);
|
}, [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
|
// 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
|
// its own initialisation is over. Once per mount, however often the pieces
|
||||||
// are later swapped out.
|
// are later swapped out.
|
||||||
const ready =
|
const ready = container !== null && rtcSession !== null && vm !== null;
|
||||||
container !== null && rtcSession !== null && mediaDevices !== null;
|
|
||||||
const announcedLoaded = useRef(false);
|
const announcedLoaded = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ready || announcedLoaded.current) return;
|
if (!ready || announcedLoaded.current) return;
|
||||||
@@ -251,8 +248,10 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
<Decoration>
|
<Decoration>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<ClientProvider client={client}>
|
<ClientProvider client={client}>
|
||||||
<MediaDevicesContext value={mediaDevices}>
|
<MediaDevicesContext value={vm.mediaDevices}>
|
||||||
<BackgroundEffectsProvider>
|
<BackgroundEffectsProvider
|
||||||
|
effects={vm.backgroundEffects}
|
||||||
|
>
|
||||||
<CallView
|
<CallView
|
||||||
client={client}
|
client={client}
|
||||||
rtcSession={rtcSession}
|
rtcSession={rtcSession}
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ export const App: FC<Props> = ({ vm, widget }) => {
|
|||||||
loaded && clientReady ? (
|
loaded && clientReady ? (
|
||||||
<ClientProvider client={widgetClient}>
|
<ClientProvider client={widgetClient}>
|
||||||
<MediaDevicesContext value={vm.mediaDevices}>
|
<MediaDevicesContext value={vm.mediaDevices}>
|
||||||
<BackgroundEffectsProvider>
|
<BackgroundEffectsProvider effects={vm.backgroundEffects}>
|
||||||
<Sentry.ErrorBoundary
|
<Sentry.ErrorBoundary
|
||||||
fallback={(error) => <ErrorPage error={error} />}
|
fallback={(error) => <ErrorPage error={error} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
syncBackgroundEffects,
|
syncBackgroundEffects,
|
||||||
useBackgroundEffects,
|
useBackgroundEffects,
|
||||||
useSyncBackgroundEffects,
|
useSyncBackgroundEffects,
|
||||||
|
createAppBackgroundEffects,
|
||||||
} from "./BackgroundEffectsContext";
|
} from "./BackgroundEffectsContext";
|
||||||
|
import { type BackgroundEffects } from "../state/BackgroundEffects";
|
||||||
import { backgroundBlur } from "../settings/settings";
|
import { backgroundBlur } 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";
|
||||||
@@ -150,16 +152,17 @@ describe("BackgroundEffectsProvider", () => {
|
|||||||
};
|
};
|
||||||
// One component for every render, so a rerender updates the tree rather than
|
// One component for every render, so a rerender updates the tree rather than
|
||||||
// mounting a second provider with a pipeline of its own.
|
// mounting a second provider with a pipeline of its own.
|
||||||
|
let effects: BackgroundEffects | undefined;
|
||||||
const Surfaces: FC<{ tracks: (LocalVideoTrack | null)[] }> = ({ tracks }) =>
|
const Surfaces: FC<{ tracks: (LocalVideoTrack | null)[] }> = ({ tracks }) =>
|
||||||
createElement(
|
createElement(BackgroundEffectsProvider, {
|
||||||
BackgroundEffectsProvider,
|
// Made on first render, once a test has set up the platform.
|
||||||
null,
|
effects: (effects ??= createAppBackgroundEffects(testScope())),
|
||||||
createElement(
|
children: createElement(
|
||||||
"div",
|
"div",
|
||||||
null,
|
null,
|
||||||
...tracks.map((track, key) => createElement(Surface, { key, track })),
|
...tracks.map((track, key) => createElement(Surface, { key, track })),
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
const surfaces = (
|
const surfaces = (
|
||||||
...tracks: (LocalVideoTrack | null)[]
|
...tracks: (LocalVideoTrack | null)[]
|
||||||
): ReturnType<typeof createElement> => createElement(Surfaces, { tracks });
|
): ReturnType<typeof createElement> => createElement(Surfaces, { tracks });
|
||||||
@@ -173,6 +176,7 @@ describe("BackgroundEffectsProvider", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
seen = [];
|
seen = [];
|
||||||
|
effects = undefined;
|
||||||
pipelines.built = 0;
|
pipelines.built = 0;
|
||||||
pipelines.destroyed = 0;
|
pipelines.destroyed = 0;
|
||||||
pipelines.switches = [];
|
pipelines.switches = [];
|
||||||
|
|||||||
@@ -10,14 +10,7 @@ import {
|
|||||||
type ProcessorWrapper,
|
type ProcessorWrapper,
|
||||||
type BackgroundOptions,
|
type BackgroundOptions,
|
||||||
} from "@livekit/track-processors";
|
} from "@livekit/track-processors";
|
||||||
import {
|
import { createContext, type FC, type JSX, use, useEffect } from "react";
|
||||||
createContext,
|
|
||||||
type FC,
|
|
||||||
type JSX,
|
|
||||||
use,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { type LocalVideoTrack } from "livekit-client";
|
import { type LocalVideoTrack } from "livekit-client";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { combineLatest } from "rxjs";
|
import { combineLatest } from "rxjs";
|
||||||
@@ -27,7 +20,7 @@ import { BackgroundEffectTransformer } from "./BackgroundEffectTransformer";
|
|||||||
import { OneStepPipeline } from "./OneStepPipeline";
|
import { OneStepPipeline } from "./OneStepPipeline";
|
||||||
import { supportsBackgroundProcessors } from "./backgroundProcessing";
|
import { supportsBackgroundProcessors } from "./backgroundProcessing";
|
||||||
import { type Behavior } from "../state/Behavior";
|
import { type Behavior } from "../state/Behavior";
|
||||||
import { ObservableScope } from "../state/ObservableScope";
|
import { type ObservableScope } from "../state/ObservableScope";
|
||||||
import {
|
import {
|
||||||
type BackgroundEffects,
|
type BackgroundEffects,
|
||||||
createBackgroundEffects,
|
createBackgroundEffects,
|
||||||
@@ -118,31 +111,27 @@ export const useSyncBackgroundEffects = (
|
|||||||
}, [processor, videoTrack]);
|
}, [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 {
|
interface Props {
|
||||||
|
effects: BackgroundEffects;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BackgroundEffectsProvider: FC<Props> = ({ children }) => {
|
export const BackgroundEffectsProvider: FC<Props> = ({ effects, children }) => (
|
||||||
const [effects, setEffects] = useState<BackgroundEffects | null>(null);
|
<BackgroundEffectsContext value={effects}>
|
||||||
useEffect(() => {
|
{children}
|
||||||
const scope = new ObservableScope();
|
</BackgroundEffectsContext>
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
MockRTCSession,
|
MockRTCSession,
|
||||||
|
testScope,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import { CallView } from "./CallView";
|
import { CallView } from "./CallView";
|
||||||
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary";
|
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary";
|
||||||
@@ -61,7 +62,10 @@ import {
|
|||||||
} from "../HostBridge";
|
} from "../HostBridge";
|
||||||
import { type JoinCallData } from "../widget";
|
import { type JoinCallData } from "../widget";
|
||||||
import { MatrixRTCTransportMissingError } from "../utils/errors";
|
import { MatrixRTCTransportMissingError } from "../utils/errors";
|
||||||
import { BackgroundEffectsProvider } from "../livekit/BackgroundEffectsContext";
|
import {
|
||||||
|
BackgroundEffectsProvider,
|
||||||
|
createAppBackgroundEffects,
|
||||||
|
} from "../livekit/BackgroundEffectsContext";
|
||||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
import { constant } from "../state/Behavior";
|
import { constant } from "../state/Behavior";
|
||||||
|
|
||||||
@@ -193,7 +197,9 @@ function createCallView(
|
|||||||
<HostBridgeProvider value={hostBridge}>
|
<HostBridgeProvider value={hostBridge}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<MediaDevicesContext value={mockMediaDevices({})}>
|
<MediaDevicesContext value={mockMediaDevices({})}>
|
||||||
<BackgroundEffectsProvider>
|
<BackgroundEffectsProvider
|
||||||
|
effects={createAppBackgroundEffects(testScope())}
|
||||||
|
>
|
||||||
{options.withErrorBoundary ? (
|
{options.withErrorBoundary ? (
|
||||||
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()}>
|
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()}>
|
||||||
{callView}
|
{callView}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
mockRemoteParticipant,
|
mockRemoteParticipant,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
type MockRTCSession,
|
type MockRTCSession,
|
||||||
|
testScope,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import {
|
import {
|
||||||
@@ -49,7 +50,10 @@ import { MediaDevicesContext } from "../MediaDevicesContext";
|
|||||||
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
||||||
import { AppBar } from "../AppBar";
|
import { AppBar } from "../AppBar";
|
||||||
import { type MatrixInfo } from "./VideoPreview";
|
import { type MatrixInfo } from "./VideoPreview";
|
||||||
import { BackgroundEffectsProvider } from "../livekit/BackgroundEffectsContext";
|
import {
|
||||||
|
BackgroundEffectsProvider,
|
||||||
|
createAppBackgroundEffects,
|
||||||
|
} from "../livekit/BackgroundEffectsContext";
|
||||||
import { initializeWidget } from "../widget";
|
import { initializeWidget } from "../widget";
|
||||||
import { RootElementProvider } from "../RootElementContext";
|
import { RootElementProvider } from "../RootElementContext";
|
||||||
|
|
||||||
@@ -242,7 +246,9 @@ describe("ActiveCall", () => {
|
|||||||
const { findByTestId } = render(
|
const { findByTestId } = render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<MediaDevicesContext value={mediaDevices}>
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
<BackgroundEffectsProvider>
|
<BackgroundEffectsProvider
|
||||||
|
effects={createAppBackgroundEffects(testScope())}
|
||||||
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
||||||
<ActiveCall
|
<ActiveCall
|
||||||
@@ -292,7 +298,9 @@ describe("ActiveCall", () => {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<RootElementProvider value={root}>
|
<RootElementProvider value={root}>
|
||||||
<MediaDevicesContext value={mediaDevices}>
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
<BackgroundEffectsProvider>
|
<BackgroundEffectsProvider
|
||||||
|
effects={createAppBackgroundEffects(testScope())}
|
||||||
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
||||||
<ActiveCall
|
<ActiveCall
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type AudioOutputOptions, MediaDevices } from "./MediaDevices";
|
import { type AudioOutputOptions, MediaDevices } from "./MediaDevices";
|
||||||
|
import { createAppBackgroundEffects } from "../livekit/BackgroundEffectsContext";
|
||||||
import { type ObservableScope } from "./ObservableScope";
|
import { type ObservableScope } from "./ObservableScope";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +17,7 @@ export class AppViewModel {
|
|||||||
this.scope,
|
this.scope,
|
||||||
this.audioOutputOptions,
|
this.audioOutputOptions,
|
||||||
);
|
);
|
||||||
|
public readonly backgroundEffects = createAppBackgroundEffects(this.scope);
|
||||||
|
|
||||||
// TODO: Move more application logic here. The CallViewModel, at the very
|
// TODO: Move more application logic here. The CallViewModel, at the very
|
||||||
// least, ought to be accessible from this object.
|
// least, ought to be accessible from this object.
|
||||||
|
|||||||
Reference in New Issue
Block a user