livekit-client 2.13.4 → 2.13.6

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 (52) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +11 -3
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +329 -76
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +5 -5
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  12. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  13. package/dist/src/e2ee/types.d.ts +1 -0
  14. package/dist/src/e2ee/types.d.ts.map +1 -1
  15. package/dist/src/e2ee/worker/FrameCryptor.d.ts +2 -1
  16. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  17. package/dist/src/room/PCTransport.d.ts +3 -2
  18. package/dist/src/room/PCTransport.d.ts.map +1 -1
  19. package/dist/src/room/PCTransportManager.d.ts +3 -3
  20. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  21. package/dist/src/room/RTCEngine.d.ts +8 -0
  22. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  23. package/dist/src/room/Room.d.ts +1 -1
  24. package/dist/src/room/Room.d.ts.map +1 -1
  25. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  26. package/dist/src/utils/dataPacketBuffer.d.ts +15 -0
  27. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -0
  28. package/dist/src/utils/ttlmap.d.ts +20 -0
  29. package/dist/src/utils/ttlmap.d.ts.map +1 -0
  30. package/dist/ts4.2/src/api/SignalClient.d.ts +5 -5
  31. package/dist/ts4.2/src/e2ee/types.d.ts +1 -0
  32. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -1
  33. package/dist/ts4.2/src/room/PCTransport.d.ts +3 -2
  34. package/dist/ts4.2/src/room/PCTransportManager.d.ts +3 -3
  35. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -0
  36. package/dist/ts4.2/src/room/Room.d.ts +1 -1
  37. package/dist/ts4.2/src/utils/dataPacketBuffer.d.ts +15 -0
  38. package/dist/ts4.2/src/utils/ttlmap.d.ts +20 -0
  39. package/package.json +8 -8
  40. package/src/api/SignalClient.ts +12 -10
  41. package/src/connectionHelper/checks/publishVideo.ts +1 -0
  42. package/src/e2ee/E2eeManager.ts +3 -0
  43. package/src/e2ee/types.ts +1 -0
  44. package/src/e2ee/worker/FrameCryptor.ts +15 -0
  45. package/src/e2ee/worker/e2ee.worker.ts +2 -0
  46. package/src/room/PCTransport.ts +30 -4
  47. package/src/room/PCTransportManager.ts +10 -7
  48. package/src/room/RTCEngine.ts +78 -9
  49. package/src/room/Room.ts +11 -11
  50. package/src/room/track/LocalVideoTrack.ts +14 -15
  51. package/src/utils/dataPacketBuffer.ts +52 -0
  52. package/src/utils/ttlmap.ts +96 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.13.4",
3
+ "version": "2.13.6",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -37,7 +37,7 @@
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
39
  "@livekit/mutex": "1.1.1",
40
- "@livekit/protocol": "1.38.0",
40
+ "@livekit/protocol": "1.39.2",
41
41
  "events": "^3.3.0",
42
42
  "loglevel": "^1.9.2",
43
43
  "sdp-transform": "^2.15.0",
@@ -50,13 +50,13 @@
50
50
  "@types/dom-mediacapture-record": "^1"
51
51
  },
