mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Merge LocalMediaInputs into MatrixRTCClientOptions
The SDK now takes `publish` requests and audio output control directly in the main options object, removing the separate `LocalMediaInputs` type. Audio output device selection moves from a reactive behavior to an imperative method on `MatrixRTCClient`, giving hosts direct control over when devices switch.
This commit is contained in:
+1
-5
@@ -19,7 +19,6 @@ Call's own `CallViewModel` is built on it. It depends on nothing in Element Call
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
import {
|
import {
|
||||||
constant,
|
|
||||||
createMatrixRTCClient,
|
createMatrixRTCClient,
|
||||||
E2eeType,
|
E2eeType,
|
||||||
MatrixRTCMode,
|
MatrixRTCMode,
|
||||||
@@ -31,13 +30,10 @@ const rtcClient = createMatrixRTCClient(
|
|||||||
scope,
|
scope,
|
||||||
client, // a matrix-js-sdk MatrixClient, logged in and syncing
|
client, // a matrix-js-sdk MatrixClient, logged in and syncing
|
||||||
room, // the matrix-js-sdk Room, from client.getRoom() once the join has synced
|
room, // the matrix-js-sdk Room, from client.getRoom() once the join has synced
|
||||||
{
|
|
||||||
publish: [{ source: "microphone" }, { source: "camera" }],
|
|
||||||
audioOutputDeviceId$: constant(undefined),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
matrixRTCMode: MatrixRTCMode.Compatibility,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
|
publish: [{ source: "microphone" }, { source: "camera" }],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
rtcClient.join();
|
rtcClient.join();
|
||||||
|
|||||||
+17
-15
@@ -18,7 +18,7 @@ therefore `Element Call → sdk`, never the reverse. The migration from today's
|
|||||||
```
|
```
|
||||||
host application (Element Call, a whiteboard, watch-together, …)
|
host application (Element Call, a whiteboard, watch-together, …)
|
||||||
│
|
│
|
||||||
│ createMatrixRTCClient(scope, matrixClient, room, localMedia, options)
|
│ createMatrixRTCClient(scope, matrixClient, room, options)
|
||||||
▼
|
▼
|
||||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ MatrixRTCClient │
|
│ MatrixRTCClient │
|
||||||
@@ -135,7 +135,7 @@ it, and `leave()` also sends the leave.
|
|||||||
| media | a `MemberMedia`: what a member sends, once it has arrived on its transport. `null` while waiting for media | Element Call's `MediaViewModel` is a view of it |
|
| media | a `MemberMedia`: what a member sends, once it has arrived on its transport. `null` while waiting for media | Element Call's `MediaViewModel` is a view of it |
|
||||||
| track | a `MediaTrack`: one publication of a member, audio or video, with `source`, `kind`, `id` and the behaviors on it. `LocalAudioMediaTrack` and `LocalVideoMediaTrack` add the controls over our own | LiveKit `TrackPublication` and `Track`, which never leave the SDK |
|
| track | a `MediaTrack`: one publication of a member, audio or video, with `source`, `kind`, `id` and the behaviors on it. `LocalAudioMediaTrack` and `LocalVideoMediaTrack` add the controls over our own | LiveKit `TrackPublication` and `Track`, which never leave the SDK |
|
||||||
| source | a `MediaSource`: what a track is captured from, `microphone`, `camera`, `screenShare`, `screenShareAudio` or `unknown`. Picked out of `tracks$` with `trackBySource$` | LiveKit `Track.Source` |
|
| source | a `MediaSource`: what a track is captured from, `microphone`, `camera`, `screenShare`, `screenShareAudio` or `unknown`. Picked out of `tracks$` with `trackBySource$` | LiveKit `Track.Source` |
|
||||||
| publish request | a `PublishRequest`: a source, where to capture it from and how to encode it. Given at the join in `LocalMediaInputs.publish`, or later to `publish` on the local member | |
|
| publish request | a `PublishRequest`: a source, where to capture it from and how to encode it. Given at the join in `options.publish`, or later to `publish` on the local member | |
|
||||||
| active | `isActive$` on an audio track: the track carries sound, as the backend measures it. "Speaking" is a call's reading of it on the microphone track | LiveKit "speaking", `isSpeaking` |
|
| active | `isActive$` on an audio track: the track carries sound, as the backend measures it. "Speaking" is a call's reading of it on the microphone track | LiveKit "speaking", `isSpeaking` |
|
||||||
| attach, detach | handing a `<video>` or `<audio>` element to a track and taking it back. The SDK owns the stream and the observers on the element | LiveKit `Track.attach` |
|
| attach, detach | handing a `<video>` or `<audio>` element to a track and taking it back. The SDK owns the stream and the observers on the element | LiveKit `Track.attach` |
|
||||||
| transport | where media is exchanged: a LiveKit service url today, described by `TransportMetadata` | MSC4143 "focus" |
|
| transport | where media is exchanged: a LiveKit service url today, described by `TransportMetadata` | MSC4143 "focus" |
|
||||||
@@ -182,7 +182,7 @@ context, it is in the view model.
|
|||||||
|
|
||||||
### Creating one
|
### Creating one
|
||||||
|
|
||||||
```ts
|
````ts
|
||||||
import { type MatrixClient, type Room } from "matrix-js-sdk";
|
import { type MatrixClient, type Room } from "matrix-js-sdk";
|
||||||
import { type RTCNotificationType } from "matrix-js-sdk/lib/matrixrtc";
|
import { type RTCNotificationType } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
@@ -190,6 +190,8 @@ export interface MatrixRTCClientOptions {
|
|||||||
encryptionSystem: EncryptionSystem;
|
encryptionSystem: EncryptionSystem;
|
||||||
/** Resolved by the host; the SDK reads neither config.json nor settings. */
|
/** Resolved by the host; the SDK reads neither config.json nor settings. */
|
||||||
matrixRTCMode: MatrixRTCMode;
|
matrixRTCMode: MatrixRTCMode;
|
||||||
|
/** Published at the join; `publish` on the local member adds to it from then on. */
|
||||||
|
publish: PublishRequest[];
|
||||||
/**
|
/**
|
||||||
* MSC4075 notification sent with the join. A parameter of the MatrixRTC
|
* MSC4075 notification sent with the join. A parameter of the MatrixRTC
|
||||||
* join itself, so it is here even though it is named after calls; reacting
|
* join itself, so it is here even though it is named after calls; reacting
|
||||||
@@ -236,18 +238,12 @@ export type PublishRequest =
|
|||||||
audio?: boolean;
|
audio?: boolean;
|
||||||
capture?: VideoCaptureSettings;
|
capture?: VideoCaptureSettings;
|
||||||
};
|
};
|
||||||
|
```
|
||||||
|
|
||||||
/**
|
Device enumeration, permission prompts and the lobby preview stay with the
|
||||||
* Device enumeration, permission prompts and the lobby preview stay with the
|
host: it hands over publish requests naming the devices it chose.
|
||||||
* host.
|
|
||||||
*/
|
|
||||||
export interface LocalMediaInputs {
|
|
||||||
/** Published at the join; `publish` on the local member adds to it from then on. */
|
|
||||||
publish: PublishRequest[];
|
|
||||||
/** Undefined where the host routes audio itself, or to leave the browser's choice. */
|
|
||||||
audioOutputDeviceId$: Behavior<string | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
```ts
|
||||||
/**
|
/**
|
||||||
* Takes the whole MatrixClient, not a narrow pick, and finds the MatrixRTC
|
* Takes the whole MatrixClient, not a narrow pick, and finds the MatrixRTC
|
||||||
* session itself. The js-sdk is the MatrixRTC implementation today; when the
|
* session itself. The js-sdk is the MatrixRTC implementation today; when the
|
||||||
@@ -263,10 +259,9 @@ export function createMatrixRTCClient(
|
|||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
room: Room,
|
room: Room,
|
||||||
localMedia: LocalMediaInputs,
|
|
||||||
options: MatrixRTCClientOptions,
|
options: MatrixRTCClientOptions,
|
||||||
): MatrixRTCClient;
|
): MatrixRTCClient;
|
||||||
```
|
````
|
||||||
|
|
||||||
### The client
|
### The client
|
||||||
|
|
||||||
@@ -328,6 +323,13 @@ export interface MatrixRTCClient {
|
|||||||
*/
|
*/
|
||||||
connectedTransports$: Behavior<TransportMetadata[]>;
|
connectedTransports$: Behavior<TransportMetadata[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the members' audio on this device from now on. Rejects where the
|
||||||
|
* browser cannot switch to it. A host that routes audio itself, or leaves
|
||||||
|
* the browser's choice, never calls it.
|
||||||
|
*/
|
||||||
|
setAudioOutputDeviceId(deviceId: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a short text to every member on the local transport, over a
|
* Sends a short text to every member on the local transport, over a
|
||||||
* reliable data channel beside the media. A packet holds about 15 KiB;
|
* reliable data channel beside the media. A packet holds about 15 KiB;
|
||||||
|
|||||||
+5
-14
@@ -15,7 +15,6 @@ import { logger } from "matrix-js-sdk/lib/logger";
|
|||||||
import { combineLatest, type Observable, of, switchMap } from "rxjs";
|
import { combineLatest, type Observable, of, switchMap } from "rxjs";
|
||||||
import {
|
import {
|
||||||
type AudioMediaTrack,
|
type AudioMediaTrack,
|
||||||
constant,
|
|
||||||
createMatrixRTCClient,
|
createMatrixRTCClient,
|
||||||
E2eeType,
|
E2eeType,
|
||||||
MatrixRTCMode,
|
MatrixRTCMode,
|
||||||
@@ -70,19 +69,11 @@ async function start(
|
|||||||
const room = await joinRoom(client, roomIdOrAlias);
|
const room = await joinRoom(client, roomIdOrAlias);
|
||||||
|
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
const rtcClient = createMatrixRTCClient(
|
const rtcClient = createMatrixRTCClient(scope, client, room, {
|
||||||
scope,
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
client,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
room,
|
publish: [{ source: "microphone" }, { source: "camera" }],
|
||||||
{
|
});
|
||||||
publish: [{ source: "microphone" }, { source: "camera" }],
|
|
||||||
audioOutputDeviceId$: constant(undefined),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
|
||||||
matrixRTCMode: MatrixRTCMode.Compatibility,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
rtcClient.status$.pipe(scope.bind()).subscribe((s) => {
|
rtcClient.status$.pipe(scope.bind()).subscribe((s) => {
|
||||||
status.textContent = s;
|
status.textContent = s;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { filterBehavior, generateItems } from "./reactive/observable";
|
|||||||
import { mapScoped } from "./utils/mapScoped";
|
import { mapScoped } from "./utils/mapScoped";
|
||||||
import {
|
import {
|
||||||
type DataMessage,
|
type DataMessage,
|
||||||
type LocalMediaInputs,
|
|
||||||
type LocalRTCMember,
|
type LocalRTCMember,
|
||||||
type MatrixRTCClient,
|
type MatrixRTCClient,
|
||||||
type MatrixRTCClientOptions,
|
type MatrixRTCClientOptions,
|
||||||
@@ -72,7 +71,6 @@ export function createMatrixRTCClient(
|
|||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
room: Room,
|
room: Room,
|
||||||
localMedia: LocalMediaInputs,
|
|
||||||
options: MatrixRTCClientOptions,
|
options: MatrixRTCClientOptions,
|
||||||
): MatrixRTCClient {
|
): MatrixRTCClient {
|
||||||
const logger = rootLogger.getChild("[MatrixRTCClient]");
|
const logger = rootLogger.getChild("[MatrixRTCClient]");
|
||||||
@@ -105,7 +103,7 @@ export function createMatrixRTCClient(
|
|||||||
const backend = createBackend(scope, client, options, {
|
const backend = createBackend(scope, client, options, {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
localMedia,
|
publish: options.publish,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
mediaKeys$: mediaKeys$(jsSdkSession),
|
mediaKeys$: mediaKeys$(jsSdkSession),
|
||||||
timings,
|
timings,
|
||||||
@@ -261,6 +259,8 @@ export function createMatrixRTCClient(
|
|||||||
),
|
),
|
||||||
keyRotationSuppressed$: createKeyRotationSuppressed$(scope, jsSdkSession),
|
keyRotationSuppressed$: createKeyRotationSuppressed$(scope, jsSdkSession),
|
||||||
connectedTransports$: transports.connected$,
|
connectedTransports$: transports.connected$,
|
||||||
|
setAudioOutputDeviceId: async (deviceId) =>
|
||||||
|
backend.setAudioOutputDeviceId(deviceId),
|
||||||
sendData: async (topic, text) => backend.sendData(topic, text),
|
sendData: async (topic, text) => backend.sendData(topic, text),
|
||||||
data$,
|
data$,
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-8
@@ -32,12 +32,14 @@ import {
|
|||||||
} from "./config";
|
} from "./config";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Session
|
// MatrixRTCClient
|
||||||
|
|
||||||
export interface MatrixRTCClientOptions {
|
export interface MatrixRTCClientOptions {
|
||||||
encryptionSystem: EncryptionSystem;
|
encryptionSystem: EncryptionSystem;
|
||||||
/** Resolved by the host; the SDK reads neither config.json nor settings. */
|
/** Resolved by the host; the SDK reads neither config.json nor settings. */
|
||||||
matrixRTCMode: MatrixRTCMode;
|
matrixRTCMode: MatrixRTCMode;
|
||||||
|
/** Published at the join; `publish` on the local member adds to it from then on. */
|
||||||
|
publish: PublishRequest[];
|
||||||
/**
|
/**
|
||||||
* MSC4075 notification sent with the join. A parameter of the MatrixRTC
|
* MSC4075 notification sent with the join. A parameter of the MatrixRTC
|
||||||
* join itself, so it is here even though it is named after calls; reacting
|
* join itself, so it is here even though it is named after calls; reacting
|
||||||
@@ -104,13 +106,6 @@ export type PublishRequest =
|
|||||||
capture?: VideoCaptureSettings;
|
capture?: VideoCaptureSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface LocalMediaInputs {
|
|
||||||
/** Published at the join; `publish` on the local member adds to it from then on. */
|
|
||||||
publish: PublishRequest[];
|
|
||||||
/** Undefined where the host routes audio itself, or to leave the browser's choice. */
|
|
||||||
audioOutputDeviceId$: Behavior<string | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConnectionStatus =
|
export type ConnectionStatus =
|
||||||
| "waitingForTransport"
|
| "waitingForTransport"
|
||||||
| "connecting"
|
| "connecting"
|
||||||
@@ -158,6 +153,13 @@ export interface MatrixRTCClient {
|
|||||||
/** Transports the session currently holds a live connection to. */
|
/** Transports the session currently holds a live connection to. */
|
||||||
connectedTransports$: Behavior<TransportMetadata[]>;
|
connectedTransports$: Behavior<TransportMetadata[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the members' audio on this device from now on. Rejects where the
|
||||||
|
* browser cannot switch to it. A host that routes audio itself, or leaves
|
||||||
|
* the browser's choice, never calls it.
|
||||||
|
*/
|
||||||
|
setAudioOutputDeviceId(deviceId: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a short text to every member on the local transport, over a
|
* Sends a short text to every member on the local transport, over a
|
||||||
* reliable data channel beside the media. A packet holds about 15 KiB;
|
* reliable data channel beside the media. A packet holds about 15 KiB;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { type Observable } from "rxjs";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type LocalAudioMediaTrack,
|
type LocalAudioMediaTrack,
|
||||||
type LocalMediaInputs,
|
|
||||||
type LocalMemberMedia,
|
type LocalMemberMedia,
|
||||||
type LocalVideoMediaTrack,
|
type LocalVideoMediaTrack,
|
||||||
type MemberMedia,
|
type MemberMedia,
|
||||||
@@ -55,7 +54,8 @@ export interface MediaKey {
|
|||||||
export interface MediaBackendContext {
|
export interface MediaBackendContext {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
localMedia: LocalMediaInputs;
|
/** What the local member publishes at the join. */
|
||||||
|
publish: PublishRequest[];
|
||||||
encryptionSystem: EncryptionSystem;
|
encryptionSystem: EncryptionSystem;
|
||||||
/**
|
/**
|
||||||
* Every media key the session knows: the current ones replayed on
|
* Every media key the session knows: the current ones replayed on
|
||||||
@@ -147,6 +147,9 @@ export interface MediaBackend {
|
|||||||
/** Every connection the backend holds. For debugging and devtool purposes.*/
|
/** Every connection the backend holds. For debugging and devtool purposes.*/
|
||||||
readonly connections$: Behavior<BackendConnection[]>;
|
readonly connections$: Behavior<BackendConnection[]>;
|
||||||
|
|
||||||
|
/** Plays the members' audio on this device, on the connections of now and later. */
|
||||||
|
setAudioOutputDeviceId(deviceId: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A text channel beside the media: sent on the local transport, received on every one.
|
* A text channel beside the media: sent on the local transport, received on every one.
|
||||||
* This could well be just another track kind. Not needed for element call. just for the sdk target.
|
* This could well be just another track kind. Not needed for element call. just for the sdk target.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { type OpenIDClientParts, type SFUConfig } from "./openIDSFU";
|
|||||||
import { type Behavior } from "../../reactive/Behavior";
|
import { type Behavior } from "../../reactive/Behavior";
|
||||||
import { Connection, type ConnectionOpts, ConnectionState } from "./Connection";
|
import { Connection, type ConnectionOpts, ConnectionState } from "./Connection";
|
||||||
import { type ObservableScope } from "../../reactive/ObservableScope";
|
import { type ObservableScope } from "../../reactive/ObservableScope";
|
||||||
import { type LocalMediaInputs, type ResolvedTransport } from "../../api";
|
import { type PublishRequest, type ResolvedTransport } from "../../api";
|
||||||
import { type MediaQuality } from "../../config";
|
import { type MediaQuality } from "../../config";
|
||||||
import {
|
import {
|
||||||
audioCaptureOptions,
|
audioCaptureOptions,
|
||||||
@@ -95,7 +95,8 @@ export class LivekitConnectionFactory implements ConnectionFactory {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly client: OpenIDClientParts,
|
private readonly client: OpenIDClientParts,
|
||||||
private readonly roomId: string,
|
private readonly roomId: string,
|
||||||
private readonly localMedia: LocalMediaInputs,
|
private readonly publish: PublishRequest[],
|
||||||
|
private readonly audioOutputDeviceId$: Behavior<string | undefined>,
|
||||||
private readonly keyProvider: BaseKeyProvider | undefined,
|
private readonly keyProvider: BaseKeyProvider | undefined,
|
||||||
private readonly mediaQuality: MediaQuality | undefined,
|
private readonly mediaQuality: MediaQuality | undefined,
|
||||||
) {}
|
) {}
|
||||||
@@ -116,7 +117,12 @@ export class LivekitConnectionFactory implements ConnectionFactory {
|
|||||||
scope,
|
scope,
|
||||||
livekitRoomFactory: () =>
|
livekitRoomFactory: () =>
|
||||||
new LivekitRoom(
|
new LivekitRoom(
|
||||||
roomOptions(this.localMedia, this.keyProvider, this.mediaQuality),
|
roomOptions(
|
||||||
|
this.publish,
|
||||||
|
this.audioOutputDeviceId$.value,
|
||||||
|
this.keyProvider,
|
||||||
|
this.mediaQuality,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
},
|
},
|
||||||
@@ -127,13 +133,14 @@ export class LivekitConnectionFactory implements ConnectionFactory {
|
|||||||
|
|
||||||
/** The initial requests become the room's defaults, so that one permission prompt covers both. */
|
/** The initial requests become the room's defaults, so that one permission prompt covers both. */
|
||||||
function roomOptions(
|
function roomOptions(
|
||||||
localMedia: LocalMediaInputs,
|
publish: PublishRequest[],
|
||||||
|
audioOutputDeviceId: string | undefined,
|
||||||
keyProvider: BaseKeyProvider | undefined,
|
keyProvider: BaseKeyProvider | undefined,
|
||||||
mediaQuality: MediaQuality | undefined,
|
mediaQuality: MediaQuality | undefined,
|
||||||
): RoomOptions {
|
): RoomOptions {
|
||||||
const base = buildLiveKitOptions(mediaQuality);
|
const base = buildLiveKitOptions(mediaQuality);
|
||||||
const microphone = initialRequest(localMedia, "microphone");
|
const microphone = initialRequest(publish, "microphone");
|
||||||
const camera = initialRequest(localMedia, "camera");
|
const camera = initialRequest(publish, "camera");
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
@@ -148,7 +155,7 @@ function roomOptions(
|
|||||||
...base.audioCaptureDefaults,
|
...base.audioCaptureDefaults,
|
||||||
...(microphone && audioCaptureOptions(microphone)),
|
...(microphone && audioCaptureOptions(microphone)),
|
||||||
},
|
},
|
||||||
audioOutput: { deviceId: localMedia.audioOutputDeviceId$.value },
|
audioOutput: { deviceId: audioOutputDeviceId },
|
||||||
// Every room needs a worker of its own: one gets confused by streams from
|
// Every room needs a worker of its own: one gets confused by streams from
|
||||||
// several rooms
|
// several rooms
|
||||||
e2ee: keyProvider && { keyProvider, worker: new E2EEWorker() },
|
e2ee: keyProvider && { keyProvider, worker: new E2EEWorker() },
|
||||||
@@ -156,10 +163,10 @@ function roomOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initialRequest<S extends "microphone" | "camera">(
|
function initialRequest<S extends "microphone" | "camera">(
|
||||||
localMedia: LocalMediaInputs,
|
publish: PublishRequest[],
|
||||||
source: S,
|
source: S,
|
||||||
): PublishRequestFor<S> | undefined {
|
): PublishRequestFor<S> | undefined {
|
||||||
return localMedia.publish.find(
|
return publish.find(
|
||||||
(request): request is PublishRequestFor<S> => request.source === source,
|
(request): request is PublishRequestFor<S> => request.source === source,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export function createLivekitBackend(
|
|||||||
{
|
{
|
||||||
roomId,
|
roomId,
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
localMedia,
|
publish,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
mediaKeys$,
|
mediaKeys$,
|
||||||
timings,
|
timings,
|
||||||
@@ -89,13 +89,17 @@ export function createLivekitBackend(
|
|||||||
const keyProvider = createKeyProvider(encryptionSystem, mediaKeys$, logger);
|
const keyProvider = createKeyProvider(encryptionSystem, mediaKeys$, logger);
|
||||||
const transports = createTransportRegistry(scope);
|
const transports = createTransportRegistry(scope);
|
||||||
const localTransport$ = new ReplaySubject<LocalTransport>(1);
|
const localTransport$ = new ReplaySubject<LocalTransport>(1);
|
||||||
|
const audioOutputDeviceId$ = new BehaviorSubject<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const connectionManager = createConnectionManager$({
|
const connectionManager = createConnectionManager$({
|
||||||
scope,
|
scope,
|
||||||
connectionFactory: new LivekitConnectionFactory(
|
connectionFactory: new LivekitConnectionFactory(
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
localMedia,
|
publish,
|
||||||
|
audioOutputDeviceId$,
|
||||||
keyProvider,
|
keyProvider,
|
||||||
mediaQuality,
|
mediaQuality,
|
||||||
),
|
),
|
||||||
@@ -215,7 +219,8 @@ export function createLivekitBackend(
|
|||||||
scope,
|
scope,
|
||||||
connectionManager,
|
connectionManager,
|
||||||
localTransport$,
|
localTransport$,
|
||||||
localMedia,
|
publish,
|
||||||
|
audioOutputDeviceId$,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
@@ -233,6 +238,7 @@ export function createLivekitBackend(
|
|||||||
local: localMedia_,
|
local: localMedia_,
|
||||||
mediaFor$,
|
mediaFor$,
|
||||||
connections$,
|
connections$,
|
||||||
|
setAudioOutputDeviceId: localMedia_.setAudioOutputDeviceId,
|
||||||
sendData,
|
sendData,
|
||||||
data$,
|
data$,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type LocalAudioMediaTrack,
|
type LocalAudioMediaTrack,
|
||||||
type LocalMediaInputs,
|
|
||||||
type LocalMemberMedia,
|
type LocalMemberMedia,
|
||||||
type LocalVideoMediaTrack,
|
type LocalVideoMediaTrack,
|
||||||
type MediaSource,
|
type MediaSource,
|
||||||
@@ -54,7 +53,9 @@ interface Props {
|
|||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
connectionManager: IConnectionManager;
|
connectionManager: IConnectionManager;
|
||||||
localTransport$: Observable<LocalTransport>;
|
localTransport$: Observable<LocalTransport>;
|
||||||
localMedia: LocalMediaInputs;
|
publish: PublishRequest[];
|
||||||
|
/** Read by the connection factory for the rooms it creates from now on. */
|
||||||
|
audioOutputDeviceId$: BehaviorSubject<string | undefined>;
|
||||||
encryptionSystem: EncryptionSystem;
|
encryptionSystem: EncryptionSystem;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,7 @@ interface Props {
|
|||||||
export interface LivekitLocalMedia extends LocalMediaBackend {
|
export interface LivekitLocalMedia extends LocalMediaBackend {
|
||||||
/** The connection the local member publishes on; the data channel sends on it. */
|
/** The connection the local member publishes on; the data channel sends on it. */
|
||||||
connection$: Behavior<Connection | null>;
|
connection$: Behavior<Connection | null>;
|
||||||
|
setAudioOutputDeviceId(deviceId: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +75,8 @@ export function createLivekitLocalMedia({
|
|||||||
scope,
|
scope,
|
||||||
connectionManager,
|
connectionManager,
|
||||||
localTransport$,
|
localTransport$,
|
||||||
localMedia,
|
publish: initialRequests,
|
||||||
|
audioOutputDeviceId$,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
logger: parentLogger,
|
logger: parentLogger,
|
||||||
}: Props): LivekitLocalMedia {
|
}: Props): LivekitLocalMedia {
|
||||||
@@ -102,7 +105,7 @@ export function createLivekitLocalMedia({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const desired: DesiredMedia = new Map(
|
const desired: DesiredMedia = new Map(
|
||||||
localMedia.publish.map((request) => [
|
initialRequests.map((request) => [
|
||||||
request.source,
|
request.source,
|
||||||
{ request, enabled: true },
|
{ request, enabled: true },
|
||||||
]),
|
]),
|
||||||
@@ -115,7 +118,6 @@ export function createLivekitLocalMedia({
|
|||||||
if (connection === null) return;
|
if (connection === null) return;
|
||||||
const publisher = new Publisher(
|
const publisher = new Publisher(
|
||||||
connection.livekitRoom,
|
connection.livekitRoom,
|
||||||
localMedia,
|
|
||||||
desired,
|
desired,
|
||||||
logger.getChild(
|
logger.getChild(
|
||||||
`[Publisher ${connection.transport.livekit_service_url}]`,
|
`[Publisher ${connection.transport.livekit_service_url}]`,
|
||||||
@@ -209,6 +211,11 @@ export function createLivekitLocalMedia({
|
|||||||
await publisher$.value?.unpublish(source);
|
await publisher$.value?.unpublish(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAudioOutputDeviceId = async (deviceId: string): Promise<void> => {
|
||||||
|
audioOutputDeviceId$.next(deviceId);
|
||||||
|
await publisher$.value?.setAudioOutputDevice(deviceId);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connection$,
|
connection$,
|
||||||
connectionState$,
|
connectionState$,
|
||||||
@@ -217,6 +224,7 @@ export function createLivekitLocalMedia({
|
|||||||
publishError$,
|
publishError$,
|
||||||
publish,
|
publish,
|
||||||
unpublish,
|
unpublish,
|
||||||
|
setAudioOutputDeviceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ import {
|
|||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { ObservableScope } from "../../reactive/ObservableScope";
|
import { type MediaSource, type PublishRequest } from "../../api";
|
||||||
import {
|
|
||||||
type LocalMediaInputs,
|
|
||||||
type MediaSource,
|
|
||||||
type PublishRequest,
|
|
||||||
} from "../../api";
|
|
||||||
import { livekitSources, mediaSources } from "./LivekitMediaTrack";
|
import { livekitSources, mediaSources } from "./LivekitMediaTrack";
|
||||||
import {
|
import {
|
||||||
audioCaptureOptions,
|
audioCaptureOptions,
|
||||||
@@ -50,12 +45,10 @@ export class Publisher {
|
|||||||
public shouldPublish = false;
|
public shouldPublish = false;
|
||||||
/** Whether the initial requests have been published; `publish` is immediate from then on. */
|
/** Whether the initial requests have been published; `publish` is immediate from then on. */
|
||||||
public started = false;
|
public started = false;
|
||||||
private readonly scope = new ObservableScope();
|
|
||||||
private readonly room: LivekitRoom;
|
private readonly room: LivekitRoom;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
room: LivekitRoom,
|
room: LivekitRoom,
|
||||||
private readonly inputs: LocalMediaInputs,
|
|
||||||
private readonly desired: DesiredMedia,
|
private readonly desired: DesiredMedia,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
) {
|
) {
|
||||||
@@ -63,7 +56,6 @@ export class Publisher {
|
|||||||
room.setE2EEEnabled(room.options.e2ee !== undefined)?.catch((e: Error) => {
|
room.setE2EEEnabled(room.options.e2ee !== undefined)?.catch((e: Error) => {
|
||||||
this.logger.error("Failed to enable E2EE on the room", e);
|
this.logger.error("Failed to enable E2EE on the room", e);
|
||||||
});
|
});
|
||||||
this.followAudioOutput();
|
|
||||||
this.onLocalTrackPublished = this.onLocalTrackPublished.bind(this);
|
this.onLocalTrackPublished = this.onLocalTrackPublished.bind(this);
|
||||||
room.localParticipant.on(
|
room.localParticipant.on(
|
||||||
ParticipantEvent.LocalTrackPublished,
|
ParticipantEvent.LocalTrackPublished,
|
||||||
@@ -72,7 +64,6 @@ export class Publisher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async destroy(): Promise<void> {
|
public async destroy(): Promise<void> {
|
||||||
this.scope.end();
|
|
||||||
this.room.localParticipant.off(
|
this.room.localParticipant.off(
|
||||||
ParticipantEvent.LocalTrackPublished,
|
ParticipantEvent.LocalTrackPublished,
|
||||||
this.onLocalTrackPublished,
|
this.onLocalTrackPublished,
|
||||||
@@ -216,19 +207,13 @@ export class Publisher {
|
|||||||
return [...this.room.localParticipant.trackPublications.values()];
|
return [...this.room.localParticipant.trackPublications.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
private followAudioOutput(): void {
|
/** A room that is not connected yet takes the device from its options instead. */
|
||||||
this.inputs.audioOutputDeviceId$
|
public async setAudioOutputDevice(deviceId: string): Promise<void> {
|
||||||
.pipe(this.scope.bind())
|
if (
|
||||||
.subscribe((deviceId) => {
|
this.room.state !== LivekitConnectionState.Connected ||
|
||||||
if (
|
this.room.getActiveDevice("audiooutput") === deviceId
|
||||||
deviceId === undefined ||
|
)
|
||||||
this.room.state !== LivekitConnectionState.Connected ||
|
return;
|
||||||
this.room.getActiveDevice("audiooutput") === deviceId
|
await this.room.switchActiveDevice("audiooutput", deviceId);
|
||||||
)
|
|
||||||
return;
|
|
||||||
this.room
|
|
||||||
.switchActiveDevice("audiooutput", deviceId)
|
|
||||||
.catch((e) => this.logger.error("Failed to switch audiooutput", e));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user