livekit-client 2.15.8 → 2.15.10
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 +589 -205
- 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/api/SignalClient.d.ts +31 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +29 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -0
- package/dist/src/api/utils.d.ts +2 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +6 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +6 -4
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/token-source/utils.d.ts +1 -1
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +6 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/api/SignalClient.d.ts +31 -2
- package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
- package/dist/ts4.2/api/utils.d.ts +2 -0
- package/dist/ts4.2/index.d.ts +2 -2
- package/dist/ts4.2/options.d.ts +6 -0
- package/dist/ts4.2/room/PCTransport.d.ts +1 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +6 -4
- package/dist/ts4.2/room/RTCEngine.d.ts +1 -1
- package/dist/ts4.2/room/token-source/utils.d.ts +1 -1
- package/dist/ts4.2/room/utils.d.ts +6 -0
- package/package.json +1 -1
- package/src/api/SignalClient.test.ts +769 -0
- package/src/api/SignalClient.ts +319 -162
- package/src/api/WebSocketStream.test.ts +625 -0
- package/src/api/WebSocketStream.ts +118 -0
- package/src/api/utils.ts +10 -0
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/webrtc.ts +1 -1
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/index.ts +2 -0
- package/src/options.ts +7 -0
- package/src/room/PCTransport.ts +7 -3
- package/src/room/PCTransportManager.ts +39 -35
- package/src/room/RTCEngine.ts +59 -17
- package/src/room/Room.ts +5 -2
- package/src/room/defaults.ts +1 -0
- package/src/room/participant/LocalParticipant.ts +2 -2
- package/src/room/token-source/TokenSource.ts +2 -2
- package/src/room/token-source/utils.test.ts +63 -0
- package/src/room/token-source/utils.ts +10 -5
- package/src/room/utils.ts +29 -0
package/src/api/SignalClient.ts
CHANGED
|
@@ -4,10 +4,13 @@ import {
|
|
|
4
4
|
AudioTrackFeature,
|
|
5
5
|
ClientInfo,
|
|
6
6
|
ConnectionQualityUpdate,
|
|
7
|
+
ConnectionSettings,
|
|
7
8
|
DisconnectReason,
|
|
9
|
+
JoinRequest,
|
|
8
10
|
JoinResponse,
|
|
9
11
|
LeaveRequest,
|
|
10
12
|
LeaveRequest_Action,
|
|
13
|
+
MediaSectionsRequirement,
|
|
11
14
|
MuteTrackRequest,
|
|
12
15
|
ParticipantInfo,
|
|
13
16
|
Ping,
|
|
@@ -38,6 +41,7 @@ import {
|
|
|
38
41
|
UpdateTrackSettings,
|
|
39
42
|
UpdateVideoLayers,
|
|
40
43
|
VideoLayer,
|
|
44
|
+
WrappedJoinRequest,
|
|
41
45
|
protoInt64,
|
|
42
46
|
} from '@livekit/protocol';
|
|
43
47
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
@@ -46,7 +50,8 @@ import CriticalTimers from '../room/timers';
|
|
|
46
50
|
import type { LoggerOptions } from '../room/types';
|
|
47
51
|
import { getClientInfo, isReactNative, sleep } from '../room/utils';
|
|
48
52
|
import { AsyncQueue } from '../utils/AsyncQueue';
|
|
49
|
-
import {
|
|
53
|
+
import { type WebSocketConnection, WebSocketStream } from './WebSocketStream';
|
|
54
|
+
import { createRtcUrl, createValidateUrl, parseSignalResponse } from './utils';
|
|
50
55
|
|
|
51
56
|
// internal options
|
|
52
57
|
interface ConnectOpts extends SignalOptions {
|
|
@@ -65,6 +70,7 @@ export interface SignalOptions {
|
|
|
65
70
|
maxRetries: number;
|
|
66
71
|
e2eeEnabled: boolean;
|
|
67
72
|
websocketTimeout: number;
|
|
73
|
+
singlePeerConnection: boolean;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
type SignalMessage = SignalRequest['message'];
|
|
@@ -94,6 +100,9 @@ export enum SignalConnectionState {
|
|
|
94
100
|
DISCONNECTED,
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
/** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
|
|
104
|
+
const MAX_WS_CLOSE_TIME = 250;
|
|
105
|
+
|
|
97
106
|
/** @internal */
|
|
98
107
|
export class SignalClient {
|
|
99
108
|
requestQueue: AsyncQueue;
|
|
@@ -151,9 +160,11 @@ export class SignalClient {
|
|
|
151
160
|
|
|
152
161
|
onRoomMoved?: (res: RoomMovedResponse) => void;
|
|
153
162
|
|
|
163
|
+
onMediaSectionsRequirement?: (requirement: MediaSectionsRequirement) => void;
|
|
164
|
+
|
|
154
165
|
connectOptions?: ConnectOpts;
|
|
155
166
|
|
|
156
|
-
ws?:
|
|
167
|
+
ws?: WebSocketStream;
|
|
157
168
|
|
|
158
169
|
get currentState() {
|
|
159
170
|
return this.state;
|
|
@@ -200,6 +211,8 @@ export class SignalClient {
|
|
|
200
211
|
|
|
201
212
|
private _requestId = 0;
|
|
202
213
|
|
|
214
|
+
private streamWriter: WritableStreamDefaultWriter<ArrayBuffer | string> | undefined;
|
|
215
|
+
|
|
203
216
|
constructor(useJSON: boolean = false, loggerOptions: LoggerOptions = {}) {
|
|
204
217
|
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.Signal);
|
|
205
218
|
this.loggerContextCb = loggerOptions.loggerContextCb;
|
|
@@ -251,39 +264,57 @@ export class SignalClient {
|
|
|
251
264
|
reconnect: true,
|
|
252
265
|
sid,
|
|
253
266
|
reconnectReason: reason,
|
|
254
|
-
})) as ReconnectResponse;
|
|
267
|
+
})) as ReconnectResponse | undefined;
|
|
255
268
|
return res;
|
|
256
269
|
}
|
|
257
270
|
|
|
258
|
-
private connect(
|
|
271
|
+
private async connect(
|
|
259
272
|
url: string,
|
|
260
273
|
token: string,
|
|
261
274
|
opts: ConnectOpts,
|
|
262
275
|
abortSignal?: AbortSignal,
|
|
263
276
|
): Promise<JoinResponse | ReconnectResponse | undefined> {
|
|
277
|
+
const unlock = await this.connectionLock.lock();
|
|
278
|
+
|
|
264
279
|
this.connectOptions = opts;
|
|
265
280
|
const clientInfo = getClientInfo();
|
|
266
|
-
const params =
|
|
281
|
+
const params = opts.singlePeerConnection
|
|
282
|
+
? createJoinRequestConnectionParams(token, clientInfo, opts)
|
|
283
|
+
: createConnectionParams(token, clientInfo, opts);
|
|
267
284
|
const rtcUrl = createRtcUrl(url, params);
|
|
268
285
|
const validateUrl = createValidateUrl(rtcUrl);
|
|
269
286
|
|
|
270
287
|
return new Promise<JoinResponse | ReconnectResponse | undefined>(async (resolve, reject) => {
|
|
271
|
-
const unlock = await this.connectionLock.lock();
|
|
272
288
|
try {
|
|
273
|
-
const
|
|
274
|
-
|
|
289
|
+
const timeoutAbortController = new AbortController();
|
|
290
|
+
|
|
291
|
+
const signals = abortSignal
|
|
292
|
+
? [timeoutAbortController.signal, abortSignal]
|
|
293
|
+
: [timeoutAbortController.signal];
|
|
294
|
+
|
|
295
|
+
const combinedAbort = AbortSignal.any(signals);
|
|
296
|
+
|
|
297
|
+
const abortHandler = async (event: Event) => {
|
|
298
|
+
// send leave if we have an active stream writer (connection is open)
|
|
299
|
+
if (this.streamWriter) {
|
|
300
|
+
this.sendLeave()
|
|
301
|
+
.then(() => this.close())
|
|
302
|
+
.catch((e) => {
|
|
303
|
+
this.log.error(e);
|
|
304
|
+
this.close();
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
this.close();
|
|
308
|
+
}
|
|
275
309
|
clearTimeout(wsTimeout);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
'room connection has been cancelled (signal)',
|
|
279
|
-
ConnectionErrorReason.Cancelled,
|
|
280
|
-
),
|
|
281
|
-
);
|
|
310
|
+
const target = event.currentTarget;
|
|
311
|
+
reject(target instanceof AbortSignal ? target.reason : target);
|
|
282
312
|
};
|
|
283
313
|
|
|
314
|
+
combinedAbort.addEventListener('abort', abortHandler);
|
|
315
|
+
|
|
284
316
|
const wsTimeout = setTimeout(() => {
|
|
285
|
-
|
|
286
|
-
reject(
|
|
317
|
+
timeoutAbortController.abort(
|
|
287
318
|
new ConnectionError(
|
|
288
319
|
'room connection has timed out (signal)',
|
|
289
320
|
ConnectionErrorReason.ServerUnreachable,
|
|
@@ -291,10 +322,13 @@ export class SignalClient {
|
|
|
291
322
|
);
|
|
292
323
|
}, opts.websocketTimeout);
|
|
293
324
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
325
|
+
const handleSignalConnected = (
|
|
326
|
+
connection: WebSocketConnection,
|
|
327
|
+
firstMessage?: SignalResponse,
|
|
328
|
+
) => {
|
|
329
|
+
this.handleSignalConnected(connection, wsTimeout, firstMessage);
|
|
330
|
+
};
|
|
331
|
+
|
|
298
332
|
const redactedUrl = new URL(rtcUrl);
|
|
299
333
|
if (redactedUrl.searchParams.has('access_token')) {
|
|
300
334
|
redactedUrl.searchParams.set('access_token', '<redacted>');
|
|
@@ -307,151 +341,131 @@ export class SignalClient {
|
|
|
307
341
|
if (this.ws) {
|
|
308
342
|
await this.close(false);
|
|
309
343
|
}
|
|
310
|
-
this.ws = new
|
|
311
|
-
this.ws.binaryType = 'arraybuffer';
|
|
344
|
+
this.ws = new WebSocketStream<ArrayBuffer>(rtcUrl, { signal: combinedAbort });
|
|
312
345
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.ws.onerror = async (ev: Event) => {
|
|
318
|
-
if (this.state !== SignalConnectionState.CONNECTED) {
|
|
319
|
-
this.state = SignalConnectionState.DISCONNECTED;
|
|
320
|
-
clearTimeout(wsTimeout);
|
|
321
|
-
try {
|
|
322
|
-
const resp = await fetch(validateUrl);
|
|
323
|
-
if (resp.status.toFixed(0).startsWith('4')) {
|
|
324
|
-
const msg = await resp.text();
|
|
325
|
-
reject(new ConnectionError(msg, ConnectionErrorReason.NotAllowed, resp.status));
|
|
326
|
-
} else {
|
|
346
|
+
try {
|
|
347
|
+
this.ws.closed
|
|
348
|
+
.then((closeInfo) => {
|
|
349
|
+
if (this.isEstablishingConnection) {
|
|
327
350
|
reject(
|
|
328
351
|
new ConnectionError(
|
|
329
|
-
`
|
|
352
|
+
`Websocket got closed during a (re)connection attempt: ${closeInfo.reason}`,
|
|
330
353
|
ConnectionErrorReason.InternalError,
|
|
331
|
-
resp.status,
|
|
332
354
|
),
|
|
333
355
|
);
|
|
334
356
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
new ConnectionError(
|
|
338
|
-
e instanceof Error ? e.message : 'server was not reachable',
|
|
339
|
-
ConnectionErrorReason.ServerUnreachable,
|
|
340
|
-
),
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
// other errors, handle
|
|
346
|
-
this.handleWSError(ev);
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
this.ws.onmessage = async (ev: MessageEvent) => {
|
|
350
|
-
// not considered connected until JoinResponse is received
|
|
351
|
-
let resp: SignalResponse;
|
|
352
|
-
if (typeof ev.data === 'string') {
|
|
353
|
-
const json = JSON.parse(ev.data);
|
|
354
|
-
resp = SignalResponse.fromJson(json, { ignoreUnknownFields: true });
|
|
355
|
-
} else if (ev.data instanceof ArrayBuffer) {
|
|
356
|
-
resp = SignalResponse.fromBinary(new Uint8Array(ev.data));
|
|
357
|
-
} else {
|
|
358
|
-
this.log.error(
|
|
359
|
-
`could not decode websocket message: ${typeof ev.data}`,
|
|
360
|
-
this.logContext,
|
|
361
|
-
);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (this.state !== SignalConnectionState.CONNECTED) {
|
|
366
|
-
let shouldProcessMessage = false;
|
|
367
|
-
// handle join message only
|
|
368
|
-
if (resp.message?.case === 'join') {
|
|
369
|
-
this.state = SignalConnectionState.CONNECTED;
|
|
370
|
-
abortSignal?.removeEventListener('abort', abortHandler);
|
|
371
|
-
this.pingTimeoutDuration = resp.message.value.pingTimeout;
|
|
372
|
-
this.pingIntervalDuration = resp.message.value.pingInterval;
|
|
373
|
-
|
|
374
|
-
if (this.pingTimeoutDuration && this.pingTimeoutDuration > 0) {
|
|
375
|
-
this.log.debug('ping config', {
|
|
357
|
+
if (closeInfo.closeCode !== 1000) {
|
|
358
|
+
this.log.warn(`websocket closed`, {
|
|
376
359
|
...this.logContext,
|
|
377
|
-
|
|
378
|
-
|
|
360
|
+
reason: closeInfo.reason,
|
|
361
|
+
code: closeInfo.closeCode,
|
|
362
|
+
wasClean: closeInfo.closeCode === 1000,
|
|
363
|
+
state: this.state,
|
|
379
364
|
});
|
|
380
|
-
this.startPingInterval();
|
|
381
365
|
}
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (resp.message?.case === 'reconnect') {
|
|
392
|
-
resolve(resp.message.value);
|
|
393
|
-
} else {
|
|
394
|
-
this.log.debug(
|
|
395
|
-
'declaring signal reconnected without reconnect response received',
|
|
396
|
-
this.logContext,
|
|
366
|
+
return;
|
|
367
|
+
})
|
|
368
|
+
.catch((reason) => {
|
|
369
|
+
if (this.isEstablishingConnection) {
|
|
370
|
+
reject(
|
|
371
|
+
new ConnectionError(
|
|
372
|
+
`Websocket error during a (re)connection attempt: ${reason}`,
|
|
373
|
+
ConnectionErrorReason.InternalError,
|
|
374
|
+
),
|
|
397
375
|
);
|
|
398
|
-
resolve(undefined);
|
|
399
|
-
shouldProcessMessage = true;
|
|
400
376
|
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
),
|
|
409
|
-
);
|
|
410
|
-
} else if (!opts.reconnect) {
|
|
411
|
-
// non-reconnect case, should receive join response first
|
|
412
|
-
reject(
|
|
413
|
-
new ConnectionError(
|
|
414
|
-
`did not receive join response, got ${resp.message?.case} instead`,
|
|
415
|
-
ConnectionErrorReason.InternalError,
|
|
416
|
-
),
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
if (!shouldProcessMessage) {
|
|
377
|
+
});
|
|
378
|
+
const connection = await this.ws.opened.catch(async (reason: unknown) => {
|
|
379
|
+
if (this.state !== SignalConnectionState.CONNECTED) {
|
|
380
|
+
this.state = SignalConnectionState.DISCONNECTED;
|
|
381
|
+
clearTimeout(wsTimeout);
|
|
382
|
+
const error = await this.handleConnectionError(reason, validateUrl);
|
|
383
|
+
reject(error);
|
|
420
384
|
return;
|
|
421
385
|
}
|
|
386
|
+
// other errors, handle
|
|
387
|
+
this.handleWSError(reason);
|
|
388
|
+
reject(reason);
|
|
389
|
+
return;
|
|
390
|
+
});
|
|
391
|
+
clearTimeout(wsTimeout);
|
|
392
|
+
if (!connection) {
|
|
393
|
+
return;
|
|
422
394
|
}
|
|
395
|
+
const signalReader = connection.readable.getReader();
|
|
396
|
+
this.streamWriter = connection.writable.getWriter();
|
|
397
|
+
const firstMessage = await signalReader.read();
|
|
398
|
+
signalReader.releaseLock();
|
|
399
|
+
if (!firstMessage.value) {
|
|
400
|
+
throw new ConnectionError(
|
|
401
|
+
'no message received as first message',
|
|
402
|
+
ConnectionErrorReason.InternalError,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const firstSignalResponse = parseSignalResponse(firstMessage.value);
|
|
423
407
|
|
|
424
|
-
|
|
425
|
-
|
|
408
|
+
// Validate the first message
|
|
409
|
+
const validation = this.validateFirstMessage(
|
|
410
|
+
firstSignalResponse,
|
|
411
|
+
opts.reconnect ?? false,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (!validation.isValid) {
|
|
415
|
+
reject(validation.error);
|
|
416
|
+
return;
|
|
426
417
|
}
|
|
427
|
-
this.handleSignalResponse(resp);
|
|
428
|
-
};
|
|
429
418
|
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
419
|
+
// Handle join response - set up ping configuration
|
|
420
|
+
if (firstSignalResponse.message?.case === 'join') {
|
|
421
|
+
this.pingTimeoutDuration = firstSignalResponse.message.value.pingTimeout;
|
|
422
|
+
this.pingIntervalDuration = firstSignalResponse.message.value.pingInterval;
|
|
423
|
+
|
|
424
|
+
if (this.pingTimeoutDuration && this.pingTimeoutDuration > 0) {
|
|
425
|
+
this.log.debug('ping config', {
|
|
426
|
+
...this.logContext,
|
|
427
|
+
timeout: this.pingTimeoutDuration,
|
|
428
|
+
interval: this.pingIntervalDuration,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
438
431
|
}
|
|
439
432
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
433
|
+
// Handle successful connection
|
|
434
|
+
const firstMessageToProcess = validation.shouldProcessFirstMessage
|
|
435
|
+
? firstSignalResponse
|
|
436
|
+
: undefined;
|
|
437
|
+
handleSignalConnected(connection, firstMessageToProcess);
|
|
438
|
+
resolve(validation.response);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
clearTimeout(wsTimeout);
|
|
441
|
+
reject(e);
|
|
442
|
+
}
|
|
449
443
|
} finally {
|
|
450
444
|
unlock();
|
|
451
445
|
}
|
|
452
446
|
});
|
|
453
447
|
}
|
|
454
448
|
|
|
449
|
+
async startReadingLoop(
|
|
450
|
+
signalReader: ReadableStreamDefaultReader<string | ArrayBuffer>,
|
|
451
|
+
firstMessage?: SignalResponse,
|
|
452
|
+
) {
|
|
453
|
+
if (firstMessage) {
|
|
454
|
+
this.handleSignalResponse(firstMessage);
|
|
455
|
+
}
|
|
456
|
+
while (true) {
|
|
457
|
+
if (this.signalLatency) {
|
|
458
|
+
await sleep(this.signalLatency);
|
|
459
|
+
}
|
|
460
|
+
const { done, value } = await signalReader.read();
|
|
461
|
+
if (done) {
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
const resp = parseSignalResponse(value);
|
|
465
|
+
this.handleSignalResponse(resp);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
455
469
|
/** @internal */
|
|
456
470
|
resetCallbacks = () => {
|
|
457
471
|
this.onAnswer = undefined;
|
|
@@ -465,6 +479,7 @@ export class SignalClient {
|
|
|
465
479
|
this.onTokenRefresh = undefined;
|
|
466
480
|
this.onTrickle = undefined;
|
|
467
481
|
this.onClose = undefined;
|
|
482
|
+
this.onMediaSectionsRequirement = undefined;
|
|
468
483
|
};
|
|
469
484
|
|
|
470
485
|
async close(updateState: boolean = true) {
|
|
@@ -475,28 +490,16 @@ export class SignalClient {
|
|
|
475
490
|
this.state = SignalConnectionState.DISCONNECTING;
|
|
476
491
|
}
|
|
477
492
|
if (this.ws) {
|
|
478
|
-
this.ws.
|
|
479
|
-
this.ws.onopen = null;
|
|
480
|
-
this.ws.onclose = null;
|
|
493
|
+
this.ws.close({ closeCode: 1000, reason: 'Close method called on signal client' });
|
|
481
494
|
|
|
482
495
|
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
|
|
483
|
-
const closePromise =
|
|
484
|
-
if (this.ws) {
|
|
485
|
-
this.ws.onclose = () => {
|
|
486
|
-
resolve();
|
|
487
|
-
};
|
|
488
|
-
} else {
|
|
489
|
-
resolve();
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
if (this.ws.readyState < this.ws.CLOSING) {
|
|
494
|
-
this.ws.close();
|
|
495
|
-
// 250ms grace period for ws to close gracefully
|
|
496
|
-
await Promise.race([closePromise, sleep(250)]);
|
|
497
|
-
}
|
|
496
|
+
const closePromise = this.ws.closed;
|
|
498
497
|
this.ws = undefined;
|
|
498
|
+
this.streamWriter = undefined;
|
|
499
|
+
await Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
499
500
|
}
|
|
501
|
+
} catch (e) {
|
|
502
|
+
this.log.debug('websocket error while closing', { ...this.logContext, error: e });
|
|
500
503
|
} finally {
|
|
501
504
|
if (updateState) {
|
|
502
505
|
this.state = SignalConnectionState.DISCONNECTED;
|
|
@@ -675,7 +678,7 @@ export class SignalClient {
|
|
|
675
678
|
this.log.debug(`skipping signal request (type: ${message.case}) - SignalClient disconnected`);
|
|
676
679
|
return;
|
|
677
680
|
}
|
|
678
|
-
if (!this.
|
|
681
|
+
if (!this.streamWriter) {
|
|
679
682
|
this.log.error(
|
|
680
683
|
`cannot send signal request before connected, type: ${message?.case}`,
|
|
681
684
|
this.logContext,
|
|
@@ -686,9 +689,9 @@ export class SignalClient {
|
|
|
686
689
|
|
|
687
690
|
try {
|
|
688
691
|
if (this.useJSON) {
|
|
689
|
-
this.
|
|
692
|
+
await this.streamWriter.write(req.toJsonString());
|
|
690
693
|
} else {
|
|
691
|
-
this.
|
|
694
|
+
await this.streamWriter.write(req.toBinary());
|
|
692
695
|
}
|
|
693
696
|
} catch (e) {
|
|
694
697
|
this.log.error('error sending signal message', { ...this.logContext, error: e });
|
|
@@ -790,6 +793,10 @@ export class SignalClient {
|
|
|
790
793
|
if (this.onRoomMoved) {
|
|
791
794
|
this.onRoomMoved(msg.value);
|
|
792
795
|
}
|
|
796
|
+
} else if (msg.case === 'mediaSectionsRequirement') {
|
|
797
|
+
if (this.onMediaSectionsRequirement) {
|
|
798
|
+
this.onMediaSectionsRequirement(msg.value);
|
|
799
|
+
}
|
|
793
800
|
} else {
|
|
794
801
|
this.log.debug('unsupported message', { ...this.logContext, msgCase: msg.case });
|
|
795
802
|
}
|
|
@@ -818,8 +825,8 @@ export class SignalClient {
|
|
|
818
825
|
}
|
|
819
826
|
}
|
|
820
827
|
|
|
821
|
-
private handleWSError(
|
|
822
|
-
this.log.error('websocket error', { ...this.logContext, error
|
|
828
|
+
private handleWSError(error: unknown) {
|
|
829
|
+
this.log.error('websocket error', { ...this.logContext, error });
|
|
823
830
|
}
|
|
824
831
|
|
|
825
832
|
/**
|
|
@@ -872,6 +879,128 @@ export class SignalClient {
|
|
|
872
879
|
CriticalTimers.clearInterval(this.pingInterval);
|
|
873
880
|
}
|
|
874
881
|
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Handles the successful connection to the signal server
|
|
885
|
+
* @param connection The WebSocket connection
|
|
886
|
+
* @param timeoutHandle The timeout handle to clear
|
|
887
|
+
* @param firstMessage Optional first message to process
|
|
888
|
+
* @internal
|
|
889
|
+
*/
|
|
890
|
+
private handleSignalConnected(
|
|
891
|
+
connection: WebSocketConnection,
|
|
892
|
+
timeoutHandle: ReturnType<typeof setTimeout>,
|
|
893
|
+
firstMessage?: SignalResponse,
|
|
894
|
+
) {
|
|
895
|
+
this.state = SignalConnectionState.CONNECTED;
|
|
896
|
+
clearTimeout(timeoutHandle);
|
|
897
|
+
this.startPingInterval();
|
|
898
|
+
this.startReadingLoop(connection.readable.getReader(), firstMessage);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Validates the first message received from the signal server
|
|
903
|
+
* @param firstSignalResponse The first signal response received
|
|
904
|
+
* @param isReconnect Whether this is a reconnection attempt
|
|
905
|
+
* @returns Validation result with response or error
|
|
906
|
+
* @internal
|
|
907
|
+
*/
|
|
908
|
+
private validateFirstMessage(
|
|
909
|
+
firstSignalResponse: SignalResponse,
|
|
910
|
+
isReconnect: boolean,
|
|
911
|
+
): {
|
|
912
|
+
isValid: boolean;
|
|
913
|
+
response?: JoinResponse | ReconnectResponse;
|
|
914
|
+
error?: ConnectionError;
|
|
915
|
+
shouldProcessFirstMessage?: boolean;
|
|
916
|
+
} {
|
|
917
|
+
if (firstSignalResponse.message?.case === 'join') {
|
|
918
|
+
return {
|
|
919
|
+
isValid: true,
|
|
920
|
+
response: firstSignalResponse.message.value,
|
|
921
|
+
};
|
|
922
|
+
} else if (
|
|
923
|
+
this.state === SignalConnectionState.RECONNECTING &&
|
|
924
|
+
firstSignalResponse.message?.case !== 'leave'
|
|
925
|
+
) {
|
|
926
|
+
if (firstSignalResponse.message?.case === 'reconnect') {
|
|
927
|
+
return {
|
|
928
|
+
isValid: true,
|
|
929
|
+
response: firstSignalResponse.message.value,
|
|
930
|
+
};
|
|
931
|
+
} else {
|
|
932
|
+
// in reconnecting, any message received means signal reconnected and we still need to process it
|
|
933
|
+
this.log.debug(
|
|
934
|
+
'declaring signal reconnected without reconnect response received',
|
|
935
|
+
this.logContext,
|
|
936
|
+
);
|
|
937
|
+
return {
|
|
938
|
+
isValid: true,
|
|
939
|
+
response: undefined,
|
|
940
|
+
shouldProcessFirstMessage: true,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
} else if (this.isEstablishingConnection && firstSignalResponse.message?.case === 'leave') {
|
|
944
|
+
return {
|
|
945
|
+
isValid: false,
|
|
946
|
+
error: new ConnectionError(
|
|
947
|
+
'Received leave request while trying to (re)connect',
|
|
948
|
+
ConnectionErrorReason.LeaveRequest,
|
|
949
|
+
undefined,
|
|
950
|
+
firstSignalResponse.message.value.reason,
|
|
951
|
+
),
|
|
952
|
+
};
|
|
953
|
+
} else if (!isReconnect) {
|
|
954
|
+
// non-reconnect case, should receive join response first
|
|
955
|
+
return {
|
|
956
|
+
isValid: false,
|
|
957
|
+
error: new ConnectionError(
|
|
958
|
+
`did not receive join response, got ${firstSignalResponse.message?.case} instead`,
|
|
959
|
+
ConnectionErrorReason.InternalError,
|
|
960
|
+
),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
isValid: false,
|
|
966
|
+
error: new ConnectionError('Unexpected first message', ConnectionErrorReason.InternalError),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Handles WebSocket connection errors by validating with the server
|
|
972
|
+
* @param reason The error that occurred
|
|
973
|
+
* @param validateUrl The URL to validate the connection with
|
|
974
|
+
* @returns A ConnectionError with appropriate reason and status
|
|
975
|
+
* @internal
|
|
976
|
+
*/
|
|
977
|
+
private async handleConnectionError(
|
|
978
|
+
reason: unknown,
|
|
979
|
+
validateUrl: string,
|
|
980
|
+
): Promise<ConnectionError> {
|
|
981
|
+
try {
|
|
982
|
+
const resp = await fetch(validateUrl);
|
|
983
|
+
if (resp.status.toFixed(0).startsWith('4')) {
|
|
984
|
+
const msg = await resp.text();
|
|
985
|
+
return new ConnectionError(msg, ConnectionErrorReason.NotAllowed, resp.status);
|
|
986
|
+
} else if (reason instanceof ConnectionError) {
|
|
987
|
+
return reason;
|
|
988
|
+
} else {
|
|
989
|
+
return new ConnectionError(
|
|
990
|
+
`Encountered unknown websocket error during connection: ${reason}`,
|
|
991
|
+
ConnectionErrorReason.InternalError,
|
|
992
|
+
resp.status,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
} catch (e) {
|
|
996
|
+
return e instanceof ConnectionError
|
|
997
|
+
? e
|
|
998
|
+
: new ConnectionError(
|
|
999
|
+
e instanceof Error ? e.message : 'server was not reachable',
|
|
1000
|
+
ConnectionErrorReason.ServerUnreachable,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
875
1004
|
}
|
|
876
1005
|
|
|
877
1006
|
function fromProtoSessionDescription(sd: SessionDescription): RTCSessionDescriptionInit {
|
|
@@ -958,3 +1087,31 @@ function createConnectionParams(
|
|
|
958
1087
|
|
|
959
1088
|
return params;
|
|
960
1089
|
}
|
|
1090
|
+
|
|
1091
|
+
function createJoinRequestConnectionParams(
|
|
1092
|
+
token: string,
|
|
1093
|
+
info: ClientInfo,
|
|
1094
|
+
opts: ConnectOpts,
|
|
1095
|
+
): URLSearchParams {
|
|
1096
|
+
const params = new URLSearchParams();
|
|
1097
|
+
params.set('access_token', token);
|
|
1098
|
+
|
|
1099
|
+
const joinRequest = new JoinRequest({
|
|
1100
|
+
clientInfo: info,
|
|
1101
|
+
connectionSettings: new ConnectionSettings({
|
|
1102
|
+
autoSubscribe: !!opts.autoSubscribe,
|
|
1103
|
+
adaptiveStream: !!opts.adaptiveStream,
|
|
1104
|
+
}),
|
|
1105
|
+
reconnect: !!opts.reconnect,
|
|
1106
|
+
participantSid: opts.sid ? opts.sid : undefined,
|
|
1107
|
+
});
|
|
1108
|
+
if (opts.reconnectReason) {
|
|
1109
|
+
joinRequest.reconnectReason = opts.reconnectReason;
|
|
1110
|
+
}
|
|
1111
|
+
const wrappedJoinRequest = new WrappedJoinRequest({
|
|
1112
|
+
joinRequest: joinRequest.toBinary(),
|
|
1113
|
+
});
|
|
1114
|
+
params.set('join_request', btoa(new TextDecoder('utf-8').decode(wrappedJoinRequest.toBinary())));
|
|
1115
|
+
|
|
1116
|
+
return params;
|
|
1117
|
+
}
|