52
52
  "devDependencies": {
53
- "@babel/core": "7.27.1",
53
+ "@babel/core": "7.27.4",
54
54
  "@babel/preset-env": "7.27.2",
55
55
  "@bufbuild/protoc-gen-es": "^1.10.0",
56
56
  "@changesets/cli": "2.29.4",
57
57
  "@livekit/changesets-changelog-github": "^0.0.4",
58
58
  "@rollup/plugin-babel": "6.0.4",
59
- "@rollup/plugin-commonjs": "28.0.3",
59
+ "@rollup/plugin-commonjs": "28.0.5",
60
60
  "@rollup/plugin-json": "6.1.0",
61
61
  "@rollup/plugin-node-resolve": "16.0.1",
62
62
  "@rollup/plugin-terser": "^0.4.4",
@@ -64,25 +64,25 @@
64
64
  "@size-limit/webpack": "^11.2.0",
65
65
  "@trivago/prettier-plugin-sort-imports": "^5.0.0",
66
66
  "@types/events": "^3.0.3",
67
- "@types/sdp-transform": "2.4.9",
67
+ "@types/sdp-transform": "2.4.10",
68
68
  "@types/ua-parser-js": "0.7.39",
69
69
  "@typescript-eslint/eslint-plugin": "7.18.0",
70
70
  "@typescript-eslint/parser": "7.18.0",
71
71
  "downlevel-dts": "^0.11.0",
72
72
  "eslint": "8.57.1",
73
73
  "eslint-config-airbnb-typescript": "18.0.0",
74
- "eslint-config-prettier": "9.1.0",
74
+ "eslint-config-prettier": "10.1.5",
75
75
  "eslint-plugin-ecmascript-compat": "^3.2.1",
76
76
  "eslint-plugin-import": "2.31.0",
77
77
  "gh-pages": "6.3.0",
78
78
  "happy-dom": "^17.2.0",
79
79
  "jsdom": "^26.1.0",
80
80
  "prettier": "^3.4.2",
81
- "rollup": "4.41.0",
81
+ "rollup": "4.43.0",
82
82
  "rollup-plugin-delete": "^2.1.0",
83
83
  "rollup-plugin-typescript2": "0.36.0",
84
84
  "size-limit": "^11.2.0",
85
- "typedoc": "0.28.4",
85
+ "typedoc": "0.28.5",
86
86
  "typedoc-plugin-no-inherit": "1.6.1",
87
87
  "typescript": "5.8.3",
88
88
  "vite": "5.4.19",
@@ -110,9 +110,9 @@ export class SignalClient {
110
110
 
111
111
  onClose?: (reason: string) => void;
112
112
 
113
- onAnswer?: (sd: RTCSessionDescriptionInit) => void;
113
+ onAnswer?: (sd: RTCSessionDescriptionInit, offerId: number) => void;
114
114
 
115
- onOffer?: (sd: RTCSessionDescriptionInit) => void;
115
+ onOffer?: (sd: RTCSessionDescriptionInit, offerId: number) => void;
116
116
 
117
117
  // when a new ICE candidate is made available
118
118
  onTrickle?: (sd: RTCIceCandidateInit, target: SignalTarget) => void;
@@ -246,12 +246,12 @@ export class SignalClient {
246
246
  // clear ping interval and restart it once reconnected
247
247
  this.clearPingInterval();
248
248
 
249
- const res = await this.connect(url, token, {
249
+ const res = (await this.connect(url, token, {
250
250
  ...this.options,
251
251
  reconnect: true,
252
252
  sid,
253
253
  reconnectReason: reason,
254
- });
254
+ })) as ReconnectResponse;
255
255
  return res;
256
256
  }
257
257
 
@@ -506,20 +506,20 @@ export class SignalClient {
506
506
  }
507
507
 
508
508
  // initial offer after joining
509
- sendOffer(offer: RTCSessionDescriptionInit) {
509
+ sendOffer(offer: RTCSessionDescriptionInit, offerId: number) {
510
510
  this.log.debug('sending offer', { ...this.logContext, offerSdp: offer.sdp });
511
511
  this.sendRequest({
512
512
  case: 'offer',
513
- value: toProtoSessionDescription(offer),
513
+ value: toProtoSessionDescription(offer, offerId),
514
514
  });
515
515
  }
516
516
 
517
517
  // answer a server-initiated offer
518
- sendAnswer(answer: RTCSessionDescriptionInit) {
518
+ sendAnswer(answer: RTCSessionDescriptionInit, offerId: number) {
519
519
  this.log.debug('sending answer', { ...this.logContext, answerSdp: answer.sdp });
520
520
  return this.sendRequest({
521
521
  case: 'answer',
522
- value: toProtoSessionDescription(answer),
522
+ value: toProtoSessionDescription(answer, offerId),
523
523
  });
524
524
  }
525
525
 
@@ -700,12 +700,12 @@ export class SignalClient {
700
700
  if (msg.case === 'answer') {
701
701
  const sd = fromProtoSessionDescription(msg.value);
702
702
  if (this.onAnswer) {
703
- this.onAnswer(sd);
703
+ this.onAnswer(sd, msg.value.id);
704
704
  }
705
705
  } else if (msg.case === 'offer') {
706
706
  const sd = fromProtoSessionDescription(msg.value);
707
707
  if (this.onOffer) {
708
- this.onOffer(sd);
708
+ this.onOffer(sd, msg.value.id);
709
709
  }
710
710
  } else if (msg.case === 'trickle') {
711
711
  const candidate: RTCIceCandidateInit = JSON.parse(msg.value.candidateInit!);
@@ -888,10 +888,12 @@ function fromProtoSessionDescription(sd: SessionDescription): RTCSessionDescript
888
888
 
889
889
  export function toProtoSessionDescription(
890
890
  rsd: RTCSessionDescription | RTCSessionDescriptionInit,
891
+ id?: number,
891
892
  ): SessionDescription {
892
893
  const sd = new SessionDescription({
893
894
  sdp: rsd.sdp!,
894
895
  type: rsd.type!,
896
+ id,
895
897
  });
896
898
  return sd;
897
899
  }
@@ -83,6 +83,7 @@ export class PublishVideoCheck extends Checker {
83
83
  video.play();
84
84
  });
85
85
 
86
+ stream.getTracks().forEach((t) => t.stop());
86
87
  video.remove();
87
88
  }
88
89
  }
@@ -371,6 +371,7 @@ export class E2EEManager
371
371
  let writable: WritableStream = receiver.writableStream;
372
372
  // @ts-ignore
373
373
  let readable: ReadableStream = receiver.readableStream;
374
+
374
375
  if (!writable || !readable) {
375
376
  // @ts-ignore
376
377
  const receiverStreams = receiver.createEncodedStreams();
@@ -390,6 +391,7 @@ export class E2EEManager
390
391
  trackId: trackId,
391
392
  codec,
392
393
  participantIdentity: participantIdentity,
394
+ isReuse: E2EE_FLAG in receiver,
393
395
  },
394
396
  };
395
397
  this.worker.postMessage(msg, [readable, writable]);
@@ -435,6 +437,7 @@ export class E2EEManager
435
437
  codec,
436
438
  trackId,
437
439
  participantIdentity: this.room.localParticipant.identity,
440
+ isReuse: false,
438
441
  },
439
442
  };
