livekit-client 2.15.8 → 2.15.9

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 (58) hide show
  1. package/dist/livekit-client.esm.mjs +577 -202
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/api/SignalClient.d.ts +31 -2
  6. package/dist/src/api/SignalClient.d.ts.map +1 -1
  7. package/dist/src/api/WebSocketStream.d.ts +29 -0
  8. package/dist/src/api/WebSocketStream.d.ts.map +1 -0
  9. package/dist/src/api/utils.d.ts +2 -0
  10. package/dist/src/api/utils.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  13. package/dist/src/index.d.ts +2 -2
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/options.d.ts +6 -0
  16. package/dist/src/options.d.ts.map +1 -1
  17. package/dist/src/room/PCTransport.d.ts +1 -0
  18. package/dist/src/room/PCTransport.d.ts.map +1 -1
  19. package/dist/src/room/PCTransportManager.d.ts +6 -4
  20. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  21. package/dist/src/room/RTCEngine.d.ts +1 -1
  22. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  23. package/dist/src/room/Room.d.ts.map +1 -1
  24. package/dist/src/room/defaults.d.ts.map +1 -1
  25. package/dist/src/room/token-source/utils.d.ts +1 -1
  26. package/dist/src/room/token-source/utils.d.ts.map +1 -1
  27. package/dist/src/room/utils.d.ts +6 -0
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/ts4.2/api/SignalClient.d.ts +31 -2
  30. package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
  31. package/dist/ts4.2/api/utils.d.ts +2 -0
  32. package/dist/ts4.2/index.d.ts +2 -2
  33. package/dist/ts4.2/options.d.ts +6 -0
  34. package/dist/ts4.2/room/PCTransport.d.ts +1 -0
  35. package/dist/ts4.2/room/PCTransportManager.d.ts +6 -4
  36. package/dist/ts4.2/room/RTCEngine.d.ts +1 -1
  37. package/dist/ts4.2/room/token-source/utils.d.ts +1 -1
  38. package/dist/ts4.2/room/utils.d.ts +6 -0
  39. package/package.json +1 -1
  40. package/src/api/SignalClient.test.ts +688 -0
  41. package/src/api/SignalClient.ts +308 -161
  42. package/src/api/WebSocketStream.test.ts +625 -0
  43. package/src/api/WebSocketStream.ts +118 -0
  44. package/src/api/utils.ts +10 -0
  45. package/src/connectionHelper/checks/turn.ts +1 -0
  46. package/src/connectionHelper/checks/webrtc.ts +1 -1
  47. package/src/connectionHelper/checks/websocket.ts +1 -0
  48. package/src/index.ts +2 -0
  49. package/src/options.ts +7 -0
  50. package/src/room/PCTransport.ts +7 -3
  51. package/src/room/PCTransportManager.ts +39 -35
  52. package/src/room/RTCEngine.ts +54 -16
  53. package/src/room/Room.ts +5 -2
  54. package/src/room/defaults.ts +1 -0
  55. package/src/room/token-source/TokenSource.ts +2 -2
  56. package/src/room/token-source/utils.test.ts +63 -0
  57. package/src/room/token-source/utils.ts +10 -5
  58. package/src/room/utils.ts +29 -0
@@ -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 { createRtcUrl, createValidateUrl } from './utils';
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?: WebSocket;
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,47 @@ 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 = createConnectionParams(token, clientInfo, opts);
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 abortHandler = async () => {
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) => {
274
298
  this.close();
275
299
  clearTimeout(wsTimeout);
276
- reject(
277
- new ConnectionError(
278
- 'room connection has been cancelled (signal)',
279
- ConnectionErrorReason.Cancelled,
280
- ),
281
- );
300
+ const target = event.currentTarget;
301
+ reject(target instanceof AbortSignal ? target.reason : target);
282
302
  };
283
303
 
