livekit-client 1.7.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. package/README.md +21 -1
  2. package/dist/livekit-client.esm.mjs +14241 -12994
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/api/SignalClient.d.ts +11 -10
  7. package/dist/src/api/SignalClient.d.ts.map +1 -1
  8. package/dist/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  9. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  10. package/dist/src/connectionHelper/checks/Checker.d.ts +1 -1
  11. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  12. package/dist/src/index.d.ts +7 -7
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  15. package/dist/src/proto/livekit_models.d.ts +37 -0
  16. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  17. package/dist/src/proto/livekit_rtc.d.ts +347 -75
  18. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  19. package/dist/src/room/RTCEngine.d.ts +12 -3
  20. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  21. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  22. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  23. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  24. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  25. package/dist/src/room/Room.d.ts +23 -15
  26. package/dist/src/room/Room.d.ts.map +1 -1
  27. package/dist/src/room/errors.d.ts +2 -1
  28. package/dist/src/room/errors.d.ts.map +1 -1
  29. package/dist/src/room/events.d.ts +23 -2
  30. package/dist/src/room/events.d.ts.map +1 -1
  31. package/dist/src/room/participant/LocalParticipant.d.ts +14 -2
  32. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  33. package/dist/src/room/participant/Participant.d.ts +4 -2
  34. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  35. package/dist/src/room/participant/RemoteParticipant.d.ts +2 -2
  36. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  37. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  38. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  39. package/dist/src/room/track/LocalTrack.d.ts +4 -3
  40. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  41. package/dist/src/room/track/LocalTrackPublication.d.ts +1 -1
  42. package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
  43. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  44. package/dist/src/room/track/RemoteAudioTrack.d.ts +1 -1
  45. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  46. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  47. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  48. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -1
  49. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  50. package/dist/src/room/track/Track.d.ts +3 -1
  51. package/dist/src/room/track/Track.d.ts.map +1 -1
  52. package/dist/src/room/track/create.d.ts.map +1 -1
  53. package/dist/src/room/types.d.ts +5 -0
  54. package/dist/src/room/types.d.ts.map +1 -1
  55. package/dist/src/room/utils.d.ts +4 -0
  56. package/dist/src/room/utils.d.ts.map +1 -1
  57. package/dist/ts4.2/src/api/SignalClient.d.ts +14 -10
  58. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  59. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +1 -1
  60. package/dist/ts4.2/src/index.d.ts +7 -6
  61. package/dist/ts4.2/src/proto/livekit_models.d.ts +37 -0
  62. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +380 -84
  63. package/dist/ts4.2/src/room/RTCEngine.d.ts +12 -3
  64. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  65. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  66. package/dist/ts4.2/src/room/Room.d.ts +23 -15
  67. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  68. package/dist/ts4.2/src/room/events.d.ts +23 -2
  69. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +14 -2
  70. package/dist/ts4.2/src/room/participant/Participant.d.ts +4 -2
  71. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +2 -2
  72. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +4 -3
  73. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -1
  74. package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +1 -1
  75. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  76. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -1
  77. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  78. package/dist/ts4.2/src/room/types.d.ts +5 -0
  79. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  80. package/package.json +21 -20
  81. package/src/api/SignalClient.ts +41 -29
  82. package/src/connectionHelper/ConnectionCheck.ts +1 -2
  83. package/src/connectionHelper/checks/Checker.ts +1 -1
  84. package/src/connectionHelper/checks/reconnect.ts +1 -1
  85. package/src/index.ts +9 -8
  86. package/src/proto/google/protobuf/timestamp.ts +15 -6
  87. package/src/proto/livekit_models.ts +917 -221
  88. package/src/proto/livekit_rtc.ts +1053 -279
  89. package/src/room/RTCEngine.ts +171 -47
  90. package/src/room/ReconnectPolicy.ts +2 -0
  91. package/src/room/RegionUrlProvider.ts +73 -0
  92. package/src/room/Room.ts +278 -177
  93. package/src/room/errors.ts +1 -0
  94. package/src/room/events.ts +24 -0
  95. package/src/room/participant/LocalParticipant.ts +30 -7
  96. package/src/room/participant/Participant.ts +27 -3
  97. package/src/room/participant/RemoteParticipant.ts +6 -3
  98. package/src/room/participant/publishUtils.test.ts +1 -1
  99. package/src/room/participant/publishUtils.ts +1 -1
  100. package/src/room/track/LocalAudioTrack.ts +14 -7
  101. package/src/room/track/LocalTrack.ts +23 -9
  102. package/src/room/track/LocalTrackPublication.ts +1 -1
  103. package/src/room/track/LocalVideoTrack.ts +15 -9
  104. package/src/room/track/RemoteAudioTrack.ts +1 -1
  105. package/src/room/track/RemoteTrackPublication.ts +4 -3
  106. package/src/room/track/RemoteVideoTrack.test.ts +1 -1
  107. package/src/room/track/RemoteVideoTrack.ts +8 -7
  108. package/src/room/track/Track.ts +46 -31
  109. package/src/room/track/create.ts +2 -2
  110. package/src/room/types.ts +17 -0
  111. package/src/room/utils.ts +53 -0
