livekit-client 0.15.1 → 0.16.0

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 (120) 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 +48 -0
  9. package/dist/proto/livekit_models.js +367 -5
  10. package/dist/proto/livekit_models.js.map +1 -1
  11. package/dist/proto/livekit_rtc.d.ts +50 -11
  12. package/dist/proto/livekit_rtc.js +300 -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 +50 -23
  27. package/dist/room/participant/LocalParticipant.js.map +1 -1
  28. package/dist/room/participant/Participant.d.ts +3 -1
  29. package/dist/room/participant/Participant.js +1 -0
  30. package/dist/room/participant/Participant.js.map +1 -1
  31. package/dist/room/participant/ParticipantTrackPermission.d.ts +19 -0
  32. package/dist/room/participant/ParticipantTrackPermission.js +16 -0
  33. package/dist/room/participant/ParticipantTrackPermission.js.map +1 -0
  34. package/dist/room/participant/RemoteParticipant.d.ts +2 -2
  35. package/dist/room/participant/RemoteParticipant.js +11 -16
  36. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  37. package/dist/room/participant/publishUtils.js +1 -1
  38. package/dist/room/participant/publishUtils.js.map +1 -1
  39. package/dist/room/participant/publishUtils.test.js +9 -0
  40. package/dist/room/participant/publishUtils.test.js.map +1 -1
  41. package/dist/room/track/LocalTrack.d.ts +0 -3
  42. package/dist/room/track/LocalTrack.js +1 -6
  43. package/dist/room/track/LocalTrack.js.map +1 -1
  44. package/dist/room/track/LocalTrackPublication.d.ts +5 -1
  45. package/dist/room/track/LocalTrackPublication.js +15 -5
  46. package/dist/room/track/LocalTrackPublication.js.map +1 -1
  47. package/dist/room/track/LocalVideoTrack.d.ts +1 -1
  48. package/dist/room/track/LocalVideoTrack.js +7 -6
  49. package/dist/room/track/LocalVideoTrack.js.map +1 -1
  50. package/dist/room/track/RemoteAudioTrack.d.ts +5 -14
  51. package/dist/room/track/RemoteAudioTrack.js +7 -32
  52. package/dist/room/track/RemoteAudioTrack.js.map +1 -1
  53. package/dist/room/track/RemoteTrack.d.ts +14 -0
  54. package/dist/room/track/RemoteTrack.js +47 -0
  55. package/dist/room/track/RemoteTrack.js.map +1 -0
  56. package/dist/room/track/RemoteTrackPublication.d.ts +10 -2
  57. package/dist/room/track/RemoteTrackPublication.js +51 -12
  58. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  59. package/dist/room/track/RemoteVideoTrack.d.ts +3 -9
  60. package/dist/room/track/RemoteVideoTrack.js +8 -29
  61. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  62. package/dist/room/track/Track.d.ts +3 -0
  63. package/dist/room/track/Track.js +14 -5
  64. package/dist/room/track/Track.js.map +1 -1
  65. package/dist/room/track/TrackPublication.d.ts +14 -1
  66. package/dist/room/track/TrackPublication.js +24 -7
  67. package/dist/room/track/TrackPublication.js.map +1 -1
  68. package/dist/room/track/create.js +5 -0
  69. package/dist/room/track/create.js.map +1 -1
  70. package/dist/room/utils.d.ts +2 -0
  71. package/dist/room/utils.js +32 -1
  72. package/dist/room/utils.js.map +1 -1
  73. package/dist/version.d.ts +2 -2
  74. package/dist/version.js +2 -2
  75. package/package.json +5 -3
  76. package/src/api/SignalClient.ts +444 -0
  77. package/src/connect.ts +100 -0
  78. package/src/index.ts +47 -0
  79. package/src/logger.ts +22 -0
  80. package/src/options.ts +152 -0
  81. package/src/proto/livekit_models.ts +1863 -0
  82. package/src/proto/livekit_rtc.ts +3415 -0
  83. package/src/room/DeviceManager.ts +57 -0
  84. package/src/room/PCTransport.ts +86 -0
  85. package/src/room/RTCEngine.ts +582 -0
  86. package/src/room/Room.ts +840 -0
  87. package/src/room/errors.ts +65 -0
  88. package/src/room/events.ts +398 -0
  89. package/src/room/participant/LocalParticipant.ts +685 -0
  90. package/src/room/participant/Participant.ts +214 -0
  91. package/src/room/participant/ParticipantTrackPermission.ts +32 -0
  92. package/src/room/participant/RemoteParticipant.ts +241 -0
  93. package/src/room/participant/publishUtils.test.ts +105 -0
  94. package/src/room/participant/publishUtils.ts +180 -0
  95. package/src/room/stats.ts +130 -0
  96. package/src/room/track/LocalAudioTrack.ts +112 -0
  97. package/src/room/track/LocalTrack.ts +124 -0
  98. package/src/room/track/LocalTrackPublication.ts +66 -0
  99. package/src/room/track/LocalVideoTrack.test.ts +70 -0
  100. package/src/room/track/LocalVideoTrack.ts +416 -0
  101. package/src/room/track/RemoteAudioTrack.ts +58 -0
  102. package/src/room/track/RemoteTrack.ts +59 -0
  103. package/src/room/track/RemoteTrackPublication.ts +198 -0
  104. package/src/room/track/RemoteVideoTrack.ts +213 -0
  105. package/src/room/track/Track.ts +307 -0
  106. package/src/room/track/TrackPublication.ts +120 -0
  107. package/src/room/track/create.ts +120 -0
  108. package/src/room/track/defaults.ts +23 -0
  109. package/src/room/track/options.ts +229 -0
  110. package/src/room/track/types.ts +8 -0
  111. package/src/room/track/utils.test.ts +93 -0
  112. package/src/room/track/utils.ts +76 -0
  113. package/src/room/utils.ts +74 -0
  114. package/src/version.ts +2 -0
  115. package/.github/workflows/publish.yaml +0 -55
  116. package/.github/workflows/test.yaml +0 -36
  117. package/example/index.html +0 -248
  118. package/example/sample.ts +0 -621
  119. package/example/styles.css +0 -144
  120. package/example/webpack.config.js +0 -33
