livekit-client 0.18.4-RC6 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/dist/api/RequestQueue.d.ts +13 -12
- package/dist/api/RequestQueue.d.ts.map +1 -0
- package/dist/api/SignalClient.d.ts +67 -66
- package/dist/api/SignalClient.d.ts.map +1 -0
- package/dist/connect.d.ts +24 -23
- package/dist/connect.d.ts.map +1 -0
- package/dist/index.d.ts +27 -26
- package/dist/index.d.ts.map +1 -0
- package/dist/livekit-client.esm.mjs +546 -486
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/logger.d.ts +26 -25
- package/dist/logger.d.ts.map +1 -0
- package/dist/options.d.ts +128 -127
- package/dist/options.d.ts.map +1 -0
- package/dist/proto/google/protobuf/timestamp.d.ts +133 -132
- package/dist/proto/google/protobuf/timestamp.d.ts.map +1 -0
- package/dist/proto/livekit_models.d.ts +876 -868
- package/dist/proto/livekit_models.d.ts.map +1 -0
- package/dist/proto/livekit_rtc.d.ts +3904 -3859
- package/dist/proto/livekit_rtc.d.ts.map +1 -0
- package/dist/room/DeviceManager.d.ts +8 -7
- package/dist/room/DeviceManager.d.ts.map +1 -0
- package/dist/room/PCTransport.d.ts +16 -15
- package/dist/room/PCTransport.d.ts.map +1 -0
- package/dist/room/RTCEngine.d.ts +67 -66
- package/dist/room/RTCEngine.d.ts.map +1 -0
- package/dist/room/Room.d.ts +166 -165
- package/dist/room/Room.d.ts.map +1 -0
- package/dist/room/errors.d.ts +29 -28
- package/dist/room/errors.d.ts.map +1 -0
- package/dist/room/events.d.ts +391 -390
- package/dist/room/events.d.ts.map +1 -0
- package/dist/room/participant/LocalParticipant.d.ts +126 -125
- package/dist/room/participant/LocalParticipant.d.ts.map +1 -0
- package/dist/room/participant/Participant.d.ts +94 -93
- package/dist/room/participant/Participant.d.ts.map +1 -0
- package/dist/room/participant/ParticipantTrackPermission.d.ts +26 -19
- package/dist/room/participant/ParticipantTrackPermission.d.ts.map +1 -0
- package/dist/room/participant/RemoteParticipant.d.ts +40 -39
- package/dist/room/participant/RemoteParticipant.d.ts.map +1 -0
- package/dist/room/participant/publishUtils.d.ts +18 -17
- package/dist/room/participant/publishUtils.d.ts.map +1 -0
- package/dist/room/stats.d.ts +66 -65
- package/dist/room/stats.d.ts.map +1 -0
- package/dist/room/track/LocalAudioTrack.d.ts +20 -19
- package/dist/room/track/LocalAudioTrack.d.ts.map +1 -0
- package/dist/room/track/LocalTrack.d.ts +28 -27
- package/dist/room/track/LocalTrack.d.ts.map +1 -0
- package/dist/room/track/LocalTrackPublication.d.ts +38 -37
- package/dist/room/track/LocalTrackPublication.d.ts.map +1 -0
- package/dist/room/track/LocalVideoTrack.d.ts +31 -30
- package/dist/room/track/LocalVideoTrack.d.ts.map +1 -0
- package/dist/room/track/RemoteAudioTrack.d.ts +20 -19
- package/dist/room/track/RemoteAudioTrack.d.ts.map +1 -0
- package/dist/room/track/RemoteTrack.d.ts +16 -15
- package/dist/room/track/RemoteTrack.d.ts.map +1 -0
- package/dist/room/track/RemoteTrackPublication.d.ts +51 -50
- package/dist/room/track/RemoteTrackPublication.d.ts.map +1 -0
- package/dist/room/track/RemoteVideoTrack.d.ts +28 -27
- package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -0
- package/dist/room/track/Track.d.ts +101 -100
- package/dist/room/track/Track.d.ts.map +1 -0
- package/dist/room/track/TrackPublication.d.ts +50 -49
- package/dist/room/track/TrackPublication.d.ts.map +1 -0
- package/dist/room/track/create.d.ts +24 -23
- package/dist/room/track/create.d.ts.map +1 -0
- package/dist/room/track/defaults.d.ts +5 -4
- package/dist/room/track/defaults.d.ts.map +1 -0
- package/dist/room/track/options.d.ts +223 -222
- package/dist/room/track/options.d.ts.map +1 -0
- package/dist/room/track/types.d.ts +19 -18
- package/dist/room/track/types.d.ts.map +1 -0
- package/dist/room/track/utils.d.ts +14 -13
- package/dist/room/track/utils.d.ts.map +1 -0
- package/dist/room/utils.d.ts +17 -15
- package/dist/room/utils.d.ts.map +1 -0
- package/dist/test/mocks.d.ts +12 -11
- package/dist/test/mocks.d.ts.map +1 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.d.ts.map +1 -0
- package/package.json +4 -5
- package/src/api/RequestQueue.ts +53 -0
- package/src/api/SignalClient.ts +497 -0
- package/src/connect.ts +98 -0
- package/src/index.ts +49 -0
- package/src/logger.ts +56 -0
- package/src/options.ts +156 -0
- package/src/proto/google/protobuf/timestamp.ts +216 -0
- package/src/proto/livekit_models.ts +2456 -0
- package/src/proto/livekit_rtc.ts +2859 -0
- package/src/room/DeviceManager.ts +80 -0
- package/src/room/PCTransport.ts +88 -0
- package/src/room/RTCEngine.ts +695 -0
- package/src/room/Room.ts +970 -0
- package/src/room/errors.ts +65 -0
- package/src/room/events.ts +438 -0
- package/src/room/participant/LocalParticipant.ts +755 -0
- package/src/room/participant/Participant.ts +287 -0
- package/src/room/participant/ParticipantTrackPermission.ts +42 -0
- package/src/room/participant/RemoteParticipant.ts +263 -0
- package/src/room/participant/publishUtils.test.ts +144 -0
- package/src/room/participant/publishUtils.ts +229 -0
- package/src/room/stats.ts +134 -0
- package/src/room/track/LocalAudioTrack.ts +134 -0
- package/src/room/track/LocalTrack.ts +229 -0
- package/src/room/track/LocalTrackPublication.ts +87 -0
- package/src/room/track/LocalVideoTrack.test.ts +72 -0
- package/src/room/track/LocalVideoTrack.ts +295 -0
- package/src/room/track/RemoteAudioTrack.ts +86 -0
- package/src/room/track/RemoteTrack.ts +62 -0
- package/src/room/track/RemoteTrackPublication.ts +207 -0
- package/src/room/track/RemoteVideoTrack.ts +240 -0
- package/src/room/track/Track.ts +358 -0
- package/src/room/track/TrackPublication.ts +120 -0
- package/src/room/track/create.ts +122 -0
- package/src/room/track/defaults.ts +27 -0
- package/src/room/track/options.ts +281 -0
- package/src/room/track/types.ts +20 -0
- package/src/room/track/utils.test.ts +110 -0
- package/src/room/track/utils.ts +113 -0
- package/src/room/utils.ts +115 -0
- package/src/test/mocks.ts +17 -0
- package/src/version.ts +2 -0
- package/CHANGELOG.md +0 -5
package/src/room/Room.ts
ADDED
@@ -0,0 +1,970 @@
|
|
1
|
+
import { EventEmitter } from 'events';
|
2
|
+
import type TypedEmitter from 'typed-emitter';
|
3
|
+
import { toProtoSessionDescription } from '../api/SignalClient';
|
4
|
+
import log from '../logger';
|
5
|
+
import { RoomConnectOptions, RoomOptions } from '../options';
|
6
|
+
import {
|
7
|
+
DataPacket_Kind,
|
8
|
+
ParticipantInfo,
|
9
|
+
ParticipantInfo_State,
|
10
|
+
ParticipantPermission,
|
11
|
+
Room as RoomModel,
|
12
|
+
SpeakerInfo,
|
13
|
+
UserPacket,
|
14
|
+
} from '../proto/livekit_models';
|
15
|
+
import {
|
16
|
+
ConnectionQualityUpdate,
|
17
|
+
JoinResponse,
|
18
|
+
SimulateScenario,
|
19
|
+
StreamStateUpdate,
|
20
|
+
SubscriptionPermissionUpdate,
|
21
|
+
} from '../proto/livekit_rtc';
|
22
|
+
import DeviceManager from './DeviceManager';
|
23
|
+
import { ConnectionError, UnsupportedServer } from './errors';
|
24
|
+
import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
|
25
|
+
import LocalParticipant from './participant/LocalParticipant';
|
26
|
+
import Participant, { ConnectionQuality } from './participant/Participant';
|
27
|
+
import RemoteParticipant from './participant/RemoteParticipant';
|
28
|
+
import RTCEngine, { maxICEConnectTimeout } from './RTCEngine';
|
29
|
+
import { audioDefaults, publishDefaults, videoDefaults } from './track/defaults';
|
30
|
+
import LocalTrackPublication from './track/LocalTrackPublication';
|
31
|
+
import RemoteTrackPublication from './track/RemoteTrackPublication';
|
32
|
+
import { Track } from './track/Track';
|
33
|
+
import { TrackPublication } from './track/TrackPublication';
|
34
|
+
import { AdaptiveStreamSettings, RemoteTrack } from './track/types';
|
35
|
+
import { getNewAudioContext } from './track/utils';
|
36
|
+
import { isWeb, unpackStreamId } from './utils';
|
37
|
+
|
38
|
+
export enum RoomState {
|
39
|
+
Disconnected = 'disconnected',
|
40
|
+
Connected = 'connected',
|
41
|
+
Reconnecting = 'reconnecting',
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* In LiveKit, a room is the logical grouping for a list of participants.
|
46
|
+
* Participants in a room can publish tracks, and subscribe to others' tracks.
|
47
|
+
*
|
48
|
+
* a Room fires [[RoomEvent | RoomEvents]].
|
49
|
+
*
|
50
|
+
* @noInheritDoc
|
51
|
+
*/
|
52
|
+
class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>) {
|
53
|
+
state: RoomState = RoomState.Disconnected;
|
54
|
+
|
55
|
+
/** map of sid: [[RemoteParticipant]] */
|
56
|
+
participants: Map<string, RemoteParticipant>;
|
57
|
+
|
58
|
+
/**
|
59
|
+
* list of participants that are actively speaking. when this changes
|
60
|
+
* a [[RoomEvent.ActiveSpeakersChanged]] event is fired
|
61
|
+
*/
|
62
|
+
activeSpeakers: Participant[] = [];
|
63
|
+
|
64
|
+
/** @internal */
|
65
|
+
engine!: RTCEngine;
|
66
|
+
|
67
|
+
// available after connected
|
68
|
+
/** server assigned unique room id */
|
69
|
+
sid: string = '';
|
70
|
+
|
71
|
+
/** user assigned name, derived from JWT token */
|
72
|
+
name: string = '';
|
73
|
+
|
74
|
+
/** the current participant */
|
75
|
+
localParticipant: LocalParticipant;
|
76
|
+
|
77
|
+
/** room metadata */
|
78
|
+
metadata: string | undefined = undefined;
|
79
|
+
|
80
|
+
/** options of room */
|
81
|
+
options: RoomOptions;
|
82
|
+
|
83
|
+
/** connect options of room */
|
84
|
+
private connOptions?: RoomConnectOptions;
|
85
|
+
|
86
|
+
private audioEnabled = true;
|
87
|
+
|
88
|
+
private audioContext?: AudioContext;
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Creates a new Room, the primary construct for a LiveKit session.
|
92
|
+
* @param options
|
93
|
+
*/
|
94
|
+
constructor(options?: RoomOptions) {
|
95
|
+
super();
|
96
|
+
this.participants = new Map();
|
97
|
+
this.options = options || {};
|
98
|
+
|
99
|
+
switch (this.options?.publishDefaults?.videoCodec) {
|
100
|
+
case 'av1':
|
101
|
+
case 'vp9':
|
102
|
+
this.options.publishDefaults.simulcast = undefined;
|
103
|
+
break;
|
104
|
+
default:
|
105
|
+
}
|
106
|
+
|
107
|
+
this.options.audioCaptureDefaults = {
|
108
|
+
...audioDefaults,
|
109
|
+
...options?.audioCaptureDefaults,
|
110
|
+
};
|
111
|
+
this.options.videoCaptureDefaults = {
|
112
|
+
...videoDefaults,
|
113
|
+
...options?.videoCaptureDefaults,
|
114
|
+
};
|
115
|
+
this.options.publishDefaults = {
|
116
|
+
...publishDefaults,
|
117
|
+
...options?.publishDefaults,
|
118
|
+
};
|
119
|
+
|
120
|
+
this.createEngine();
|
121
|
+
|
122
|
+
this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
|
123
|
+
}
|
124
|
+
|
125
|
+
private createEngine() {
|
126
|
+
if (this.engine) {
|
127
|
+
return;
|
128
|
+
}
|
129
|
+
|
130
|
+
this.engine = new RTCEngine();
|
131
|
+
|
132
|
+
this.engine.client.signalLatency = this.options.expSignalLatency;
|
133
|
+
this.engine.client.onParticipantUpdate = this.handleParticipantUpdates;
|
134
|
+
this.engine.client.onRoomUpdate = this.handleRoomUpdate;
|
135
|
+
this.engine.client.onSpeakersChanged = this.handleSpeakersChanged;
|
136
|
+
this.engine.client.onStreamStateUpdate = this.handleStreamStateUpdate;
|
137
|
+
this.engine.client.onSubscriptionPermissionUpdate = this.handleSubscriptionPermissionUpdate;
|
138
|
+
this.engine.client.onConnectionQuality = this.handleConnectionQualityUpdate;
|
139
|
+
|
140
|
+
this.engine
|
141
|
+
.on(
|
142
|
+
EngineEvent.MediaTrackAdded,
|
143
|
+
(mediaTrack: MediaStreamTrack, stream: MediaStream, receiver?: RTCRtpReceiver) => {
|
144
|
+
this.onTrackAdded(mediaTrack, stream, receiver);
|
145
|
+
},
|
146
|
+
)
|
147
|
+
.on(EngineEvent.Disconnected, () => {
|
148
|
+
this.handleDisconnect();
|
149
|
+
})
|
150
|
+
.on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
|
151
|
+
.on(EngineEvent.DataPacketReceived, this.handleDataPacket)
|
152
|
+
.on(EngineEvent.Resuming, () => {
|
153
|
+
this.state = RoomState.Reconnecting;
|
154
|
+
this.emit(RoomEvent.Reconnecting);
|
155
|
+
this.emit(RoomEvent.StateChanged, this.state);
|
156
|
+
})
|
157
|
+
.on(EngineEvent.Resumed, () => {
|
158
|
+
this.state = RoomState.Connected;
|
159
|
+
this.emit(RoomEvent.Reconnected);
|
160
|
+
this.emit(RoomEvent.StateChanged, this.state);
|
161
|
+
this.updateSubscriptions();
|
162
|
+
})
|
163
|
+
.on(EngineEvent.SignalResumed, () => {
|
164
|
+
if (this.state === RoomState.Reconnecting) {
|
165
|
+
this.sendSyncState();
|
166
|
+
}
|
167
|
+
})
|
168
|
+
.on(EngineEvent.Restarting, this.handleRestarting)
|
169
|
+
.on(EngineEvent.Restarted, this.handleRestarted);
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* getLocalDevices abstracts navigator.mediaDevices.enumerateDevices.
|
174
|
+
* In particular, it handles Chrome's unique behavior of creating `default`
|
175
|
+
* devices. When encountered, it'll be removed from the list of devices.
|
176
|
+
* The actual default device will be placed at top.
|
177
|
+
* @param kind
|
178
|
+
* @returns a list of available local devices
|
179
|
+
*/
|
180
|
+
static getLocalDevices(
|
181
|
+
kind?: MediaDeviceKind,
|
182
|
+
requestPermissions: boolean = true,
|
183
|
+
): Promise<MediaDeviceInfo[]> {
|
184
|
+
return DeviceManager.getInstance().getDevices(kind, requestPermissions);
|
185
|
+
}
|
186
|
+
|
187
|
+
connect = async (url: string, token: string, opts?: RoomConnectOptions) => {
|
188
|
+
// guard against calling connect
|
189
|
+
if (this.state !== RoomState.Disconnected) {
|
190
|
+
log.warn(`already connected to room ${this.name}`);
|
191
|
+
return;
|
192
|
+
}
|
193
|
+
|
194
|
+
// recreate engine if previously disconnected
|
195
|
+
this.createEngine();
|
196
|
+
|
197
|
+
this.acquireAudioContext();
|
198
|
+
|
199
|
+
if (opts?.rtcConfig) {
|
200
|
+
this.engine.rtcConfig = opts.rtcConfig;
|
201
|
+
}
|
202
|
+
|
203
|
+
this.connOptions = opts;
|
204
|
+
|
205
|
+
try {
|
206
|
+
const joinResponse = await this.engine.join(url, token, {
|
207
|
+
autoSubscribe: opts?.autoSubscribe,
|
208
|
+
publishOnly: opts?.publishOnly,
|
209
|
+
adaptiveStream:
|
210
|
+
typeof this.options?.adaptiveStream === 'object' ? true : this.options?.adaptiveStream,
|
211
|
+
});
|
212
|
+
log.debug(
|
213
|
+
`connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`,
|
214
|
+
);
|
215
|
+
|
216
|
+
if (!joinResponse.serverVersion) {
|
217
|
+
throw new UnsupportedServer('unknown server version');
|
218
|
+
}
|
219
|
+
|
220
|
+
if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
|
221
|
+
log.debug('disabling dynacast due to server version');
|
222
|
+
// dynacast has a bug in 0.15.1, so we cannot use it then
|
223
|
+
this.options.dynacast = false;
|
224
|
+
}
|
225
|
+
|
226
|
+
this.state = RoomState.Connected;
|
227
|
+
const pi = joinResponse.participant!;
|
228
|
+
|
229
|
+
this.localParticipant.sid = pi.sid;
|
230
|
+
this.localParticipant.identity = pi.identity;
|
231
|
+
|
232
|
+
this.localParticipant.updateInfo(pi);
|
233
|
+
// forward metadata changed for the local participant
|
234
|
+
this.localParticipant
|
235
|
+
.on(ParticipantEvent.MetadataChanged, (metadata: string | undefined) => {
|
236
|
+
this.emit(RoomEvent.MetadataChanged, metadata, this.localParticipant);
|
237
|
+
})
|
238
|
+
.on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
|
239
|
+
this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
|
240
|
+
})
|
241
|
+
.on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
|
242
|
+
this.emit(RoomEvent.TrackMuted, pub, this.localParticipant);
|
243
|
+
})
|
244
|
+
.on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
|
245
|
+
this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
|
246
|
+
})
|
247
|
+
.on(ParticipantEvent.LocalTrackPublished, (pub: LocalTrackPublication) => {
|
248
|
+
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
249
|
+
})
|
250
|
+
.on(ParticipantEvent.LocalTrackUnpublished, (pub: LocalTrackPublication) => {
|
251
|
+
this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
|
252
|
+
})
|
253
|
+
.on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
|
254
|
+
this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
|
255
|
+
})
|
256
|
+
.on(ParticipantEvent.MediaDevicesError, (e: Error) => {
|
257
|
+
this.emit(RoomEvent.MediaDevicesError, e);
|
258
|
+
})
|
259
|
+
.on(
|
260
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
261
|
+
(prevPermissions: ParticipantPermission) => {
|
262
|
+
this.emit(
|
263
|
+
RoomEvent.ParticipantPermissionsChanged,
|
264
|
+
prevPermissions,
|
265
|
+
this.localParticipant,
|
266
|
+
);
|
267
|
+
},
|
268
|
+
);
|
269
|
+
|
270
|
+
// populate remote participants, these should not trigger new events
|
271
|
+
joinResponse.otherParticipants.forEach((info) => {
|
272
|
+
this.getOrCreateParticipant(info.sid, info);
|
273
|
+
});
|
274
|
+
|
275
|
+
this.name = joinResponse.room!.name;
|
276
|
+
this.sid = joinResponse.room!.sid;
|
277
|
+
this.metadata = joinResponse.room!.metadata;
|
278
|
+
this.emit(RoomEvent.StateChanged, this.state);
|
279
|
+
} catch (err) {
|
280
|
+
this.engine.close();
|
281
|
+
throw err;
|
282
|
+
}
|
283
|
+
|
284
|
+
// don't return until ICE connected
|
285
|
+
return new Promise<Room>((resolve, reject) => {
|
286
|
+
const connectTimeout = setTimeout(() => {
|
287
|
+
// timeout
|
288
|
+
this.engine.close();
|
289
|
+
reject(new ConnectionError('could not connect after timeout'));
|
290
|
+
}, maxICEConnectTimeout);
|
291
|
+
|
292
|
+
this.engine.once(EngineEvent.Connected, () => {
|
293
|
+
clearTimeout(connectTimeout);
|
294
|
+
|
295
|
+
// also hook unload event
|
296
|
+
if (isWeb()) {
|
297
|
+
window.addEventListener('beforeunload', this.onBeforeUnload);
|
298
|
+
navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
|
299
|
+
}
|
300
|
+
|
301
|
+
resolve(this);
|
302
|
+
});
|
303
|
+
});
|
304
|
+
};
|
305
|
+
|
306
|
+
/**
|
307
|
+
* disconnects the room, emits [[RoomEvent.Disconnected]]
|
308
|
+
*/
|
309
|
+
disconnect = (stopTracks = true) => {
|
310
|
+
// send leave
|
311
|
+
if (this.engine) {
|
312
|
+
this.engine.client.sendLeave();
|
313
|
+
this.engine.close();
|
314
|
+
}
|
315
|
+
this.handleDisconnect(stopTracks);
|
316
|
+
/* @ts-ignore */
|
317
|
+
this.engine = undefined;
|
318
|
+
};
|
319
|
+
|
320
|
+
/**
|
321
|
+
* retrieves a participant by identity
|
322
|
+
* @param identity
|
323
|
+
* @returns
|
324
|
+
*/
|
325
|
+
getParticipantByIdentity(identity: string): Participant | undefined {
|
326
|
+
for (const [, p] of this.participants) {
|
327
|
+
if (p.identity === identity) {
|
328
|
+
return p;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
if (this.localParticipant.identity === identity) {
|
332
|
+
return this.localParticipant;
|
333
|
+
}
|
334
|
+
}
|
335
|
+
|
336
|
+
/**
|
337
|
+
* @internal for testing
|
338
|
+
*/
|
339
|
+
simulateScenario(scenario: string) {
|
340
|
+
let req: SimulateScenario | undefined;
|
341
|
+
switch (scenario) {
|
342
|
+
case 'speaker':
|
343
|
+
req = SimulateScenario.fromPartial({
|
344
|
+
speakerUpdate: 3,
|
345
|
+
});
|
346
|
+
break;
|
347
|
+
case 'node-failure':
|
348
|
+
req = SimulateScenario.fromPartial({
|
349
|
+
nodeFailure: true,
|
350
|
+
});
|
351
|
+
break;
|
352
|
+
case 'server-leave':
|
353
|
+
req = SimulateScenario.fromPartial({
|
354
|
+
serverLeave: true,
|
355
|
+
});
|
356
|
+
break;
|
357
|
+
case 'migration':
|
358
|
+
req = SimulateScenario.fromPartial({
|
359
|
+
migration: true,
|
360
|
+
});
|
361
|
+
break;
|
362
|
+
default:
|
363
|
+
}
|
364
|
+
if (req) {
|
365
|
+
this.engine.client.sendSimulateScenario(req);
|
366
|
+
}
|
367
|
+
}
|
368
|
+
|
369
|
+
private onBeforeUnload = () => {
|
370
|
+
this.disconnect();
|
371
|
+
};
|
372
|
+
|
373
|
+
/**
|
374
|
+
* Browsers have different policies regarding audio playback. Most requiring
|
375
|
+
* some form of user interaction (click/tap/etc).
|
376
|
+
* In those cases, audio will be silent until a click/tap triggering one of the following
|
377
|
+
* - `startAudio`
|
378
|
+
* - `getUserMedia`
|
379
|
+
*/
|
380
|
+
async startAudio() {
|
381
|
+
this.acquireAudioContext();
|
382
|
+
|
383
|
+
const elements: Array<HTMLMediaElement> = [];
|
384
|
+
this.participants.forEach((p) => {
|
385
|
+
p.audioTracks.forEach((t) => {
|
386
|
+
if (t.track) {
|
387
|
+
t.track.attachedElements.forEach((e) => {
|
388
|
+
elements.push(e);
|
389
|
+
});
|
390
|
+
}
|
391
|
+
});
|
392
|
+
});
|
393
|
+
|
394
|
+
try {
|
395
|
+
await Promise.all(elements.map((e) => e.play()));
|
396
|
+
this.handleAudioPlaybackStarted();
|
397
|
+
} catch (err) {
|
398
|
+
this.handleAudioPlaybackFailed(err);
|
399
|
+
throw err;
|
400
|
+
}
|
401
|
+
}
|
402
|
+
|
403
|
+
/**
|
404
|
+
* Returns true if audio playback is enabled
|
405
|
+
*/
|
406
|
+
get canPlaybackAudio(): boolean {
|
407
|
+
return this.audioEnabled;
|
408
|
+
}
|
409
|
+
|
410
|
+
/**
|
411
|
+
* Switches all active device used in this room to the given device.
|
412
|
+
*
|
413
|
+
* Note: setting AudioOutput is not supported on some browsers. See [setSinkId](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility)
|
414
|
+
*
|
415
|
+
* @param kind use `videoinput` for camera track,
|
416
|
+
* `audioinput` for microphone track,
|
417
|
+
* `audiooutput` to set speaker for all incoming audio tracks
|
418
|
+
* @param deviceId
|
419
|
+
*/
|
420
|
+
async switchActiveDevice(kind: MediaDeviceKind, deviceId: string) {
|
421
|
+
if (kind === 'audioinput') {
|
422
|
+
const tracks = Array.from(this.localParticipant.audioTracks.values()).filter(
|
423
|
+
(track) => track.source === Track.Source.Microphone,
|
424
|
+
);
|
425
|
+
await Promise.all(tracks.map((t) => t.audioTrack?.setDeviceId(deviceId)));
|
426
|
+
this.options.audioCaptureDefaults!.deviceId = deviceId;
|
427
|
+
} else if (kind === 'videoinput') {
|
428
|
+
const tracks = Array.from(this.localParticipant.videoTracks.values()).filter(
|
429
|
+
(track) => track.source === Track.Source.Camera,
|
430
|
+
);
|
431
|
+
await Promise.all(tracks.map((t) => t.videoTrack?.setDeviceId(deviceId)));
|
432
|
+
this.options.videoCaptureDefaults!.deviceId = deviceId;
|
433
|
+
} else if (kind === 'audiooutput') {
|
434
|
+
const elements: HTMLMediaElement[] = [];
|
435
|
+
this.participants.forEach((p) => {
|
436
|
+
p.audioTracks.forEach((t) => {
|
437
|
+
if (t.isSubscribed && t.track) {
|
438
|
+
t.track.attachedElements.forEach((e) => {
|
439
|
+
elements.push(e);
|
440
|
+
});
|
441
|
+
}
|
442
|
+
});
|
443
|
+
});
|
444
|
+
|
445
|
+
await Promise.all(
|
446
|
+
elements.map(async (e) => {
|
447
|
+
if ('setSinkId' in e) {
|
448
|
+
/* @ts-ignore */
|
449
|
+
await e.setSinkId(deviceId);
|
450
|
+
}
|
451
|
+
}),
|
452
|
+
);
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
456
|
+
private onTrackAdded(
|
457
|
+
mediaTrack: MediaStreamTrack,
|
458
|
+
stream: MediaStream,
|
459
|
+
receiver?: RTCRtpReceiver,
|
460
|
+
) {
|
461
|
+
const parts = unpackStreamId(stream.id);
|
462
|
+
const participantId = parts[0];
|
463
|
+
let trackId = parts[1];
|
464
|
+
if (!trackId || trackId === '') trackId = mediaTrack.id;
|
465
|
+
|
466
|
+
const participant = this.getOrCreateParticipant(participantId);
|
467
|
+
let adaptiveStreamSettings: AdaptiveStreamSettings | undefined;
|
468
|
+
if (this.options.adaptiveStream) {
|
469
|
+
if (typeof this.options.adaptiveStream === 'object') {
|
470
|
+
adaptiveStreamSettings = this.options.adaptiveStream;
|
471
|
+
} else {
|
472
|
+
adaptiveStreamSettings = {};
|
473
|
+
}
|
474
|
+
}
|
475
|
+
participant.addSubscribedMediaTrack(
|
476
|
+
mediaTrack,
|
477
|
+
trackId,
|
478
|
+
stream,
|
479
|
+
receiver,
|
480
|
+
adaptiveStreamSettings,
|
481
|
+
);
|
482
|
+
}
|
483
|
+
|
484
|
+
private handleRestarting = () => {
|
485
|
+
this.state = RoomState.Reconnecting;
|
486
|
+
this.emit(RoomEvent.Reconnecting);
|
487
|
+
this.emit(RoomEvent.StateChanged, this.state);
|
488
|
+
|
489
|
+
// also unwind existing participants & existing subscriptions
|
490
|
+
for (const p of this.participants.values()) {
|
491
|
+
this.handleParticipantDisconnected(p.sid, p);
|
492
|
+
}
|
493
|
+
};
|
494
|
+
|
495
|
+
private handleRestarted = async (joinResponse: JoinResponse) => {
|
496
|
+
log.debug(`reconnected to server region ${joinResponse.serverRegion}`);
|
497
|
+
this.state = RoomState.Connected;
|
498
|
+
this.emit(RoomEvent.Reconnected);
|
499
|
+
this.emit(RoomEvent.StateChanged, this.state);
|
500
|
+
|
501
|
+
// rehydrate participants
|
502
|
+
if (joinResponse.participant) {
|
503
|
+
// with a restart, the sid will have changed, we'll map our understanding to it
|
504
|
+
this.localParticipant.sid = joinResponse.participant.sid;
|
505
|
+
this.handleParticipantUpdates([joinResponse.participant]);
|
506
|
+
}
|
507
|
+
this.handleParticipantUpdates(joinResponse.otherParticipants);
|
508
|
+
|
509
|
+
// unpublish & republish tracks
|
510
|
+
const localPubs: LocalTrackPublication[] = [];
|
511
|
+
this.localParticipant.tracks.forEach((pub) => {
|
512
|
+
if (pub.track) {
|
513
|
+
localPubs.push(pub);
|
514
|
+
}
|
515
|
+
});
|
516
|
+
|
517
|
+
await Promise.all(
|
518
|
+
localPubs.map(async (pub) => {
|
519
|
+
const track = pub.track!;
|
520
|
+
this.localParticipant.unpublishTrack(track, false);
|
521
|
+
this.localParticipant.publishTrack(track, pub.options);
|
522
|
+
}),
|
523
|
+
);
|
524
|
+
};
|
525
|
+
|
526
|
+
private handleDisconnect(shouldStopTracks = true) {
|
527
|
+
if (this.state === RoomState.Disconnected) {
|
528
|
+
return;
|
529
|
+
}
|
530
|
+
this.participants.forEach((p) => {
|
531
|
+
p.tracks.forEach((pub) => {
|
532
|
+
p.unpublishTrack(pub.trackSid);
|
533
|
+
});
|
534
|
+
});
|
535
|
+
|
536
|
+
this.localParticipant.tracks.forEach((pub) => {
|
537
|
+
if (pub.track) {
|
538
|
+
this.localParticipant.unpublishTrack(pub.track);
|
539
|
+
}
|
540
|
+
if (shouldStopTracks) {
|
541
|
+
pub.track?.detach();
|
542
|
+
pub.track?.stop();
|
543
|
+
}
|
544
|
+
});
|
545
|
+
|
546
|
+
this.participants.clear();
|
547
|
+
this.activeSpeakers = [];
|
548
|
+
if (this.audioContext) {
|
549
|
+
this.audioContext.close();
|
550
|
+
this.audioContext = undefined;
|
551
|
+
}
|
552
|
+
if (isWeb()) {
|
553
|
+
window.removeEventListener('beforeunload', this.onBeforeUnload);
|
554
|
+
navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
|
555
|
+
}
|
556
|
+
this.state = RoomState.Disconnected;
|
557
|
+
this.emit(RoomEvent.Disconnected);
|
558
|
+
this.emit(RoomEvent.StateChanged, this.state);
|
559
|
+
}
|
560
|
+
|
561
|
+
private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
|
562
|
+
// handle changes to participant state, and send events
|
563
|
+
participantInfos.forEach((info) => {
|
564
|
+
if (
|
565
|
+
info.sid === this.localParticipant.sid ||
|
566
|
+
info.identity === this.localParticipant.identity
|
567
|
+
) {
|
568
|
+
this.localParticipant.updateInfo(info);
|
569
|
+
return;
|
570
|
+
}
|
571
|
+
|
572
|
+
let remoteParticipant = this.participants.get(info.sid);
|
573
|
+
const isNewParticipant = !remoteParticipant;
|
574
|
+
|
575
|
+
// create participant if doesn't exist
|
576
|
+
remoteParticipant = this.getOrCreateParticipant(info.sid, info);
|
577
|
+
|
578
|
+
// when it's disconnected, send updates
|
579
|
+
if (info.state === ParticipantInfo_State.DISCONNECTED) {
|
580
|
+
this.handleParticipantDisconnected(info.sid, remoteParticipant);
|
581
|
+
} else if (isNewParticipant) {
|
582
|
+
// fire connected event
|
583
|
+
this.emit(RoomEvent.ParticipantConnected, remoteParticipant);
|
584
|
+
} else {
|
585
|
+
// just update, no events
|
586
|
+
remoteParticipant.updateInfo(info);
|
587
|
+
}
|
588
|
+
});
|
589
|
+
};
|
590
|
+
|
591
|
+
private handleParticipantDisconnected(sid: string, participant?: RemoteParticipant) {
|
592
|
+
// remove and send event
|
593
|
+
this.participants.delete(sid);
|
594
|
+
if (!participant) {
|
595
|
+
return;
|
596
|
+
}
|
597
|
+
|
598
|
+
participant.tracks.forEach((publication) => {
|
599
|
+
participant.unpublishTrack(publication.trackSid);
|
600
|
+
});
|
601
|
+
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
602
|
+
}
|
603
|
+
|
604
|
+
// updates are sent only when there's a change to speaker ordering
|
605
|
+
private handleActiveSpeakersUpdate = (speakers: SpeakerInfo[]) => {
|
606
|
+
const activeSpeakers: Participant[] = [];
|
607
|
+
const seenSids: any = {};
|
608
|
+
speakers.forEach((speaker) => {
|
609
|
+
seenSids[speaker.sid] = true;
|
610
|
+
if (speaker.sid === this.localParticipant.sid) {
|
611
|
+
this.localParticipant.audioLevel = speaker.level;
|
612
|
+
this.localParticipant.setIsSpeaking(true);
|
613
|
+
activeSpeakers.push(this.localParticipant);
|
614
|
+
} else {
|
615
|
+
const p = this.participants.get(speaker.sid);
|
616
|
+
if (p) {
|
617
|
+
p.audioLevel = speaker.level;
|
618
|
+
p.setIsSpeaking(true);
|
619
|
+
activeSpeakers.push(p);
|
620
|
+
}
|
621
|
+
}
|
622
|
+
});
|
623
|
+
|
624
|
+
if (!seenSids[this.localParticipant.sid]) {
|
625
|
+
this.localParticipant.audioLevel = 0;
|
626
|
+
this.localParticipant.setIsSpeaking(false);
|
627
|
+
}
|
628
|
+
this.participants.forEach((p) => {
|
629
|
+
if (!seenSids[p.sid]) {
|
630
|
+
p.audioLevel = 0;
|
631
|
+
p.setIsSpeaking(false);
|
632
|
+
}
|
633
|
+
});
|
634
|
+
|
635
|
+
this.activeSpeakers = activeSpeakers;
|
636
|
+
this.emit(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
|
637
|
+
};
|
638
|
+
|
639
|
+
// process list of changed speakers
|
640
|
+
private handleSpeakersChanged = (speakerUpdates: SpeakerInfo[]) => {
|
641
|
+
const lastSpeakers = new Map<string, Participant>();
|
642
|
+
this.activeSpeakers.forEach((p) => {
|
643
|
+
lastSpeakers.set(p.sid, p);
|
644
|
+
});
|
645
|
+
speakerUpdates.forEach((speaker) => {
|
646
|
+
let p: Participant | undefined = this.participants.get(speaker.sid);
|
647
|
+
if (speaker.sid === this.localParticipant.sid) {
|
648
|
+
p = this.localParticipant;
|
649
|
+
}
|
650
|
+
if (!p) {
|
651
|
+
return;
|
652
|
+
}
|
653
|
+
p.audioLevel = speaker.level;
|
654
|
+
p.setIsSpeaking(speaker.active);
|
655
|
+
|
656
|
+
if (speaker.active) {
|
657
|
+
lastSpeakers.set(speaker.sid, p);
|
658
|
+
} else {
|
659
|
+
lastSpeakers.delete(speaker.sid);
|
660
|
+
}
|
661
|
+
});
|
662
|
+
const activeSpeakers = Array.from(lastSpeakers.values());
|
663
|
+
activeSpeakers.sort((a, b) => b.audioLevel - a.audioLevel);
|
664
|
+
this.activeSpeakers = activeSpeakers;
|
665
|
+
this.emit(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
|
666
|
+
};
|
667
|
+
|
668
|
+
private handleStreamStateUpdate = (streamStateUpdate: StreamStateUpdate) => {
|
669
|
+
streamStateUpdate.streamStates.forEach((streamState) => {
|
670
|
+
const participant = this.participants.get(streamState.participantSid);
|
671
|
+
if (!participant) {
|
672
|
+
return;
|
673
|
+
}
|
674
|
+
const pub = participant.getTrackPublication(streamState.trackSid);
|
675
|
+
if (!pub || !pub.track) {
|
676
|
+
return;
|
677
|
+
}
|
678
|
+
pub.track.streamState = Track.streamStateFromProto(streamState.state);
|
679
|
+
participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
|
680
|
+
this.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState, participant);
|
681
|
+
});
|
682
|
+
};
|
683
|
+
|
684
|
+
private handleSubscriptionPermissionUpdate = (update: SubscriptionPermissionUpdate) => {
|
685
|
+
const participant = this.participants.get(update.participantSid);
|
686
|
+
if (!participant) {
|
687
|
+
return;
|
688
|
+
}
|
689
|
+
const pub = participant.getTrackPublication(update.trackSid);
|
690
|
+
if (!pub) {
|
691
|
+
return;
|
692
|
+
}
|
693
|
+
|
694
|
+
pub._allowed = update.allowed;
|
695
|
+
participant.emit(
|
696
|
+
ParticipantEvent.TrackSubscriptionPermissionChanged,
|
697
|
+
pub,
|
698
|
+
pub.subscriptionStatus,
|
699
|
+
);
|
700
|
+
this.emit(
|
701
|
+
ParticipantEvent.TrackSubscriptionPermissionChanged,
|
702
|
+
pub,
|
703
|
+
pub.subscriptionStatus,
|
704
|
+
participant,
|
705
|
+
);
|
706
|
+
};
|
707
|
+
|
708
|
+
private handleDataPacket = (userPacket: UserPacket, kind: DataPacket_Kind) => {
|
709
|
+
// find the participant
|
710
|
+
const participant = this.participants.get(userPacket.participantSid);
|
711
|
+
|
712
|
+
this.emit(RoomEvent.DataReceived, userPacket.payload, participant, kind);
|
713
|
+
|
714
|
+
// also emit on the participant
|
715
|
+
participant?.emit(ParticipantEvent.DataReceived, userPacket.payload, kind);
|
716
|
+
};
|
717
|
+
|
718
|
+
private handleAudioPlaybackStarted = () => {
|
719
|
+
if (this.canPlaybackAudio) {
|
720
|
+
return;
|
721
|
+
}
|
722
|
+
this.audioEnabled = true;
|
723
|
+
this.emit(RoomEvent.AudioPlaybackStatusChanged, true);
|
724
|
+
};
|
725
|
+
|
726
|
+
private handleAudioPlaybackFailed = (e: any) => {
|
727
|
+
log.warn('could not playback audio', e);
|
728
|
+
if (!this.canPlaybackAudio) {
|
729
|
+
return;
|
730
|
+
}
|
731
|
+
this.audioEnabled = false;
|
732
|
+
this.emit(RoomEvent.AudioPlaybackStatusChanged, false);
|
733
|
+
};
|
734
|
+
|
735
|
+
private handleDeviceChange = async () => {
|
736
|
+
this.emit(RoomEvent.MediaDevicesChanged);
|
737
|
+
};
|
738
|
+
|
739
|
+
private handleRoomUpdate = (r: RoomModel) => {
|
740
|
+
this.metadata = r.metadata;
|
741
|
+
this.emit(RoomEvent.RoomMetadataChanged, r.metadata);
|
742
|
+
};
|
743
|
+
|
744
|
+
private handleConnectionQualityUpdate = (update: ConnectionQualityUpdate) => {
|
745
|
+
update.updates.forEach((info) => {
|
746
|
+
if (info.participantSid === this.localParticipant.sid) {
|
747
|
+
this.localParticipant.setConnectionQuality(info.quality);
|
748
|
+
return;
|
749
|
+
}
|
750
|
+
const participant = this.participants.get(info.participantSid);
|
751
|
+
if (participant) {
|
752
|
+
participant.setConnectionQuality(info.quality);
|
753
|
+
}
|
754
|
+
});
|
755
|
+
};
|
756
|
+
|
757
|
+
private acquireAudioContext() {
|
758
|
+
if (this.audioContext) {
|
759
|
+
this.audioContext.close();
|
760
|
+
}
|
761
|
+
// by using an AudioContext, it reduces lag on audio elements
|
762
|
+
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
763
|
+
const ctx = getNewAudioContext();
|
764
|
+
if (ctx) {
|
765
|
+
this.audioContext = ctx;
|
766
|
+
}
|
767
|
+
}
|
768
|
+
|
769
|
+
private createParticipant(id: string, info?: ParticipantInfo): RemoteParticipant {
|
770
|
+
let participant: RemoteParticipant;
|
771
|
+
if (info) {
|
772
|
+
participant = RemoteParticipant.fromParticipantInfo(this.engine.client, info);
|
773
|
+
} else {
|
774
|
+
participant = new RemoteParticipant(this.engine.client, id, '');
|
775
|
+
}
|
776
|
+
return participant;
|
777
|
+
}
|
778
|
+
|
779
|
+
private getOrCreateParticipant(id: string, info?: ParticipantInfo): RemoteParticipant {
|
780
|
+
if (this.participants.has(id)) {
|
781
|
+
return this.participants.get(id) as RemoteParticipant;
|
782
|
+
}
|
783
|
+
// it's possible for the RTC track to arrive before signaling data
|
784
|
+
// when this happens, we'll create the participant and make the track work
|
785
|
+
const participant = this.createParticipant(id, info);
|
786
|
+
this.participants.set(id, participant);
|
787
|
+
|
788
|
+
// also forward events
|
789
|
+
// trackPublished is only fired for tracks added after both local participant
|
790
|
+
// and remote participant joined the room
|
791
|
+
participant
|
792
|
+
.on(ParticipantEvent.TrackPublished, (trackPublication: RemoteTrackPublication) => {
|
793
|
+
this.emit(RoomEvent.TrackPublished, trackPublication, participant);
|
794
|
+
})
|
795
|
+
.on(
|
796
|
+
ParticipantEvent.TrackSubscribed,
|
797
|
+
(track: RemoteTrack, publication: RemoteTrackPublication) => {
|
798
|
+
// monitor playback status
|
799
|
+
if (track.kind === Track.Kind.Audio) {
|
800
|
+
track.on(TrackEvent.AudioPlaybackStarted, this.handleAudioPlaybackStarted);
|
801
|
+
track.on(TrackEvent.AudioPlaybackFailed, this.handleAudioPlaybackFailed);
|
802
|
+
}
|
803
|
+
this.emit(RoomEvent.TrackSubscribed, track, publication, participant);
|
804
|
+
},
|
805
|
+
)
|
806
|
+
.on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
|
807
|
+
this.emit(RoomEvent.TrackUnpublished, publication, participant);
|
808
|
+
})
|
809
|
+
.on(
|
810
|
+
ParticipantEvent.TrackUnsubscribed,
|
811
|
+
(track: RemoteTrack, publication: RemoteTrackPublication) => {
|
812
|
+
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
|
813
|
+
},
|
814
|
+
)
|
815
|
+
.on(ParticipantEvent.TrackSubscriptionFailed, (sid: string) => {
|
816
|
+
this.emit(RoomEvent.TrackSubscriptionFailed, sid, participant);
|
817
|
+
})
|
818
|
+
.on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
|
819
|
+
this.emit(RoomEvent.TrackMuted, pub, participant);
|
820
|
+
})
|
821
|
+
.on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
|
822
|
+
this.emit(RoomEvent.TrackUnmuted, pub, participant);
|
823
|
+
})
|
824
|
+
.on(ParticipantEvent.MetadataChanged, (metadata: string | undefined) => {
|
825
|
+
this.emit(RoomEvent.MetadataChanged, metadata, participant);
|
826
|
+
})
|
827
|
+
.on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
|
828
|
+
this.emit(RoomEvent.ParticipantMetadataChanged, metadata, participant);
|
829
|
+
})
|
830
|
+
.on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
|
831
|
+
this.emit(RoomEvent.ConnectionQualityChanged, quality, participant);
|
832
|
+
})
|
833
|
+
.on(
|
834
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
835
|
+
(prevPermissions: ParticipantPermission) => {
|
836
|
+
this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, participant);
|
837
|
+
},
|
838
|
+
);
|
839
|
+
return participant;
|
840
|
+
}
|
841
|
+
|
842
|
+
private sendSyncState() {
|
843
|
+
if (
|
844
|
+
this.engine.subscriber === undefined ||
|
845
|
+
this.engine.subscriber.pc.localDescription === null
|
846
|
+
) {
|
847
|
+
return;
|
848
|
+
}
|
849
|
+
const previousSdp = this.engine.subscriber.pc.localDescription;
|
850
|
+
|
851
|
+
/* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
|
852
|
+
in this case, we send unsub tracks, so server add all tracks to this
|
853
|
+
subscribe pc and unsub special tracks from it.
|
854
|
+
2. autosubscribe off, we send subscribed tracks.
|
855
|
+
*/
|
856
|
+
const sendUnsub = this.connOptions?.autoSubscribe || false;
|
857
|
+
const trackSids = new Array<string>();
|
858
|
+
this.participants.forEach((participant) => {
|
859
|
+
participant.tracks.forEach((track) => {
|
860
|
+
if (track.isSubscribed !== sendUnsub) {
|
861
|
+
trackSids.push(track.trackSid);
|
862
|
+
}
|
863
|
+
});
|
864
|
+
});
|
865
|
+
|
866
|
+
this.engine.client.sendSyncState({
|
867
|
+
answer: toProtoSessionDescription({
|
868
|
+
sdp: previousSdp.sdp,
|
869
|
+
type: previousSdp.type,
|
870
|
+
}),
|
871
|
+
subscription: {
|
872
|
+
trackSids,
|
873
|
+
subscribe: !sendUnsub,
|
874
|
+
participantTracks: [],
|
875
|
+
},
|
876
|
+
publishTracks: this.localParticipant.publishedTracksInfo(),
|
877
|
+
dataChannels: this.localParticipant.dataChannelsInfo(),
|
878
|
+
});
|
879
|
+
}
|
880
|
+
|
881
|
+
/**
|
882
|
+
* After resuming, we'll need to notify the server of the current
|
883
|
+
* subscription settings.
|
884
|
+
*/
|
885
|
+
private updateSubscriptions() {
|
886
|
+
for (const p of this.participants.values()) {
|
887
|
+
for (const pub of p.videoTracks.values()) {
|
888
|
+
if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
|
889
|
+
pub.emitTrackUpdate();
|
890
|
+
}
|
891
|
+
}
|
892
|
+
}
|
893
|
+
}
|
894
|
+
|
895
|
+
// /** @internal */
|
896
|
+
emit<E extends keyof RoomEventCallbacks>(
|
897
|
+
event: E,
|
898
|
+
...args: Parameters<RoomEventCallbacks[E]>
|
899
|
+
): boolean {
|
900
|
+
log.debug('room event', { event, args });
|
901
|
+
return super.emit(event, ...args);
|
902
|
+
}
|
903
|
+
}
|
904
|
+
|
905
|
+
export default Room;
|
906
|
+
|
907
|
+
export type RoomEventCallbacks = {
|
908
|
+
reconnecting: () => void;
|
909
|
+
reconnected: () => void;
|
910
|
+
disconnected: () => void;
|
911
|
+
stateChanged: (state: RoomState) => void;
|
912
|
+
mediaDevicesChanged: () => void;
|
913
|
+
participantConnected: (participant: RemoteParticipant) => void;
|
914
|
+
participantDisconnected: (participant: RemoteParticipant) => void;
|
915
|
+
trackPublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
|
916
|
+
trackSubscribed: (
|
917
|
+
track: RemoteTrack,
|
918
|
+
publication: RemoteTrackPublication,
|
919
|
+
participant: RemoteParticipant,
|
920
|
+
) => void;
|
921
|
+
trackSubscriptionFailed: (trackSid: string, participant: RemoteParticipant) => void;
|
922
|
+
trackUnpublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
|
923
|
+
trackUnsubscribed: (
|
924
|
+
track: RemoteTrack,
|
925
|
+
publication: RemoteTrackPublication,
|
926
|
+
participant: RemoteParticipant,
|
927
|
+
) => void;
|
928
|
+
trackMuted: (publication: TrackPublication, participant: Participant) => void;
|
929
|
+
trackUnmuted: (publication: TrackPublication, participant: Participant) => void;
|
930
|
+
localTrackPublished: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
|
931
|
+
localTrackUnpublished: (
|
932
|
+
publication: LocalTrackPublication,
|
933
|
+
participant: LocalParticipant,
|
934
|
+
) => void;
|
935
|
+
/**
|
936
|
+
* @deprecated use [[participantMetadataChanged]] instead
|
937
|
+
*/
|
938
|
+
metadataChanged: (
|
939
|
+
metadata: string | undefined,
|
940
|
+
participant?: RemoteParticipant | LocalParticipant,
|
941
|
+
) => void;
|
942
|
+
participantMetadataChanged: (
|
943
|
+
metadata: string | undefined,
|
944
|
+
participant: RemoteParticipant | LocalParticipant,
|
945
|
+
) => void;
|
946
|
+
participantPermissionsChanged: (
|
947
|
+
prevPermissions: ParticipantPermission,
|
948
|
+
participant: RemoteParticipant | LocalParticipant,
|
949
|
+
) => void;
|
950
|
+
activeSpeakersChanged: (speakers: Array<Participant>) => void;
|
951
|
+
roomMetadataChanged: (metadata: string) => void;
|
952
|
+
dataReceived: (
|
953
|
+
payload: Uint8Array,
|
954
|
+
participant?: RemoteParticipant,
|
955
|
+
kind?: DataPacket_Kind,
|
956
|
+
) => void;
|
957
|
+
connectionQualityChanged: (quality: ConnectionQuality, participant: Participant) => void;
|
958
|
+
mediaDevicesError: (error: Error) => void;
|
959
|
+
trackStreamStateChanged: (
|
960
|
+
publication: RemoteTrackPublication,
|
961
|
+
streamState: Track.StreamState,
|
962
|
+
participant: RemoteParticipant,
|
963
|
+
) => void;
|
964
|
+
trackSubscriptionPermissionChanged: (
|
965
|
+
publication: RemoteTrackPublication,
|
966
|
+
status: TrackPublication.SubscriptionStatus,
|
967
|
+
participant: RemoteParticipant,
|
968
|
+
) => void;
|
969
|
+
audioPlaybackChanged: (playing: boolean) => void;
|
970
|
+
};
|