package/src/room/Room.ts CHANGED
@@ -29,6 +29,9 @@ import {
29
29
  StreamStateUpdate,
30
30
  SubscriptionPermissionUpdate,
31
31
  } from '../proto/livekit_rtc';
32
+ import DeviceManager from './DeviceManager';
33
+ import RTCEngine from './RTCEngine';
34
+ import { RegionUrlProvider } from './RegionUrlProvider';
32
35
  import {
33
36
  audioDefaults,
34
37
  publishDefaults,
@@ -36,15 +39,12 @@ import {
36
39
  roomOptionDefaults,
37
40
  videoDefaults,
38
41
  } from './defaults';
39
- import DeviceManager from './DeviceManager';
40
- import { ConnectionError, UnsupportedServer } from './errors';
42
+ import { ConnectionError, ConnectionErrorReason, UnsupportedServer } from './errors';
41
43
  import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
42
44
  import LocalParticipant from './participant/LocalParticipant';
43
45
  import type Participant from './participant/Participant';
44
46
  import type { ConnectionQuality } from './participant/Participant';
45
47
  import RemoteParticipant from './participant/RemoteParticipant';
46
- import RTCEngine from './RTCEngine';
47
- import CriticalTimers from './timers';
48
48
  import LocalAudioTrack from './track/LocalAudioTrack';
49
49
  import LocalTrackPublication from './track/LocalTrackPublication';
50
50
  import LocalVideoTrack from './track/LocalVideoTrack';
@@ -54,13 +54,14 @@ import { Track } from './track/Track';
54
54
  import type { TrackPublication } from './track/TrackPublication';
55
55
  import type { AdaptiveStreamSettings } from './track/types';
56
56
  import { getNewAudioContext } from './track/utils';
57
- import type { SimulationOptions } from './types';
57
+ import type { SimulationOptions, SimulationScenario } from './types';
58
58
  import {
59
- createDummyVideoStreamTrack,
60
59
  Future,
60
+ Mutex,
61
+ createDummyVideoStreamTrack,
61
62
  getEmptyAudioStreamTrack,
63
+ isCloud,
62
64
  isWeb,
63
- Mutex,
64
65
  supportsSetSinkId,
65
66
  unpackStreamId,
66
67
  } from './utils';
@@ -98,23 +99,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
98
99
  /** @internal */
99
100
  engine!: RTCEngine;
100
101
 
101
- // available after connected
102
- /** server assigned unique room id */
103
- sid: string = '';
104
-
105
- /** user assigned name, derived from JWT token */
106
- name: string = '';
107
-
108
102
  /** the current participant */
109
103
  localParticipant: LocalParticipant;
110
104
 
111
- /** room metadata */
112
- metadata: string | undefined = undefined;
113
-
114
105
  /** options of room */
115
106
  options: InternalRoomOptions;
116
107
 
117
- private _isRecording: boolean = false;
108
+ private roomInfo?: RoomModel;
118
109
 
119
110
  private identityToSid: Map<string, string>;
120
111
 
@@ -164,6 +155,36 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
164
155
  this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
165
156
  }
166
157
 
