livekit-client 0.15.4 → 0.16.3

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 (73) hide show
  1. package/dist/api/RequestQueue.d.ts +12 -0
  2. package/dist/api/RequestQueue.js +61 -0
  3. package/dist/api/RequestQueue.js.map +1 -0
  4. package/dist/api/SignalClient.d.ts +7 -3
  5. package/dist/api/SignalClient.js +25 -4
  6. package/dist/api/SignalClient.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/options.d.ts +0 -10
  11. package/dist/proto/livekit_rtc.d.ts +15 -10
  12. package/dist/proto/livekit_rtc.js +36 -22
  13. package/dist/proto/livekit_rtc.js.map +1 -1
  14. package/dist/room/RTCEngine.d.ts +27 -6
  15. package/dist/room/RTCEngine.js +163 -46
  16. package/dist/room/RTCEngine.js.map +1 -1
  17. package/dist/room/Room.d.ts +50 -6
  18. package/dist/room/Room.js +128 -67
  19. package/dist/room/Room.js.map +1 -1
  20. package/dist/room/events.d.ts +13 -4
  21. package/dist/room/events.js +15 -6
  22. package/dist/room/events.js.map +1 -1
  23. package/dist/room/participant/LocalParticipant.d.ts +1 -2
  24. package/dist/room/participant/LocalParticipant.js +7 -8
  25. package/dist/room/participant/LocalParticipant.js.map +1 -1
  26. package/dist/room/participant/Participant.d.ts +30 -4
  27. package/dist/room/participant/Participant.js +2 -2
  28. package/dist/room/participant/Participant.js.map +1 -1
  29. package/dist/room/participant/RemoteParticipant.d.ts +3 -4
  30. package/dist/room/participant/RemoteParticipant.js +3 -0
  31. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  32. package/dist/room/track/LocalAudioTrack.js +8 -1
  33. package/dist/room/track/LocalAudioTrack.js.map +1 -1
  34. package/dist/room/track/LocalTrackPublication.d.ts +2 -0
  35. package/dist/room/track/LocalTrackPublication.js.map +1 -1
  36. package/dist/room/track/LocalVideoTrack.d.ts +1 -5
  37. package/dist/room/track/LocalVideoTrack.js +12 -117
  38. package/dist/room/track/LocalVideoTrack.js.map +1 -1
  39. package/dist/room/track/RemoteTrackPublication.d.ts +1 -1
  40. package/dist/room/track/RemoteTrackPublication.js +7 -1
  41. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  42. package/dist/room/track/RemoteVideoTrack.js +12 -7
  43. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  44. package/dist/room/track/Track.d.ts +16 -3
  45. package/dist/room/track/Track.js +30 -20
  46. package/dist/room/track/Track.js.map +1 -1
  47. package/dist/room/track/types.d.ts +4 -4
  48. package/dist/room/utils.d.ts +1 -0
  49. package/dist/room/utils.js +5 -21
  50. package/dist/room/utils.js.map +1 -1
  51. package/dist/version.d.ts +2 -2
  52. package/dist/version.js +2 -2
  53. package/package.json +3 -4
  54. package/src/api/RequestQueue.ts +53 -0
  55. package/src/api/SignalClient.ts +33 -5
  56. package/src/index.ts +1 -1
  57. package/src/options.ts +0 -12
  58. package/src/proto/livekit_rtc.ts +55 -41
  59. package/src/room/RTCEngine.ts +198 -53
  60. package/src/room/Room.ts +227 -96
  61. package/src/room/events.ts +15 -4
  62. package/src/room/participant/LocalParticipant.ts +6 -7
  63. package/src/room/participant/Participant.ts +39 -4
  64. package/src/room/participant/RemoteParticipant.ts +9 -4
  65. package/src/room/track/LocalAudioTrack.ts +8 -1
  66. package/src/room/track/LocalTrackPublication.ts +3 -0
  67. package/src/room/track/LocalVideoTrack.ts +11 -142
  68. package/src/room/track/RemoteTrackPublication.ts +8 -2
  69. package/src/room/track/RemoteVideoTrack.ts +14 -7
  70. package/src/room/track/Track.ts +46 -24
  71. package/src/room/track/types.ts +4 -4
  72. package/src/room/utils.ts +4 -16
  73. package/src/version.ts +2 -2
