mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Merge pull request #4231 from element-hq/matthew/blur-processor-ended-track
Don't attach the background blur processor to an ended camera track
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { type LocalVideoTrack } from "livekit-client";
|
||||||
|
import {
|
||||||
|
type BackgroundOptions,
|
||||||
|
type ProcessorWrapper,
|
||||||
|
} from "@livekit/track-processors";
|
||||||
|
|
||||||
|
import { applyProcessor, trackProcessorSync } from "./TrackProcessorContext";
|
||||||
|
import { constant } from "../state/Behavior";
|
||||||
|
import { flushPromises, testScope } from "../utils/test";
|
||||||
|
|
||||||
|
const processor = {} as ProcessorWrapper<BackgroundOptions>;
|
||||||
|
|
||||||
|
function mockTrack(
|
||||||
|
readyState: MediaStreamTrackState,
|
||||||
|
current?: ProcessorWrapper<BackgroundOptions>,
|
||||||
|
): LocalVideoTrack {
|
||||||
|
return {
|
||||||
|
mediaStreamTrack: { readyState },
|
||||||
|
getProcessor: vi.fn().mockReturnValue(current),
|
||||||
|
setProcessor: vi.fn().mockResolvedValue(undefined),
|
||||||
|
stopProcessor: vi.fn().mockResolvedValue(undefined),
|
||||||
|
} as unknown as LocalVideoTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("applyProcessor", () => {
|
||||||
|
it("attaches the processor to a live track", () => {
|
||||||
|
const track = mockTrack("live");
|
||||||
|
applyProcessor(track, processor);
|
||||||
|
expect(track.setProcessor).toHaveBeenCalledWith(processor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not attach the processor to an ended track", () => {
|
||||||
|
const track = mockTrack("ended");
|
||||||
|
applyProcessor(track, processor);
|
||||||
|
expect(track.setProcessor).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not surface a rejected setProcessor", async () => {
|
||||||
|
const track = mockTrack("live");
|
||||||
|
vi.mocked(track.setProcessor).mockRejectedValue(
|
||||||
|
new TypeError("Input track cannot be ended"),
|
||||||
|
);
|
||||||
|
const unhandled = vi.fn();
|
||||||
|
process.on("unhandledRejection", unhandled);
|
||||||
|
applyProcessor(track, processor);
|
||||||
|
await flushPromises();
|
||||||
|
process.off("unhandledRejection", unhandled);
|
||||||
|
expect(unhandled).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops the processor when none is wanted", () => {
|
||||||
|
const track = mockTrack("live", processor);
|
||||||
|
applyProcessor(track, undefined);
|
||||||
|
expect(track.stopProcessor).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not surface a rejected stopProcessor", async () => {
|
||||||
|
const track = mockTrack("live", processor);
|
||||||
|
vi.mocked(track.stopProcessor).mockRejectedValue(new Error("nope"));
|
||||||
|
const unhandled = vi.fn();
|
||||||
|
process.on("unhandledRejection", unhandled);
|
||||||
|
applyProcessor(track, undefined);
|
||||||
|
await flushPromises();
|
||||||
|
process.off("unhandledRejection", unhandled);
|
||||||
|
expect(unhandled).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("trackProcessorSync", () => {
|
||||||
|
it("applies the processor to the current track", () => {
|
||||||
|
const track = mockTrack("live");
|
||||||
|
trackProcessorSync(
|
||||||
|
testScope(),
|
||||||
|
constant(track),
|
||||||
|
constant({ supported: true, processor }),
|
||||||
|
);
|
||||||
|
expect(track.setProcessor).toHaveBeenCalledWith(processor);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { type LocalVideoTrack } from "livekit-client";
|
import { type LocalVideoTrack } from "livekit-client";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { combineLatest, map, type Observable } from "rxjs";
|
import { combineLatest, map, type Observable } from "rxjs";
|
||||||
import { useObservable } from "observable-hooks";
|
import { useObservable } from "observable-hooks";
|
||||||
|
|
||||||
@@ -65,6 +66,34 @@ export function useTrackProcessorObservable$(): Observable<ProcessorState> {
|
|||||||
return state$;
|
return state$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches or detaches the processor so that the track matches the desired
|
||||||
|
* state, without throwing.
|
||||||
|
*/
|
||||||
|
export function applyProcessor(
|
||||||
|
videoTrack: LocalVideoTrack,
|
||||||
|
processor: ProcessorWrapper<BackgroundOptions> | undefined,
|
||||||
|
): void {
|
||||||
|
if (processor && !videoTrack.getProcessor()) {
|
||||||
|
// A MediaStreamTrackProcessor cannot be constructed on an ended track
|
||||||
|
// (e.g. the camera was stopped while the processor was being applied),
|
||||||
|
// and setProcessor rejects with a TypeError. The track is going away
|
||||||
|
// anyway, so there is nothing to attach to.
|
||||||
|
if (videoTrack.mediaStreamTrack.readyState === "ended") {
|
||||||
|
logger.debug("Not attaching video processor to an ended track");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
videoTrack.setProcessor(processor).catch((e) => {
|
||||||
|
logger.warn("Failed to attach video processor", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!processor && videoTrack.getProcessor()) {
|
||||||
|
videoTrack.stopProcessor().catch((e) => {
|
||||||
|
logger.warn("Failed to stop video processor", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates your video tracks to always use the given processor.
|
* Updates your video tracks to always use the given processor.
|
||||||
*/
|
*/
|
||||||
@@ -78,13 +107,7 @@ export const trackProcessorSync = (
|
|||||||
.subscribe(([videoTrack, processorState]) => {
|
.subscribe(([videoTrack, processorState]) => {
|
||||||
if (!processorState) return;
|
if (!processorState) return;
|
||||||
if (!videoTrack) return;
|
if (!videoTrack) return;
|
||||||
const { processor } = processorState;
|
applyProcessor(videoTrack, processorState.processor);
|
||||||
if (processor && !videoTrack.getProcessor()) {
|
|
||||||
void videoTrack.setProcessor(processor);
|
|
||||||
}
|
|
||||||
if (!processor && videoTrack.getProcessor()) {
|
|
||||||
void videoTrack.stopProcessor();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,12 +117,7 @@ export const useTrackProcessorSync = (
|
|||||||
const { processor } = useTrackProcessor();
|
const { processor } = useTrackProcessor();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!videoTrack) return;
|
if (!videoTrack) return;
|
||||||
if (processor && !videoTrack.getProcessor()) {
|
applyProcessor(videoTrack, processor);
|
||||||
void videoTrack.setProcessor(processor);
|
|
||||||
}
|
|
||||||
if (!processor && videoTrack.getProcessor()) {
|
|
||||||
void videoTrack.stopProcessor();
|
|
||||||
}
|
|
||||||
}, [processor, videoTrack]);
|
}, [processor, videoTrack]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user