livekit-client 0.15.2 → 0.16.1

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