@@ -0,0 +1,582 @@
1
+ import { EventEmitter } from 'events';
2
+ import { SignalClient, SignalOptions } from '../api/SignalClient';
3
+ import log from '../logger';
4
+ import { DataPacket, DataPacket_Kind, TrackInfo } from '../proto/livekit_models';
5
+ import {
6
+ AddTrackRequest, JoinResponse,
7
+ LeaveRequest,
8
+ SignalTarget,
9
+ TrackPublishedResponse,
10
+ } from '../proto/livekit_rtc';
11
+ import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
12
+ import { EngineEvent } from './events';
13
+ import PCTransport from './PCTransport';
14
+ import { sleep } from './utils';
15
+
16
+ const lossyDataChannel = '_lossy';
17
+ const reliableDataChannel = '_reliable';
18
+ const maxReconnectRetries = 10;
19
+ const minReconnectWait = 1 * 1000;
20
+ const maxReconnectDuration = 60 * 1000;
21
+ export const maxICEConnectTimeout = 15 * 1000;
22
+
23
+ /** @internal */
24
+ export default class RTCEngine extends EventEmitter {
25
+ publisher?: PCTransport;
26
+
27
+ subscriber?: PCTransport;
28
+
29
+ client: SignalClient;
30
+
31
+ rtcConfig: RTCConfiguration = {};
32
+
33
+ private lossyDC?: RTCDataChannel;
34
+
35
+ // @ts-ignore noUnusedLocals
36
+ private lossyDCSub?: RTCDataChannel;
37
+
38
+ private reliableDC?: RTCDataChannel;
39
+
40
+ // @ts-ignore noUnusedLocals
41
+ private reliableDCSub?: RTCDataChannel;
42
+
43
+ private subscriberPrimary: boolean = false;
44
+
45
+ private primaryPC?: RTCPeerConnection;
46
+
47
+ private pcConnected: boolean = false;
48
+
49
+ private isClosed: boolean = true;
50
+
51
+ private pendingTrackResolvers: { [key: string]: (info: TrackInfo) => void } = {};
52
+
53
+ // true if publisher connection has already been established.
54
+ // this is helpful to know if we need to restart ICE on the publisher connection
55
+ private hasPublished: boolean = false;
56
+
57
+ // keep join info around for reconnect
58
+ private url?: string;
59
+
60
+ private token?: string;
61
+
62
+ private signalOpts?: SignalOptions;
63
+
64
+ private reconnectAttempts: number = 0;
65
+
66
+ private reconnectStart: number = 0;
67
+
68
+ private fullReconnect: boolean = false;
69
+
70
+ private connectedServerAddr?: string;
71
+
72
+ constructor() {
73
+ super();
74
+ this.client = new SignalClient();
75
+ }
76
+
77
+ async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
78
+ this.url = url;
79
+ this.token = token;
80
+ this.signalOpts = opts;
81
+
82
+ const joinResponse = await this.client.join(url, token, opts);
83
+ this.isClosed = false;
84
+
85
+ this.subscriberPrimary = joinResponse.subscriberPrimary;
86
+ if (!this.publisher) {
87
+ this.configure(joinResponse);
88
+ }
89
+
90
+ // create offer
91
+ if (!this.subscriberPrimary) {
92
+ this.negotiate();
93
+ }
94
+
95
+ return joinResponse;
96
+ }
97
+
98
+ close() {
99
+ this.isClosed = true;
100
+
101
+ this.removeAllListeners();
102
+ if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
103
+ this.publisher.pc.getSenders().forEach((sender) => {
104
+ try {
105
+ this.publisher?.pc.removeTrack(sender);
106
+ } catch (e) {
107
+ log.warn('could not removeTrack', e);
108
+ }
109
+ });
110
+ this.publisher.close();
111
+ this.publisher = undefined;
112
+ }
113
+ if (this.subscriber) {
114
+ this.subscriber.close();
115
+ this.subscriber = undefined;
116
+ }
117
+ this.client.close();
118
+ }
119
+
120
+ addTrack(req: AddTrackRequest): Promise<TrackInfo> {
121
+ if (this.pendingTrackResolvers[req.cid]) {
122
+ throw new TrackInvalidError(
123
+ 'a track with the same ID has already been published',
124
+ );
125
+ }
126
+ return new Promise<TrackInfo>((resolve) => {
127
+ this.pendingTrackResolvers[req.cid] = resolve;
128
+ this.client.sendAddTrack(req);
129
+ });
130
+ }
131
+
132
+ updateMuteStatus(trackSid: string, muted: boolean) {
133
+ this.client.sendMuteTrack(trackSid, muted);
134
+ }
135
+
136
+ get dataSubscriberReadyState(): string | undefined {
137
+ return this.reliableDCSub?.readyState;
138
+ }
139
+
140
+ get connectedServerAddress(): string | undefined {
141
+ return this.connectedServerAddr;
142
+ }
143
+
144
+ private configure(joinResponse: JoinResponse) {
145
+ // already configured
146
+ if (this.publisher || this.subscriber) {
147
+ return;
148
+ }
149
+
150
+ // update ICE servers before creating PeerConnection
151
+ if (joinResponse.iceServers && !this.rtcConfig.iceServers) {
152
+ const rtcIceServers: RTCIceServer[] = [];
153
+ joinResponse.iceServers.forEach((iceServer) => {
154
+ const rtcIceServer: RTCIceServer = {
155
+ urls: iceServer.urls,
156
+ };
157
+ if (iceServer.username) rtcIceServer.username = iceServer.username;
158
+ if (iceServer.credential) { rtcIceServer.credential = iceServer.credential; }
159
+ rtcIceServers.push(rtcIceServer);
160
+ });
161
+ this.rtcConfig.iceServers = rtcIceServers;
162
+ }
163
+
164
+ this.publisher = new PCTransport(this.rtcConfig);
165
+ this.subscriber = new PCTransport(this.rtcConfig);
166
+
167
+ this.publisher.pc.onicecandidate = (ev) => {
168
+ if (!ev.candidate) return;
169
+ log.trace('adding ICE candidate for peer', ev.candidate);
170
+ this.client.sendIceCandidate(ev.candidate, SignalTarget.PUBLISHER);
171
+ };
172
+
173
+ this.subscriber.pc.onicecandidate = (ev) => {
174
+ if (!ev.candidate) return;
175
+ this.client.sendIceCandidate(ev.candidate, SignalTarget.SUBSCRIBER);
176
+ };
177
+
178
+ this.publisher.onOffer = (offer) => {
179
+ this.client.sendOffer(offer);
180
+ };
181
+
182
+ let primaryPC = this.publisher.pc;
183
+ if (joinResponse.subscriberPrimary) {
184
+ primaryPC = this.subscriber.pc;
185
+ // in subscriber primary mode, server side opens sub data channels.
186
+ this.subscriber.pc.ondatachannel = this.handleDataChannel;
187
+ }
188
+ this.primaryPC = primaryPC;
189
+ primaryPC.onconnectionstatechange = () => {
190
+ if (primaryPC.connectionState === 'connected') {
191
+ log.trace('pc connected');
192
+ if (!this.pcConnected) {
193
+ this.pcConnected = true;
194
+ this.emit(EngineEvent.Connected);
195
+ }
196
+ getConnectedAddress(primaryPC).then((v) => {
197
+ this.connectedServerAddr = v;
198
+ });
199
+ } else if (primaryPC.connectionState === 'failed') {
200
+ // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
201
+ log.trace('pc disconnected');
202
+ if (this.pcConnected) {
203
+ this.pcConnected = false;
204
+
205
+ this.handleDisconnect('peerconnection');
206
+ }
207
+ }
208
+ };
209
+
210
+ this.subscriber.pc.ontrack = (ev: RTCTrackEvent) => {
211
+ this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
212
+ };
213
+
214
+ // data channels
215
+ this.lossyDC = this.publisher.pc.createDataChannel(lossyDataChannel, {
216
+ // will drop older packets that arrive
217
+ ordered: true,
218
+ maxRetransmits: 0,
219
+ });
220
+ this.reliableDC = this.publisher.pc.createDataChannel(reliableDataChannel, {
221
+ ordered: true,
222
+ });
223
+
224
+ // also handle messages over the pub channel, for backwards compatibility
225
+ this.lossyDC.onmessage = this.handleDataMessage;
226
+ this.reliableDC.onmessage = this.handleDataMessage;
227
+
228
+ // configure signaling client
229
+ this.client.onAnswer = async (sd) => {
230
+ if (!this.publisher) {
231
+ return;
232
+ }
233
+ log.debug(
234
+ 'received server answer',
235
+ sd.type,
236
+ this.publisher.pc.signalingState,
237
+ );
238
+ await this.publisher.setRemoteDescription(sd);
239
+ };
240
+
241
+ // add candidate on trickle
242
+ this.client.onTrickle = (candidate, target) => {
243
+ if (!this.publisher || !this.subscriber) {
244
+ return;
245
+ }
246
+ log.trace('got ICE candidate from peer', candidate, target);
247
+ if (target === SignalTarget.PUBLISHER) {
248
+ this.publisher.addIceCandidate(candidate);
249
+ } else {
250
+ this.subscriber.addIceCandidate(candidate);
251
+ }
252
+ };
253
+
254
+ // when server creates an offer for the client
255
+ this.client.onOffer = async (sd) => {
256
+ if (!this.subscriber) {
257
+ return;
258
+ }
259
+ log.debug(
260
+ 'received server offer',
261
+ sd.type,
262
+ this.subscriber.pc.signalingState,
263
+ );
264
+ await this.subscriber.setRemoteDescription(sd);
265
+
266
+ // answer the offer
267
+ const answer = await this.subscriber.pc.createAnswer();
268
+ await this.subscriber.pc.setLocalDescription(answer);
269
+ this.client.sendAnswer(answer);
270
+ };
271
+
272
+ this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
273
+ log.debug('received trackPublishedResponse', res);
274
+ const resolve = this.pendingTrackResolvers[res.cid];
275
+ if (!resolve) {
276
+ log.error('missing track resolver for ', res.cid);
277
+ return;
278
+ }
279
+ delete this.pendingTrackResolvers[res.cid];
280
+ resolve(res.track!);
281
+ };
282
+
283
+ this.client.onTokenRefresh = (token: string) => {
284
+ this.token = token;
285
+ };
286
+
287
+ this.client.onClose = () => {
288
+ this.handleDisconnect('signal');
289
+ };
290
+
291
+ this.client.onLeave = (leave?: LeaveRequest) => {
292
+ if (leave?.canReconnect) {
293
+ this.fullReconnect = true;
294
+ this.primaryPC = undefined;
295
+ } else {
296
+ this.emit(EngineEvent.Disconnected);
297
+ this.close();
298
+ }
299
+ };
300
+ }
301
+
302
+ private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
303
+ if (!channel) {
304
+ return;
305
+ }
306
+ if (channel.label === reliableDataChannel) {
307
+ this.reliableDCSub = channel;
308
+ } else if (channel.label === lossyDataChannel) {
309
+ this.lossyDCSub = channel;
310
+ } else {
311
+ return;
312
+ }
313
+ channel.onmessage = this.handleDataMessage;
314
+ };
315
+
316
+ private handleDataMessage = async (message: MessageEvent) => {
317
+ // decode
318
+ let buffer: ArrayBuffer | undefined;
319
+ if (message.data instanceof ArrayBuffer) {
320
+ buffer = message.data;
321
+ } else if (message.data instanceof Blob) {
322
+ buffer = await message.data.arrayBuffer();
323
+ } else {
324
+ log.error('unsupported data type', message.data);
325
+ return;
326
+ }
327
+ const dp = DataPacket.decode(new Uint8Array(buffer));
328
+ if (dp.speaker) {
329
+ // dispatch speaker updates
330
+ this.emit(EngineEvent.ActiveSpeakersUpdate, dp.speaker.speakers);
331
+ } else if (dp.user) {
332
+ this.emit(EngineEvent.DataPacketReceived, dp.user, dp.kind);
333
+ }
334
+ };
335
+
336
+ // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
337
+ // continues to work, we can reconnect to websocket to continue the session
338
+ // after a number of retries, we'll close and give up permanently
339
+ private handleDisconnect = (connection: string) => {
340
+ if (this.isClosed) {
341
+ return;
342
+ }
343
+ log.debug(`${connection} disconnected`);
344
+ if (this.reconnectAttempts === 0) {
345
+ // only reset start time on the first try
346
+ this.reconnectStart = Date.now();
347
+ }
348
+
349
+ const delay = (this.reconnectAttempts * this.reconnectAttempts) * 300;
350
+ setTimeout(async () => {
351
+ if (this.isClosed) {
352
+ return;
353
+ }
354
+
355
+ try {
356
+ if (this.fullReconnect) {
357
+ await this.restartConnection();
358
+ } else {
359
+ await this.resumeConnection();
360
+ }
361
+ this.reconnectAttempts = 0;
362
+ this.fullReconnect = false;
363
+ } catch (e) {
364
+ this.reconnectAttempts += 1;
365
+ let recoverable = true;
366
+ if (e instanceof UnexpectedConnectionState) {
367
+ log.debug('received unrecoverable error', e.message);
368
+ // unrecoverable
369
+ recoverable = false;
370
+ } else if (!(e instanceof SignalReconnectError)) {
371
+ // cannot resume
372
+ this.fullReconnect = true;
373
+ }
374
+
375
+ const duration = Date.now() - this.reconnectStart;
376
+ if (this.reconnectAttempts >= maxReconnectRetries || duration > maxReconnectDuration) {
377
+ recoverable = false;
378
+ }
379
+
380
+ if (recoverable) {
381
+ this.handleDisconnect('reconnect');
382
+ } else {
383
+ log.info(
384
+ `could not recover connection after ${maxReconnectRetries} attempts, ${duration}ms. giving up`,
385
+ );
386
+ this.emit(EngineEvent.Disconnected);
387
+ this.close();
388
+ }
389
+ }
390
+ }, delay);
391
+ };
392
+
393
+ private async restartConnection() {
394
+ if (!this.url || !this.token) {
395
+ // permanent failure, don't attempt reconnection
396
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
397
+ }
398
+
399
+ log.info('reconnecting, attempt', this.reconnectAttempts);
400
+ if (this.reconnectAttempts === 0) {
401
+ this.emit(EngineEvent.Restarting);
402
+ }
403
+
404
+ this.primaryPC = undefined;
405
+ this.publisher?.close();
406
+ this.publisher = undefined;
407
+ this.subscriber?.close();
408
+ this.subscriber = undefined;
409
+
410
+ let joinResponse: JoinResponse;
411
+ try {
412
+ joinResponse = await this.join(this.url, this.token, this.signalOpts);
413
+ } catch (e) {
414
+ throw new SignalReconnectError();
415
+ }
416
+
417
+ await this.waitForPCConnected();
418
+
419
+ // reconnect success
420
+ this.emit(EngineEvent.Restarted, joinResponse);
421
+ }
422
+
423
+ private async resumeConnection(): Promise<void> {
424
+ if (!this.url || !this.token) {
425
+ // permanent failure, don't attempt reconnection
426
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
427
+ }
428
+ // trigger publisher reconnect
429
+ if (!this.publisher || !this.subscriber) {
430
+ throw new UnexpectedConnectionState('publisher and subscriber connections unset');
431
+ }
432
+ log.info('resuming signal connection, attempt', this.reconnectAttempts);
433
+ if (this.reconnectAttempts === 0) {
434
+ this.emit(EngineEvent.Resuming);
435
+ }
436
+
437
+ try {
438
+ await this.client.reconnect(this.url, this.token);
439
+ } catch (e) {
440
+ throw new SignalReconnectError();
441
+ }
442
+ this.emit(EngineEvent.SignalResumed);
443
+
444
+ this.subscriber.restartingIce = true;
445
+
446
+ // only restart publisher if it's needed
447
+ if (this.hasPublished) {
448
+ await this.publisher.createAndSendOffer({ iceRestart: true });
449
+ }
450
+
451
+ await this.waitForPCConnected();
452
+
453
+ // resume success
454
+ this.emit(EngineEvent.Resumed);
455
+ }
456
+
457
+ async waitForPCConnected() {
458
+ const startTime = (new Date()).getTime();
459
+ let now = startTime;
460
+ this.pcConnected = false;
461
+
462
+ while (now - startTime < maxICEConnectTimeout) {
463
+ // if there is no connectionstatechange callback fired
464
+ // check connectionstate after minReconnectWait
465
+ if (this.primaryPC === undefined) {
466
+ // we can abort early, connection is hosed
467
+ break;
468
+ } else if (now - startTime > minReconnectWait && this.primaryPC?.connectionState === 'connected') {
469
+ this.pcConnected = true;
470
+ }
471
+ if (this.pcConnected) {
472
+ return;
473
+ }
474
+ await sleep(100);
475
+ now = (new Date()).getTime();
476
+ }
477
+
478
+ // have not reconnected, throw
479
+ throw new ConnectionError('could not establish PC connection');
480
+ }
481
+
482
+ /* @internal */
483
+ async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) {
484
+ const msg = DataPacket.encode(packet).finish();
485
+
486
+ // make sure we do have a data connection
487
+ await this.ensurePublisherConnected(kind);
488
+
489
+ if (kind === DataPacket_Kind.LOSSY && this.lossyDC) {
490
+ this.lossyDC.send(msg);
491
+ } else if (kind === DataPacket_Kind.RELIABLE && this.reliableDC) {
492
+ this.reliableDC.send(msg);
493
+ }
494
+ }
495
+
496
+ private async ensurePublisherConnected(kind: DataPacket_Kind) {
497
+ if (!this.subscriberPrimary) {
498
+ return;
499
+ }
500
+
501
+ if (!this.publisher) {
502
+ throw new ConnectionError('publisher connection not set');
503
+ }
504
+
505
+ if (!this.publisher.isICEConnected && this.publisher.pc.iceConnectionState !== 'checking') {
506
+ // start negotiation
507
+ this.negotiate();
508
+ }
509
+
510
+ const targetChannel = this.dataChannelForKind(kind);
511
+ if (targetChannel?.readyState === 'open') {
512
+ return;
513
+ }
514
+
515
+ // wait until publisher ICE connected
516
+ const endTime = (new Date()).getTime() + maxICEConnectTimeout;
517
+ while ((new Date()).getTime() < endTime) {
518
+ if (this.publisher.isICEConnected && this.dataChannelForKind(kind)?.readyState === 'open') {
519
+ return;
520
+ }
521
+ await sleep(50);
522
+ }
523
+
524
+ throw new ConnectionError(`could not establish publisher connection, state ${this.publisher?.pc.iceConnectionState}`);
525
+ }
526
+
527
+ /** @internal */
528
+ negotiate() {
529
+ if (!this.publisher) {
530
+ return;
531
+ }
532
+
533
+ this.hasPublished = true;
534
+
535
+ this.publisher.negotiate();
536
+ }
537
+
538
+ private dataChannelForKind(kind: DataPacket_Kind): RTCDataChannel | undefined {
539
+ if (kind === DataPacket_Kind.LOSSY) {
540
+ return this.lossyDC;
541
+ } if (kind === DataPacket_Kind.RELIABLE) {
542
+ return this.reliableDC;
543
+ }
544
+ }
545
+ }
546
+
547
+ async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | undefined> {
548
+ let selectedCandidatePairId = '';
549
+ const candidatePairs = new Map<string, RTCIceCandidatePairStats>();
550
+ // id -> candidate ip
551
+ const candidates = new Map<string, string>();
552
+ const stats: RTCStatsReport = await pc.getStats();
553
+ stats.forEach((v) => {
554
+ switch (v.type) {
555
+ case 'transport':
556
+ selectedCandidatePairId = v.selectedCandidatePairId;
557
+ break;
558
+ case 'candidate-pair':
559
+ if (selectedCandidatePairId === '' && v.selected) {
560
+ selectedCandidatePairId = v.id;
561
+ }
562
+ candidatePairs.set(v.id, v);
563
+ break;
564
+ case 'remote-candidate':
565
+ candidates.set(v.id, `${v.address}:${v.port}`);
566
+ break;
567
+ default:
568
+ }
569
+ });
570
+
571
+ if (selectedCandidatePairId === '') {
572
+ return undefined;
573
+ }
574
+ const selectedID = candidatePairs.get(selectedCandidatePairId)?.remoteCandidateId;
575
+ if (selectedID === undefined) {
576
+ return undefined;
577
+ }
578
+ return candidates.get(selectedID);
579
+ }
580
+
581
+ class SignalReconnectError extends Error {
582
+ }