[SDK] refactor track processor api to be lk independent

This commit is contained in:
Timo K.
2026-10-08 16:36:17 +02:00
parent aa897ae9bc
commit 5f914b8a01
6 changed files with 101 additions and 14 deletions
+2
View File
@@ -61,6 +61,8 @@ export {
createLivekitBackend,
type LivekitBackendOptions,
} from "./src/media-backend/livekit/LivekitBackend";
// For a host that creates LiveKit tracks itself, such as a preview before joining
export { convertToLivekitProcessor } from "./src/media-backend/livekit/videoProcessor";
// For a developer panel to check a transport url before using it
export {
getSFUConfigWithOpenID as authenticateWithTransport,
+10 -12
View File
@@ -30,23 +30,21 @@ export interface VideoCaptureSettings {
codec?: VideoCodec;
}
/** What a processor is given to work on: the captured track and, where a view is attached, its element. */
export interface VideoProcessorInit {
track: MediaStreamTrack;
element?: HTMLMediaElement;
}
/**
* Transforms a camera track before it is published: background blur and the
* like. `processedTrack` is what gets published once `init` has resolved;
* `restart` is called with the new track where the camera changes. The shape
* is that of a LiveKit track processor, so one of those can be passed as is.
* like. `init` resolves with the track to publish in place of the captured
* one; `restart` does the same for a new capture, where the camera changes.
*/
export interface VideoProcessor {
name: string;
processedTrack?: MediaStreamTrack;
init(opts: VideoProcessorInit): Promise<void>;
restart(opts: VideoProcessorInit): Promise<void>;
init(
track: MediaStreamTrack,
element?: HTMLMediaElement,
): Promise<MediaStreamTrack>;
restart(
track: MediaStreamTrack,
element?: HTMLMediaElement,
): Promise<MediaStreamTrack>;
destroy(): Promise<void>;
}
@@ -49,6 +49,7 @@ import {
} from "../../media-api";
import { MatrixRTCError } from "../../errors";
import { LazyBehavior } from "../../utils/LazyBehavior";
import { convertToLivekitProcessor } from "./videoProcessor";
export const mediaSources: Record<Track.Source, MediaSource> = {
[Track.Source.Microphone]: "microphone",
@@ -219,7 +220,8 @@ export function createLocalLivekitMediaTrack(
const track = videoTrack();
// A processor cannot be built on a track that has already ended
if (!track || track.mediaStreamTrack.readyState === "ended") return;
if (processor) await track.setProcessor(processor);
if (processor)
await track.setProcessor(convertToLivekitProcessor(processor));
else if (track.getProcessor()) await track.stopProcessor();
},
};
@@ -16,6 +16,7 @@ import {
type PublishRequest,
type VideoCaptureSettings,
} from "../../media-api";
import { convertToLivekitProcessor } from "./videoProcessor";
export type PublishRequestFor<S extends PublishRequest["source"]> = Extract<
PublishRequest,
@@ -39,7 +40,7 @@ export function videoCaptureOptions({
if (!deviceId && !processor && !capture?.resolution) return undefined;
return {
...(deviceId && { deviceId }),
...(processor && { processor }),
...(processor && { processor: convertToLivekitProcessor(processor) }),
...(capture?.resolution && { resolution: capture.resolution }),
};
}
@@ -0,0 +1,50 @@
/*
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 { Track, type VideoProcessorOptions } from "livekit-client";
import { describe, expect, it, vi } from "vitest";
import { type VideoProcessor } from "../../media-api";
import { convertToLivekitProcessor } from "./videoProcessor";
describe("convertToLivekitProcessor", () => {
const captured = { id: "captured" } as MediaStreamTrack;
const processed = { id: "processed" } as MediaStreamTrack;
const restarted = { id: "restarted" } as MediaStreamTrack;
const element = document.createElement("video");
const options: VideoProcessorOptions = {
kind: Track.Kind.Video,
track: captured,
element,
};
function fakeProcessor(): VideoProcessor {
return {
name: "blur",
init: vi.fn().mockResolvedValue(processed),
restart: vi.fn().mockResolvedValue(restarted),
destroy: vi.fn().mockResolvedValue(undefined),
};
}
it("holds the processed track where LiveKit reads it", async () => {
const processor = fakeProcessor();
const adapter = convertToLivekitProcessor(processor);
expect(adapter.name).toBe("blur");
expect(adapter.processedTrack).toBeUndefined();
await adapter.init(options);
expect(processor.init).toHaveBeenCalledWith(captured, element);
expect(adapter.processedTrack).toBe(processed);
await adapter.restart(options);
expect(adapter.processedTrack).toBe(restarted);
await adapter.destroy();
expect(processor.destroy).toHaveBeenCalled();
});
});
@@ -0,0 +1,34 @@
/*
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 { type Track, type TrackProcessor } from "livekit-client";
import { type VideoProcessor } from "../../media-api";
/**
* A `VideoProcessor` as livekit-client wants it. LiveKit reads the processed
* track off the processor after `init` and `restart` rather than taking it as
* their result, so the adapter keeps it there.
*/
export function convertToLivekitProcessor(
processor: VideoProcessor,
): TrackProcessor<Track.Kind.Video> {
const adapter: TrackProcessor<Track.Kind.Video> = {
name: processor.name,
init: async (opts) => {
adapter.processedTrack = await processor.init(opts.track, opts.element);
},
restart: async (opts) => {
adapter.processedTrack = await processor.restart(
opts.track,
opts.element,
);
},
destroy: async () => processor.destroy(),
};
return adapter;
}