304
+ combinedAbort.addEventListener('abort', abortHandler);
305
+
284
306
  const wsTimeout = setTimeout(() => {
285
- this.close();
286
- reject(
307
+ timeoutAbortController.abort(
287
308
  new ConnectionError(
288
309
  'room connection has timed out (signal)',
289
310
  ConnectionErrorReason.ServerUnreachable,
@@ -291,10 +312,13 @@ export class SignalClient {
291
312
  );
292
313
  }, opts.websocketTimeout);
293
314
 
294
- if (abortSignal?.aborted) {
295
- abortHandler();
296
- }
297
- abortSignal?.addEventListener('abort', abortHandler);
315
+ const handleSignalConnected = (
316
+ connection: WebSocketConnection,
317
+ firstMessage?: SignalResponse,
318
+ ) => {
319
+ this.handleSignalConnected(connection, wsTimeout, firstMessage);
320
+ };
321
+
298
322
  const redactedUrl = new URL(rtcUrl);
299
323
  if (redactedUrl.searchParams.has('access_token')) {
300
324
  redactedUrl.searchParams.set('access_token', '<redacted>');
@@ -307,151 +331,130 @@ export class SignalClient {
307
331
  if (this.ws) {
308
332
  await this.close(false);
309
333
  }
310
- this.ws = new WebSocket(rtcUrl);
311
- this.ws.binaryType = 'arraybuffer';
334
+ this.ws = new WebSocketStream<ArrayBuffer>(rtcUrl, { signal: combinedAbort });
312
335
 
313
- this.ws.onopen = () => {
314
- clearTimeout(wsTimeout);
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 {
336
+ try {
337
+ this.ws.closed
338
+ .then((closeInfo) => {
339
+ if (this.isEstablishingConnection) {
327
340
  reject(
328
341
  new ConnectionError(
329
- `Encountered unknown websocket error during connection: ${ev.toString()}`,
342
+ `Websocket got closed during a (re)connection attempt: ${closeInfo.reason}`,
330
343
  ConnectionErrorReason.InternalError,
331
- resp.status,
332
344
  ),
333
345
  );
334
346
  }
335
- } catch (e) {
336
- reject(
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', {
347
+ if (closeInfo.closeCode !== 1000) {
348
+ this.log.warn(`websocket closed`, {
376
349
  ...this.logContext,
377
- timeout: this.pingTimeoutDuration,
378
- interval: this.pingIntervalDuration,
350
+ reason: closeInfo.reason,
351
+ code: closeInfo.closeCode,
352
+ wasClean: closeInfo.closeCode === 1000,
353
+ state: this.state,
379
354
  });
380
- this.startPingInterval();
381
355
  }
382
- resolve(resp.message.value);
383
- } else if (
384
- this.state === SignalConnectionState.RECONNECTING &&
385
- resp.message.case !== 'leave'
386
- ) {
387
- // in reconnecting, any message received means signal reconnected
388
- this.state = SignalConnectionState.CONNECTED;
389
- abortSignal?.removeEventListener('abort', abortHandler);
390
- this.startPingInterval();
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,
356
+ return;
357
+ })
358
+ .catch((reason) => {
359
+ if (this.isEstablishingConnection) {
360
+ reject(
361
+ new ConnectionError(
362
+ `Websocket error during a (re)connection attempt: ${reason}`,
363
+ ConnectionErrorReason.InternalError,
364
+ ),
397
365
  );
398
- resolve(undefined);
399
- shouldProcessMessage = true;
400
366
  }
401
- } else if (this.isEstablishingConnection && resp.message.case === 'leave') {
402
- reject(
403
- new ConnectionError(
404
- 'Received leave request while trying to (re)connect',
405
- ConnectionErrorReason.LeaveRequest,
406
- undefined,
407
- resp.message.value.reason,
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) {
367
+ });
368
+ const connection = await this.ws.opened.catch(async (reason: unknown) => {
369
+ if (this.state !== SignalConnectionState.CONNECTED) {
370
+ this.state = SignalConnectionState.DISCONNECTED;
371
+ clearTimeout(wsTimeout);
372
+ const error = await this.handleConnectionError(reason, validateUrl);
373
+ reject(error);
420
374
  return;
421
375
  }
376
+ // other errors, handle
377
+ this.handleWSError(reason);
378
+ reject(reason);
379
+ return;
380
+ });
381
+ clearTimeout(wsTimeout);
382
+ if (!connection) {
383
+ return;
384
+ }
385
+ const signalReader = connection.readable.getReader();
386
+ const firstMessage = await signalReader.read();
387
+ signalReader.releaseLock();
388
+ if (!firstMessage.value) {
389
+ throw new ConnectionError(
390
+ 'no message received as first message',
391
+ ConnectionErrorReason.InternalError,
392
+ );
422
393
  }
423
394
 
424
- if (this.signalLatency) {
425
- await sleep(this.signalLatency);
395
+ const firstSignalResponse = parseSignalResponse(firstMessage.value);
396
+
397
+ // Validate the first message
398
+ const validation = this.validateFirstMessage(
399
+ firstSignalResponse,
400
+ opts.reconnect ?? false,
401
+ );
402
+
403
+ if (!validation.isValid) {
404
+ reject(validation.error);
405
+ return;
426
406
  }
427
- this.handleSignalResponse(resp);
428
- };
429
407
 
430
- this.ws.onclose = (ev: CloseEvent) => {
431
- if (this.isEstablishingConnection) {
432
- reject(
433
- new ConnectionError(
434
- 'Websocket got closed during a (re)connection attempt',
435
- ConnectionErrorReason.InternalError,
436
- ),
437
- );
408
+ // Handle join response - set up ping configuration
409
+ if (firstSignalResponse.message?.case === 'join') {
410
+ this.pingTimeoutDuration = firstSignalResponse.message.value.pingTimeout;
411
+ this.pingIntervalDuration = firstSignalResponse.message.value.pingInterval;
412
+
413
+ if (this.pingTimeoutDuration && this.pingTimeoutDuration > 0) {
414
+ this.log.debug('ping config', {
415
+ ...this.logContext,
416
+ timeout: this.pingTimeoutDuration,
417
+ interval: this.pingIntervalDuration,
418
+ });
419
+ }
438
420
  }
439
421
 
440
- this.log.warn(`websocket closed`, {
441
- ...this.logContext,
442
- reason: ev.reason,
443
- code: ev.code,
444
- wasClean: ev.wasClean,
445
- state: this.state,
446
- });
447
- this.handleOnClose(ev.reason);
448
- };
422
+ // Handle successful connection
423
+ const firstMessageToProcess = validation.shouldProcessFirstMessage
424
+ ? firstSignalResponse
425
+ : undefined;
426
+ handleSignalConnected(connection, firstMessageToProcess);
427
+ resolve(validation.response);
428
+ } catch (e) {
429
+ clearTimeout(wsTimeout);
430
+ reject(e);
431
+ }
449
432
  } finally {
450
433
  unlock();
451
434
  }
452
435
  });
453
436
  }
454
437
 
438
+ async startReadingLoop(
439
+ signalReader: ReadableStreamDefaultReader<string | ArrayBuffer>,
440
+ firstMessage?: SignalResponse,
441
+ ) {
442
+ if (firstMessage) {
443
+ this.handleSignalResponse(firstMessage);
444
+ }
445
+ while (true) {
446
+ if (this.signalLatency) {
447
+ await sleep(this.signalLatency);
448
+ }
449
+ const { done, value } = await signalReader.read();
450
+ if (done) {
451
+ break;
452
+ }
453
+ const resp = parseSignalResponse(value);
454
+ this.handleSignalResponse(resp);
455
+ }
456
+ }
457
+
455
458
  /** @internal */
456
459
  resetCallbacks = () => {
457
460
  this.onAnswer = undefined;
@@ -465,6 +468,7 @@ export class SignalClient {
465
468
  this.onTokenRefresh = undefined;
466
469
  this.onTrickle = undefined;
467
470
  this.onClose = undefined;
471
+ this.onMediaSectionsRequirement = undefined;
468
472
  };
469
473
 
470
474
  async close(updateState: boolean = true) {
@@ -475,28 +479,16 @@ export class SignalClient {
475
479
  this.state = SignalConnectionState.DISCONNECTING;
476
480
  }
477
481
  if (this.ws) {
478
- this.ws.onmessage = null;
479
- this.ws.onopen = null;
480
- this.ws.onclose = null;
482
+ this.ws.close({ closeCode: 1000, reason: 'Close method called on signal client' });
481
483
 
482
484
  // calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
483
- const closePromise = new Promise<void>((resolve) => {
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
- }
485
+ const closePromise = this.ws.closed;
498
486
  this.ws = undefined;
487
+ this.streamWriter = undefined;
488
+ await Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
499
489
  }
490
+ } catch (e) {
491
+ this.log.debug('websocket error while closing', { ...this.logContext, error: e });
500
492
  } finally {
501
493
  if (updateState) {
502
494
  this.state = SignalConnectionState.DISCONNECTED;
@@ -675,7 +667,7 @@ export class SignalClient {
675
667
  this.log.debug(`skipping signal request (type: ${message.case}) - SignalClient disconnected`);
676
668
  return;
677
669
  }
678
- if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
670
+ if (!this.streamWriter) {
679
671
  this.log.error(
680
672
  `cannot send signal request before connected, type: ${message?.case}`,
681
673
  this.logContext,
@@ -686,9 +678,9 @@ export class SignalClient {
686
678
 
687
679
  try {
688
680
  if (this.useJSON) {
689
- this.ws.send(req.toJsonString());
681
+ await this.streamWriter.write(req.toJsonString());
690
682
  } else {
691
- this.ws.send(req.toBinary());
683
+ await this.streamWriter.write(req.toBinary());
692
684
  }
693
685
  } catch (e) {
694
686
  this.log.error('error sending signal message', { ...this.logContext, error: e });
@@ -790,6 +782,10 @@ export class SignalClient {
790
782
  if (this.onRoomMoved) {
791
783
  this.onRoomMoved(msg.value);
792
784
  }
785
+ } else if (msg.case === 'mediaSectionsRequirement') {
786
+ if (this.onMediaSectionsRequirement) {
787
+ this.onMediaSectionsRequirement(msg.value);
788
+ }
793
789
  } else {
794
790
  this.log.debug('unsupported message', { ...this.logContext, msgCase: msg.case });
795
791
  }
@@ -818,8 +814,8 @@ export class SignalClient {
818
814
  }
819
815
  }
820
816
 
821
- private handleWSError(ev: Event) {
822
- this.log.error('websocket error', { ...this.logContext, error: ev });
817
+ private handleWSError(error: unknown) {
818
+ this.log.error('websocket error', { ...this.logContext, error });
823
819
  }
824
820
 
825
821
  /**
@@ -872,6 +868,129 @@ export class SignalClient {
872
868
  CriticalTimers.clearInterval(this.pingInterval);
873
869
  }
874
870
  }
871
+
872
+ /**
873
+ * Handles the successful connection to the signal server
874
+ * @param connection The WebSocket connection
875
+ * @param timeoutHandle The timeout handle to clear
876
+ * @param firstMessage Optional first message to process
877
+ * @internal
878
+ */
879
+ private handleSignalConnected(
880
+ connection: WebSocketConnection,
881
+ timeoutHandle: ReturnType<typeof setTimeout>,
882
+ firstMessage?: SignalResponse,
883
+ ) {
884
+ this.state = SignalConnectionState.CONNECTED;
885
+ clearTimeout(timeoutHandle);
886
+ this.startPingInterval();
887
+ this.startReadingLoop(connection.readable.getReader(), firstMessage);
888
+ this.streamWriter = connection.writable.getWriter();
889
+ }
890
+
891
+ /**
892
+ * Validates the first message received from the signal server
893
+ * @param firstSignalResponse The first signal response received
894
+ * @param isReconnect Whether this is a reconnection attempt
895
+ * @returns Validation result with response or error
896
+ * @internal
897
+ */
898
+ private validateFirstMessage(
899
+ firstSignalResponse: SignalResponse,
900
+ isReconnect: boolean,
901
+ ): {
902
+ isValid: boolean;
903
+ response?: JoinResponse | ReconnectResponse;
904
+ error?: ConnectionError;
905
+ shouldProcessFirstMessage?: boolean;
906
+ } {
907
+ if (firstSignalResponse.message?.case === 'join') {
908
+ return {
909
+ isValid: true,
910
+ response: firstSignalResponse.message.value,
911
+ };
912
+ } else if (
913
+ this.state === SignalConnectionState.RECONNECTING &&
914
+ firstSignalResponse.message?.case !== 'leave'
915
+ ) {
916
+ if (firstSignalResponse.message?.case === 'reconnect') {
917
+ return {
918
+ isValid: true,
919
+ response: firstSignalResponse.message.value,
920
+ };
921
+ } else {
922
+ // in reconnecting, any message received means signal reconnected and we still need to process it
923
+ this.log.debug(
924
+ 'declaring signal reconnected without reconnect response received',
925
+ this.logContext,
926
+ );
927
+ return {
928
+ isValid: true,
929
+ response: undefined,
930
+ shouldProcessFirstMessage: true,
931
+ };
932
+ }
933
+ } else if (this.isEstablishingConnection && firstSignalResponse.message?.case === 'leave') {
934
+ return {
935
+ isValid: false,
936
+ error: new ConnectionError(
937
+ 'Received leave request while trying to (re)connect',
938
+ ConnectionErrorReason.LeaveRequest,
939
+ undefined,
940
+ firstSignalResponse.message.value.reason,
941
+ ),
942
+ };
943
+ } else if (!isReconnect) {
944
+ // non-reconnect case, should receive join response first
945
+ return {
946
+ isValid: false,
947
+ error: new ConnectionError(
948
+ `did not receive join response, got ${firstSignalResponse.message?.case} instead`,
949
+ ConnectionErrorReason.InternalError,
950
+ ),
951
+ };
952
+ }
953
+
954
+ return {
955
+ isValid: false,
956
+ error: new ConnectionError('Unexpected first message', ConnectionErrorReason.InternalError),
957
+ };
958
+ }
959
+
960
+ /**
961
+ * Handles WebSocket connection errors by validating with the server
962
+ * @param reason The error that occurred
963
+ * @param validateUrl The URL to validate the connection with
964
+ * @returns A ConnectionError with appropriate reason and status
965
+ * @internal
966
+ */
967
+ private async handleConnectionError(
968
+ reason: unknown,
969
+ validateUrl: string,
970
+ ): Promise<ConnectionError> {
971
+ try {
972
+ const resp = await fetch(validateUrl);
973
+ if (resp.status.toFixed(0).startsWith('4')) {
974
+ const msg = await resp.text();
975
+ return new ConnectionError(msg, ConnectionErrorReason.NotAllowed, resp.status);
976
+ } else if (reason instanceof ConnectionError) {
977
+ return reason;
978
+ } else {
979
+ return new ConnectionError(
980
+ `Encountered unknown websocket error during connection: ${reason}`,
981
+ ConnectionErrorReason.InternalError,
982
+ resp.status,
983
+ );
984
+ }
985
+ } catch (e) {
986
+ return e instanceof ConnectionError
987
+ ? e
988
+ : new ConnectionError(
989
+ e instanceof Error ? e.message : 'server was not reachable',
990
+ ConnectionErrorReason.ServerUnreachable,
991
+ );
992
+ }
993
+ }
875
994
  }
876
995
 
877
996
  function fromProtoSessionDescription(sd: SessionDescription): RTCSessionDescriptionInit {
@@ -958,3 +1077,31 @@ function createConnectionParams(
958
1077
 
959
1078
  return params;
960
1079
  }
1080
+
1081
+ function createJoinRequestConnectionParams(
1082
+ token: string,
1083
+ info: ClientInfo,
1084
+ opts: ConnectOpts,
1085
+ ): URLSearchParams {
1086
+ const params = new URLSearchParams();
1087
+ params.set('access_token', token);
1088
+
1089
+ const joinRequest = new JoinRequest({
1090
+ clientInfo: info,
1091
+ connectionSettings: new ConnectionSettings({
1092
+ autoSubscribe: !!opts.autoSubscribe,
1093
+ adaptiveStream: !!opts.adaptiveStream,
1094
+ }),
1095
+ reconnect: !!opts.reconnect,
1096
+ participantSid: opts.sid ? opts.sid : undefined,
1097
+ });
1098
+ if (opts.reconnectReason) {
1099
+ joinRequest.reconnectReason = opts.reconnectReason;
1100
+ }
1101
+ const wrappedJoinRequest = new WrappedJoinRequest({
1102
+ joinRequest: joinRequest.toBinary(),
1103
+ });
1104
+ params.set('join_request', btoa(new TextDecoder('utf-8').decode(wrappedJoinRequest.toBinary())));
1105
+
1106
+ return params;
1107
+ }