livekit-client 0.18.4-RC6 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +2 -5
  2. package/dist/api/RequestQueue.d.ts +13 -12
  3. package/dist/api/RequestQueue.d.ts.map +1 -0
  4. package/dist/api/SignalClient.d.ts +67 -66
  5. package/dist/api/SignalClient.d.ts.map +1 -0
  6. package/dist/connect.d.ts +24 -23
  7. package/dist/connect.d.ts.map +1 -0
  8. package/dist/index.d.ts +27 -26
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/livekit-client.esm.mjs +546 -486
  11. package/dist/livekit-client.esm.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/logger.d.ts +26 -25
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/options.d.ts +128 -127
  17. package/dist/options.d.ts.map +1 -0
  18. package/dist/proto/google/protobuf/timestamp.d.ts +133 -132
  19. package/dist/proto/google/protobuf/timestamp.d.ts.map +1 -0
  20. package/dist/proto/livekit_models.d.ts +876 -868
  21. package/dist/proto/livekit_models.d.ts.map +1 -0
  22. package/dist/proto/livekit_rtc.d.ts +3904 -3859
  23. package/dist/proto/livekit_rtc.d.ts.map +1 -0
  24. package/dist/room/DeviceManager.d.ts +8 -7
  25. package/dist/room/DeviceManager.d.ts.map +1 -0
  26. package/dist/room/PCTransport.d.ts +16 -15
  27. package/dist/room/PCTransport.d.ts.map +1 -0
  28. package/dist/room/RTCEngine.d.ts +67 -66
  29. package/dist/room/RTCEngine.d.ts.map +1 -0
  30. package/dist/room/Room.d.ts +166 -165
  31. package/dist/room/Room.d.ts.map +1 -0
  32. package/dist/room/errors.d.ts +29 -28
  33. package/dist/room/errors.d.ts.map +1 -0
  34. package/dist/room/events.d.ts +391 -390
  35. package/dist/room/events.d.ts.map +1 -0
  36. package/dist/room/participant/LocalParticipant.d.ts +126 -125
  37. package/dist/room/participant/LocalParticipant.d.ts.map +1 -0
  38. package/dist/room/participant/Participant.d.ts +94 -93
  39. package/dist/room/participant/Participant.d.ts.map +1 -0
  40. package/dist/room/participant/ParticipantTrackPermission.d.ts +26 -19
  41. package/dist/room/participant/ParticipantTrackPermission.d.ts.map +1 -0
  42. package/dist/room/participant/RemoteParticipant.d.ts +40 -39
  43. package/dist/room/participant/RemoteParticipant.d.ts.map +1 -0
  44. package/dist/room/participant/publishUtils.d.ts +18 -17
  45. package/dist/room/participant/publishUtils.d.ts.map +1 -0
  46. package/dist/room/stats.d.ts +66 -65
  47. package/dist/room/stats.d.ts.map +1 -0
  48. package/dist/room/track/LocalAudioTrack.d.ts +20 -19
  49. package/dist/room/track/LocalAudioTrack.d.ts.map +1 -0
  50. package/dist/room/track/LocalTrack.d.ts +28 -27
  51. package/dist/room/track/LocalTrack.d.ts.map +1 -0
  52. package/dist/room/track/LocalTrackPublication.d.ts +38 -37
  53. package/dist/room/track/LocalTrackPublication.d.ts.map +1 -0
  54. package/dist/room/track/LocalVideoTrack.d.ts +31 -30
  55. package/dist/room/track/LocalVideoTrack.d.ts.map +1 -0
  56. package/dist/room/track/RemoteAudioTrack.d.ts +20 -19
  57. package/dist/room/track/RemoteAudioTrack.d.ts.map +1 -0
  58. package/dist/room/track/RemoteTrack.d.ts +16 -15
  59. package/dist/room/track/RemoteTrack.d.ts.map +1 -0
  60. package/dist/room/track/RemoteTrackPublication.d.ts +51 -50
  61. package/dist/room/track/RemoteTrackPublication.d.ts.map +1 -0
  62. package/dist/room/track/RemoteVideoTrack.d.ts +28 -27
  63. package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -0
  64. package/dist/room/track/Track.d.ts +101 -100
  65. package/dist/room/track/Track.d.ts.map +1 -0
  66. package/dist/room/track/TrackPublication.d.ts +50 -49
  67. package/dist/room/track/TrackPublication.d.ts.map +1 -0
  68. package/dist/room/track/create.d.ts +24 -23
  69. package/dist/room/track/create.d.ts.map +1 -0
  70. package/dist/room/track/defaults.d.ts +5 -4
  71. package/dist/room/track/defaults.d.ts.map +1 -0
  72. package/dist/room/track/options.d.ts +223 -222
  73. package/dist/room/track/options.d.ts.map +1 -0
  74. package/dist/room/track/types.d.ts +19 -18
  75. package/dist/room/track/types.d.ts.map +1 -0
  76. package/dist/room/track/utils.d.ts +14 -13
  77. package/dist/room/track/utils.d.ts.map +1 -0
  78. package/dist/room/utils.d.ts +17 -15
  79. package/dist/room/utils.d.ts.map +1 -0
  80. package/dist/test/mocks.d.ts +12 -11
  81. package/dist/test/mocks.d.ts.map +1 -0
  82. package/dist/version.d.ts +3 -2
  83. package/dist/version.d.ts.map +1 -0
  84. package/package.json +4 -5
  85. package/src/api/RequestQueue.ts +53 -0
  86. package/src/api/SignalClient.ts +497 -0
  87. package/src/connect.ts +98 -0
  88. package/src/index.ts +49 -0
  89. package/src/logger.ts +56 -0
  90. package/src/options.ts +156 -0
  91. package/src/proto/google/protobuf/timestamp.ts +216 -0
  92. package/src/proto/livekit_models.ts +2456 -0
  93. package/src/proto/livekit_rtc.ts +2859 -0
  94. package/src/room/DeviceManager.ts +80 -0
  95. package/src/room/PCTransport.ts +88 -0
  96. package/src/room/RTCEngine.ts +695 -0
  97. package/src/room/Room.ts +970 -0
  98. package/src/room/errors.ts +65 -0
  99. package/src/room/events.ts +438 -0
  100. package/src/room/participant/LocalParticipant.ts +755 -0
  101. package/src/room/participant/Participant.ts +287 -0
  102. package/src/room/participant/ParticipantTrackPermission.ts +42 -0
  103. package/src/room/participant/RemoteParticipant.ts +263 -0
  104. package/src/room/participant/publishUtils.test.ts +144 -0
  105. package/src/room/participant/publishUtils.ts +229 -0
  106. package/src/room/stats.ts +134 -0
  107. package/src/room/track/LocalAudioTrack.ts +134 -0
  108. package/src/room/track/LocalTrack.ts +229 -0
  109. package/src/room/track/LocalTrackPublication.ts +87 -0
  110. package/src/room/track/LocalVideoTrack.test.ts +72 -0
  111. package/src/room/track/LocalVideoTrack.ts +295 -0
  112. package/src/room/track/RemoteAudioTrack.ts +86 -0
  113. package/src/room/track/RemoteTrack.ts +62 -0
  114. package/src/room/track/RemoteTrackPublication.ts +207 -0
  115. package/src/room/track/RemoteVideoTrack.ts +240 -0
  116. package/src/room/track/Track.ts +358 -0
  117. package/src/room/track/TrackPublication.ts +120 -0
  118. package/src/room/track/create.ts +122 -0
  119. package/src/room/track/defaults.ts +27 -0
  120. package/src/room/track/options.ts +281 -0
  121. package/src/room/track/types.ts +20 -0
  122. package/src/room/track/utils.test.ts +110 -0
  123. package/src/room/track/utils.ts +113 -0
  124. package/src/room/utils.ts +115 -0
  125. package/src/test/mocks.ts +17 -0
  126. package/src/version.ts +2 -0
  127. package/CHANGELOG.md +0 -5