158
+ /**
159
+ * if the current room has a participant with `recorder: true` in its JWT grant
160
+ **/
161
+ get isRecording(): boolean {
162
+ return this.roomInfo?.activeRecording ?? false;
163
+ }
164
+
165
+ /** server assigned unique room id */
166
+ get sid(): string {
167
+ return this.roomInfo?.sid ?? '';
168
+ }
169
+
170
+ /** user assigned name, derived from JWT token */
171
+ get name(): string {
172
+ return this.roomInfo?.name ?? '';
173
+ }
174
+
175
+ /** room metadata */
176
+ get metadata(): string | undefined {
177
+ return this.roomInfo?.metadata;
178
+ }
179
+
180
+ get numParticipants(): number {
181
+ return this.roomInfo?.numParticipants ?? 0;
182
+ }
183
+
184
+ get numPublishers(): number {
185
+ return this.roomInfo?.numPublishers ?? 0;
186
+ }
187
+
167
188
  private maybeCreateEngine() {
168
189
  if (this.engine) {
169
190
  return;
@@ -206,7 +227,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
206
227
  }
207
228
  })
208
229
  .on(EngineEvent.Restarting, this.handleRestarting)
209
- .on(EngineEvent.Restarted, this.handleRestarted);
230
+ .on(EngineEvent.SignalRestarted, this.handleSignalRestarted)
231
+ .on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
232
+ this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
233
+ });
210
234
 
