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:
Timo K.
2026-10-08 13:53:57 +02:00
parent 5b13e1da81
commit f1ea4cc5a2
10 changed files with 88 additions and 88 deletions
+1 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}); });
+3 -3
View File
@@ -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
View File
@@ -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;
+5 -2
View File
@@ -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$,
}; };
+13 -5
View File
@@ -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,
}; };
} }
+9 -24
View File
@@ -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));
});
} }
} }