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
@@ -0,0 +1,118 @@
1
+ // https://github.com/CarterLi/websocketstream-polyfill
2
+ import { sleep } from '../room/utils';
3
+
4
+ export interface WebSocketConnection<T extends ArrayBuffer | string = ArrayBuffer | string> {
5
+ readable: ReadableStream<T>;
6
+ writable: WritableStream<T>;
7
+ protocol: string;
8
+ extensions: string;
9
+ }
10
+
11
+ export interface WebSocketCloseInfo {
12
+ closeCode?: number;
13
+ reason?: string;
14
+ }
15
+
16
+ export interface WebSocketStreamOptions {
17
+ protocols?: string[];
18
+ signal?: AbortSignal;
19
+ }
20
+
21
+ /**
22
+ * [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
23
+ *
24
+ * @see https://web.dev/websocketstream/
25
+ */
26
+ export class WebSocketStream<T extends ArrayBuffer | string = ArrayBuffer | string> {
27
+ readonly url: string;
28
+
29
+ readonly opened: Promise<WebSocketConnection<T>>;
30
+
31
+ readonly closed: Promise<WebSocketCloseInfo>;
32
+
33
+ readonly close: (closeInfo?: WebSocketCloseInfo) => void;
34
+
35
+ get readyState(): number {
36
+ return this.ws.readyState;
37
+ }
38
+
39
+ private ws: WebSocket;
40
+
41
+ constructor(url: string, options: WebSocketStreamOptions = {}) {
42
+ if (options.signal?.aborted) {
43
+ throw new DOMException('This operation was aborted', 'AbortError');
44
+ }
45
+
46
+ this.url = url;
47
+
48
+ const ws = new WebSocket(url, options.protocols ?? []);
49
+ ws.binaryType = 'arraybuffer';
50
+ this.ws = ws;
51
+
52
+ const closeWithInfo = ({ closeCode: code, reason }: WebSocketCloseInfo = {}) =>
53
+ ws.close(code, reason);
54
+
55
+ this.opened = new Promise((resolve, reject) => {
56
+ ws.onopen = () => {
57
+ resolve({
58
+ readable: new ReadableStream<T>({
59
+ start(controller) {
60
+ ws.onmessage = ({ data }) => controller.enqueue(data);
61
+ ws.onerror = (e) => controller.error(e);
62
+ },
63
+ cancel: closeWithInfo,
64
+ }),
65
+ writable: new WritableStream<T>({
66
+ write(chunk) {
67
+ ws.send(chunk);
68
+ },
69
+ abort() {
70
+ ws.close();
71
+ },
72
+ close: closeWithInfo,
73
+ }),
74
+ protocol: ws.protocol,
75
+ extensions: ws.extensions,
76
+ });
77
+ ws.removeEventListener('error', reject);
78
+ };
79
+ ws.addEventListener('error', reject);
80
+ });
81
+
82
+ this.closed = new Promise<WebSocketCloseInfo>((resolve, reject) => {
83
+ const rejectHandler = async () => {
84
+ const closePromise = new Promise<CloseEvent>((res) => {
85
+ if (ws.readyState === WebSocket.CLOSED) return;
86
+ else {
87
+ ws.addEventListener(
88
+ 'close',
89
+ (closeEv: CloseEvent) => {
90
+ res(closeEv);
91
+ },
92
+ { once: true },
93
+ );
94
+ }
95
+ });
96
+ const reason = await Promise.race([sleep(250), closePromise]);
97
+ if (!reason) {
98
+ reject(new Error('Encountered unspecified websocket error without a timely close event'));
99
+ } else {
100
+ // if we can infer the close reason from the close event then resolve the promise, we don't need to throw
101
+ resolve(reason);
102
+ }
103
+ };
104
+ ws.onclose = ({ code, reason }) => {
105
+ resolve({ closeCode: code, reason });
106
+ ws.removeEventListener('error', rejectHandler);
107
+ };
108
+
109
+ ws.addEventListener('error', rejectHandler);
110
+ });
111
+
112
+ if (options.signal) {
113
+ options.signal.onabort = () => ws.close();
114
+ }
115
+
116
+ this.close = closeWithInfo;
117
+ }
118
+ }
package/src/api/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SignalResponse } from '@livekit/protocol';
1
2
  import { toHttpUrl, toWebsocketUrl } from '../room/utils';