211
235
  if (this.localParticipant) {
212
236
  this.localParticipant.setupEngine(this.engine);
@@ -258,155 +282,201 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
258
282
 
259
283
  this.setAndEmitConnectionState(ConnectionState.Connecting);
260
284
 
261
- const connectFn = async (resolve: () => void, reject: (reason: any) => void) => {
262
- if (!this.abortController || this.abortController.signal.aborted) {
263
- this.abortController = new AbortController();
285
+ const urlProvider = new RegionUrlProvider(url, token);
286
+
287
+ const connectFn = async (
288
+ resolve: () => void,
289
+ reject: (reason: any) => void,
290
+ regionUrl?: string,
291
+ ) => {
292
+ if (this.abortController) {
293
+ this.abortController.abort();
264
294
  }
295
+ this.abortController = new AbortController();
296
+
265
297
  // at this point the intention to connect has been signalled so we can allow cancelling of the connection via disconnect() again
266
- unlockDisconnect();
298
+ unlockDisconnect?.();
267
299
 
268
- if (this.state === ConnectionState.Reconnecting) {
269
- log.info('Reconnection attempt replaced by new connection attempt');
270
- // make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
271
- this.recreateEngine();
272
- } else {
273
- // create engine if previously disconnected
274
- this.maybeCreateEngine();
300
+ try {
301
+ await this.attemptConnection(regionUrl ?? url, token, opts, this.abortController);
302
+ this.abortController = undefined;
303
+ resolve();
304
+ } catch (e) {
305
+ if (
306
+ isCloud(new URL(url)) &&
307
+ e instanceof ConnectionError &&
308
+ e.reason !== ConnectionErrorReason.Cancelled
309
+ ) {
310
+ let nextUrl: string | null = null;
311
+ try {
312
+ nextUrl = await urlProvider.getNextBestRegionUrl(this.abortController?.signal);
313
+ } catch (error) {
314
+ if (
315
+ error instanceof ConnectionError &&
316
+ (error.status === 401 || error.reason === ConnectionErrorReason.Cancelled)
317
+ ) {
318
+ reject(error);
319
+ return;
320
+ }
321
+ }
322
+ if (nextUrl) {
323
+ log.debug('initial connection failed, retrying with another region');
324
+ await connectFn(resolve, reject, nextUrl);
325
+ } else {
326
+ reject(e);
327
+ }
328
+ } else {
329
+ reject(e);
330
+ }
275
331
  }
332
+ };
333
+ this.connectFuture = new Future(connectFn, () => {
334
+ this.clearConnectionFutures();
335
+ });
276
336
 
277
- this.acquireAudioContext();
337
+ return this.connectFuture.promise;
338
+ };
278
339
 
279
- this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
340
+ private connectSignal = async (
341
+ url: string,
342
+ token: string,
343
+ engine: RTCEngine,
344
+ connectOptions: InternalRoomConnectOptions,
345
+ roomOptions: InternalRoomOptions,
346
+ abortController: AbortController,
347
+ ): Promise<JoinResponse> => {
348
+ const joinResponse = await engine.join(
349
+ url,
350
+ token,
351
+ {
352
+ autoSubscribe: connectOptions.autoSubscribe,
353
+ publishOnly: connectOptions.publishOnly,
354
+ adaptiveStream:
355
+ typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
356
+ maxRetries: connectOptions.maxRetries,
357
+ },
358
+ abortController.signal,
359
+ );
280
360
 
281
- if (this.connOptions.rtcConfig) {
282
- this.engine.rtcConfig = this.connOptions.rtcConfig;
283
- }
284
- if (this.connOptions.peerConnectionTimeout) {
285
- this.engine.peerConnectionTimeout = this.connOptions.peerConnectionTimeout;
286
- }
361
+ let serverInfo: Partial<ServerInfo> | undefined = joinResponse.serverInfo;
362
+ if (!serverInfo) {
363
+ serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
364
+ }
287
365
 
288
- try {
289
- const joinResponse = await this.engine.join(
290
- url,
291
- token,
292
- {
293
- autoSubscribe: this.connOptions.autoSubscribe,
294
- publishOnly: this.connOptions.publishOnly,
295
- adaptiveStream:
296
- typeof this.options.adaptiveStream === 'object' ? true : this.options.adaptiveStream,
297
- maxRetries: this.connOptions.maxRetries,
298
- },
299
- this.abortController.signal,
300
- );
366
+ log.debug(
367
+ `connected to Livekit Server ${Object.entries(serverInfo)
368
+ .map(([key, value]) => `${key}: ${value}`)
369
+ .join(', ')}`,
370
+ );
301
371
 
302
- let serverInfo: Partial<ServerInfo> | undefined = joinResponse.serverInfo;
303
- if (!serverInfo) {
304
- serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
305
- }
372
+ if (!joinResponse.serverVersion) {
373
+ throw new UnsupportedServer('unknown server version');
374
+ }
306
375
 
307
- log.debug(
308
- `connected to Livekit Server ${Object.entries(serverInfo)
309
- .map(([key, value]) => `${key}: ${value}`)
310
- .join(', ')}`,
311
- );
376
+ if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
377
+ log.debug('disabling dynacast due to server version');
378
+ // dynacast has a bug in 0.15.1, so we cannot use it then
379
+ roomOptions.dynacast = false;
380
+ }
312
381
 
313
- if (!joinResponse.serverVersion) {
314
- throw new UnsupportedServer('unknown server version');
315
- }
382
+ return joinResponse;
383
+ };
316
384
 
317
- if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
318
- log.debug('disabling dynacast due to server version');
319
- // dynacast has a bug in 0.15.1, so we cannot use it then
320
- this.options.dynacast = false;
321
- }
385
+ private applyJoinResponse = (joinResponse: JoinResponse) => {
386
+ const pi = joinResponse.participant!;
322
387
 
323
- const pi = joinResponse.participant!;
388
+ this.localParticipant.sid = pi.sid;
389
+ this.localParticipant.identity = pi.identity;
324
390
 
325
- this.localParticipant.sid = pi.sid;
326
- this.localParticipant.identity = pi.identity;
391
+ // populate remote participants, these should not trigger new events
392
+ this.handleParticipantUpdates([pi, ...joinResponse.otherParticipants]);
327
393
 
328
- this.localParticipant.updateInfo(pi);
329
- // forward metadata changed for the local participant
330
- this.setupLocalParticipantEvents();
394
+ if (joinResponse.room) {
395
+ this.handleRoomUpdate(joinResponse.room);
396
+ }
397
+ };
331
398
 
332
- // populate remote participants, these should not trigger new events
333
- joinResponse.otherParticipants.forEach((info) => {
334
- if (
335
- info.sid !== this.localParticipant.sid &&
336
- info.identity !== this.localParticipant.identity
337
- ) {
338
- this.getOrCreateParticipant(info.sid, info);
339
- } else {
340
- log.warn('received info to create local participant as remote participant', {
341
- info,
342
- localParticipant: this.localParticipant,
343
- });
344
- }
345
- });
399
+ private attemptConnection = async (
400
+ url: string,
401
+ token: string,
402
+ opts: RoomConnectOptions | undefined,
403
+ abortController: AbortController,
404
+ ) => {
405
+ if (this.state === ConnectionState.Reconnecting) {
406
+ log.info('Reconnection attempt replaced by new connection attempt');
407
+ // make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
408
+ this.recreateEngine();
409
+ } else {
410
+ // create engine if previously disconnected
411
+ this.maybeCreateEngine();
412
+ }
346
413
 
347
- this.name = joinResponse.room!.name;
348
- this.sid = joinResponse.room!.sid;
349
- this.metadata = joinResponse.room!.metadata;
350
- if (this._isRecording !== joinResponse.room!.activeRecording) {
351
- this._isRecording = joinResponse.room!.activeRecording;
352
- this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
353
- }
354
- this.emit(RoomEvent.SignalConnected);
355
- } catch (err) {
356
- this.recreateEngine();
357
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
358
- const resultingError = new ConnectionError(`could not establish signal connection`);
359
- if (err instanceof Error) {
360
- resultingError.message = `${resultingError.message}: ${err.message}`;
361
- }
362
- if (err instanceof ConnectionError) {
363
- resultingError.reason = err.reason;
364
- resultingError.status = err.status;
365
- }
366
- log.debug(`error trying to establish signal connection`, { error: err });
367
- reject(resultingError);
368
- return;
369
- }
414
+ this.acquireAudioContext();
370
415
 
371
- // don't return until ICE connected
372
- const connectTimeout = CriticalTimers.setTimeout(() => {
373
- // timeout
374
- this.recreateEngine();
375
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
376
- reject(new ConnectionError('could not connect PeerConnection after timeout'));
377
- }, this.connOptions.peerConnectionTimeout);
378
- const abortHandler = () => {
379
- log.warn('closing engine');
380
- CriticalTimers.clearTimeout(connectTimeout);
381
- this.recreateEngine();
382
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
383
- reject(new ConnectionError('room connection has been cancelled'));
384
- };
385
- if (this.abortController?.signal.aborted) {
386
- abortHandler();
416
+ this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
417
+
418
+ if (this.connOptions.rtcConfig) {
419
+ this.engine.rtcConfig = this.connOptions.rtcConfig;
420
+ }
421
+ if (this.connOptions.peerConnectionTimeout) {
422
+ this.engine.peerConnectionTimeout = this.connOptions.peerConnectionTimeout;
423
+ }
424
+
425
+ try {
426
+ const joinResponse = await this.connectSignal(
427
+ url,
428
+ token,
429
+ this.engine,
430
+ this.connOptions,
431
+ this.options,
432
+ abortController,
433
+ );
434
+
435
+ this.applyJoinResponse(joinResponse);
436
+ // forward metadata changed for the local participant
437
+ this.setupLocalParticipantEvents();
438
+ this.emit(RoomEvent.SignalConnected);
439
+ } catch (err) {
440
+ this.recreateEngine();
441
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
442
+ const resultingError = new ConnectionError(`could not establish signal connection`);
443
+ if (err instanceof Error) {
444
+ resultingError.message = `${resultingError.message}: ${err.message}`;
387
445
  }
388
- this.abortController?.signal.addEventListener('abort', abortHandler);
389
-
390
- this.engine.once(EngineEvent.Connected, () => {
391
- CriticalTimers.clearTimeout(connectTimeout);
392
- this.abortController?.signal.removeEventListener('abort', abortHandler);
393
- // also hook unload event
394
- if (isWeb() && this.options.disconnectOnPageLeave) {
395
- // capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
396
- window.addEventListener('pagehide', this.onPageLeave);
397
- window.addEventListener('beforeunload', this.onPageLeave);
398
- navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
399
- }
400
- this.setAndEmitConnectionState(ConnectionState.Connected);
401
- this.emit(RoomEvent.Connected);
402
- resolve();
403
- });
404
- };
405
- this.connectFuture = new Future(connectFn, () => {
406
- this.clearConnectionFutures();
407
- });
446
+ if (err instanceof ConnectionError) {
447
+ resultingError.reason = err.reason;
448
+ resultingError.status = err.status;
449
+ }
450
+ log.debug(`error trying to establish signal connection`, { error: err });
451
+ throw resultingError;
452
+ }
408
453
 
409
- return this.connectFuture.promise;
454
+ if (abortController.signal.aborted) {
455
+ this.recreateEngine();
456
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
457
+ throw new ConnectionError(`Connection attempt aborted`);
458
+ }
459
+
460
+ try {
461
+ await this.engine.waitForPCInitialConnection(
462
+ this.connOptions.peerConnectionTimeout,
463
+ abortController,
464
+ );
465
+ } catch (e) {
466
+ this.recreateEngine();
467
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
468
+ throw e;
469
+ }
470
+
471
+ // also hook unload event
472
+ if (isWeb() && this.options.disconnectOnPageLeave) {
473
+ // capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
474
+ window.addEventListener('pagehide', this.onPageLeave);
475
+ window.addEventListener('beforeunload', this.onPageLeave);
476
+ navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
477
+ }
478
+ this.setAndEmitConnectionState(ConnectionState.Connected);
479
+ this.emit(RoomEvent.Connected);
410
480
  };
411
481
 
412
482
  /**
@@ -466,17 +536,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
466
536
  this.connectFuture = undefined;
467
537
  }
468
538
 
469
- /**
470
- * if the current room has a participant with `recorder: true` in its JWT grant
471
- **/
472
- get isRecording() {
473
- return this._isRecording;
474
- }
475
-
476
539
  /**
477
540
  * @internal for testing
478
541
  */
479
- async simulateScenario(scenario: string) {
542
+ async simulateScenario(scenario: SimulationScenario) {
480
543
  let postAction = () => {};
481
544
  let req: SimulateScenario | undefined;
482
545
  switch (scenario) {
@@ -525,6 +588,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
525
588
  this.engine.client.onClose('simulate resume-reconnect');
526
589
  }
527
590
  break;
591
+ case 'full-reconnect':
592
+ this.engine.fullReconnectOnNext = true;
593
+ await this.engine.client.close();
594
+ if (this.engine.client.onClose) {
595
+ this.engine.client.onClose('simulate full-reconnect');
596
+ }
597
+ break;
528
598
  case 'force-tcp':
529
599
  case 'force-tls':
530
600
  req = SimulateScenario.fromPartial({
@@ -666,6 +736,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
666
736
  private setupLocalParticipantEvents() {
667
737
  this.localParticipant
668
738
  .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
739
+ .on(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged)
669
740
  .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
670
741
  .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
671
742
  .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
@@ -757,20 +828,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
757
828
  }
758
829
  };
759
830
 
760
- private handleRestarted = async (joinResponse: JoinResponse) => {
761
- log.debug(`reconnected to server`, {
831
+ private handleSignalRestarted = async (joinResponse: JoinResponse) => {
832
+ log.debug(`signal reconnected to server`, {
762
833
  region: joinResponse.serverRegion,
763
834
  });
764
835
 
765
- try {
766
- // rehydrate participants
767
- if (joinResponse.participant) {
768
- // with a restart, the sid will have changed, we'll map our understanding to it
769
- this.localParticipant.sid = joinResponse.participant.sid;
770
- this.handleParticipantUpdates([joinResponse.participant]);
771
- }
772
- this.handleParticipantUpdates(joinResponse.otherParticipants);
836
+ this.applyJoinResponse(joinResponse);
773
837
 
838
+ try {
774
839
  // unpublish & republish tracks
775
840
  const localPubs: LocalTrackPublication[] = [];
776
841
  this.localParticipant.tracks.forEach((pub) => {
@@ -804,10 +869,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
804
869
  );
805
870
  } catch (error) {
806
871
  log.error('error trying to re-publish tracks after reconnection', { error });
807
- } finally {
808
- this.setAndEmitConnectionState(ConnectionState.Connected);
809
- this.emit(RoomEvent.Reconnected);
810
872
  }
873
+
874
+ try {
875
+ await this.engine.waitForRestarted();
876
+ log.debug(`fully reconnected to server`, {
877
+ region: joinResponse.serverRegion,
878
+ });
879
+ } catch {
880
+ // reconnection failed, handleDisconnect is being invoked already, just return here
881
+ return;
882
+ }
883
+ this.setAndEmitConnectionState(ConnectionState.Connected);
884
+ this.emit(RoomEvent.Reconnected);
885
+
886
+ // emit participant connected events after connection has been re-established
887
+ this.participants.forEach((participant) => {
888
+ this.emit(RoomEvent.ParticipantConnected, participant);
889
+ });
811
890
  };
812
891
 
813
892
  private handleDisconnect(shouldStopTracks = true, reason?: DisconnectReason) {
@@ -833,6 +912,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
833
912
 
834
913
  this.localParticipant
835
914
  .off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
915
+ .off(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged)
836
916
  .off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
837
917
  .off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
838
918
  .off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
@@ -1038,14 +1118,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1038
1118
  this.emit(RoomEvent.MediaDevicesChanged);
1039
1119
  };
1040
1120
 
1041
- private handleRoomUpdate = (r: RoomModel) => {
1042
- if (this._isRecording !== r.activeRecording) {
1043
- this._isRecording = r.activeRecording;
1044
- this.emit(RoomEvent.RecordingStatusChanged, r.activeRecording);
1121
+ private handleRoomUpdate = (room: RoomModel) => {
1122
+ const oldRoom = this.roomInfo;
1123
+ this.roomInfo = room;
1124
+ if (oldRoom && oldRoom.metadata !== room.metadata) {
1125
+ this.emitWhenConnected(RoomEvent.RoomMetadataChanged, room.metadata);
1045
1126
  }
1046
- if (this.metadata !== r.metadata) {
1047
- this.metadata = r.metadata;
1048
- this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
1127
+ if (oldRoom?.activeRecording !== room.activeRecording) {
1128
+ this.emitWhenConnected(RoomEvent.RecordingStatusChanged, room.activeRecording);
1049
1129
  }
1050
1130
  };
1051
1131
 
@@ -1154,6 +1234,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1154
1234
  .on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
1155
1235
  this.emitWhenConnected(RoomEvent.ParticipantMetadataChanged, metadata, participant);
1156
1236
  })
1237
+ .on(ParticipantEvent.ParticipantNameChanged, (name) => {
1238
+ this.emitWhenConnected(RoomEvent.ParticipantNameChanged, name, participant);
1239
+ })
1157
1240
  .on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
1158
1241
  this.emitWhenConnected(RoomEvent.ConnectionQualityChanged, quality, participant);
1159
1242
  })
@@ -1270,6 +1353,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1270
1353
  this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
1271
1354
  };
1272
1355
 
1356
+ private onLocalParticipantNameChanged = (name: string) => {
1357
+ this.emit(RoomEvent.ParticipantNameChanged, name, this.localParticipant);
1358
+ };
1359
+
1273
1360
  private onLocalTrackMuted = (pub: TrackPublication) => {
1274
1361
  this.emit(RoomEvent.TrackMuted, pub, this.localParticipant);
1275
1362
  };
@@ -1324,7 +1411,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1324
1411
  ...options.participants,
1325
1412
  };
1326
1413
  this.handleDisconnect();
1327
- this.name = 'simulated-room';
1414
+ this.roomInfo = {
1415
+ sid: 'RM_SIMULATED',
1416
+ name: 'simulated-room',
1417
+ emptyTimeout: 0,
1418
+ maxParticipants: 0,
1419
+ creationTime: new Date().getTime(),
1420
+ metadata: '',
1421
+ numParticipants: 1,
1422
+ numPublishers: 1,
1423
+ turnPassword: '',
1424
+ enabledCodecs: [],
1425
+ activeRecording: false,
1426
+ };
1328
1427
 
1329
1428
  this.localParticipant.updateInfo(
1330
1429
  ParticipantInfo.fromPartial({
@@ -1469,6 +1568,7 @@ export type RoomEventCallbacks = {
1469
1568
  metadata: string | undefined,
1470
1569
  participant: RemoteParticipant | LocalParticipant,
1471
1570
  ) => void;
1571
+ participantNameChanged: (name: string, participant: RemoteParticipant | LocalParticipant) => void;
1472
1572
  participantPermissionsChanged: (
1473
1573
  prevPermissions: ParticipantPermission | undefined,
1474
1574
  participant: RemoteParticipant | LocalParticipant,
@@ -1501,4 +1601,5 @@ export type RoomEventCallbacks = {
1501
1601
  audioPlaybackChanged: (playing: boolean) => void;
1502
1602
  signalConnected: () => void;
1503
1603
  recordingStatusChanged: (recording: boolean) => void;
1604
+ dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1504
1605
  };
@@ -11,6 +11,7 @@ export const enum ConnectionErrorReason {
11
11
  NotAllowed,
12
12
  ServerUnreachable,
13
13
  InternalError,
14
+ Cancelled,
14
15
  }
15
16
 
16
17
  export class ConnectionError extends LivekitError {