@@ -0,0 +1,695 @@
1
+ import { EventEmitter } from 'events';
2
+ import type TypedEventEmitter from 'typed-emitter';
3
+ import { SignalClient, SignalOptions } from '../api/SignalClient';
4
+ import log from '../logger';
5
+ import {
6
+ ClientConfigSetting,
7
+ ClientConfiguration,
8
+ DataPacket,
9
+ DataPacket_Kind,
10
+ SpeakerInfo,
11
+ TrackInfo,
12
+ UserPacket,
13
+ } from '../proto/livekit_models';
14
+ import {
15
+ AddTrackRequest,
16
+ JoinResponse,
17
+ LeaveRequest,
18
+ SignalTarget,
19
+ TrackPublishedResponse,
20
+ } from '../proto/livekit_rtc';
21
+ import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
22
+ import { EngineEvent } from './events';
23
+ import PCTransport from './PCTransport';
24
+ import { isFireFox, isWeb, sleep } from './utils';
25
+
26
+ const lossyDataChannel = '_lossy';
27
+ const reliableDataChannel = '_reliable';
28
+ const maxReconnectRetries = 10;
29
+ const minReconnectWait = 2 * 1000;
30
+ const maxReconnectDuration = 60 * 1000;
31
+ export const maxICEConnectTimeout = 15 * 1000;
32
+
33
+ enum PCState {
34
+ New,
35
+ Connected,
36
+ Disconnected,
37
+ Reconnecting,
38
+ Closed,
39
+ }
40
+
41
+ /** @internal */
42
+ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmitter<EngineEventCallbacks>) {
43
+ publisher?: PCTransport;
44
+
45
+ subscriber?: PCTransport;
46
+
47
+ client: SignalClient;
48
+
49
+ rtcConfig: RTCConfiguration = {};
50
+
51
+ private lossyDC?: RTCDataChannel;
52
+
53
+ // @ts-ignore noUnusedLocals
54
+ private lossyDCSub?: RTCDataChannel;
55
+
56
+ private reliableDC?: RTCDataChannel;
57
+
58
+ // @ts-ignore noUnusedLocals
59
+ private reliableDCSub?: RTCDataChannel;
60
+
61
+ private subscriberPrimary: boolean = false;
62
+
63
+ private primaryPC?: RTCPeerConnection;
64
+
65
+ private pcState: PCState = PCState.New;
66
+
67
+ private isClosed: boolean = true;
68
+
69
+ private pendingTrackResolvers: { [key: string]: (info: TrackInfo) => void } = {};
70
+
71
+ // true if publisher connection has already been established.
72
+ // this is helpful to know if we need to restart ICE on the publisher connection
73
+ private hasPublished: boolean = false;
74
+
75
+ // keep join info around for reconnect
76
+ private url?: string;
77
+
78
+ private token?: string;
79
+
80
+ private signalOpts?: SignalOptions;
81
+
82
+ private reconnectAttempts: number = 0;
83
+
84
+ private reconnectStart: number = 0;
85
+
86
+ private fullReconnectOnNext: boolean = false;
87
+
88
+ private clientConfiguration?: ClientConfiguration;
89
+
90
+ private connectedServerAddr?: string;
91
+
92
+ constructor() {
93
+ super();
94
+ this.client = new SignalClient();
95
+ }
96
+
97
+ async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
98
+ this.url = url;
99
+ this.token = token;
100
+ this.signalOpts = opts;
101
+
102
+ const joinResponse = await this.client.join(url, token, opts);
103
+ this.isClosed = false;
104
+
105
+ this.subscriberPrimary = joinResponse.subscriberPrimary;
106
+ if (!this.publisher) {
107
+ this.configure(joinResponse);
108
+ }
109
+
110
+ // create offer
111
+ if (!this.subscriberPrimary) {
112
+ this.negotiate();
113
+ }
114
+ this.clientConfiguration = joinResponse.clientConfiguration;
115
+
116
+ return joinResponse;
117
+ }
118
+
119
+ close() {
120
+ this.isClosed = true;
121
+
122
+ this.removeAllListeners();
123
+ if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
124
+ this.publisher.pc.getSenders().forEach((sender) => {
125
+ try {
126
+ // TODO: react-native-webrtc doesn't have removeTrack yet.
127
+ if (this.publisher?.pc.removeTrack) {
128
+ this.publisher?.pc.removeTrack(sender);
129
+ }
130
+ } catch (e) {
131
+ log.warn('could not removeTrack', { error: e });
132
+ }
133
+ });
134
+ this.publisher.close();
135
+ this.publisher = undefined;
136
+ }
137
+ if (this.subscriber) {
138
+ this.subscriber.close();
139
+ this.subscriber = undefined;
140
+ }
141
+ this.client.close();
142
+ }
143
+
144
+ addTrack(req: AddTrackRequest): Promise<TrackInfo> {
145
+ if (this.pendingTrackResolvers[req.cid]) {
146
+ throw new TrackInvalidError('a track with the same ID has already been published');
147
+ }
148
+ return new Promise<TrackInfo>((resolve) => {
149
+ this.pendingTrackResolvers[req.cid] = resolve;
150
+ this.client.sendAddTrack(req);
151
+ });
152
+ }
153
+
154
+ updateMuteStatus(trackSid: string, muted: boolean) {
155
+ this.client.sendMuteTrack(trackSid, muted);
156
+ }
157
+
158
+ get dataSubscriberReadyState(): string | undefined {
159
+ return this.reliableDCSub?.readyState;
160
+ }
161
+
162
+ get connectedServerAddress(): string | undefined {
163
+ return this.connectedServerAddr;
164
+ }
165
+
166
+ private configure(joinResponse: JoinResponse) {
167
+ // already configured
168
+ if (this.publisher || this.subscriber) {
169
+ return;
170
+ }
171
+
172
+ // update ICE servers before creating PeerConnection
173
+ if (joinResponse.iceServers && !this.rtcConfig.iceServers) {
174
+ const rtcIceServers: RTCIceServer[] = [];
175
+ joinResponse.iceServers.forEach((iceServer) => {
176
+ const rtcIceServer: RTCIceServer = {
177
+ urls: iceServer.urls,
178
+ };
179
+ if (iceServer.username) rtcIceServer.username = iceServer.username;
180
+ if (iceServer.credential) {
181
+ rtcIceServer.credential = iceServer.credential;
182
+ }
183
+ rtcIceServers.push(rtcIceServer);
184
+ });
185
+ this.rtcConfig.iceServers = rtcIceServers;
186
+ }
187
+
188
+ // @ts-ignore
189
+ this.rtcConfig.sdpSemantics = 'unified-plan';
190
+ // @ts-ignore
191
+ this.rtcConfig.continualGatheringPolicy = 'gather_continually';
192
+
193
+ this.publisher = new PCTransport(this.rtcConfig);
194
+ this.subscriber = new PCTransport(this.rtcConfig);
195
+
196
+ this.emit(EngineEvent.TransportsCreated, this.publisher, this.subscriber);
197
+
198
+ this.publisher.pc.onicecandidate = (ev) => {
199
+ if (!ev.candidate) return;
200
+ log.trace('adding ICE candidate for peer', ev.candidate);
201
+ this.client.sendIceCandidate(ev.candidate, SignalTarget.PUBLISHER);
202
+ };
203
+
204
+ this.subscriber.pc.onicecandidate = (ev) => {
205
+ if (!ev.candidate) return;
206
+ this.client.sendIceCandidate(ev.candidate, SignalTarget.SUBSCRIBER);
207
+ };
208
+
209
+ this.publisher.onOffer = (offer) => {
210
+ this.client.sendOffer(offer);
211
+ };
212
+
213
+ let primaryPC = this.publisher.pc;
214
+ let secondaryPC = this.subscriber.pc;
215
+ if (joinResponse.subscriberPrimary) {
216
+ primaryPC = this.subscriber.pc;
217
+ secondaryPC = this.publisher.pc;
218
+ // in subscriber primary mode, server side opens sub data channels.
219
+ this.subscriber.pc.ondatachannel = this.handleDataChannel;
220
+ }
221
+ this.primaryPC = primaryPC;
222
+ primaryPC.onconnectionstatechange = async () => {
223
+ log.trace('connection state changed', {
224
+ state: primaryPC.connectionState,
225
+ });
226
+ if (primaryPC.connectionState === 'connected') {
227
+ try {
228
+ this.connectedServerAddr = await getConnectedAddress(primaryPC);
229
+ } catch (e) {
230
+ log.warn('could not get connected server address', { error: e });
231
+ }
232
+ const shouldEmit = this.pcState === PCState.New;
233
+ this.pcState = PCState.Connected;
234
+ if (shouldEmit) {
235
+ this.emit(EngineEvent.Connected);
236
+ }
237
+ } else if (primaryPC.connectionState === 'failed') {
238
+ // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
239
+ if (this.pcState === PCState.Connected) {
240
+ this.pcState = PCState.Disconnected;
241
+
242
+ this.handleDisconnect('primary peerconnection');
243
+ }
244
+ }
245
+ };
246
+ secondaryPC.onconnectionstatechange = async () => {
247
+ // also reconnect if secondary peerconnection fails
248
+ if (secondaryPC.connectionState === 'failed') {
249
+ this.handleDisconnect('secondary peerconnection');
250
+ }
251
+ };
252
+
253
+ if (isWeb()) {
254
+ this.subscriber.pc.ontrack = (ev: RTCTrackEvent) => {
255
+ this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
256
+ };
257
+ } else {
258
+ // TODO: react-native-webrtc doesn't have ontrack yet, replace when ready.
259
+ // @ts-ignore
260
+ this.subscriber.pc.onaddstream = (ev: { stream: MediaStream }) => {
261
+ const track = ev.stream.getTracks()[0];
262
+ this.emit(EngineEvent.MediaTrackAdded, track, ev.stream);
263
+ };
264
+ }
265
+ // data channels
266
+ this.lossyDC = this.publisher.pc.createDataChannel(lossyDataChannel, {
267
+ // will drop older packets that arrive
268
+ ordered: true,
269
+ maxRetransmits: 0,
270
+ });
271
+ this.reliableDC = this.publisher.pc.createDataChannel(reliableDataChannel, {
272
+ ordered: true,
273
+ });
274
+
275
+ // also handle messages over the pub channel, for backwards compatibility
276
+ this.lossyDC.onmessage = this.handleDataMessage;
277
+ this.reliableDC.onmessage = this.handleDataMessage;
278
+
279
+ // handle datachannel errors
280
+ this.lossyDC.onerror = this.handleDataError;
281
+ this.reliableDC.onerror = this.handleDataError;
282
+
283
+ // configure signaling client
284
+ this.client.onAnswer = async (sd) => {
285
+ if (!this.publisher) {
286
+ return;
287
+ }
288
+ log.debug('received server answer', {
289
+ RTCSdpType: sd.type,
290
+ signalingState: this.publisher.pc.signalingState,
291
+ });
292
+ await this.publisher.setRemoteDescription(sd);
293
+ };
294
+
295
+ // add candidate on trickle
296
+ this.client.onTrickle = (candidate, target) => {
297
+ if (!this.publisher || !this.subscriber) {
298
+ return;
299
+ }
300
+ log.trace('got ICE candidate from peer', { candidate, target });
301
+ if (target === SignalTarget.PUBLISHER) {
302
+ this.publisher.addIceCandidate(candidate);
303
+ } else {
304
+ this.subscriber.addIceCandidate(candidate);
305
+ }
306
+ };
307
+
308
+ // when server creates an offer for the client
309
+ this.client.onOffer = async (sd) => {
310
+ if (!this.subscriber) {
311
+ return;
312
+ }
313
+ log.debug('received server offer', {
314
+ RTCSdpType: sd.type,
315
+ signalingState: this.subscriber.pc.signalingState,
316
+ });
317
+ await this.subscriber.setRemoteDescription(sd);
318
+
319
+ // answer the offer
320
+ const answer = await this.subscriber.pc.createAnswer();
321
+ await this.subscriber.pc.setLocalDescription(answer);
322
+ this.client.sendAnswer(answer);
323
+ };
324
+
325
+ this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
326
+ log.debug('received trackPublishedResponse', res);
327
+ const resolve = this.pendingTrackResolvers[res.cid];
328
+ if (!resolve) {
329
+ log.error(`missing track resolver for ${res.cid}`);
330
+ return;
331
+ }
332
+ delete this.pendingTrackResolvers[res.cid];
333
+ resolve(res.track!);
334
+ };
335
+
336
+ this.client.onTokenRefresh = (token: string) => {
337
+ this.token = token;
338
+ };
339
+
340
+ this.client.onClose = () => {
341
+ this.handleDisconnect('signal');
342
+ };
343
+
344
+ this.client.onLeave = (leave?: LeaveRequest) => {
345
+ if (leave?.canReconnect) {
346
+ this.fullReconnectOnNext = true;
347
+ this.primaryPC = undefined;
348
+ } else {
349
+ this.emit(EngineEvent.Disconnected);
350
+ this.close();
351
+ }
352
+ };
353
+ }
354
+
355
+ private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
356
+ if (!channel) {
357
+ return;
358
+ }
359
+ if (channel.label === reliableDataChannel) {
360
+ this.reliableDCSub = channel;
361
+ } else if (channel.label === lossyDataChannel) {
362
+ this.lossyDCSub = channel;
363
+ } else {
364
+ return;
365
+ }
366
+ log.debug(`on data channel ${channel.id}, ${channel.label}`);
367
+ channel.onmessage = this.handleDataMessage;
368
+ };
369
+
370
+ private handleDataMessage = async (message: MessageEvent) => {
371
+ // decode
372
+ let buffer: ArrayBuffer | undefined;
373
+ if (message.data instanceof ArrayBuffer) {
374
+ buffer = message.data;
375
+ } else if (message.data instanceof Blob) {
376
+ buffer = await message.data.arrayBuffer();
377
+ } else {
378
+ log.error('unsupported data type', message.data);
379
+ return;
380
+ }
381
+ const dp = DataPacket.decode(new Uint8Array(buffer));
382
+ if (dp.speaker) {
383
+ // dispatch speaker updates
384
+ this.emit(EngineEvent.ActiveSpeakersUpdate, dp.speaker.speakers);
385
+ } else if (dp.user) {
386
+ this.emit(EngineEvent.DataPacketReceived, dp.user, dp.kind);
387
+ }
388
+ };
389
+
390
+ private handleDataError = (event: Event) => {
391
+ const channel = event.currentTarget as RTCDataChannel;
392
+ const channelKind = channel.maxRetransmits === 0 ? 'lossy' : 'reliable';
393
+
394
+ if (event instanceof ErrorEvent) {
395
+ const { error } = event.error;
396
+ log.error(`DataChannel error on ${channelKind}: ${event.message}`, error);
397
+ } else {
398
+ log.error(`Unknown DataChannel Error on ${channelKind}`, event);
399
+ }
400
+ };
401
+
402
+ // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
403
+ // continues to work, we can reconnect to websocket to continue the session
404
+ // after a number of retries, we'll close and give up permanently
405
+ private handleDisconnect = (connection: string) => {
406
+ if (this.isClosed) {
407
+ return;
408
+ }
409
+ log.debug(`${connection} disconnected`);
410
+ if (this.reconnectAttempts === 0) {
411
+ // only reset start time on the first try
412
+ this.reconnectStart = Date.now();
413
+ }
414
+
415
+ const delay = this.reconnectAttempts * this.reconnectAttempts * 300;
416
+ setTimeout(async () => {
417
+ if (this.isClosed) {
418
+ return;
419
+ }
420
+ if (
421
+ isFireFox() || // TODO remove once clientConfiguration handles firefox case server side
422
+ this.clientConfiguration?.resumeConnection === ClientConfigSetting.DISABLED
423
+ ) {
424
+ this.fullReconnectOnNext = true;
425
+ }
426
+
427
+ try {
428
+ if (this.fullReconnectOnNext) {
429
+ await this.restartConnection();
430
+ } else {
431
+ await this.resumeConnection();
432
+ }
433
+ this.reconnectAttempts = 0;
434
+ this.fullReconnectOnNext = false;
435
+ } catch (e) {
436
+ this.reconnectAttempts += 1;
437
+ let recoverable = true;
438
+ if (e instanceof UnexpectedConnectionState) {
439
+ log.debug('received unrecoverable error', { error: e });
440
+ // unrecoverable
441
+ recoverable = false;
442
+ } else if (!(e instanceof SignalReconnectError)) {
443
+ // cannot resume
444
+ this.fullReconnectOnNext = true;
445
+ }
446
+
447
+ const duration = Date.now() - this.reconnectStart;
448
+ if (this.reconnectAttempts >= maxReconnectRetries || duration > maxReconnectDuration) {
449
+ recoverable = false;
450
+ }
451
+
452
+ if (recoverable) {
453
+ this.handleDisconnect('reconnect');
454
+ } else {
455
+ log.info(
456
+ `could not recover connection after ${maxReconnectRetries} attempts, ${duration}ms. giving up`,
457
+ );
458
+ this.emit(EngineEvent.Disconnected);
459
+ this.close();
460
+ }
461
+ }
462
+ }, delay);
463
+ };
464
+
465
+ private async restartConnection() {
466
+ if (!this.url || !this.token) {
467
+ // permanent failure, don't attempt reconnection
468
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
469
+ }
470
+
471
+ log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
472
+ if (this.reconnectAttempts === 0) {
473
+ this.emit(EngineEvent.Restarting);
474
+ }
475
+
476
+ this.primaryPC = undefined;
477
+ this.publisher?.close();
478
+ this.publisher = undefined;
479
+ this.subscriber?.close();
480
+ this.subscriber = undefined;
481
+
482
+ let joinResponse: JoinResponse;
483
+ try {
484
+ joinResponse = await this.join(this.url, this.token, this.signalOpts);
485
+ } catch (e) {
486
+ throw new SignalReconnectError();
487
+ }
488
+
489
+ await this.waitForPCConnected();
490
+ this.client.setReconnected();
491
+
492
+ // reconnect success
493
+ this.emit(EngineEvent.Restarted, joinResponse);
494
+ }
495
+
496
+ private async resumeConnection(): Promise<void> {
497
+ if (!this.url || !this.token) {
498
+ // permanent failure, don't attempt reconnection
499
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
500
+ }
501
+ // trigger publisher reconnect
502
+ if (!this.publisher || !this.subscriber) {
503
+ throw new UnexpectedConnectionState('publisher and subscriber connections unset');
504
+ }
505
+ log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`);
506
+ if (this.reconnectAttempts === 0) {
507
+ this.emit(EngineEvent.Resuming);
508
+ }
509
+
510
+ try {
511
+ await this.client.reconnect(this.url, this.token);
512
+ } catch (e) {
513
+ throw new SignalReconnectError();
514
+ }
515
+ this.emit(EngineEvent.SignalResumed);
516
+
517
+ this.subscriber.restartingIce = true;
518
+
519
+ // only restart publisher if it's needed
520
+ if (this.hasPublished) {
521
+ await this.publisher.createAndSendOffer({ iceRestart: true });
522
+ }
523
+
524
+ await this.waitForPCConnected();
525
+ this.client.setReconnected();
526
+
527
+ // resume success
528
+ this.emit(EngineEvent.Resumed);
529
+ }
530
+
531
+ async waitForPCConnected() {
532
+ const startTime = Date.now();
533
+ let now = startTime;
534
+ this.pcState = PCState.Reconnecting;
535
+
536
+ log.debug('waiting for peer connection to reconnect');
537
+ while (now - startTime < maxICEConnectTimeout) {
538
+ if (this.primaryPC === undefined) {
539
+ // we can abort early, connection is hosed
540
+ break;
541
+ } else if (
542
+ // on Safari, we don't get a connectionstatechanged event during ICE restart
543
+ // this means we'd have to check its status manually and update address
544
+ // manually
545
+ now - startTime > minReconnectWait &&
546
+ this.primaryPC?.connectionState === 'connected'
547
+ ) {
548
+ this.pcState = PCState.Connected;
549
+ try {
550
+ this.connectedServerAddr = await getConnectedAddress(this.primaryPC);
551
+ } catch (e) {
552
+ log.warn('could not get connected server address', { error: e });
553
+ }
554
+ }
555
+ if (this.pcState === PCState.Connected) {
556
+ return;
557
+ }
558
+ await sleep(100);
559
+ now = Date.now();
560
+ }
561
+
562
+ // have not reconnected, throw
563
+ throw new ConnectionError('could not establish PC connection');
564
+ }
565
+
566
+ /* @internal */
567
+ async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) {
568
+ const msg = DataPacket.encode(packet).finish();
569
+
570
+ // make sure we do have a data connection
571
+ await this.ensurePublisherConnected(kind);
572
+
573
+ if (kind === DataPacket_Kind.LOSSY && this.lossyDC) {
574
+ this.lossyDC.send(msg);
575
+ } else if (kind === DataPacket_Kind.RELIABLE && this.reliableDC) {
576
+ this.reliableDC.send(msg);
577
+ }
578
+ }
579
+
580
+ private async ensurePublisherConnected(kind: DataPacket_Kind) {
581
+ if (!this.subscriberPrimary) {
582
+ return;
583
+ }
584
+
585
+ if (!this.publisher) {
586
+ throw new ConnectionError('publisher connection not set');
587
+ }
588
+
589
+ if (!this.publisher.isICEConnected && this.publisher.pc.iceConnectionState !== 'checking') {
590
+ // start negotiation
591
+ this.negotiate();
592
+ }
593
+
594
+ const targetChannel = this.dataChannelForKind(kind);
595
+ if (targetChannel?.readyState === 'open') {
596
+ return;
597
+ }
598
+
599
+ // wait until publisher ICE connected
600
+ const endTime = new Date().getTime() + maxICEConnectTimeout;
601
+ while (new Date().getTime() < endTime) {
602
+ if (this.publisher.isICEConnected && this.dataChannelForKind(kind)?.readyState === 'open') {
603
+ return;
604
+ }
605
+ await sleep(50);
606
+ }
607
+
608
+ throw new ConnectionError(
609
+ `could not establish publisher connection, state ${this.publisher?.pc.iceConnectionState}`,
610
+ );
611
+ }
612
+
613
+ /** @internal */
614
+ negotiate() {
615
+ if (!this.publisher) {
616
+ return;
617
+ }
618
+
619
+ this.hasPublished = true;
620
+
621
+ this.publisher.negotiate();
622
+ }
623
+
624
+ dataChannelForKind(kind: DataPacket_Kind, sub?: boolean): RTCDataChannel | undefined {
625
+ if (!sub) {
626
+ if (kind === DataPacket_Kind.LOSSY) {
627
+ return this.lossyDC;
628
+ }
629
+ if (kind === DataPacket_Kind.RELIABLE) {
630
+ return this.reliableDC;
631
+ }
632
+ } else {
633
+ if (kind === DataPacket_Kind.LOSSY) {
634
+ return this.lossyDCSub;
635
+ }
636
+ if (kind === DataPacket_Kind.RELIABLE) {
637
+ return this.reliableDCSub;
638
+ }
639
+ }
640
+ }
641
+ }
642
+
643
+ async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | undefined> {
644
+ let selectedCandidatePairId = '';
645
+ const candidatePairs = new Map<string, RTCIceCandidatePairStats>();
646
+ // id -> candidate ip
647
+ const candidates = new Map<string, string>();
648
+ const stats: RTCStatsReport = await pc.getStats();
649
+ stats.forEach((v) => {
650
+ switch (v.type) {
651
+ case 'transport':
652
+ selectedCandidatePairId = v.selectedCandidatePairId;
653
+ break;
654
+ case 'candidate-pair':
655
+ if (selectedCandidatePairId === '' && v.selected) {
656
+ selectedCandidatePairId = v.id;
657
+ }
658
+ candidatePairs.set(v.id, v);
659
+ break;
660
+ case 'remote-candidate':
661
+ candidates.set(v.id, `${v.address}:${v.port}`);
662
+ break;
663
+ default:
664
+ }
665
+ });
666
+
667
+ if (selectedCandidatePairId === '') {
668
+ return undefined;
669
+ }
670
+ const selectedID = candidatePairs.get(selectedCandidatePairId)?.remoteCandidateId;
671
+ if (selectedID === undefined) {
672
+ return undefined;
673
+ }
674
+ return candidates.get(selectedID);
675
+ }
676
+
677
+ class SignalReconnectError extends Error {}
678
+
679
+ export type EngineEventCallbacks = {
680
+ connected: () => void;
681
+ disconnected: () => void;
682
+ resuming: () => void;
683
+ resumed: () => void;
684
+ restarting: () => void;
685
+ restarted: (joinResp: JoinResponse) => void;
686
+ signalResumed: () => void;
687
+ mediaTrackAdded: (
688
+ track: MediaStreamTrack,
689
+ streams: MediaStream,
690
+ receiver?: RTCRtpReceiver,
691
+ ) => void;
692
+ activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
693
+ dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
694
+ transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
695
+ };