@@ -110,7 +110,7 @@ export interface SignalRequest {
110
110
  */
111
111
  updateLayers?: UpdateVideoLayers | undefined;
112
112
  /** Update subscriber permissions */
113
- subscriptionPermissions?: UpdateSubscriptionPermissions | undefined;
113
+ subscriptionPermission?: SubscriptionPermission | undefined;
114
114
  /** sync client's subscribe state to server during reconnect */
115
115
  syncState?: SyncState | undefined;
116
116
  /** Simulate conditions, for client validations */
@@ -140,12 +140,17 @@ export interface SignalResponse {
140
140
  roomUpdate?: RoomUpdate | undefined;
141
141
  /** when connection quality changed */
142
142
  connectionQuality?: ConnectionQualityUpdate | undefined;
143
- /** when streamed tracks state changed */
143
+ /**
144
+ * when streamed tracks state changed, used to notify when any of the streams were paused due to
145
+ * congestion
146
+ */
144
147
  streamStateUpdate?: StreamStateUpdate | undefined;
145
- /** when max subscribe quality changed */
148
+ /** when max subscribe quality changed, used by dynamic broadcasting to disable unused layers */
146
149
  subscribedQualityUpdate?: SubscribedQualityUpdate | undefined;
147
150
  /** when subscription permission changed */
148
151
  subscriptionPermissionUpdate?: SubscriptionPermissionUpdate | undefined;
152
+ /** update the token the client was using, to prevent an active client from using an expired token */
153
+ refreshToken: string | undefined;
149
154
  }
150
155
 
151
156
  export interface AddTrackRequest {
@@ -286,7 +291,7 @@ export interface TrackPermission {
286
291
  trackSids: string[];
287
292
  }
288
293
 
289
- export interface UpdateSubscriptionPermissions {
294
+ export interface SubscriptionPermission {
290
295
  allParticipants: boolean;
291
296
  trackPermissions: TrackPermission[];
292
297
  }
@@ -366,9 +371,9 @@ export const SignalRequest = {
366
371
  writer.uint32(82).fork()
367
372
  ).ldelim();
368
373
  }
369
- if (message.subscriptionPermissions !== undefined) {
370
- UpdateSubscriptionPermissions.encode(
371
- message.subscriptionPermissions,
374
+ if (message.subscriptionPermission !== undefined) {
375
+ SubscriptionPermission.encode(
376
+ message.subscriptionPermission,
372
377
  writer.uint32(90).fork()
373
378
  ).ldelim();
374
379
  }
@@ -428,8 +433,10 @@ export const SignalRequest = {
428
433
  );
429
434
  break;
430
435
  case 11:
431
- message.subscriptionPermissions =
432
- UpdateSubscriptionPermissions.decode(reader, reader.uint32());
436
+ message.subscriptionPermission = SubscriptionPermission.decode(
437
+ reader,
438
+ reader.uint32()
439
+ );
433
440
  break;
434
441
  case 12:
435
442
  message.syncState = SyncState.decode(reader, reader.uint32());
@@ -493,14 +500,14 @@ export const SignalRequest = {
493
500
  message.updateLayers = undefined;
494
501
  }
495
502
  if (
496
- object.subscriptionPermissions !== undefined &&
497
- object.subscriptionPermissions !== null
503
+ object.subscriptionPermission !== undefined &&
504
+ object.subscriptionPermission !== null
498
505
  ) {
499
- message.subscriptionPermissions = UpdateSubscriptionPermissions.fromJSON(
500
- object.subscriptionPermissions
506
+ message.subscriptionPermission = SubscriptionPermission.fromJSON(
507
+ object.subscriptionPermission
501
508
  );
502
509
  } else {
503
- message.subscriptionPermissions = undefined;
510
+ message.subscriptionPermission = undefined;
504
511
  }
505
512
  if (object.syncState !== undefined && object.syncState !== null) {
506
513
  message.syncState = SyncState.fromJSON(object.syncState);
@@ -553,9 +560,9 @@ export const SignalRequest = {
553
560
  (obj.updateLayers = message.updateLayers
554
561
  ? UpdateVideoLayers.toJSON(message.updateLayers)
555
562
  : undefined);
556
- message.subscriptionPermissions !== undefined &&
557
- (obj.subscriptionPermissions = message.subscriptionPermissions
558
- ? UpdateSubscriptionPermissions.toJSON(message.subscriptionPermissions)
563
+ message.subscriptionPermission !== undefined &&
564
+ (obj.subscriptionPermission = message.subscriptionPermission
565
+ ? SubscriptionPermission.toJSON(message.subscriptionPermission)
559
566
  : undefined);
560
567
  message.syncState !== undefined &&
561
568
  (obj.syncState = message.syncState
@@ -620,15 +627,14 @@ export const SignalRequest = {
620
627
  message.updateLayers = undefined;
621
628
  }
622
629
  if (
623
- object.subscriptionPermissions !== undefined &&
624
- object.subscriptionPermissions !== null
630
+ object.subscriptionPermission !== undefined &&
631
+ object.subscriptionPermission !== null
625
632
  ) {
626
- message.subscriptionPermissions =
627
- UpdateSubscriptionPermissions.fromPartial(
628
- object.subscriptionPermissions
629
- );
633
+ message.subscriptionPermission = SubscriptionPermission.fromPartial(
634
+ object.subscriptionPermission
635
+ );
630
636
  } else {
631
- message.subscriptionPermissions = undefined;
637
+ message.subscriptionPermission = undefined;
632
638
  }
633
639
  if (object.syncState !== undefined && object.syncState !== null) {
634
640
  message.syncState = SyncState.fromPartial(object.syncState);
@@ -720,6 +726,9 @@ export const SignalResponse = {
720
726
  writer.uint32(122).fork()
721
727
  ).ldelim();
722
728
  }
729
+ if (message.refreshToken !== undefined) {
730
+ writer.uint32(130).string(message.refreshToken);
731
+ }
723
732
  return writer;
724
733
  },
725
734
 
@@ -788,6 +797,9 @@ export const SignalResponse = {
788
797
  message.subscriptionPermissionUpdate =
789
798
  SubscriptionPermissionUpdate.decode(reader, reader.uint32());
790
799
  break;
800
+ case 16:
801
+ message.refreshToken = reader.string();
802
+ break;
791
803
  default:
792
804
  reader.skipType(tag & 7);
793
805
  break;
@@ -896,6 +908,11 @@ export const SignalResponse = {
896
908
  } else {
897
909
  message.subscriptionPermissionUpdate = undefined;
898
910
  }
911
+ if (object.refreshToken !== undefined && object.refreshToken !== null) {
912
+ message.refreshToken = String(object.refreshToken);
913
+ } else {
914
+ message.refreshToken = undefined;
915
+ }
899
916
  return message;
900
917
  },
901
918
 
@@ -957,6 +974,8 @@ export const SignalResponse = {
957
974
  message.subscriptionPermissionUpdate
958
975
  )
959
976
  : undefined);
977
+ message.refreshToken !== undefined &&
978
+ (obj.refreshToken = message.refreshToken);
960
979
  return obj;
961
980
  },
962
981
 
@@ -1060,6 +1079,7 @@ export const SignalResponse = {
1060
1079
  } else {
1061
1080
  message.subscriptionPermissionUpdate = undefined;
1062
1081
  }
1082
+ message.refreshToken = object.refreshToken ?? undefined;
1063
1083
  return message;
1064
1084
  },
1065
1085
  };
@@ -2962,11 +2982,11 @@ export const TrackPermission = {
2962
2982
  },
2963
2983
  };
2964
2984
 
2965
- const baseUpdateSubscriptionPermissions: object = { allParticipants: false };
2985
+ const baseSubscriptionPermission: object = { allParticipants: false };
2966
2986
 
2967
- export const UpdateSubscriptionPermissions = {
2987
+ export const SubscriptionPermission = {
2968
2988
  encode(
2969
- message: UpdateSubscriptionPermissions,
2989
+ message: SubscriptionPermission,
2970
2990
  writer: _m0.Writer = _m0.Writer.create()
2971
2991
  ): _m0.Writer {
2972
2992
  if (message.allParticipants === true) {
@@ -2981,12 +3001,10 @@ export const UpdateSubscriptionPermissions = {
2981
3001
  decode(
2982
3002
  input: _m0.Reader | Uint8Array,
2983
3003
  length?: number
2984
- ): UpdateSubscriptionPermissions {
3004
+ ): SubscriptionPermission {
2985
3005
  const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
2986
3006
  let end = length === undefined ? reader.len : reader.pos + length;
2987
- const message = {
2988
- ...baseUpdateSubscriptionPermissions,
2989
- } as UpdateSubscriptionPermissions;
3007
+ const message = { ...baseSubscriptionPermission } as SubscriptionPermission;
2990
3008
  message.trackPermissions = [];
2991
3009
  while (reader.pos < end) {
2992
3010
  const tag = reader.uint32();
@@ -3007,10 +3025,8 @@ export const UpdateSubscriptionPermissions = {
3007
3025
  return message;
3008
3026
  },
3009
3027
 
3010
- fromJSON(object: any): UpdateSubscriptionPermissions {
3011
- const message = {
3012
- ...baseUpdateSubscriptionPermissions,
3013
- } as UpdateSubscriptionPermissions;
3028
+ fromJSON(object: any): SubscriptionPermission {
3029
+ const message = { ...baseSubscriptionPermission } as SubscriptionPermission;
3014
3030
  message.trackPermissions = [];
3015
3031
  if (
3016
3032
  object.allParticipants !== undefined &&
@@ -3031,7 +3047,7 @@ export const UpdateSubscriptionPermissions = {
3031
3047
  return message;
3032
3048
  },
3033
3049
 
3034
- toJSON(message: UpdateSubscriptionPermissions): unknown {
3050
+ toJSON(message: SubscriptionPermission): unknown {
3035
3051
  const obj: any = {};
3036
3052
  message.allParticipants !== undefined &&
3037
3053
  (obj.allParticipants = message.allParticipants);
@@ -3046,11 +3062,9 @@ export const UpdateSubscriptionPermissions = {
3046
3062
  },
3047
3063
 
3048
3064
  fromPartial(
3049
- object: DeepPartial<UpdateSubscriptionPermissions>
3050
- ): UpdateSubscriptionPermissions {
3051
- const message = {
3052
- ...baseUpdateSubscriptionPermissions,
3053
- } as UpdateSubscriptionPermissions;
3065
+ object: DeepPartial<SubscriptionPermission>
3066
+ ): SubscriptionPermission {
3067
+ const message = { ...baseSubscriptionPermission } as SubscriptionPermission;
3054
3068
  message.allParticipants = object.allParticipants ?? false;
3055
3069
  message.trackPermissions = [];
3056
3070
  if (
@@ -1,24 +1,32 @@
1
1
  import { EventEmitter } from 'events';
2
+ import type TypedEventEmitter from 'typed-emitter';
2
3
  import { SignalClient, SignalOptions } from '../api/SignalClient';
3
4
  import log from '../logger';
4
- import { DataPacket, DataPacket_Kind, TrackInfo } from '../proto/livekit_models';
5
+ import {
6
+ DataPacket, DataPacket_Kind, SpeakerInfo, TrackInfo, UserPacket,
7
+ } from '../proto/livekit_models';
5
8
  import {
6
9
  AddTrackRequest, JoinResponse,
10
+ LeaveRequest,
7
11
  SignalTarget,
8
12
  TrackPublishedResponse,
9
13
  } from '../proto/livekit_rtc';
10
14
  import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
11
15
  import { EngineEvent } from './events';
12
16
  import PCTransport from './PCTransport';
13
- import { sleep } from './utils';
17
+ import { isFireFox, sleep } from './utils';
14
18
 
15
19
  const lossyDataChannel = '_lossy';
16
20
  const reliableDataChannel = '_reliable';
17
- const maxReconnectRetries = 5;
18
- export const maxICEConnectTimeout = 5 * 1000;
21
+ const maxReconnectRetries = 10;
22
+ const minReconnectWait = 1 * 1000;
23
+ const maxReconnectDuration = 60 * 1000;
24
+ export const maxICEConnectTimeout = 15 * 1000;
19
25
 
20
26
  /** @internal */
21
- export default class RTCEngine extends EventEmitter {
27
+ export default class RTCEngine extends (
28
+ EventEmitter as new () => TypedEventEmitter<EngineEventCallbacks>
29
+ ) {
22
30
  publisher?: PCTransport;
23
31
 
24
32
  subscriber?: PCTransport;
@@ -39,7 +47,9 @@ export default class RTCEngine extends EventEmitter {
39
47
 
40
48
  private subscriberPrimary: boolean = false;
41
49
 
42
- private iceConnected: boolean = false;
50
+ private primaryPC?: RTCPeerConnection;
51
+
52
+ private pcConnected: boolean = false;
43
53
 
44
54
  private isClosed: boolean = true;
45
55
 
@@ -54,8 +64,14 @@ export default class RTCEngine extends EventEmitter {
54
64
 
55
65
  private token?: string;
56
66
 
67
+ private signalOpts?: SignalOptions;
68
+
57
69
  private reconnectAttempts: number = 0;
58
70
 
71
+ private reconnectStart: number = 0;
72
+
73
+ private fullReconnect: boolean = false;
74
+
59
75
  private connectedServerAddr?: string;
60
76
 
61
77
  constructor() {
@@ -66,9 +82,9 @@ export default class RTCEngine extends EventEmitter {
66
82
  async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
67
83
  this.url = url;
68
84
  this.token = token;
85
+ this.signalOpts = opts;
69
86
 
70
87
  const joinResponse = await this.client.join(url, token, opts);
71
- this.emit(EngineEvent.SignalConnected);
72
88
  this.isClosed = false;
73
89
 
74
90
  this.subscriberPrimary = joinResponse.subscriberPrimary;
@@ -153,6 +169,8 @@ export default class RTCEngine extends EventEmitter {
153
169
  this.publisher = new PCTransport(this.rtcConfig);
154
170
  this.subscriber = new PCTransport(this.rtcConfig);
155
171
 
172
+ this.emit(EngineEvent.TransportsCreated, this.publisher, this.subscriber);
173
+
156
174
  this.publisher.pc.onicecandidate = (ev) => {
157
175
  if (!ev.candidate) return;
158
176
  log.trace('adding ICE candidate for peer', ev.candidate);
@@ -174,21 +192,24 @@ export default class RTCEngine extends EventEmitter {
174
192
  // in subscriber primary mode, server side opens sub data channels.
175
193
  this.subscriber.pc.ondatachannel = this.handleDataChannel;
176
194
  }
177
- primaryPC.oniceconnectionstatechange = () => {
178
- if (primaryPC.iceConnectionState === 'connected') {
179
- log.trace('ICE connected');
180
- if (!this.iceConnected) {
181
- this.iceConnected = true;
195
+ this.primaryPC = primaryPC;
196
+ primaryPC.onconnectionstatechange = async () => {
197
+ if (primaryPC.connectionState === 'connected') {
198
+ log.trace('pc connected');
199
+ try {
200
+ this.connectedServerAddr = await getConnectedAddress(primaryPC);
201
+ } catch (e) {
202
+ log.warn('could not get connected server address', e);
203
+ }
204
+ if (!this.pcConnected) {
205
+ this.pcConnected = true;
182
206
  this.emit(EngineEvent.Connected);
183
207
  }
184
- getConnectedAddress(primaryPC).then((v) => {
185
- this.connectedServerAddr = v;
186
- });
187
- } else if (primaryPC.iceConnectionState === 'failed') {
208
+ } else if (primaryPC.connectionState === 'failed') {
188
209
  // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
189
- log.trace('ICE disconnected');
190
- if (this.iceConnected) {
191
- this.iceConnected = false;
210
+ log.trace('pc disconnected');
211
+ if (this.pcConnected) {
212
+ this.pcConnected = false;
192
213
 
193
214
  this.handleDisconnect('peerconnection');
194
215
  }
@@ -213,6 +234,10 @@ export default class RTCEngine extends EventEmitter {
213
234
  this.lossyDC.onmessage = this.handleDataMessage;
214
235
  this.reliableDC.onmessage = this.handleDataMessage;
215
236
 
237
+ // handle datachannel errors
238
+ this.lossyDC.onerror = this.handleDataError;
239
+ this.reliableDC.onerror = this.handleDataError;
240
+
216
241
  // configure signaling client
217
242
  this.client.onAnswer = async (sd) => {
218
243
  if (!this.publisher) {
@@ -268,13 +293,22 @@ export default class RTCEngine extends EventEmitter {
268
293
  resolve(res.track!);
269
294
  };
270
295
 
296
+ this.client.onTokenRefresh = (token: string) => {
297
+ this.token = token;
298
+ };
299
+
271
300
  this.client.onClose = () => {
272
301
  this.handleDisconnect('signal');
273
302
  };
274
303
 
275
- this.client.onLeave = () => {
276
- this.emit(EngineEvent.Disconnected);
277
- this.close();
304
+ this.client.onLeave = (leave?: LeaveRequest) => {
305
+ if (leave?.canReconnect) {
306
+ this.fullReconnect = true;
307
+ this.primaryPC = undefined;
308
+ } else {
309
+ this.emit(EngineEvent.Disconnected);
310
+ this.close();
311
+ }
278
312
  };
279
313
  }
280
314
 
@@ -312,6 +346,18 @@ export default class RTCEngine extends EventEmitter {
312
346
  }
313
347
  };
314
348
 
349
+ private handleDataError = (event: Event) => {
350
+ const channel = event.currentTarget as RTCDataChannel;
351
+ const channelKind = channel.maxRetransmits === 0 ? 'lossy' : 'reliable';
352
+
353
+ if (event instanceof ErrorEvent) {
354
+ const { error } = event.error;
355
+ log.error(`DataChannel error on ${channelKind}: ${event.message}`, error);
356
+ } else {
357
+ log.error(`Unknown DataChannel Error on ${channelKind}`, event);
358
+ }
359
+ };
360
+
315
361
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
316
362
  // continues to work, we can reconnect to websocket to continue the session
317
363
  // after a number of retries, we'll close and give up permanently
@@ -320,48 +366,110 @@ export default class RTCEngine extends EventEmitter {
320
366
  return;
321
367
  }
322
368
  log.debug(`${connection} disconnected`);
323
- if (this.reconnectAttempts >= maxReconnectRetries) {
324
- log.info(
325
- 'could not connect to signal after',
326
- maxReconnectRetries,
327
- 'attempts. giving up',
328
- );
329
- this.emit(EngineEvent.Disconnected);
330
- this.close();
331
- return;
369
+ if (this.reconnectAttempts === 0) {
370
+ // only reset start time on the first try
371
+ this.reconnectStart = Date.now();
332
372
  }
333
373
 
334
374
  const delay = (this.reconnectAttempts * this.reconnectAttempts) * 300;
335
- setTimeout(() => {
336
- this.reconnect()
337
- .then(() => {
338
- this.reconnectAttempts = 0;
339
- })
340
- .catch(this.handleDisconnect);
375
+ setTimeout(async () => {
376
+ if (this.isClosed) {
377
+ return;
378
+ }
379
+ if (isFireFox()) {
380
+ // FF does not support DTLS restart.
381
+ this.fullReconnect = true;
382
+ }
383
+
384
+ try {
385
+ if (this.fullReconnect) {
386
+ await this.restartConnection();
387
+ } else {
388
+ await this.resumeConnection();
389
+ }
390
+ this.reconnectAttempts = 0;
391
+ this.fullReconnect = false;
392
+ } catch (e) {
393
+ this.reconnectAttempts += 1;
394
+ let recoverable = true;
395
+ if (e instanceof UnexpectedConnectionState) {
396
+ log.debug('received unrecoverable error', e.message);
397
+ // unrecoverable
398
+ recoverable = false;
399
+ } else if (!(e instanceof SignalReconnectError)) {
400
+ // cannot resume
401
+ this.fullReconnect = true;
402
+ }
403
+
404
+ const duration = Date.now() - this.reconnectStart;
405
+ if (this.reconnectAttempts >= maxReconnectRetries || duration > maxReconnectDuration) {
406
+ recoverable = false;
407
+ }
408
+
409
+ if (recoverable) {
410
+ this.handleDisconnect('reconnect');
411
+ } else {
412
+ log.info(
413
+ `could not recover connection after ${maxReconnectRetries} attempts, ${duration}ms. giving up`,
414
+ );
415
+ this.emit(EngineEvent.Disconnected);
416
+ this.close();
417
+ }
418
+ }
341
419
  }, delay);
342
420
  };
343
421
 
344
- private async reconnect(): Promise<void> {
345
- if (this.isClosed) {
346
- return;
347
- }
422
+ private async restartConnection() {
348
423
  if (!this.url || !this.token) {
349
- throw new ConnectionError('could not reconnect, url or token not saved');
424
+ // permanent failure, don't attempt reconnection
425
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
350
426
  }
351
- log.info('reconnecting to signal connection, attempt', this.reconnectAttempts);
352
427
 
428
+ log.info('reconnecting, attempt', this.reconnectAttempts);
353
429
  if (this.reconnectAttempts === 0) {
354
- this.emit(EngineEvent.Reconnecting);
430
+ this.emit(EngineEvent.Restarting);
431
+ }
432
+
433
+ this.primaryPC = undefined;
434
+ this.publisher?.close();
435
+ this.publisher = undefined;
436
+ this.subscriber?.close();
437
+ this.subscriber = undefined;
438
+
439
+ let joinResponse: JoinResponse;
440
+ try {
441
+ joinResponse = await this.join(this.url, this.token, this.signalOpts);
442
+ } catch (e) {
443
+ throw new SignalReconnectError();
355
444
  }
356
- this.reconnectAttempts += 1;
357
445
 
358
- await this.client.reconnect(this.url, this.token);
359
- this.emit(EngineEvent.SignalConnected);
446
+ await this.waitForPCConnected();
360
447
 
448
+ // reconnect success
449
+ this.emit(EngineEvent.Restarted, joinResponse);
450
+ }
451
+
452
+ private async resumeConnection(): Promise<void> {
453
+ if (!this.url || !this.token) {
454
+ // permanent failure, don't attempt reconnection
455
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
456
+ }
361
457
  // trigger publisher reconnect
362
458
  if (!this.publisher || !this.subscriber) {
363
459
  throw new UnexpectedConnectionState('publisher and subscriber connections unset');
364
460
  }
461
+ log.info('resuming signal connection, attempt', this.reconnectAttempts);
462
+ if (this.reconnectAttempts === 0) {
463
+ this.emit(EngineEvent.Resuming);
464
+ }
465
+
466
+ try {
467
+ await this.client.reconnect(this.url, this.token);
468
+ } catch (e) {
469
+ throw new SignalReconnectError();
470
+ }
471
+ this.emit(EngineEvent.SignalResumed);
472
+
365
473
  this.subscriber.restartingIce = true;
366
474
 
367
475
  // only restart publisher if it's needed
@@ -369,19 +477,35 @@ export default class RTCEngine extends EventEmitter {
369
477
  await this.publisher.createAndSendOffer({ iceRestart: true });
370
478
  }
371
479
 
372
- const startTime = (new Date()).getTime();
480
+ await this.waitForPCConnected();
481
+
482
+ // resume success
483
+ this.emit(EngineEvent.Resumed);
484
+ }
373
485
 
374
- while ((new Date()).getTime() - startTime < maxICEConnectTimeout * 2) {
375
- if (this.iceConnected) {
376
- // reconnect success
377
- this.emit(EngineEvent.Reconnected);
486
+ async waitForPCConnected() {
487
+ const startTime = (new Date()).getTime();
488
+ let now = startTime;
489
+ this.pcConnected = false;
490
+
491
+ while (now - startTime < maxICEConnectTimeout) {
492
+ // if there is no connectionstatechange callback fired
493
+ // check connectionstate after minReconnectWait
494
+ if (this.primaryPC === undefined) {
495
+ // we can abort early, connection is hosed
496
+ break;
497
+ } else if (now - startTime > minReconnectWait && this.primaryPC?.connectionState === 'connected') {
498
+ this.pcConnected = true;
499
+ }
500
+ if (this.pcConnected) {
378
501
  return;
379
502
  }
380
503
  await sleep(100);
504
+ now = (new Date()).getTime();
381
505
  }
382
506
 
383
507
  // have not reconnected, throw
384
- throw new ConnectionError('could not establish ICE connection');
508
+ throw new ConnectionError('could not establish PC connection');
385
509
  }
386
510
 
387
511
  /* @internal */
@@ -482,3 +606,24 @@ async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | unde
482
606
  }
483
607
  return candidates.get(selectedID);
484
608
  }
609
+
610
+ class SignalReconnectError extends Error {
611
+ }
612
+
613
+ export type EngineEventCallbacks = {
614
+ connected: () => void,
615
+ disconnected: () => void,
616
+ resuming: () => void,
617
+ resumed: () => void,
618
+ restarting: () => void,
619
+ restarted: (joinResp: JoinResponse) => void,
620
+ signalResumed: () => void,
621
+ mediaTrackAdded: (
622
+ track: MediaStreamTrack,
623
+ streams: MediaStream,
624
+ receiver: RTCRtpReceiver
625
+ ) => void,
626
+ activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void,
627
+ dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void,
628
+ transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void,
629
+ };