[SDK] simplify public api (no lk processor, no encryption$ as obs)

This commit is contained in:
Timo K.
2026-10-08 16:36:11 +02:00
parent 2bb870f6e7
commit aa897ae9bc
6 changed files with 47 additions and 52 deletions
+5 -7
View File
@@ -30,7 +30,7 @@ therefore `Element Call → sdk`, never the reverse. The migration from today's
│ │ │ │
│ per member RTCMember { id userId deviceId membership$ displayName$ │ │ per member RTCMember { id userId deviceId membership$ displayName$ │
│ avatarUrl$ transport$ tracks$ encryptionError$ }│ │ avatarUrl$ transport$ tracks$ encryptionError$ }│
│ per track MediaTrack { source kind muted$ encrypted$ stats$ attach() }│ │ per track MediaTrack { source kind muted$ encrypted stats$ attach() } │
│ │ │ │
│ ┌───────────────────────────┐ MediaBackend ┌───────────────────────────┐ │ │ ┌───────────────────────────┐ MediaBackend ┌───────────────────────────┐ │
│ │ session: MatrixRTC │ media-backend/ │ media backend │ │ │ │ session: MatrixRTC │ media-backend/ │ media backend │ │
@@ -232,7 +232,7 @@ export type PublishRequest =
source: "camera"; source: "camera";
deviceId?: string; deviceId?: string;
/** Background blur and the like. */ /** Background blur and the like. */
processor?: TrackProcessor<Track.Kind.Video>; processor?: VideoProcessor;
capture?: VideoCaptureSettings; capture?: VideoCaptureSettings;
} }
| { | {
@@ -596,8 +596,8 @@ export interface MediaTrack {
/** Stable for the life of the track. */ /** Stable for the life of the track. */
id: string; id: string;
muted$: Behavior<boolean>; muted$: Behavior<boolean>;
/** False when the SFU reports the track as unencrypted. */ /** False when the SFU reports the track as unencrypted. Fixed for the life of the track. */
encrypted$: Behavior<boolean>; encrypted: boolean;
/** /**
* Inbound statistics for a remote track, outbound for a local one. Polled * Inbound statistics for a remote track, outbound for a local one. Polled
* once a second, but only while someone is subscribed, so a statistics * once a second, but only while someone is subscribed, so a statistics
@@ -651,9 +651,7 @@ export interface LocalVideoMediaTrack extends VideoMediaTrack, LocalMediaTrack {
/** Restarts the camera facing the other way; resolves with the device now in use. */ /** Restarts the camera facing the other way; resolves with the device now in use. */
switchFacingMode(): Promise<string | undefined>; switchFacingMode(): Promise<string | undefined>;
/** Background blur and the like; undefined removes the processor. */ /** Background blur and the like; undefined removes the processor. */
setProcessor( setProcessor(processor: VideoProcessor | undefined): Promise<void>;
processor: TrackProcessor<Track.Kind.Video> | undefined,
): Promise<void>;
} }
export type EncryptionError = "MissingKey" | "InvalidKey"; export type EncryptionError = "MissingKey" | "InvalidKey";
+1 -1
View File
@@ -263,7 +263,7 @@ function render(
); );
label( label(
"encrypted", "encrypted",
of$((t) => t.encrypted$), of$((t) => of(t.encrypted)),
); );
label( label(
"active", "active",
+30 -18
View File
@@ -11,11 +11,6 @@ Please see LICENSE in the repository root for full details.
* `api.ts`. * `api.ts`.
*/ */
import {
type Track,
type TrackProcessor,
type VideoCodec,
} from "livekit-client";
import { type Observable } from "rxjs"; import { type Observable } from "rxjs";
import { type Behavior } from "./reactive/Behavior"; import { type Behavior } from "./reactive/Behavior";
@@ -26,6 +21,8 @@ export interface AudioCaptureSettings {
autoGainControl?: boolean; autoGainControl?: boolean;
} }
export type VideoCodec = "vp8" | "h264" | "vp9" | "av1" | "h265";
export interface VideoCaptureSettings { export interface VideoCaptureSettings {
resolution?: { width: number; height: number; frameRate?: number }; resolution?: { width: number; height: number; frameRate?: number };
maxBitrate?: number; maxBitrate?: number;
@@ -33,6 +30,26 @@ export interface VideoCaptureSettings {
codec?: VideoCodec; 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.
*/
export interface VideoProcessor {
name: string;
processedTrack?: MediaStreamTrack;
init(opts: VideoProcessorInit): Promise<void>;
restart(opts: VideoProcessorInit): Promise<void>;
destroy(): Promise<void>;
}
/** /**
* One thing to publish: a source, where to capture it from and how to encode * One thing to publish: a source, where to capture it from and how to encode
* it. Where the device and the settings are left out, the browser's and * it. Where the device and the settings are left out, the browser's and
@@ -43,13 +60,8 @@ export type PublishRequest =
| { | {
source: "camera"; source: "camera";
deviceId?: string; deviceId?: string;
/** Background blur and the like. */ /** Background blur and the like, applied before the first frame is published. */
// REVIEW: this is ugly makes us need to import lk processor?: VideoProcessor;
// We need an interface:
// - super high level: blur + background img
// - lower level: rtc stream insertable stream (copy lk TrackProcessor concept)
// - florian: should be independent of webCodec or WebRtc
processor?: TrackProcessor<Track.Kind.Video>;
capture?: VideoCaptureSettings; capture?: VideoCaptureSettings;
} }
| { | {
@@ -79,9 +91,11 @@ export interface MediaTrack {
/** Stable for the life of the track. */ /** Stable for the life of the track. */
id: string; id: string;
muted$: Behavior<boolean>; muted$: Behavior<boolean>;
/** False when the SFU reports the track as unencrypted. */ /**
// REVIEW: does it need to be a behavior * False when the SFU reports the track as unencrypted. Fixed for the life
encrypted$: Behavior<boolean>; * of the track: a publisher that changes its encryption publishes anew.
*/
encrypted: boolean;
/** Polled while subscribed. */ /** Polled while subscribed. */
// REVIEW: we want the perf gain of not having it always run. // REVIEW: we want the perf gain of not having it always run.
// //
@@ -143,9 +157,7 @@ export interface LocalVideoMediaTrack extends VideoMediaTrack, LocalMediaTrack {
*/ */
switchFacingMode(): Promise<string | undefined>; switchFacingMode(): Promise<string | undefined>;
/** Background blur and the like; undefined removes the processor. */ /** Background blur and the like; undefined removes the processor. */
setProcessor( setProcessor(processor: VideoProcessor | undefined): Promise<void>;
processor: TrackProcessor<Track.Kind.Video> | undefined,
): Promise<void>;
} }
export type EncryptionError = "MissingKey" | "InvalidKey"; export type EncryptionError = "MissingKey" | "InvalidKey";
@@ -33,21 +33,20 @@ describe("createLivekitMediaTrack", () => {
afterEach(() => scope.end()); afterEach(() => scope.end());
it("describes the publication", () => { it("describes the publication", () => {
const { participant, publication, room } = fakes(); const { participant, publication } = fakes();
const track = createLivekitMediaTrack( const track = createLivekitMediaTrack(
scope, scope,
participant, participant,
publication as unknown as TrackPublication, publication as unknown as TrackPublication,
room,
); );
expect(track.kind).toBe("video"); expect(track.kind).toBe("video");
expect(track.source).toBe("camera"); expect(track.source).toBe("camera");
expect(track.id).toBe("TR_1"); expect(track.id).toBe("TR_1");
expect(track.encrypted$.value).toBe(true); expect(track.encrypted).toBe(true);
}); });
it("attaches elements to the track, even one that arrives later", () => { it("attaches elements to the track, even one that arrives later", () => {
const { participant, publication, room, emitter } = fakes({ const { participant, publication, emitter } = fakes({
track: undefined, track: undefined,
}); });
const element = document.createElement("video"); const element = document.createElement("video");
@@ -55,7 +54,6 @@ describe("createLivekitMediaTrack", () => {
scope, scope,
participant, participant,
publication as unknown as TrackPublication, publication as unknown as TrackPublication,
room,
); );
track.attach(element); track.attach(element);
track.attach(element); track.attach(element);
@@ -71,14 +69,13 @@ describe("createLivekitMediaTrack", () => {
}); });
it("detaches everything when its scope ends", () => { it("detaches everything when its scope ends", () => {
const { participant, publication, room } = fakes(); const { participant, publication } = fakes();
const element = document.createElement("video"); const element = document.createElement("video");
const scope = new ObservableScope(); const scope = new ObservableScope();
const track = createLivekitMediaTrack( const track = createLivekitMediaTrack(
scope, scope,
participant, participant,
publication as unknown as TrackPublication, publication as unknown as TrackPublication,
room,
); );
track.attach(element); track.attach(element);
scope.end(); scope.end();
@@ -86,12 +83,11 @@ describe("createLivekitMediaTrack", () => {
}); });
it("follows the mute state", () => { it("follows the mute state", () => {
const { participant, publication, room, emitter } = fakes(); const { participant, publication, emitter } = fakes();
const track = createLivekitMediaTrack( const track = createLivekitMediaTrack(
scope, scope,
participant, participant,
publication as unknown as TrackPublication, publication as unknown as TrackPublication,
room,
); );
expect(track.muted$.value).toBe(false); expect(track.muted$.value).toBe(false);
publication.isMuted = true; publication.isMuted = true;
@@ -100,28 +96,26 @@ describe("createLivekitMediaTrack", () => {
}); });
it("scales a remote member's volume on the participant", () => { it("scales a remote member's volume on the participant", () => {
const { participant, publication, room } = fakes({ const { participant, publication } = fakes({
kind: Track.Kind.Audio, kind: Track.Kind.Audio,
}); });
const track = createLivekitMediaTrack( const track = createLivekitMediaTrack(
scope, scope,
participant, participant,
publication as unknown as TrackPublication, publication as unknown as TrackPublication,
room,
) as AudioMediaTrack; ) as AudioMediaTrack;
track.setVolume(0.5); track.setVolume(0.5);
expect(publication.track!.setVolume).toHaveBeenCalledWith(0.5); expect(publication.track!.setVolume).toHaveBeenCalledWith(0.5);
}); });
it("is active while the member speaks and the track is not muted", () => { it("is active while the member speaks and the track is not muted", () => {
const { participant, publication, room, emitter } = fakes({ const { participant, publication, emitter } = fakes({
kind: Track.Kind.Audio, kind: Track.Kind.Audio,
}); });
const track = createLivekitMediaTrack( const track = createLivekitMediaTrack(
scope, scope,
participant, participant,
publication as unknown as TrackPublication, publication as unknown as TrackPublication,
room,
) as AudioMediaTrack; ) as AudioMediaTrack;
expect(track.isActive$.value).toBe(false); expect(track.isActive$.value).toBe(false);
participant.isSpeaking = true; participant.isSpeaking = true;
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
import { import {
observeParticipantEvents, observeParticipantEvents,
observeParticipantMedia, observeParticipantMedia,
roomEventSelector,
} from "@livekit/components-core"; } from "@livekit/components-core";
import { import {
facingModeFromLocalTrack, facingModeFromLocalTrack,
@@ -20,7 +19,6 @@ import {
RemoteAudioTrack, RemoteAudioTrack,
RemoteTrack, RemoteTrack,
type Room as LivekitRoom, type Room as LivekitRoom,
RoomEvent,
Track, Track,
TrackEvent, TrackEvent,
type TrackPublication, type TrackPublication,
@@ -81,7 +79,6 @@ export function createLivekitMediaTrack(
scope: ObservableScope, scope: ObservableScope,
participant: Participant, participant: Participant,
publication: TrackPublication, publication: TrackPublication,
room: LivekitRoom,
): AudioMediaTrack | VideoMediaTrack { ): AudioMediaTrack | VideoMediaTrack {
const mediaChanged$ = observeParticipantMedia(participant); const mediaChanged$ = observeParticipantMedia(participant);
const track$ = publicationTrack$(scope, participant, publication); const track$ = publicationTrack$(scope, participant, publication);
@@ -118,13 +115,7 @@ export function createLivekitMediaTrack(
kind: publication.kind === Track.Kind.Audio ? "audio" : "video", kind: publication.kind === Track.Kind.Audio ? "audio" : "video",
id: publication.trackSid, id: publication.trackSid,
muted$, muted$,
encrypted$: scope.behavior( encrypted: publication.isEncrypted,
merge(
mediaChanged$,
roomEventSelector(room, RoomEvent.ParticipantEncryptionStatusChanged),
).pipe(map(() => publication.isEncrypted)),
publication.isEncrypted,
),
stats$: new LazyBehavior<MediaStreamStats>( stats$: new LazyBehavior<MediaStreamStats>(
refreshStats$.pipe( refreshStats$.pipe(
switchMap(async () => rtpStreamStats(publication, participant.isLocal)), switchMap(async () => rtpStreamStats(publication, participant.isLocal)),
@@ -189,7 +180,7 @@ export function createLocalLivekitMediaTrack(
room: LivekitRoom, room: LivekitRoom,
setEnabled: (source: MediaSource, enabled: boolean) => Promise<boolean>, setEnabled: (source: MediaSource, enabled: boolean) => Promise<boolean>,
): LocalAudioMediaTrack | LocalVideoMediaTrack { ): LocalAudioMediaTrack | LocalVideoMediaTrack {
const base = createLivekitMediaTrack(scope, participant, publication, room); const base = createLivekitMediaTrack(scope, participant, publication);
const local = { const local = {
setEnabled: async (enabled: boolean) => setEnabled(base.source, enabled), setEnabled: async (enabled: boolean) => setEnabled(base.source, enabled),
setDevice: async (deviceId: string): Promise<void> => { setDevice: async (deviceId: string): Promise<void> => {
@@ -54,8 +54,8 @@ export function createLivekitMemberMedia(
tracks$: memberTracks$( tracks$: memberTracks$(
scope, scope,
source$, source$,
(trackScope, participant, publication, room) => (trackScope, participant, publication) =>
createLivekitMediaTrack(trackScope, participant, publication, room), createLivekitMediaTrack(trackScope, participant, publication),
), ),
encryptionError$: encryptionErrors$(source$, encryptionSystem), encryptionError$: encryptionErrors$(source$, encryptionSystem),
}; };