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.
Files changed (59) hide show
  1. package/dist/livekit-client.esm.mjs +589 -205
  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 +769 -0
  41. package/src/api/SignalClient.ts +319 -162
  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 +59 -17
  53. package/src/room/Room.ts +5 -2
  54. package/src/room/defaults.ts +1 -0
  55. package/src/room/participant/LocalParticipant.ts +2 -2
  56. package/src/room/token-source/TokenSource.ts +2 -2
  57. package/src/room/token-source/utils.test.ts +63 -0
  58. package/src/room/token-source/utils.ts +10 -5
  59. 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,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 = 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 () => {
274
- this.close();
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
- reject(
277
- new ConnectionError(
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
- this.close();
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
- if (abortSignal?.aborted) {
295
- abortHandler();
296
- }
297
- abortSignal?.addEventListener('abort', abortHandler);
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 WebSocket(rtcUrl);
311
- this.ws.binaryType = 'arraybuffer';
344
+ this.ws = new WebSocketStream<ArrayBuffer>(rtcUrl, { signal: combinedAbort });
312
345
 
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 {
346
+ try {
347
+ this.ws.closed
348
+ .then((closeInfo) => {
349
+ if (this.isEstablishingConnection) {
327
350
  reject(
328
351
  new ConnectionError(
329
- `Encountered unknown websocket error during connection: ${ev.toString()}`,
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
- } 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', {
357
+ if (closeInfo.closeCode !== 1000) {
358
+ this.log.warn(`websocket closed`, {
376
359
  ...this.logContext,
377
- timeout: this.pingTimeoutDuration,
378
- interval: this.pingIntervalDuration,
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
- 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,
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
- } 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) {
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
- if (this.signalLatency) {
425
- await sleep(this.signalLatency);
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
- 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
- );
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
- 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
- };
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.onmessage = null;
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 = 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
- }
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.ws || this.ws.readyState !== this.ws.OPEN) {
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.ws.send(req.toJsonString());
692
+ await this.streamWriter.write(req.toJsonString());
690
693
  } else {
691
- this.ws.send(req.toBinary());
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(ev: Event) {
822
- this.log.error('websocket error', { ...this.logContext, error: ev });
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
+ }