livekit-client 1.2.2 → 1.2.5
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.
- package/dist/livekit-client.esm.mjs +1990 -905
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +34 -34
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +124 -124
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/DefaultReconnectPolicy.d.ts +8 -0
- package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -0
- package/dist/src/room/DeviceManager.d.ts +1 -0
- package/dist/src/room/DeviceManager.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +5 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +7 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/ReconnectPolicy.d.ts +23 -0
- package/dist/src/room/ReconnectPolicy.d.ts.map +1 -0
- package/dist/src/room/Room.d.ts +12 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +0 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts +0 -2
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/package.json +37 -33
- package/src/index.ts +2 -0
- package/src/options.ts +6 -0
- package/src/room/DefaultReconnectPolicy.ts +35 -0
- package/src/room/DeviceManager.ts +23 -1
- package/src/room/PCTransport.ts +91 -17
- package/src/room/RTCEngine.ts +105 -33
- package/src/room/ReconnectPolicy.ts +25 -0
- package/src/room/Room.ts +190 -167
- package/src/room/events.ts +2 -2
- package/src/room/participant/LocalParticipant.ts +38 -14
- package/src/room/participant/RemoteParticipant.ts +14 -0
- package/src/room/track/LocalAudioTrack.ts +0 -2
- package/src/room/track/LocalVideoTrack.ts +3 -8
- package/src/room/track/RemoteTrackPublication.ts +1 -1
- package/src/room/track/RemoteVideoTrack.ts +0 -3
- package/src/room/track/create.ts +16 -1
- package/src/room/utils.ts +7 -5
@@ -1,3 +1,6 @@
|
|
1
|
+
import log from '../logger';
|
2
|
+
import { isSafari } from './utils';
|
3
|
+
|
1
4
|
const defaultId = 'default';
|
2
5
|
|
3
6
|
export default class DeviceManager {
|
@@ -12,13 +15,32 @@ export default class DeviceManager {
|
|
12
15
|
return this.instance;
|
13
16
|
}
|
14
17
|
|
18
|
+
static userMediaPromiseMap: Map<MediaDeviceKind, Promise<MediaStream>> = new Map();
|
19
|
+
|
15
20
|
async getDevices(
|
16
21
|
kind?: MediaDeviceKind,
|
17
22
|
requestPermissions: boolean = true,
|
18
23
|
): Promise<MediaDeviceInfo[]> {
|
24
|
+
if (DeviceManager.userMediaPromiseMap?.size > 0) {
|
25
|
+
log.debug('awaiting getUserMedia promise');
|
26
|
+
try {
|
27
|
+
if (kind) {
|
28
|
+
await DeviceManager.userMediaPromiseMap.get(kind);
|
29
|
+
} else {
|
30
|
+
await Promise.all(DeviceManager.userMediaPromiseMap.values());
|
31
|
+
}
|
32
|
+
} catch (e: any) {
|
33
|
+
log.warn('error waiting for media permissons');
|
34
|
+
}
|
35
|
+
}
|
19
36
|
let devices = await navigator.mediaDevices.enumerateDevices();
|
20
37
|
|
21
|
-
if (
|
38
|
+
if (
|
39
|
+
requestPermissions &&
|
40
|
+
kind &&
|
41
|
+
// for safari we need to skip this check, as otherwise it will re-acquire user media and fail on iOS https://bugs.webkit.org/show_bug.cgi?id=179363
|
42
|
+
(!DeviceManager.userMediaPromiseMap.get(kind) || !isSafari())
|
43
|
+
) {
|
22
44
|
const isDummyDeviceOrEmpty =
|
23
45
|
devices.length === 0 ||
|
24
46
|
devices.some((device) => {
|
package/src/room/PCTransport.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import { debounce } from 'ts-debounce';
|
2
|
+
import { MediaDescription, parse, write } from 'sdp-transform';
|
2
3
|
import log from '../logger';
|
3
4
|
|
4
5
|
/** @internal */
|
@@ -88,31 +89,71 @@ export default class PCTransport {
|
|
88
89
|
log.debug('starting to negotiate');
|
89
90
|
const offer = await this.pc.createOffer(options);
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
if (
|
96
|
-
|
92
|
+
const sdpParsed = parse(offer.sdp ?? '');
|
93
|
+
sdpParsed.media.forEach((media) => {
|
94
|
+
if (media.type === 'audio') {
|
95
|
+
ensureAudioNack(media);
|
96
|
+
} else if (media.type === 'video') {
|
97
|
+
// mung sdp for codec bitrate setting that can't apply by sendEncoding
|
98
|
+
this.trackBitrates.some((trackbr): boolean => {
|
99
|
+
if (!media.msid || !media.msid.includes(trackbr.sid)) {
|
100
|
+
return false;
|
101
|
+
}
|
102
|
+
|
103
|
+
let codecPayload = 0;
|
104
|
+
media.rtp.some((rtp): boolean => {
|
105
|
+
if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
|
106
|
+
codecPayload = rtp.payload;
|
107
|
+
return true;
|
108
|
+
}
|
109
|
+
return false;
|
110
|
+
});
|
111
|
+
|
112
|
+
// add x-google-max-bitrate to fmtp line if not exist
|
113
|
+
if (codecPayload > 0) {
|
114
|
+
if (
|
115
|
+
!media.fmtp.some((fmtp): boolean => {
|
116
|
+
if (fmtp.payload === codecPayload) {
|
117
|
+
if (!fmtp.config.includes('x-google-max-bitrate')) {
|
118
|
+
fmtp.config += `;x-google-max-bitrate=${trackbr.maxbr}`;
|
119
|
+
}
|
120
|
+
return true;
|
121
|
+
}
|
122
|
+
return false;
|
123
|
+
})
|
124
|
+
) {
|
125
|
+
media.fmtp.push({
|
126
|
+
payload: codecPayload,
|
127
|
+
config: `x-google-max-bitrate=${trackbr.maxbr}`,
|
128
|
+
});
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
return true;
|
133
|
+
});
|
97
134
|
}
|
98
|
-
|
99
|
-
const mlineStart = sdp.substring(0, sidIndex).lastIndexOf('m=');
|
100
|
-
const mlineEnd = sdp.indexOf('m=', sidIndex);
|
101
|
-
const mediaSection = sdp.substring(mlineStart, mlineEnd);
|
102
|
-
|
103
|
-
const mungedMediaSection = mediaSection.replace(
|
104
|
-
new RegExp(`a=rtpmap:(\\d+) ${trackbr.codec}/\\d+`, 'i'),
|
105
|
-
'$'.concat(`&\r\na=fmtp:$1 x-google-max-bitrate=${trackbr.maxbr}`), // Unity replaces '$&' by some random values when building ( Using concat as a workaround )
|
106
|
-
);
|
107
|
-
sdp = sdp.substring(0, mlineStart) + mungedMediaSection + sdp.substring(mlineEnd);
|
108
|
-
offer.sdp = sdp;
|
109
135
|
});
|
136
|
+
|
137
|
+
offer.sdp = write(sdpParsed);
|
110
138
|
this.trackBitrates = [];
|
111
139
|
|
112
140
|
await this.pc.setLocalDescription(offer);
|
113
141
|
this.onOffer(offer);
|
114
142
|
}
|
115
143
|
|
144
|
+
async createAndSetAnswer(): Promise<RTCSessionDescriptionInit> {
|
145
|
+
const answer = await this.pc.createAnswer();
|
146
|
+
const sdpParsed = parse(answer.sdp ?? '');
|
147
|
+
sdpParsed.media.forEach((media) => {
|
148
|
+
if (media.type === 'audio') {
|
149
|
+
ensureAudioNack(media);
|
150
|
+
}
|
151
|
+
});
|
152
|
+
answer.sdp = write(sdpParsed);
|
153
|
+
await this.pc.setLocalDescription(answer);
|
154
|
+
return answer;
|
155
|
+
}
|
156
|
+
|
116
157
|
setTrackCodecBitrate(sid: string, codec: string, maxbr: number) {
|
117
158
|
this.trackBitrates.push({
|
118
159
|
sid,
|
@@ -125,3 +166,36 @@ export default class PCTransport {
|
|
125
166
|
this.pc.close();
|
126
167
|
}
|
127
168
|
}
|
169
|
+
|
170
|
+
function ensureAudioNack(
|
171
|
+
media: {
|
172
|
+
type: string;
|
173
|
+
port: number;
|
174
|
+
protocol: string;
|
175
|
+
payloads?: string | undefined;
|
176
|
+
} & MediaDescription,
|
177
|
+
) {
|
178
|
+
// found opus codec to add nack fb
|
179
|
+
let opusPayload = 0;
|
180
|
+
media.rtp.some((rtp): boolean => {
|
181
|
+
if (rtp.codec === 'opus') {
|
182
|
+
opusPayload = rtp.payload;
|
183
|
+
return true;
|
184
|
+
}
|
185
|
+
return false;
|
186
|
+
});
|
187
|
+
|
188
|
+
// add nack rtcpfb if not exist
|
189
|
+
if (opusPayload > 0) {
|
190
|
+
if (!media.rtcpFb) {
|
191
|
+
media.rtcpFb = [];
|
192
|
+
}
|
193
|
+
|
194
|
+
if (!media.rtcpFb.some((fb) => fb.payload === opusPayload && fb.type === 'nack')) {
|
195
|
+
media.rtcpFb.push({
|
196
|
+
payload: opusPayload,
|
197
|
+
type: 'nack',
|
198
|
+
});
|
199
|
+
}
|
200
|
+
}
|
201
|
+
}
|
package/src/room/RTCEngine.ts
CHANGED
@@ -2,6 +2,7 @@ import { EventEmitter } from 'events';
|
|
2
2
|
import type TypedEventEmitter from 'typed-emitter';
|
3
3
|
import { SignalClient, SignalOptions } from '../api/SignalClient';
|
4
4
|
import log from '../logger';
|
5
|
+
import { RoomOptions } from '../options';
|
5
6
|
import {
|
6
7
|
ClientConfigSetting,
|
7
8
|
ClientConfiguration,
|
@@ -19,16 +20,16 @@ import {
|
|
19
20
|
SignalTarget,
|
20
21
|
TrackPublishedResponse,
|
21
22
|
} from '../proto/livekit_rtc';
|
23
|
+
import DefaultReconnectPolicy from './DefaultReconnectPolicy';
|
22
24
|
import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
|
23
25
|
import { EngineEvent } from './events';
|
24
26
|
import PCTransport from './PCTransport';
|
27
|
+
import { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
25
28
|
import { isFireFox, isWeb, sleep } from './utils';
|
26
29
|
|
27
30
|
const lossyDataChannel = '_lossy';
|
28
31
|
const reliableDataChannel = '_reliable';
|
29
|
-
const maxReconnectRetries = 10;
|
30
32
|
const minReconnectWait = 2 * 1000;
|
31
|
-
const maxReconnectDuration = 60 * 1000;
|
32
33
|
export const maxICEConnectTimeout = 15 * 1000;
|
33
34
|
|
34
35
|
enum PCState {
|
@@ -71,7 +72,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
71
72
|
|
72
73
|
private _isClosed: boolean = true;
|
73
74
|
|
74
|
-
private pendingTrackResolvers: {
|
75
|
+
private pendingTrackResolvers: {
|
76
|
+
[key: string]: { resolve: (info: TrackInfo) => void; reject: () => void };
|
77
|
+
} = {};
|
75
78
|
|
76
79
|
// true if publisher connection has already been established.
|
77
80
|
// this is helpful to know if we need to restart ICE on the publisher connection
|
@@ -96,9 +99,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
96
99
|
|
97
100
|
private attemptingReconnect: boolean = false;
|
98
101
|
|
99
|
-
|
102
|
+
private reconnectPolicy: ReconnectPolicy;
|
103
|
+
|
104
|
+
private reconnectTimeout?: ReturnType<typeof setTimeout>;
|
105
|
+
|
106
|
+
constructor(private options: RoomOptions) {
|
100
107
|
super();
|
101
108
|
this.client = new SignalClient();
|
109
|
+
this.client.signalLatency = this.options.expSignalLatency;
|
110
|
+
this.reconnectPolicy = this.options.reconnectPolicy ?? new DefaultReconnectPolicy();
|
102
111
|
}
|
103
112
|
|
104
113
|
async join(
|
@@ -157,12 +166,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
157
166
|
if (this.pendingTrackResolvers[req.cid]) {
|
158
167
|
throw new TrackInvalidError('a track with the same ID has already been published');
|
159
168
|
}
|
160
|
-
return new Promise<TrackInfo>((resolve) => {
|
161
|
-
|
169
|
+
return new Promise<TrackInfo>((resolve, reject) => {
|
170
|
+
const publicationTimeout = setTimeout(() => {
|
171
|
+
delete this.pendingTrackResolvers[req.cid];
|
172
|
+
reject(
|
173
|
+
new ConnectionError('publication of local track timed out, no response from server'),
|
174
|
+
);
|
175
|
+
}, 10_000);
|
176
|
+
this.pendingTrackResolvers[req.cid] = {
|
177
|
+
resolve: (info: TrackInfo) => {
|
178
|
+
clearTimeout(publicationTimeout);
|
179
|
+
resolve(info);
|
180
|
+
},
|
181
|
+
reject: () => {
|
182
|
+
clearTimeout(publicationTimeout);
|
183
|
+
reject('Cancelled publication by calling unpublish');
|
184
|
+
},
|
185
|
+
};
|
162
186
|
this.client.sendAddTrack(req);
|
163
187
|
});
|
164
188
|
}
|
165
189
|
|
190
|
+
removeTrack(sender: RTCRtpSender) {
|
191
|
+
if (sender.track && this.pendingTrackResolvers[sender.track.id]) {
|
192
|
+
const { reject } = this.pendingTrackResolvers[sender.track.id];
|
193
|
+
if (reject) {
|
194
|
+
reject();
|
195
|
+
}
|
196
|
+
delete this.pendingTrackResolvers[sender.track.id];
|
197
|
+
}
|
198
|
+
try {
|
199
|
+
this.publisher?.pc.removeTrack(sender);
|
200
|
+
} catch (e: unknown) {
|
201
|
+
log.warn('failed to remove track', { error: e, method: 'removeTrack' });
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
166
205
|
updateMuteStatus(trackSid: string, muted: boolean) {
|
167
206
|
this.client.sendMuteTrack(trackSid, muted);
|
168
207
|
}
|
@@ -317,14 +356,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
317
356
|
await this.subscriber.setRemoteDescription(sd);
|
318
357
|
|
319
358
|
// answer the offer
|
320
|
-
const answer = await this.subscriber.
|
321
|
-
await this.subscriber.pc.setLocalDescription(answer);
|
359
|
+
const answer = await this.subscriber.createAndSetAnswer();
|
322
360
|
this.client.sendAnswer(answer);
|
323
361
|
};
|
324
362
|
|
325
363
|
this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
|
326
364
|
log.debug('received trackPublishedResponse', res);
|
327
|
-
const resolve = this.pendingTrackResolvers[res.cid];
|
365
|
+
const { resolve } = this.pendingTrackResolvers[res.cid];
|
328
366
|
if (!resolve) {
|
329
367
|
log.error(`missing track resolver for ${res.cid}`);
|
330
368
|
return;
|
@@ -437,18 +475,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
437
475
|
// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
|
438
476
|
// continues to work, we can reconnect to websocket to continue the session
|
439
477
|
// after a number of retries, we'll close and give up permanently
|
440
|
-
private handleDisconnect = (connection: string) => {
|
478
|
+
private handleDisconnect = (connection: string, signalEvents: boolean = false) => {
|
441
479
|
if (this._isClosed) {
|
442
480
|
return;
|
443
481
|
}
|
482
|
+
|
444
483
|
log.debug(`${connection} disconnected`);
|
445
484
|
if (this.reconnectAttempts === 0) {
|
446
485
|
// only reset start time on the first try
|
447
486
|
this.reconnectStart = Date.now();
|
448
487
|
}
|
449
488
|
|
450
|
-
const
|
451
|
-
|
489
|
+
const disconnect = (duration: number) => {
|
490
|
+
log.info(
|
491
|
+
`could not recover connection after ${this.reconnectAttempts} attempts, ${duration}ms. giving up`,
|
492
|
+
);
|
493
|
+
this.emit(EngineEvent.Disconnected);
|
494
|
+
this.close();
|
495
|
+
};
|
496
|
+
|
497
|
+
const duration = Date.now() - this.reconnectStart;
|
498
|
+
const delay = this.getNextRetryDelay({
|
499
|
+
elapsedMs: duration,
|
500
|
+
retryCount: this.reconnectAttempts,
|
501
|
+
});
|
502
|
+
|
503
|
+
if (delay === null) {
|
504
|
+
disconnect(duration);
|
505
|
+
return;
|
506
|
+
}
|
507
|
+
|
508
|
+
log.debug(`reconnecting in ${delay}ms`);
|
509
|
+
|
510
|
+
if (this.reconnectTimeout) {
|
511
|
+
clearTimeout(this.reconnectTimeout);
|
512
|
+
}
|
513
|
+
this.reconnectTimeout = setTimeout(async () => {
|
452
514
|
if (this._isClosed) {
|
453
515
|
return;
|
454
516
|
}
|
@@ -469,16 +531,20 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
469
531
|
try {
|
470
532
|
this.attemptingReconnect = true;
|
471
533
|
if (this.fullReconnectOnNext) {
|
472
|
-
await this.restartConnection();
|
534
|
+
await this.restartConnection(signalEvents);
|
473
535
|
} else {
|
474
|
-
await this.resumeConnection();
|
536
|
+
await this.resumeConnection(signalEvents);
|
475
537
|
}
|
476
538
|
this.reconnectAttempts = 0;
|
477
539
|
this.fullReconnectOnNext = false;
|
540
|
+
if (this.reconnectTimeout) {
|
541
|
+
clearTimeout(this.reconnectTimeout);
|
542
|
+
}
|
478
543
|
} catch (e) {
|
479
544
|
this.reconnectAttempts += 1;
|
480
545
|
let reconnectRequired = false;
|
481
546
|
let recoverable = true;
|
547
|
+
let requireSignalEvents = false;
|
482
548
|
if (e instanceof UnexpectedConnectionState) {
|
483
549
|
log.debug('received unrecoverable error', { error: e });
|
484
550
|
// unrecoverable
|
@@ -488,26 +554,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
488
554
|
reconnectRequired = true;
|
489
555
|
}
|
490
556
|
|
491
|
-
// when we flip from resume to reconnect
|
492
|
-
//
|
557
|
+
// when we flip from resume to reconnect
|
558
|
+
// we need to fire the right reconnecting events
|
493
559
|
if (reconnectRequired && !this.fullReconnectOnNext) {
|
494
560
|
this.fullReconnectOnNext = true;
|
495
|
-
|
496
|
-
}
|
497
|
-
|
498
|
-
const duration = Date.now() - this.reconnectStart;
|
499
|
-
if (this.reconnectAttempts >= maxReconnectRetries || duration > maxReconnectDuration) {
|
500
|
-
recoverable = false;
|
561
|
+
requireSignalEvents = true;
|
501
562
|
}
|
502
563
|
|
503
564
|
if (recoverable) {
|
504
|
-
this.handleDisconnect('reconnect');
|
565
|
+
this.handleDisconnect('reconnect', requireSignalEvents);
|
505
566
|
} else {
|
506
|
-
|
507
|
-
`could not recover connection after ${maxReconnectRetries} attempts, ${duration}ms. giving up`,
|
508
|
-
);
|
509
|
-
this.emit(EngineEvent.Disconnected);
|
510
|
-
this.close();
|
567
|
+
disconnect(Date.now() - this.reconnectStart);
|
511
568
|
}
|
512
569
|
} finally {
|
513
570
|
this.attemptingReconnect = false;
|
@@ -515,14 +572,25 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
515
572
|
}, delay);
|
516
573
|
};
|
517
574
|
|
518
|
-
private
|
575
|
+
private getNextRetryDelay(context: ReconnectContext) {
|
576
|
+
try {
|
577
|
+
return this.reconnectPolicy.nextRetryDelayInMs(context);
|
578
|
+
} catch (e) {
|
579
|
+
log.warn('encountered error in reconnect policy', { error: e });
|
580
|
+
}
|
581
|
+
|
582
|
+
// error in user code with provided reconnect policy, stop reconnecting
|
583
|
+
return null;
|
584
|
+
}
|
585
|
+
|
586
|
+
private async restartConnection(emitRestarting: boolean = false) {
|
519
587
|
if (!this.url || !this.token) {
|
520
588
|
// permanent failure, don't attempt reconnection
|
521
589
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
522
590
|
}
|
523
591
|
|
524
592
|
log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
|
525
|
-
if (this.reconnectAttempts === 0) {
|
593
|
+
if (emitRestarting || this.reconnectAttempts === 0) {
|
526
594
|
this.emit(EngineEvent.Restarting);
|
527
595
|
}
|
528
596
|
|
@@ -550,7 +618,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
550
618
|
this.emit(EngineEvent.Restarted, joinResponse);
|
551
619
|
}
|
552
620
|
|
553
|
-
private async resumeConnection(): Promise<void> {
|
621
|
+
private async resumeConnection(emitResuming: boolean = false): Promise<void> {
|
554
622
|
if (!this.url || !this.token) {
|
555
623
|
// permanent failure, don't attempt reconnection
|
556
624
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
@@ -561,14 +629,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
561
629
|
}
|
562
630
|
|
563
631
|
log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`);
|
564
|
-
if (this.reconnectAttempts === 0) {
|
632
|
+
if (emitResuming || this.reconnectAttempts === 0) {
|
565
633
|
this.emit(EngineEvent.Resuming);
|
566
634
|
}
|
567
635
|
|
568
636
|
try {
|
569
637
|
await this.client.reconnect(this.url, this.token);
|
570
638
|
} catch (e) {
|
571
|
-
|
639
|
+
let message = '';
|
640
|
+
if (e instanceof Error) {
|
641
|
+
message = e.message;
|
642
|
+
}
|
643
|
+
throw new SignalReconnectError(message);
|
572
644
|
}
|
573
645
|
this.emit(EngineEvent.SignalResumed);
|
574
646
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
/** Controls reconnecting of the client */
|
2
|
+
export interface ReconnectPolicy {
|
3
|
+
/** Called after disconnect was detected
|
4
|
+
*
|
5
|
+
* @returns {number | null} Amount of time in milliseconds to delay the next reconnect attempt, `null` signals to stop retrying.
|
6
|
+
*/
|
7
|
+
nextRetryDelayInMs(context: ReconnectContext): number | null;
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface ReconnectContext {
|
11
|
+
/**
|
12
|
+
* Number of failed reconnect attempts
|
13
|
+
*/
|
14
|
+
readonly retryCount: number;
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Elapsed amount of time in milliseconds since the disconnect.
|
18
|
+
*/
|
19
|
+
readonly elapsedMs: number;
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Reason for retrying
|
23
|
+
*/
|
24
|
+
readonly retryReason?: Error;
|
25
|
+
}
|