mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
[SDK] simplify public api (no lk processor, no encryption$ as obs)
This commit is contained in:
@@ -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
@@ -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
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user