440
443
  this.worker.postMessage(msg, [senderStreams.readable, senderStreams.writable]);
package/src/e2ee/types.ts CHANGED
@@ -49,6 +49,7 @@ export interface EncodeMessage extends BaseMessage {
49
49
  writableStream: WritableStream;
50
50
  trackId: string;
51
51
  codec?: VideoCodec;
52
+ isReuse: boolean;
52
53
  };
53
54
  }
54
55
 
@@ -69,6 +69,8 @@ export class FrameCryptor extends BaseFrameCryptor {
69
69
 
70
70
  private detectedCodec?: VideoCodec;
71
71
 
72
+ private isTransformActive: boolean = false;
73
+
72
74
  constructor(opts: {
73
75
  keys: ParticipantKeyHandler;
74
76
  participantIdentity: string;
@@ -159,6 +161,7 @@ export class FrameCryptor extends BaseFrameCryptor {
159
161
  readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
160
162
  writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
161
163
  trackId: string,
164
+ isReuse: boolean,
162
165
  codec?: VideoCodec,
163
166
  ) {
164
167
  if (codec) {
@@ -173,11 +176,20 @@ export class FrameCryptor extends BaseFrameCryptor {
173
176
  ...this.logContext,
174
177
  });
175
178
 
179
+ if (isReuse && this.isTransformActive) {
180
+ workerLogger.debug('reuse transform', {
181
+ ...this.logContext,
182
+ });
183
+ return;
184
+ }
185
+
176
186
  const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
177
187
  const transformStream = new TransformStream({
178
188
  transform: transformFn.bind(this),
179
189
  });
180
190
 
191
+ this.isTransformActive = true;
192
+
181
193
  readable
182
194
  .pipeThrough(transformStream)
183
195
  .pipeTo(writable)
@@ -189,6 +201,9 @@ export class FrameCryptor extends BaseFrameCryptor {
189
201
  ? e
190
202
  : new CryptorError(e.message, undefined, this.participantIdentity),
191
203
  );
204
+ })
205
+ .finally(() => {
206
+ this.isTransformActive = false;
192
207
  });
193
208
  this.trackId = trackId;
194
209
  }
@@ -65,6 +65,7 @@ onmessage = (ev) => {
65
65
  data.readableStream,
66
66
  data.writableStream,
67
67
  data.trackId,
68
+ data.isReuse,
68
69
  data.codec,
69
70
  );
70
71
  break;
@@ -75,6 +76,7 @@ onmessage = (ev) => {
75
76
  data.readableStream,
76
77
  data.writableStream,
77
78
  data.trackId,
79
+ data.isReuse,
78
80
  data.codec,
79
81
  );
80
82
  break;
@@ -50,6 +50,8 @@ export default class PCTransport extends EventEmitter {
50
50
 
51
51
  private ddExtID = 0;
52
52
 
53
+ private latestOfferId: number = 0;
54
+
53
55
  pendingCandidates: RTCIceCandidateInit[] = [];
54
56
 
55
57
  restartingIce: boolean = false;
@@ -62,7 +64,7 @@ export default class PCTransport extends EventEmitter {
62
64
 
63
65
  remoteNackMids: string[] = [];
64
66
 
65
- onOffer?: (offer: RTCSessionDescriptionInit) => void;
67
+ onOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
66
68
 
67
69
  onIceCandidate?: (candidate: RTCIceCandidate) => void;
68
70
 
@@ -137,7 +139,20 @@ export default class PCTransport extends EventEmitter {
137
139
  this.pendingCandidates.push(candidate);
138
140
  }
139
141
 
140
- async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<void> {
142
+ async setRemoteDescription(sd: RTCSessionDescriptionInit, offerId: number): Promise<boolean> {
143
+ if (
144
+ sd.type === 'answer' &&
145
+ this.latestOfferId > 0 &&
146
+ offerId > 0 &&
147
+ offerId !== this.latestOfferId
148
+ ) {
149
+ this.log.warn('ignoring answer for old offer', {
150
+ ...this.logContext,
151
+ offerId,
152
+ latestOfferId: this.latestOfferId,
153
+ });
154
+ return false;
155
+ }
141
156
  let mungedSDP: string | undefined = undefined;
142
157
  if (sd.type === 'offer') {
143
158
  let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
@@ -218,6 +233,7 @@ export default class PCTransport extends EventEmitter {
218
233
  });
219
234
  }
220
235
  }
236
+ return true;
221
237
  }
222
238
 
223
239
  // debounced negotiate interface
@@ -235,6 +251,9 @@ export default class PCTransport extends EventEmitter {
235
251
  }, debounceInterval);
236
252
 
237
253
  async createAndSendOffer(options?: RTCOfferOptions) {
254
+ // increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
255
+ const offerId = this.latestOfferId + 1;
256
+ this.latestOfferId = offerId;
238
257
  if (this.onOffer === undefined) {
239
258
  return;
240
259
  }
@@ -317,9 +336,16 @@ export default class PCTransport extends EventEmitter {
317
336
  });
318
337
  }