2
3
 
3
4
  export function createRtcUrl(url: string, searchParams: URLSearchParams) {
@@ -21,3 +22,12 @@ function appendUrlPath(urlObj: URL, path: string) {
21
22
  urlObj.pathname = `${ensureTrailingSlash(urlObj.pathname)}${path}`;
22
23
  return urlObj.toString();
23
24
  }
25
+
26
+ export function parseSignalResponse(value: ArrayBuffer | string) {
27
+ if (typeof value === 'string') {
28
+ return SignalResponse.fromJson(JSON.parse(value), { ignoreUnknownFields: true });
29
+ } else if (value instanceof ArrayBuffer) {
30
+ return SignalResponse.fromBinary(new Uint8Array(value));
31
+ }
32
+ throw new Error(`could not decode websocket message: ${typeof value}`);
33
+ }
@@ -13,6 +13,7 @@ export class TURNCheck extends Checker {
13
13
  maxRetries: 0,
14
14
  e2eeEnabled: false,
15
15
  websocketTimeout: 15_000,
16
+ singlePeerConnection: false,
16
17
  });
17
18
 
18
19
  let hasTLS = false;
@@ -38,7 +38,7 @@ export class WebRTCCheck extends Checker {
38
38
  }
39
39
  };
40
40
 
41
- if (this.room.engine.pcManager) {
41
+ if (this.room.engine.pcManager?.subscriber) {
42
42
  this.room.engine.pcManager.subscriber.onIceCandidateError = (ev) => {
43
43
  if (ev instanceof RTCPeerConnectionIceErrorEvent) {
44
44
  this.appendWarning(
@@ -18,6 +18,7 @@ export class WebSocketCheck extends Checker {
18
18
  maxRetries: 0,
19
19
  e2eeEnabled: false,
20
20
  websocketTimeout: 15_000,
21
+ singlePeerConnection: false,
21
22
  });
22
23
  this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
23
24
  if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  isVideoTrack,
51
51
  supportsAV1,
52
52
  supportsAdaptiveStream,
53
+ supportsAudioOutputSelection,
53
54
  supportsDynacast,
54
55
  supportsVP9,
55
56
  } from './room/utils';
@@ -121,6 +122,7 @@ export {
121
122
  setLogLevel,
122
123
  supportsAV1,
123
124
  supportsAdaptiveStream,
125
+ supportsAudioOutputSelection,
124
126
  supportsDynacast,
125
127
  supportsVP9,
126
128
  Mutex,
package/src/options.ts CHANGED
@@ -99,6 +99,13 @@ export interface InternalRoomOptions {
99
99
  encryption?: E2EEOptions;
100
100
 
101
101
  loggerName?: string;
102
+
103
+ /**
104
+ * @experimental
105
+ * only supported on LiveKit Cloud
106
+ * and LiveKit OSS >= 1.9.2
107
+ */
108
+ singlePeerConnection: boolean;
102
109
  }
103
110
 
104
111
  /**
@@ -167,7 +167,7 @@ export default class PCTransport extends EventEmitter {
167
167
  sdpParsed.media.forEach((media) => {
168
168
  const mid = getMidString(media.mid!);
169
169
  if (media.type === 'audio') {
170
- // mung sdp for opus bitrate settings
170
+ // munge sdp for opus bitrate settings
171
171
  this.trackBitrates.some((trackbr): boolean => {
172
172
  if (!trackbr.transceiver || mid != trackbr.transceiver.mid) {
173
173
  return false;
@@ -297,7 +297,7 @@ export default class PCTransport extends EventEmitter {
297
297
  sdpParsed.media.forEach((media) => {
298
298
  ensureIPAddrMatchVersion(media);
299
299
  if (media.type === 'audio') {
300
- ensureAudioNackAndStereo(media, [], []);
300
+ ensureAudioNackAndStereo(media, ['all'], []);
301
301
  } else if (media.type === 'video') {
302
302
  this.trackBitrates.some((trackbr): boolean => {
303
303
  if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
@@ -380,6 +380,10 @@ export default class PCTransport extends EventEmitter {
380
380
  return this.pc.addTransceiver(mediaStreamTrack, transceiverInit);
381
381
  }
382
382
 
383
+ addTransceiverOfKind(kind: 'audio' | 'video', transceiverInit: RTCRtpTransceiverInit) {
384
+ return this.pc.addTransceiver(kind, transceiverInit);
385
+ }
386
+
383
387
  addTrack(track: MediaStreamTrack) {
384
388
  if (!this._pc) {
385
389
  throw new UnexpectedConnectionState('PC closed, cannot add track');
@@ -623,7 +627,7 @@ function ensureAudioNackAndStereo(
623
627
  });
624
628
  }
625
629
 
626
- if (stereoMids.includes(mid)) {
630
+ if (stereoMids.includes(mid) || (stereoMids.length === 1 && stereoMids[0] === 'all')) {
627
631
  media.fmtp.some((fmtp): boolean => {
628
632
  if (fmtp.payload === opusPayload) {
629
633
  if (!fmtp.config.includes('stereo=1')) {
@@ -17,10 +17,11 @@ export enum PCTransportState {
17
17
  CLOSED,
18
18
  }
19
19
 
20
+ type PCMode = 'subscriber-primary' | 'publisher-primary' | 'publisher-only';
20
21
  export class PCTransportManager {
21
22
  public publisher: PCTransport;
22
23
 
23
- public subscriber: PCTransport;
24
+ public subscriber?: PCTransport;
24
25
 
25
26
  public peerConnectionTimeout: number = roomConnectOptionDefaults.peerConnectionTimeout;
26
27
 
@@ -39,7 +40,7 @@ export class PCTransportManager {
39
40
  public onStateChange?: (
40
41
  state: PCTransportState,
41
42
  pubState: RTCPeerConnectionState,
42
- subState: RTCPeerConnectionState,
43
+ subState?: RTCPeerConnectionState,
43
44
  ) => void;
44
45
 
45
46
  public onIceCandidate?: (ev: RTCIceCandidate, target: SignalTarget) => void;
@@ -64,38 +65,40 @@ export class PCTransportManager {
64
65
 
65
66
  private loggerOptions: LoggerOptions;
66
67
 
67
- constructor(
68
- rtcConfig: RTCConfiguration,
69
- subscriberPrimary: boolean,
70
- loggerOptions: LoggerOptions,
71
- ) {
68
+ constructor(rtcConfig: RTCConfiguration, mode: PCMode, loggerOptions: LoggerOptions) {
72
69
  this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager);
73
70
  this.loggerOptions = loggerOptions;
74
71
 
75
- this.isPublisherConnectionRequired = !subscriberPrimary;
76
- this.isSubscriberConnectionRequired = subscriberPrimary;
72
+ this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
73
+ this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
77
74
  this.publisher = new PCTransport(rtcConfig, loggerOptions);
78
- this.subscriber = new PCTransport(rtcConfig, loggerOptions);
75
+ if (mode !== 'publisher-only') {
76
+ this.subscriber = new PCTransport(rtcConfig, loggerOptions);
77
+ this.subscriber.onConnectionStateChange = this.updateState;
78
+ this.subscriber.onIceConnectionStateChange = this.updateState;
79
+ this.subscriber.onSignalingStatechange = this.updateState;
80
+ this.subscriber.onIceCandidate = (candidate) => {
81
+ this.onIceCandidate?.(candidate, SignalTarget.SUBSCRIBER);
82
+ };
83
+ // in subscriber primary mode, server side opens sub data channels.
84
+ this.subscriber.onDataChannel = (ev) => {
85
+ this.onDataChannel?.(ev);
86
+ };
87
+ this.subscriber.onTrack = (ev) => {
88
+ this.onTrack?.(ev);
89
+ };
90
+ }
79
91
 
80
92
  this.publisher.onConnectionStateChange = this.updateState;
81
- this.subscriber.onConnectionStateChange = this.updateState;
82
93
  this.publisher.onIceConnectionStateChange = this.updateState;
83
- this.subscriber.onIceConnectionStateChange = this.updateState;
84
94
  this.publisher.onSignalingStatechange = this.updateState;
85
- this.subscriber.onSignalingStatechange = this.updateState;
86
95
  this.publisher.onIceCandidate = (candidate) => {
87
96
  this.onIceCandidate?.(candidate, SignalTarget.PUBLISHER);
88
97
  };
89
- this.subscriber.onIceCandidate = (candidate) => {
90
- this.onIceCandidate?.(candidate, SignalTarget.SUBSCRIBER);
91
- };
92
- // in subscriber primary mode, server side opens sub data channels.
93
- this.subscriber.onDataChannel = (ev) => {
94
- this.onDataChannel?.(ev);
95
- };
96
- this.subscriber.onTrack = (ev) => {
98
+ this.publisher.onTrack = (ev) => {
97
99
  this.onTrack?.(ev);
98
100
  };
101
+
99
102
  this.publisher.onOffer = (offer, offerId) => {
100
103
  this.onPublisherOffer?.(offer, offerId);
101
104
  };
@@ -117,11 +120,6 @@ export class PCTransportManager {
117
120
  this.updateState();
118
121
  }
119
122
 
120
- requireSubscriber(require = true) {
121
- this.isSubscriberConnectionRequired = require;
122
- this.updateState();
123
- }
124
-
125
123
  createAndSendPublisherOffer(options?: RTCOfferOptions) {
126
124
  return this.publisher.createAndSendOffer(options);
127
125
  }
@@ -148,12 +146,14 @@ export class PCTransportManager {
148
146
  }
149
147
  }
150
148
  }
151
- await Promise.all([this.publisher.close(), this.subscriber.close()]);
149
+ await Promise.all([this.publisher.close(), this.subscriber?.close()]);
152
150
  this.updateState();
153
151
  }
154
152
 
155
153
  async triggerIceRestart() {
156
- this.subscriber.restartingIce = true;
154
+ if (this.subscriber) {
155
+ this.subscriber.restartingIce = true;
156
+ }
157
157
  // only restart publisher if it's needed
158
158
  if (this.needsPublisher) {
159
159
  await this.createAndSendPublisherOffer({ iceRestart: true });
@@ -164,7 +164,7 @@ export class PCTransportManager {
164
164
  if (target === SignalTarget.PUBLISHER) {
165
165
  await this.publisher.addIceCandidate(candidate);
166
166
  } else {
167
- await this.subscriber.addIceCandidate(candidate);
167
+ await this.subscriber?.addIceCandidate(candidate);
168
168
  }
169
169
  }
170
170
 
@@ -173,17 +173,17 @@ export class PCTransportManager {
173
173
  ...this.logContext,
174
174
  RTCSdpType: sd.type,
175
175
  sdp: sd.sdp,
176
- signalingState: this.subscriber.getSignallingState().toString(),
176
+ signalingState: this.subscriber?.getSignallingState().toString(),
177
177
  });
178
178
  const unlock = await this.remoteOfferLock.lock();
179
179
  try {
180
- const success = await this.subscriber.setRemoteDescription(sd, offerId);
180
+ const success = await this.subscriber?.setRemoteDescription(sd, offerId);
181
181
  if (!success) {
182
182
  return undefined;
183
183
  }
184
184
 
185
185
  // answer the offer
186
- const answer = await this.subscriber.createAndSetAnswer();
186
+ const answer = await this.subscriber?.createAndSetAnswer();
187
187
  return answer;
188
188
  } finally {
189
189
  unlock();
@@ -192,7 +192,7 @@ export class PCTransportManager {
192
192
 
193
193
  updateConfiguration(config: RTCConfiguration, iceRestart?: boolean) {
194
194
  this.publisher.setConfiguration(config);
195
- this.subscriber.setConfiguration(config);
195
+ this.subscriber?.setConfiguration(config);
196
196
  if (iceRestart) {
197
197
  this.triggerIceRestart();
198
198
  }
@@ -252,6 +252,10 @@ export class PCTransportManager {
252
252
  return this.publisher.addTransceiver(track, transceiverInit);
253
253
  }
254
254
 
255
+ addPublisherTransceiverOfKind(kind: 'audio' | 'video', transceiverInit: RTCRtpTransceiverInit) {
256
+ return this.publisher.addTransceiverOfKind(kind, transceiverInit);
257
+ }
258
+
255
259
  addPublisherTrack(track: MediaStreamTrack) {
256
260
  return this.publisher.addTrack(track);
257
261
  }
@@ -277,7 +281,7 @@ export class PCTransportManager {
277
281
  if (this.isPublisherConnectionRequired) {
278
282
  transports.push(this.publisher);
279
283
  }
280
- if (this.isSubscriberConnectionRequired) {
284
+ if (this.isSubscriberConnectionRequired && this.subscriber) {
281
285
  transports.push(this.subscriber);
282
286
  }
283
287
  return transports;
@@ -311,7 +315,7 @@ export class PCTransportManager {
311
315
  this.onStateChange?.(
312
316
  this.state,
313
317
  this.publisher.getConnectionState(),
314
- this.subscriber.getConnectionState(),
318
+ this.subscriber?.getConnectionState(),
315
319
  );
316
320
  }
317
321
  };
@@ -15,6 +15,7 @@ import {
15
15
  type JoinResponse,
16
16
  type LeaveRequest,
17
17
  LeaveRequest_Action,
18
+ MediaSectionsRequirement,
18
19
  ParticipantInfo,
19
20
  ReconnectReason,
20
21
  type ReconnectResponse,
@@ -425,7 +426,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
425
426
 
426
427
  this.pcManager = new PCTransportManager(
427
428
  rtcConfig,
428
- joinResponse.subscriberPrimary,
429
+ this.options.singlePeerConnection
430
+ ? 'publisher-only'
431
+ : joinResponse.subscriberPrimary
432
+ ? 'subscriber-primary'
433
+ : 'publisher-primary',
429
434
  this.loggerOptions,
430
435
  );
431
436
 
@@ -481,6 +486,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
481
486
  }
482
487
  };
483
488
  this.pcManager.onTrack = (ev: RTCTrackEvent) => {
489
+ // this fires after the underlying transceiver is stopped and potentially
490
+ // peer connection closed, so do not bubble up if there are no streams
491
+ if (ev.streams.length === 0) return;
484
492
  this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
485
493
  };
486
494
 
@@ -495,7 +503,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
495
503
  if (!this.pcManager) {
496
504
  return;
497
505
  }
498
- this.log.debug('received server answer', { ...this.logContext, RTCSdpType: sd.type });
506
+ this.log.debug('received server answer', {
507
+ ...this.logContext,
508
+ RTCSdpType: sd.type,
509
+ sdp: sd.sdp,
510
+ });
499
511
  await this.pcManager.setPublisherAnswer(sd, offerId);
500
512
  };
501
513
 
@@ -566,6 +578,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
566
578
  this.emit(EngineEvent.RoomMoved, res);
567
579
  };
568
580
 
581
+ this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
582
+ const transceiverInit: RTCRtpTransceiverInit = { direction: 'recvonly' };
583
+ for (let i: number = 0; i < requirement.numAudios; i++) {
584
+ this.pcManager?.addPublisherTransceiverOfKind('audio', transceiverInit);
585
+ }
586
+ for (let i: number = 0; i < requirement.numVideos; i++) {
587
+ this.pcManager?.addPublisherTransceiverOfKind('video', transceiverInit);
588
+ }
589
+
590
+ this.negotiate();
591
+ };
592
+
569
593
  this.client.onClose = () => {
570
594
  this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
571
595
  };
@@ -734,6 +758,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
734
758
  const decryptedPacket = EncryptedPacketPayload.fromBinary(decryptedData.payload);
735
759
  const newDp = new DataPacket({
736
760
  value: decryptedPacket.value,
761
+ participantIdentity: dp.participantIdentity,
762
+ participantSid: dp.participantSid,
737
763
  });
738
764
  if (newDp.value?.case === 'user') {
739
765
  // compatibility
@@ -1478,8 +1504,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1478
1504
  this.log.warn('sync state cannot be sent without peer connection setup', this.logContext);
1479
1505
  return;
1480
1506
  }
1481
- const previousAnswer = this.pcManager.subscriber.getLocalDescription();
1482
- const previousOffer = this.pcManager.subscriber.getRemoteDescription();
1507
+ const previousPublisherOffer = this.pcManager.publisher.getLocalDescription();
1508
+ const previousPublisherAnswer = this.pcManager.publisher.getRemoteDescription();
1509
+ const previousSubscriberOffer = this.pcManager.subscriber?.getRemoteDescription();
1510
+ const previousSubscriberAnswer = this.pcManager.subscriber?.getLocalDescription();
1483
1511
 
1484
1512
  /* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
1485
1513
  in this case, we send unsub tracks, so server add all tracks to this
@@ -1501,18 +1529,32 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1501
1529
 
1502
1530
  this.client.sendSyncState(
1503
1531
  new SyncState({
1504
- answer: previousAnswer
1505
- ? toProtoSessionDescription({
1506
- sdp: previousAnswer.sdp,
1507
- type: previousAnswer.type,
1508
- })
1509
- : undefined,
1510
- offer: previousOffer
1511
- ? toProtoSessionDescription({
1512
- sdp: previousOffer.sdp,
1513
- type: previousOffer.type,
1514
- })
1515
- : undefined,
1532
+ answer: this.options.singlePeerConnection
1533
+ ? previousPublisherAnswer
1534
+ ? toProtoSessionDescription({
1535
+ sdp: previousPublisherAnswer.sdp,
1536
+ type: previousPublisherAnswer.type,
1537
+ })
1538
+ : undefined
1539
+ : previousSubscriberAnswer
1540
+ ? toProtoSessionDescription({
1541
+ sdp: previousSubscriberAnswer.sdp,
1542
+ type: previousSubscriberAnswer.type,
1543
+ })
1544
+ : undefined,
1545
+ offer: this.options.singlePeerConnection
1546
+ ? previousPublisherOffer
1547
+ ? toProtoSessionDescription({
1548
+ sdp: previousPublisherOffer.sdp,
1549
+ type: previousPublisherOffer.type,
1550
+ })
1551
+ : undefined
1552
+ : previousSubscriberOffer
1553
+ ? toProtoSessionDescription({
1554
+ sdp: previousSubscriberOffer.sdp,
1555
+ type: previousSubscriberOffer.type,
1556
+ })
1557
+ : undefined,
1516
1558
  subscription: new UpdateSubscription({
1517
1559
  trackSids,
1518
1560
  subscribe: !autoSubscribe,
@@ -1609,7 +1651,7 @@ export type EngineEventCallbacks = {
1609
1651
  activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
1610
1652
  dataPacketReceived: (packet: DataPacket, encryptionType: Encryption_Type) => void;
1611
1653
  transcriptionReceived: (transcription: Transcription) => void;
1612
- transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
1654
+ transportsCreated: (publisher: PCTransport, subscriber?: PCTransport) => void;
1613
1655
  /** @internal */
1614
1656
  trackSenderAdded: (track: Track, sender: RTCRtpSender) => void;
1615
1657
  rtpVideoMapUpdate: (rtpMap: Map<number, VideoCodec>) => void;
package/src/room/Room.ts CHANGED
@@ -370,6 +370,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
370
370
  if (e2eeOptions) {
371
371
  if ('e2eeManager' in e2eeOptions) {
372
372
  this.e2eeManager = e2eeOptions.e2eeManager;
373
+ this.e2eeManager.isDataChannelEncryptionEnabled = dcEncryptionEnabled;
373
374
  } else {
374
375
  this.e2eeManager = new E2EEManager(e2eeOptions, dcEncryptionEnabled);
375
376
  }
@@ -761,6 +762,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
761
762
  maxRetries: connectOptions.maxRetries,
762
763
  e2eeEnabled: !!this.e2eeManager,
763
764
  websocketTimeout: connectOptions.websocketTimeout,
765
+ singlePeerConnection: roomOptions.singlePeerConnection,
764
766
  },
765
767
  abortController.signal,
766
768
  );
@@ -939,8 +941,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
939
941
  this.isResuming
940
942
  ) {
941
943
  // try aborting pending connection attempt
942
- this.log.warn('abort connection attempt', this.logContext);
943
- this.abortController?.abort();
944
+ const msg = 'Abort connection attempt due to user initiated disconnect';
945
+ this.log.warn(msg, this.logContext);
946
+ this.abortController?.abort(msg);
944
947
  // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
945
948
  this.connectFuture?.reject?.(
946
949
  new ConnectionError('Client initiated disconnect', ConnectionErrorReason.Cancelled),
@@ -42,6 +42,7 @@ export const roomOptionDefaults: InternalRoomOptions = {
42
42
  reconnectPolicy: new DefaultReconnectPolicy(),
43
43
  disconnectOnPageLeave: true,
44
44
  webAudioMix: false,
45
+ singlePeerConnection: false,
45
46
  } as const;
46
47
 
47
48
  export const roomConnectOptionDefaults: InternalRoomConnectOptions = {
@@ -1771,9 +1771,9 @@ export default class LocalParticipant extends Participant {
1771
1771
  destinationIdentity,
1772
1772
  method,
1773
1773
  payload,
1774
- responseTimeout = 10000,
1774
+ responseTimeout = 15000,
1775
1775
  }: PerformRpcParams): Promise<string> {
1776
- const maxRoundTripLatency = 2000;
1776
+ const maxRoundTripLatency = 7000;
1777
1777
 
1778
1778
  return new Promise(async (resolve, reject) => {
1779
1779
  if (byteLength(payload) > MAX_PAYLOAD_BYTES) {
@@ -11,7 +11,7 @@ import {
11
11
  TokenSourceFixed,
12
12
  type TokenSourceResponseObject,
13
13
  } from './types';
14
- import { decodeTokenPayload, isResponseExpired } from './utils';
14
+ import { decodeTokenPayload, isResponseTokenValid } from './utils';
15
15
 
16
16
  /** A TokenSourceCached is a TokenSource which caches the last {@link TokenSourceResponseObject} value and returns it
17
17
  * until a) it expires or b) the {@link TokenSourceFetchOptions} provided to .fetch(...) change. */
@@ -56,7 +56,7 @@ abstract class TokenSourceCached extends TokenSourceConfigurable {
56
56
  if (!this.cachedResponse) {
57
57
  return false;
58
58
  }
59
- if (isResponseExpired(this.cachedResponse)) {
59
+ if (!isResponseTokenValid(this.cachedResponse)) {
60
60
  return false;
61
61
  }
62
62
  if (this.isSameAsCachedFetchOptions(fetchOptions)) {