319
338
  });
320
-
339
+ if (this.latestOfferId > offerId) {
340
+ this.log.warn('latestOfferId mismatch', {
341
+ ...this.logContext,
342
+ latestOfferId: this.latestOfferId,
343
+ offerId,
344
+ });
345
+ return;
346
+ }
321
347
  await this.setMungedSDP(offer, write(sdpParsed));
322
- this.onOffer(offer);
348
+ this.onOffer(offer, this.latestOfferId);
323
349
  }
324
350
 
325
351
  async createAndSetAnswer(): Promise<RTCSessionDescriptionInit> {
@@ -48,7 +48,7 @@ export class PCTransportManager {
48
48
 
49
49
  public onTrack?: (ev: RTCTrackEvent) => void;
50
50
 
51
- public onPublisherOffer?: (offer: RTCSessionDescriptionInit) => void;
51
+ public onPublisherOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
52
52
 
53
53
  private isPublisherConnectionRequired: boolean;
54
54
 
@@ -96,8 +96,8 @@ export class PCTransportManager {
96
96
  this.subscriber.onTrack = (ev) => {
97
97
  this.onTrack?.(ev);
98
98
  };
99
- this.publisher.onOffer = (offer) => {
100
- this.onPublisherOffer?.(offer);
99
+ this.publisher.onOffer = (offer, offerId) => {
100
+ this.onPublisherOffer?.(offer, offerId);
101
101
  };
102
102
 
103
103
  this.state = PCTransportState.NEW;
@@ -126,8 +126,8 @@ export class PCTransportManager {
126
126
  return this.publisher.createAndSendOffer(options);
127
127
  }
128
128
 
129
- setPublisherAnswer(sd: RTCSessionDescriptionInit) {
130
- return this.publisher.setRemoteDescription(sd);
129
+ setPublisherAnswer(sd: RTCSessionDescriptionInit, offerId: number) {
130
+ return this.publisher.setRemoteDescription(sd, offerId);
131
131
  }
132
132
 
133
133
  removeTrack(sender: RTCRtpSender) {
@@ -168,7 +168,7 @@ export class PCTransportManager {
168
168
  }
169
169
  }
170
170
 
171
- async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit) {
171
+ async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit, offerId: number) {
172
172
  this.log.debug('received server offer', {
173
173
  ...this.logContext,
174
174
  RTCSdpType: sd.type,
@@ -177,7 +177,10 @@ export class PCTransportManager {
177
177
  });
178
178
  const unlock = await this.remoteOfferLock.lock();
179
179
  try {
180
- await this.subscriber.setRemoteDescription(sd);
180
+ const success = await this.subscriber.setRemoteDescription(sd, offerId);
181
+ if (!success) {
182
+ return undefined;
183
+ }
181
184
 
182
185
  // answer the offer
183
186
  const answer = await this.subscriber.createAndSetAnswer();
@@ -5,6 +5,7 @@ import {
5
5
  ClientConfiguration,
6
6
  type ConnectionQualityUpdate,
7
7
  DataChannelInfo,
8
+ DataChannelReceiveState,
8
9
  DataPacket,
9
10
  DataPacket_Kind,
10
11
  DisconnectReason,
@@ -44,6 +45,8 @@ import {
44
45
  } from '../api/SignalClient';
45
46
  import log, { LoggerNames, getLogger } from '../logger';
46
47
  import type { InternalRoomOptions } from '../options';
48
+ import { DataPacketBuffer } from '../utils/dataPacketBuffer';
49
+ import { TTLMap } from '../utils/ttlmap';
47
50
  import PCTransport, { PCEvents } from './PCTransport';
48
51
  import { PCTransportManager, PCTransportState } from './PCTransportManager';
49
52
  import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
@@ -81,6 +84,7 @@ const lossyDataChannel = '_lossy';
81
84
  const reliableDataChannel = '_reliable';
82
85
  const minReconnectWait = 2 * 1000;
83
86
  const leaveReconnect = 'leave-reconnect';
87
+ const reliabeReceiveStateTTL = 30_000;
84
88
 
85
89
  enum PCState {
86
90
  New,
@@ -107,6 +111,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
107
111
  */
108
112
  latestJoinResponse?: JoinResponse;
109
113
 
114
+ /**
115
+ * @internal
116
+ */
117
+ latestRemoteOfferId: number = 0;
118
+
110
119
  get isClosed() {
111
120
  return this._isClosed;
112
121
  }
@@ -178,6 +187,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
178
187
 
179
188
  private publisherConnectionPromise: Promise<void> | undefined;
180
189
 
190
+ private reliableDataSequence: number = 1;
191
+
192
+ private reliableMessageBuffer = new DataPacketBuffer();
193
+
194
+ private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL);
195
+
181
196
  constructor(private options: InternalRoomOptions) {
182
197
  super();
183
198
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine);
@@ -310,6 +325,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
310
325
  this.lossyDCSub = undefined;
311
326
  this.reliableDC = undefined;
312
327
  this.reliableDCSub = undefined;
328
+ this.reliableMessageBuffer = new DataPacketBuffer();
329
+ this.reliableDataSequence = 1;
330
+ this.reliableReceivedState.clear();
313
331
  }
314
332
 
315
333
  async cleanupClient() {
@@ -407,8 +425,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
407
425
  this.client.sendIceCandidate(candidate, target);
408
426
  };
409
427
 
410
- this.pcManager.onPublisherOffer = (offer) => {
411
- this.client.sendOffer(offer);
428
+ this.pcManager.onPublisherOffer = (offer, offerId) => {
429
+ this.client.sendOffer(offer, offerId);
412
430
  };
413
431
 
414
432
  this.pcManager.onDataChannel = this.handleDataChannel;
@@ -463,12 +481,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
463
481
 
464
482
  private setupSignalClientCallbacks() {
465
483
  // configure signaling client
466
- this.client.onAnswer = async (sd) => {
484
+ this.client.onAnswer = async (sd, offerId) => {
467
485
  if (!this.pcManager) {
468
486
  return;
469
487
  }
470
488
  this.log.debug('received server answer', { ...this.logContext, RTCSdpType: sd.type });
471
- await this.pcManager.setPublisherAnswer(sd);
489
+ await this.pcManager.setPublisherAnswer(sd, offerId);
472
490
  };
473
491
 
474
492
  // add candidate on trickle
@@ -481,12 +499,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
481
499
  };
482
500
 
483
501
  // when server creates an offer for the client
484
- this.client.onOffer = async (sd) => {
502
+ this.client.onOffer = async (sd, offerId) => {
503
+ this.latestRemoteOfferId = offerId;
485
504
  if (!this.pcManager) {
486
505
  return;
487
506
  }
488
- const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd);
489
- this.client.sendAnswer(answer);
507
+ const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd, offerId);
508
+ if (answer) {
509
+ this.client.sendAnswer(answer, offerId);
510
+ }
490
511
  };
491
512
 
492
513
  this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
@@ -677,6 +698,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
677
698
  }
678
699
  const dp = DataPacket.fromBinary(new Uint8Array(buffer));
679
700
 
701
+ if (dp.sequence > 0 && dp.participantSid !== '') {
702
+ const lastSeq = this.reliableReceivedState.get(dp.participantSid);
703
+ if (lastSeq && dp.sequence <= lastSeq) {
704
+ // ignore duplicate or out-of-order packets in reliable channel
705
+ return;
706
+ }
707
+ this.reliableReceivedState.set(dp.participantSid, dp.sequence);
708
+ }
709
+
680
710
  if (dp.value?.case === 'speaker') {
681
711
  // dispatch speaker updates
682
712
  this.emit(EngineEvent.ActiveSpeakersUpdate, dp.value.value.speakers);
@@ -1033,6 +1063,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1033
1063
  if (res) {
1034
1064
  const rtcConfig = this.makeRTCConfiguration(res);
1035
1065
  this.pcManager.updateConfiguration(rtcConfig);
1066
+ if (this.latestJoinResponse) {
1067
+ this.latestJoinResponse.serverInfo = res.serverInfo;
1068
+ }
1036
1069
  } else {
1037
1070
  this.log.warn('Did not receive reconnect response', this.logContext);
1038
1071
  }
@@ -1059,6 +1092,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1059
1092
  this.createDataChannels();
1060
1093
  }
1061
1094
 
1095
+ if (res?.lastMessageSeq) {
1096
+ this.resendReliableMessagesForResume(res.lastMessageSeq);
1097
+ }
1098
+
1062
1099
  // resume success
1063
1100
  this.emit(EngineEvent.Resumed);
1064
1101
  }
@@ -1151,19 +1188,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1151
1188
 
1152
1189
  /* @internal */
1153
1190
  async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) {
1154
- const msg = packet.toBinary();
1155
-
1156
1191
  // make sure we do have a data connection
1157
1192
  await this.ensurePublisherConnected(kind);
1158
1193
 
1194
+ if (kind === DataPacket_Kind.RELIABLE) {
1195
+ packet.sequence = this.reliableDataSequence;
1196
+ this.reliableDataSequence += 1;
1197
+ }
1198
+ const msg = packet.toBinary();
1159
1199
  const dc = this.dataChannelForKind(kind);
1160
1200
  if (dc) {
1201
+ if (kind === DataPacket_Kind.RELIABLE) {
1202
+ this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence });
1203
+ }
1204
+
1205
+ if (this.attemptingReconnect) {
1206
+ return;
1207
+ }
1208
+
1161
1209
  dc.send(msg);
1162
1210
  }
1163
1211
 
1164
1212
  this.updateAndEmitDCBufferStatus(kind);
1165
1213
  }
1166
1214
 
1215
+ private async resendReliableMessagesForResume(lastMessageSeq: number) {
1216
+ await this.ensurePublisherConnected(DataPacket_Kind.RELIABLE);
1217
+ const dc = this.dataChannelForKind(DataPacket_Kind.RELIABLE);
1218
+ if (dc) {
1219
+ this.reliableMessageBuffer.popToSequence(lastMessageSeq);
1220
+ this.reliableMessageBuffer.getAll().forEach((msg) => {
1221
+ dc.send(msg.data);
1222
+ });
1223
+ }
1224
+ this.updateAndEmitDCBufferStatus(DataPacket_Kind.RELIABLE);
1225
+ }
1226
+
1167
1227
  private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => {
1168
1228
  const status = this.isBufferStatusLow(kind);
1169
1229
  if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
@@ -1175,6 +1235,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1175
1235
  private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => {
1176
1236
  const dc = this.dataChannelForKind(kind);
1177
1237
  if (dc) {
1238
+ if (kind === DataPacket_Kind.RELIABLE) {
1239
+ this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
1240
+ }
1178
1241
  return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
1179
1242
  }
1180
1243
  };
@@ -1409,6 +1472,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1409
1472
  publishTracks: getTrackPublicationInfo(localTracks),
1410
1473
  dataChannels: this.dataChannelsInfo(),
1411
1474
  trackSidsDisabled,
1475
+ datachannelReceiveStates: this.reliableReceivedState.map((seq, sid) => {
1476
+ return new DataChannelReceiveState({
1477
+ publisherSid: sid,
1478
+ lastSeq: seq,
1479
+ });
1480
+ }),
1412
1481
  }),
1413
1482
  );
1414
1483
  }
package/src/room/Room.ts CHANGED
@@ -601,7 +601,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
601
601
  this.handleParticipantDisconnected(identity, participant);
602
602
  });
603
603
 
604
- this.emit(RoomEvent.Moved, roomMoved.room!.name, roomMoved.token);
604
+ this.emit(RoomEvent.Moved, roomMoved.room!.name);
605
605
 
606
606
  if (roomMoved.participant) {
607
607
  this.handleParticipantUpdates([roomMoved.participant, ...roomMoved.otherParticipants]);
@@ -1301,10 +1301,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1301
1301
  */
1302
1302
  async switchActiveDevice(kind: MediaDeviceKind, deviceId: string, exact: boolean = true) {
1303
1303
  let success = true;
1304
- let needsUpdateWithoutTracks = false;
1304
+ let shouldTriggerImmediateDeviceChange = false;
1305
1305
  const deviceConstraint = exact ? { exact: deviceId } : deviceId;
1306
1306
  if (kind === 'audioinput') {
1307
- needsUpdateWithoutTracks = this.localParticipant.audioTrackPublications.size === 0;
1307
+ shouldTriggerImmediateDeviceChange = this.localParticipant.audioTrackPublications.size === 0;
1308
1308
  const prevDeviceId =
1309
1309
  this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
1310
1310
  this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
@@ -1319,8 +1319,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1319
1319
  this.options.audioCaptureDefaults!.deviceId = prevDeviceId;
1320
1320
  throw e;
1321
1321
  }
1322
+ const isMuted = tracks.some((t) => t.track?.isMuted ?? false);
1323
+ if (success && isMuted) shouldTriggerImmediateDeviceChange = true;
1322
1324
  } else if (kind === 'videoinput') {
1323
- needsUpdateWithoutTracks = this.localParticipant.videoTrackPublications.size === 0;
1325
+ shouldTriggerImmediateDeviceChange = this.localParticipant.videoTrackPublications.size === 0;
1324
1326
  const prevDeviceId =
1325
1327
  this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
1326
1328
  this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
@@ -1336,6 +1338,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1336
1338
  throw e;
1337
1339
  }
1338
1340
  } else if (kind === 'audiooutput') {
1341
+ shouldTriggerImmediateDeviceChange = true;
1339
1342
  if (
1340
1343
  (!supportsSetSinkId() && !this.options.webAudioMix) ||
1341
1344
  (this.options.webAudioMix && this.audioContext && !('setSinkId' in this.audioContext))
@@ -1367,12 +1370,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1367
1370
  throw e;
1368
1371
  }
1369
1372
  }
1370
- if (needsUpdateWithoutTracks || kind === 'audiooutput') {
1371
- // if there are not active tracks yet or we're switching audiooutput, we need to manually update the active device map here as changing audio output won't result in a track restart
1372
- this.localParticipant.activeDeviceMap.set(
1373
- kind,
1374
- (kind === 'audiooutput' && this.options.audioOutput?.deviceId) || deviceId,
1375
- );
1373
+
1374
+ if (shouldTriggerImmediateDeviceChange) {
1375
+ this.localParticipant.activeDeviceMap.set(kind, deviceId);
1376
1376
  this.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
1377
1377
  }
1378
1378
 
@@ -2649,7 +2649,7 @@ export type RoomEventCallbacks = {
2649
2649
  reconnected: () => void;
2650
2650
  disconnected: (reason?: DisconnectReason) => void;
2651
2651
  connectionStateChanged: (state: ConnectionState) => void;
2652
- moved: (name: string, token: string) => void;
2652
+ moved: (name: string) => void;
2653
2653
  mediaDevicesChanged: () => void;
2654
2654
  participantConnected: (participant: RemoteParticipant) => void;
2655
2655
  participantDisconnected: (participant: RemoteParticipant) => void;