livekit-client 1.14.0 → 1.14.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) 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 +25 -44
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +418 -206
  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/e2ee/E2eeManager.d.ts.map +1 -1
  10. package/dist/src/e2ee/utils.d.ts +0 -1
  11. package/dist/src/e2ee/utils.d.ts.map +1 -1
  12. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  13. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  14. package/dist/src/proto/livekit_models_pb.d.ts +87 -11
  15. package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
  16. package/dist/src/proto/livekit_rtc_pb.d.ts +0 -4
  17. package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
  18. package/dist/src/room/PCTransport.d.ts +20 -1
  19. package/dist/src/room/PCTransport.d.ts.map +1 -1
  20. package/dist/src/room/RTCEngine.d.ts +1 -1
  21. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  22. package/dist/src/room/Room.d.ts.map +1 -1
  23. package/dist/src/room/defaults.d.ts +1 -0
  24. package/dist/src/room/defaults.d.ts.map +1 -1
  25. package/dist/src/room/events.d.ts +1 -1
  26. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  27. package/dist/src/room/timers.d.ts +1 -1
  28. package/dist/src/room/timers.d.ts.map +1 -1
  29. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  30. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  31. package/dist/src/room/track/LocalTrack.d.ts +3 -3
  32. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/LocalVideoTrack.d.ts +2 -1
  34. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  35. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  36. package/dist/src/room/track/options.d.ts +6 -1
  37. package/dist/src/room/track/options.d.ts.map +1 -1
  38. package/dist/src/room/track/utils.d.ts +2 -1
  39. package/dist/src/room/track/utils.d.ts.map +1 -1
  40. package/dist/src/room/utils.d.ts.map +1 -1
  41. package/dist/src/utils/cloneDeep.d.ts +2 -0
  42. package/dist/src/utils/cloneDeep.d.ts.map +1 -0
  43. package/dist/src/version.d.ts +1 -1
  44. package/dist/src/version.d.ts.map +1 -1
  45. package/dist/ts4.2/src/e2ee/utils.d.ts +0 -1
  46. package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +87 -11
  47. package/dist/ts4.2/src/proto/livekit_rtc_pb.d.ts +0 -4
  48. package/dist/ts4.2/src/room/PCTransport.d.ts +20 -1
  49. package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -1
  50. package/dist/ts4.2/src/room/defaults.d.ts +1 -0
  51. package/dist/ts4.2/src/room/events.d.ts +1 -1
  52. package/dist/ts4.2/src/room/timers.d.ts +1 -1
  53. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
  54. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
  55. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +2 -1
  56. package/dist/ts4.2/src/room/track/options.d.ts +6 -1
  57. package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
  58. package/dist/ts4.2/src/utils/cloneDeep.d.ts +2 -0
  59. package/dist/ts4.2/src/version.d.ts +1 -1
  60. package/package.json +14 -14
  61. package/src/connectionHelper/checks/webrtc.ts +1 -1
  62. package/src/e2ee/E2eeManager.ts +2 -1
  63. package/src/e2ee/utils.ts +0 -10
  64. package/src/e2ee/worker/FrameCryptor.ts +13 -14
  65. package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -5
  66. package/src/e2ee/worker/e2ee.worker.ts +3 -1
  67. package/src/proto/livekit_models_pb.ts +140 -15
  68. package/src/proto/livekit_rtc_pb.ts +1 -7
  69. package/src/room/PCTransport.ts +116 -1
  70. package/src/room/RTCEngine.ts +49 -85
  71. package/src/room/Room.ts +15 -11
  72. package/src/room/defaults.ts +4 -2
  73. package/src/room/events.ts +1 -1
  74. package/src/room/participant/LocalParticipant.ts +45 -56
  75. package/src/room/track/LocalAudioTrack.ts +1 -1
  76. package/src/room/track/LocalTrack.ts +8 -5
  77. package/src/room/track/LocalVideoTrack.ts +2 -1
  78. package/src/room/track/RemoteTrack.ts +8 -6
  79. package/src/room/track/options.ts +7 -7
  80. package/src/room/track/utils.ts +17 -8
  81. package/src/room/utils.ts +3 -0
  82. package/src/utils/cloneDeep.test.ts +54 -0
  83. package/src/utils/cloneDeep.ts +11 -0
  84. package/src/version.ts +1 -1
@@ -108,7 +108,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
108
108
 
109
109
  private subscriberPrimary: boolean = false;
110
110
 
111
- private primaryPC?: RTCPeerConnection;
111
+ private primaryTransport?: PCTransport;
112
112
 
113
113
  private pcState: PCState = PCState.New;
114
114
 
@@ -247,12 +247,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
247
247
  }
248
248
 
249
249
  async cleanupPeerConnections() {
250
- if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
251
- this.publisher.pc.getSenders().forEach((sender) => {
250
+ if (this.publisher && this.publisher.getSignallingState() !== 'closed') {
251
+ this.publisher.getSenders().forEach((sender) => {
252
252
  try {
253
253
  // TODO: react-native-webrtc doesn't have removeTrack yet.
254
- if (this.publisher?.pc.removeTrack) {
255
- this.publisher?.pc.removeTrack(sender);
254
+ if (this.publisher?.canRemoveTrack()) {
255
+ this.publisher?.removeTrack(sender);
256
256
  }
257
257
  } catch (e) {
258
258
  log.warn('could not removeTrack', { error: e });
@@ -268,7 +268,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
268
268
  this.subscriber = undefined;
269
269
  }
270
270
  this.hasPublished = false;
271
- this.primaryPC = undefined;
271
+ this.primaryTransport = undefined;
272
272
 
273
273
  const dcCleanup = (dc: RTCDataChannel | undefined) => {
274
274
  if (!dc) return;
@@ -336,7 +336,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
336
336
  delete this.pendingTrackResolvers[sender.track.id];
337
337
  }
338
338
  try {
339
- this.publisher?.pc.removeTrack(sender);
339
+ this.publisher?.removeTrack(sender);
340
340
  return true;
341
341
  } catch (e: unknown) {
342
342
  log.warn('failed to remove track', { error: e, method: 'removeTrack' });
@@ -353,10 +353,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
353
353
  }
354
354
 
355
355
  async getConnectedServerAddress(): Promise<string | undefined> {
356
- if (this.primaryPC === undefined) {
356
+ if (this.primaryTransport === undefined) {
357
357
  return undefined;
358
358
  }
359
- return getConnectedAddress(this.primaryPC);
359
+ return this.primaryTransport.getConnectedAddress();
360
360
  }
361
361
 
362
362
  /* @internal */
@@ -387,40 +387,38 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
387
387
 
388
388
  this.emit(EngineEvent.TransportsCreated, this.publisher, this.subscriber);
389
389
 
390
- this.publisher.pc.onicecandidate = (ev) => {
391
- if (!ev.candidate) return;
392
- log.trace('adding ICE candidate for peer', ev.candidate);
393
- this.client.sendIceCandidate(ev.candidate, SignalTarget.PUBLISHER);
390
+ this.publisher.onIceCandidate = (candidate) => {
391
+ log.trace('adding ICE candidate for peer', candidate);
392
+ this.client.sendIceCandidate(candidate, SignalTarget.PUBLISHER);
394
393
  };
395
394
 
396
- this.subscriber.pc.onicecandidate = (ev) => {
397
- if (!ev.candidate) return;
398
- this.client.sendIceCandidate(ev.candidate, SignalTarget.SUBSCRIBER);
395
+ this.subscriber.onIceCandidate = (candidate) => {
396
+ this.client.sendIceCandidate(candidate, SignalTarget.SUBSCRIBER);
399
397
  };
400
398
 
401
399
  this.publisher.onOffer = (offer) => {
402
400
  this.client.sendOffer(offer);
403
401
  };
404
402
 
405
- let primaryPC = this.publisher.pc;
406
- let secondaryPC = this.subscriber.pc;
403
+ let primaryTransport = this.publisher;
404
+ let secondaryTransport = this.subscriber;
407
405
  let subscriberPrimary = joinResponse.subscriberPrimary;
408
406
  if (subscriberPrimary) {
409
- primaryPC = this.subscriber.pc;
410
- secondaryPC = this.publisher.pc;
407
+ primaryTransport = this.subscriber;
408
+ secondaryTransport = this.publisher;
411
409
  // in subscriber primary mode, server side opens sub data channels.
412
- this.subscriber.pc.ondatachannel = this.handleDataChannel;
410
+ this.subscriber.onDataChannel = this.handleDataChannel;
413
411
  }
414
- this.primaryPC = primaryPC;
415
- primaryPC.onconnectionstatechange = async () => {
416
- log.debug(`primary PC state changed ${primaryPC.connectionState}`);
417
- if (primaryPC.connectionState === 'connected') {
412
+ this.primaryTransport = primaryTransport;
413
+ primaryTransport.onConnectionStateChange = async (connectionState) => {
414
+ log.debug(`primary PC state changed ${connectionState}`);
415
+ if (connectionState === 'connected') {
418
416
  const shouldEmit = this.pcState === PCState.New;
419
417
  this.pcState = PCState.Connected;
420
418
  if (shouldEmit) {
421
419
  this.emit(EngineEvent.Connected, joinResponse);
422
420
  }
423
- } else if (primaryPC.connectionState === 'failed') {
421
+ } else if (connectionState === 'failed') {
424
422
  // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
425
423
  if (this.pcState === PCState.Connected) {
426
424
  this.pcState = PCState.Disconnected;
@@ -434,10 +432,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
434
432
  }
435
433
  }
436
434
  };
437
- secondaryPC.onconnectionstatechange = async () => {
438
- log.debug(`secondary PC state changed ${secondaryPC.connectionState}`);
435
+ secondaryTransport.onConnectionStateChange = async (connectionState) => {
436
+ log.debug(`secondary PC state changed ${connectionState}`);
439
437
  // also reconnect if secondary peerconnection fails
440
- if (secondaryPC.connectionState === 'failed') {
438
+ if (connectionState === 'failed') {
441
439
  this.handleDisconnect(
442
440
  'secondary peerconnection',
443
441
  subscriberPrimary
@@ -447,7 +445,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
447
445
  }
448
446
  };
449
447
 
450
- this.subscriber.pc.ontrack = (ev: RTCTrackEvent) => {
448
+ this.subscriber.onTrack = (ev: RTCTrackEvent) => {
451
449
  this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
452
450
  };
453
451
 
@@ -462,7 +460,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
462
460
  }
463
461
  log.debug('received server answer', {
464
462
  RTCSdpType: sd.type,
465
- signalingState: this.publisher.pc.signalingState.toString(),
463
+ signalingState: this.publisher.getSignallingState().toString(),
466
464
  });
467
465
  await this.publisher.setRemoteDescription(sd);
468
466
  };
@@ -487,7 +485,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
487
485
  }
488
486
  log.debug('received server offer', {
489
487
  RTCSdpType: sd.type,
490
- signalingState: this.subscriber.pc.signalingState.toString(),
488
+ signalingState: this.subscriber.getSignallingState().toString(),
491
489
  });
492
490
  await this.subscriber.setRemoteDescription(sd);
493
491
 
@@ -518,7 +516,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
518
516
  this.client.onLeave = (leave?: LeaveRequest) => {
519
517
  if (leave?.canReconnect) {
520
518
  this.fullReconnectOnNext = true;
521
- this.primaryPC = undefined;
519
+ this.primaryTransport = undefined;
522
520
  // reconnect immediately instead of waiting for next attempt
523
521
  this.handleDisconnect(leaveReconnect);
524
522
  } else {
@@ -579,12 +577,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
579
577
  }
580
578
 
581
579
  // create data channels
582
- this.lossyDC = this.publisher.pc.createDataChannel(lossyDataChannel, {
580
+ this.lossyDC = this.publisher.createDataChannel(lossyDataChannel, {
583
581
  // will drop older packets that arrive
584
582
  ordered: true,
585
583
  maxRetransmits: 0,
586
584
  });
587
- this.reliableDC = this.publisher.pc.createDataChannel(reliableDataChannel, {
585
+ this.reliableDC = this.publisher.createDataChannel(reliableDataChannel, {
588
586
  ordered: true,
589
587
  });
590
588
 
@@ -765,7 +763,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
765
763
  transceiverInit.sendEncodings = encodings;
766
764
  }
767
765
  // addTransceiver for react-native is async. web is synchronous, but await won't effect it.
768
- const transceiver = await this.publisher.pc.addTransceiver(
766
+ const transceiver = await this.publisher.addTransceiver(
769
767
  track.mediaStreamTrack,
770
768
  transceiverInit,
771
769
  );
@@ -791,7 +789,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
791
789
  transceiverInit.sendEncodings = encodings;
792
790
  }
793
791
  // addTransceiver for react-native is async. web is synchronous, but await won't effect it.
794
- const transceiver = await this.publisher.pc.addTransceiver(
792
+ const transceiver = await this.publisher.addTransceiver(
795
793
  simulcastTrack.mediaStreamTrack,
796
794
  transceiverInit,
797
795
  );
@@ -807,7 +805,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
807
805
  if (!this.publisher) {
808
806
  throw new UnexpectedConnectionState('publisher is closed');
809
807
  }
810
- return this.publisher.pc.addTrack(track);
808
+ return this.publisher.addTrack(track);
811
809
  }
812
810
 
813
811
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
@@ -872,7 +870,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
872
870
  this.clientConfiguration?.resumeConnection === ClientConfigSetting.DISABLED ||
873
871
  // signaling state could change to closed due to hardware sleep
874
872
  // those connections cannot be resumed
875
- (this.primaryPC?.signalingState ?? 'closed') === 'closed'
873
+ (this.primaryTransport?.getSignallingState() ?? 'closed') === 'closed'
876
874
  ) {
877
875
  this.fullReconnectOnNext = true;
878
876
  }
@@ -999,8 +997,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
999
997
  const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
1000
998
  if (res) {
1001
999
  const rtcConfig = this.makeRTCConfiguration(res);
1002
- this.publisher.pc.setConfiguration(rtcConfig);
1003
- this.subscriber.pc.setConfiguration(rtcConfig);
1000
+ this.publisher.setConfiguration(rtcConfig);
1001
+ this.subscriber.setConfiguration(rtcConfig);
1004
1002
  }
1005
1003
  } catch (e) {
1006
1004
  let message = '';
@@ -1084,7 +1082,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1084
1082
 
1085
1083
  log.debug('waiting for peer connection to reconnect');
1086
1084
  while (now - startTime < this.peerConnectionTimeout) {
1087
- if (this.primaryPC === undefined) {
1085
+ if (this.primaryTransport === undefined) {
1088
1086
  // we can abort early, connection is hosed
1089
1087
  break;
1090
1088
  } else if (
@@ -1092,8 +1090,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1092
1090
  // this means we'd have to check its status manually and update address
1093
1091
  // manually
1094
1092
  now - startTime > minReconnectWait &&
1095
- this.primaryPC?.connectionState === 'connected' &&
1096
- (!this.hasPublished || this.publisher?.pc.connectionState === 'connected')
1093
+ this.primaryTransport?.getConnectionState() === 'connected' &&
1094
+ (!this.hasPublished || this.publisher?.getConnectionState() === 'connected')
1097
1095
  ) {
1098
1096
  this.pcState = PCState.Connected;
1099
1097
  }
@@ -1172,7 +1170,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1172
1170
  if (
1173
1171
  !subscriber &&
1174
1172
  !this.publisher?.isICEConnected &&
1175
- this.publisher?.pc.iceConnectionState !== 'checking'
1173
+ this.publisher?.getICEConnectionState() !== 'checking'
1176
1174
  ) {
1177
1175
  // start negotiation
1178
1176
  this.negotiate();
@@ -1196,7 +1194,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1196
1194
  }
1197
1195
 
1198
1196
  throw new ConnectionError(
1199
- `could not establish ${transportName} connection, state: ${transport.pc.iceConnectionState}`,
1197
+ `could not establish ${transportName} connection, state: ${transport.getICEConnectionState()}`,
1200
1198
  );
1201
1199
  }
1202
1200
 
@@ -1207,12 +1205,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1207
1205
  /* @internal */
1208
1206
  verifyTransport(): boolean {
1209
1207
  // primary connection
1210
- if (!this.primaryPC) {
1208
+ if (!this.primaryTransport) {
1211
1209
  return false;
1212
1210
  }
1213
1211
  if (
1214
- this.primaryPC.connectionState === 'closed' ||
1215
- this.primaryPC.connectionState === 'failed'
1212
+ this.primaryTransport.getConnectionState() === 'closed' ||
1213
+ this.primaryTransport.getConnectionState() === 'failed'
1216
1214
  ) {
1217
1215
  return false;
1218
1216
  }
@@ -1223,8 +1221,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1223
1221
  return false;
1224
1222
  }
1225
1223
  if (
1226
- this.publisher.pc.connectionState === 'closed' ||
1227
- this.publisher.pc.connectionState === 'failed'
1224
+ this.publisher.getConnectionState() === 'closed' ||
1225
+ this.publisher.getConnectionState() === 'failed'
1228
1226
  ) {
1229
1227
  return false;
1230
1228
  }
@@ -1355,40 +1353,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1355
1353
  }
1356
1354
  }
1357
1355
 
1358
- async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | undefined> {
1359
- let selectedCandidatePairId = '';
1360
- const candidatePairs = new Map<string, RTCIceCandidatePairStats>();
1361
- // id -> candidate ip
1362
- const candidates = new Map<string, string>();
1363
- const stats: RTCStatsReport = await pc.getStats();
1364
- stats.forEach((v) => {
1365
- switch (v.type) {
1366
- case 'transport':
1367
- selectedCandidatePairId = v.selectedCandidatePairId;
1368
- break;
1369
- case 'candidate-pair':
1370
- if (selectedCandidatePairId === '' && v.selected) {
1371
- selectedCandidatePairId = v.id;
1372
- }
1373
- candidatePairs.set(v.id, v);
1374
- break;
1375
- case 'remote-candidate':
1376
- candidates.set(v.id, `${v.address}:${v.port}`);
1377
- break;
1378
- default:
1379
- }
1380
- });
1381
-
1382
- if (selectedCandidatePairId === '') {
1383
- return undefined;
1384
- }
1385
- const selectedID = candidatePairs.get(selectedCandidatePairId)?.remoteCandidateId;
1386
- if (selectedID === undefined) {
1387
- return undefined;
1388
- }
1389
- return candidates.get(selectedID);
1390
- }
1391
-
1392
1356
  class SignalReconnectError extends Error {}
1393
1357
 
1394
1358
  export type EngineEventCallbacks = {
package/src/room/Room.ts CHANGED
@@ -452,6 +452,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
452
452
  error instanceof ConnectionError &&
453
453
  (error.status === 401 || error.reason === ConnectionErrorReason.Cancelled)
454
454
  ) {
455
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
455
456
  reject(error);
456
457
  return;
457
458
  }
@@ -462,9 +463,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
462
463
  );
463
464
  await connectFn(resolve, reject, nextUrl);
464
465
  } else {
466
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
465
467
  reject(e);
466
468
  }
467
469
  } else {
470
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
468
471
  reject(e);
469
472
  }
470
473
  }
@@ -593,8 +596,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
593
596
  this.setupLocalParticipantEvents();
594
597
  this.emit(RoomEvent.SignalConnected);
595
598
  } catch (err) {
599
+ await this.engine.close();
596
600
  this.recreateEngine();
597
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
598
601
  const resultingError = new ConnectionError(`could not establish signal connection`);
599
602
  if (err instanceof Error) {
600
603
  resultingError.message = `${resultingError.message}: ${err.message}`;
@@ -608,8 +611,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
608
611
  }
609
612
 
610
613
  if (abortController.signal.aborted) {
614
+ await this.engine.close();
611
615
  this.recreateEngine();
612
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
613
616
  throw new ConnectionError(`Connection attempt aborted`);
614
617
  }
615
618
 
@@ -619,8 +622,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
619
622
  abortController,
620
623
  );
621
624
  } catch (e) {
625
+ await this.engine.close();
622
626
  this.recreateEngine();
623
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
624
627
  throw e;
625
628
  }
626
629
 
@@ -1018,8 +1021,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1018
1021
  }
1019
1022
  const parts = unpackStreamId(stream.id);
1020
1023
  const participantId = parts[0];
1021
- let trackId = parts[1];
1022
- if (!trackId || trackId === '') trackId = mediaTrack.id;
1024
+ let streamId = parts[1];
1025
+ let trackId = mediaTrack.id;
1026
+ // firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
1027
+ // and generates its own track id instead of infer from sdp track id.
1028
+ if (streamId && streamId.startsWith('TR')) trackId = streamId;
1023
1029
 
1024
1030
  if (participantId === this.localParticipant.sid) {
1025
1031
  log.warn('tried to create RemoteParticipant for local participant');
@@ -1543,14 +1549,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1543
1549
  }
1544
1550
 
1545
1551
  private sendSyncState() {
1546
- if (
1547
- this.engine.subscriber === undefined ||
1548
- this.engine.subscriber.pc.localDescription === null
1549
- ) {
1552
+ const previousAnswer = this.engine.subscriber?.getLocalDescription();
1553
+ const previousOffer = this.engine.subscriber?.getRemoteDescription();
1554
+
1555
+ if (!previousAnswer) {
1550
1556
  return;
1551
1557
  }
1552
- const previousAnswer = this.engine.subscriber.pc.localDescription;
1553
- const previousOffer = this.engine.subscriber.pc.remoteDescription;
1554
1558
 
1555
1559
  /* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
1556
1560
  in this case, we send unsub tracks, so server add all tracks to this
@@ -1,11 +1,13 @@
1
1
  import type { InternalRoomConnectOptions, InternalRoomOptions } from '../options';
2
2
  import DefaultReconnectPolicy from './DefaultReconnectPolicy';
3
- import { AudioPresets, ScreenSharePresets, VideoPresets } from './track/options';
4
3
  import type {
5
4
  AudioCaptureOptions,
6
5
  TrackPublishDefaults,
7
6
  VideoCaptureOptions,
8
7
  } from './track/options';
8
+ import { AudioPresets, ScreenSharePresets, VideoPresets } from './track/options';
9
+
10
+ export const defaultVideoCodec = 'vp8';
9
11
 
10
12
  export const publishDefaults: TrackPublishDefaults = {
11
13
  /**
@@ -19,7 +21,7 @@ export const publishDefaults: TrackPublishDefaults = {
19
21
  simulcast: true,
20
22
  screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
21
23
  stopMicTrackOnMute: false,
22
- videoCodec: 'vp8',
24
+ videoCodec: defaultVideoCodec,
23
25
  backupCodec: false,
24
26
  } as const;
25
27
 
@@ -223,7 +223,7 @@ export enum RoomEvent {
223
223
  * be emitted.
224
224
  *
225
225
  * args: (pub: [[RemoteTrackPublication]],
226
- * status: [[TrackPublication.SubscriptionStatus]],
226
+ * status: [[TrackPublication.PermissionStatus]],
227
227
  * participant: [[RemoteParticipant]])
228
228
  */
229
229
  TrackSubscriptionPermissionChanged = 'trackSubscriptionPermissionChanged',
@@ -18,6 +18,7 @@ import {
18
18
  TrackUnpublishedResponse,
19
19
  } from '../../proto/livekit_rtc_pb';
20
20
  import type RTCEngine from '../RTCEngine';
21
+ import { defaultVideoCodec } from '../defaults';
21
22
  import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
22
23
  import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
23
24
  import LocalAudioTrack from '../track/LocalAudioTrack';
@@ -33,10 +34,11 @@ import type {
33
34
  TrackPublishOptions,
34
35
  VideoCaptureOptions,
35
36
  } from '../track/options';
36
- import { VideoPresets, isBackupCodec, isCodecEqual } from '../track/options';
37
+ import { VideoPresets, isBackupCodec } from '../track/options';
37
38
  import {
38
39
  constraintsForOptions,
39
40
  mergeDefaultOptions,
41
+ mimeTypeToVideoCodecString,
40
42
  screenCaptureToDisplayMediaStreamOptions,
41
43
  } from '../track/utils';
42
44
  import type { DataPublishOptions } from '../types';
@@ -629,6 +631,10 @@ export default class LocalParticipant extends Participant {
629
631
  if (opts.videoCodec === 'vp9' && !supportsVP9()) {
630
632
  opts.videoCodec = undefined;
631
633
  }
634
+ if (opts.videoCodec === undefined) {
635
+ opts.videoCodec = defaultVideoCodec;
636
+ }
637
+ const videoCodec = opts.videoCodec;
632
638
 
633
639
  // handle track actions
634
640
  track.on(TrackEvent.Muted, this.onTrackMuted);
@@ -649,11 +655,11 @@ export default class LocalParticipant extends Participant {
649
655
  encryption: this.encryptionType,
650
656
  stereo: isStereo,
651
657
  disableRed: this.isE2EEEnabled || !(opts.red ?? true),
658
+ stream: opts?.stream,
652
659
  });
653
660
 
654
661
  // compute encodings and layers for video
655
662
  let encodings: RTCRtpEncodingParameters[] | undefined;
656
- let simEncodings: RTCRtpEncodingParameters[] | undefined;
657
663
  if (track.kind === Track.Kind.Video) {
658
664
  let dims: Track.Dimensions = {
659
665
  width: 0,
@@ -678,53 +684,40 @@ export default class LocalParticipant extends Participant {
678
684
  req.height = dims.height;
679
685
  // for svc codecs, disable simulcast and use vp8 for backup codec
680
686
  if (track instanceof LocalVideoTrack) {
681
- if (isSVCCodec(opts.videoCodec)) {
687
+ if (isSVCCodec(videoCodec)) {
682
688
  // vp9 svc with screenshare has problem to encode, always use L1T3 here
683
- if (track.source === Track.Source.ScreenShare && opts.videoCodec === 'vp9') {
689
+ if (track.source === Track.Source.ScreenShare && videoCodec === 'vp9') {
684
690
  opts.scalabilityMode = 'L1T3';
685
691
  }
686
692
  // set scalabilityMode to 'L3T3_KEY' by default
687
693
  opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3_KEY';
688
694
  }
689
695
 
696
+ req.simulcastCodecs = [
697
+ new SimulcastCodec({
698
+ codec: videoCodec,
699
+ cid: track.mediaStreamTrack.id,
700
+ }),
701
+ ];
702
+
690
703
  // set up backup
691
- if (opts.videoCodec && opts.backupCodec && opts.videoCodec !== opts.backupCodec.codec) {
704
+ if (opts.backupCodec && videoCodec !== opts.backupCodec.codec) {
692
705
  if (!this.roomOptions.dynacast) {
693
706
  this.roomOptions.dynacast = true;
694
707
  }
695
- const simOpts = { ...opts };
696
- simOpts.simulcast = true;
697
- simEncodings = computeTrackBackupEncodings(track, opts.backupCodec.codec, simOpts);
698
-
699
- req.simulcastCodecs = [
700
- new SimulcastCodec({
701
- codec: opts.videoCodec,
702
- cid: track.mediaStreamTrack.id,
703
- enableSimulcastLayers: true,
704
- }),
708
+ req.simulcastCodecs.push(
705
709
  new SimulcastCodec({
706
710
  codec: opts.backupCodec.codec,
707
711
  cid: '',
708
- enableSimulcastLayers: true,
709
712
  }),
710
- ];
711
- } else if (opts.videoCodec) {
712
- // pass codec info to sfu so it can prefer codec for the client which don't support
713
- // setCodecPreferences
714
- req.simulcastCodecs = [
715
- new SimulcastCodec({
716
- codec: opts.videoCodec,
717
- cid: track.mediaStreamTrack.id,
718
- enableSimulcastLayers: opts.simulcast ?? false,
719
- }),
720
- ];
713
+ );
721
714
  }
722
715
  }
723
716
 
724
717
  encodings = computeVideoEncodings(
725
718
  track.source === Track.Source.ScreenShare,
726
- dims.width,
727
- dims.height,
719
+ req.width,
720
+ req.height,
728
721
  opts,
729
722
  );
730
723
  req.layers = videoLayersFromEncodings(
@@ -748,30 +741,28 @@ export default class LocalParticipant extends Participant {
748
741
  }
749
742
 
750
743
  const ti = await this.engine.addTrack(req);
751
- let primaryCodecSupported = false;
752
- let backupCodecSupported = false;
753
- ti.codecs.forEach((c) => {
754
- if (isCodecEqual(c.mimeType, opts.videoCodec)) {
755
- primaryCodecSupported = true;
756
- } else if (opts.backupCodec && isCodecEqual(c.mimeType, opts.backupCodec.codec)) {
757
- backupCodecSupported = true;
744
+ // server might not support the codec the client has requested, in that case, fallback
745
+ // to a supported codec
746
+ let primaryCodecMime: string | undefined;
747
+ ti.codecs.forEach((codec) => {
748
+ if (primaryCodecMime === undefined) {
749
+ primaryCodecMime = codec.mimeType;
758
750
  }
759
751
  });
760
-
761
- if (req.simulcastCodecs.length > 0) {
762
- if (!primaryCodecSupported && !backupCodecSupported) {
763
- throw Error('cannot publish track, codec not supported by server');
764
- }
765
-
766
- if (!primaryCodecSupported && opts.backupCodec) {
767
- const backupCodec = opts.backupCodec;
768
- opts = { ...opts };
769
- log.debug(
770
- `primary codec ${opts.videoCodec} not supported, fallback to ${backupCodec.codec}`,
752
+ if (primaryCodecMime && track.kind === Track.Kind.Video) {
753
+ const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
754
+ if (updatedCodec !== videoCodec) {
755
+ log.debug('falling back to server selected codec', { codec: updatedCodec });
756
+ /* @ts-ignore */
757
+ opts.videoCodec = updatedCodec;
758
+
759
+ // recompute encodings since bitrates/etc could have changed
760
+ encodings = computeVideoEncodings(
761
+ track.source === Track.Source.ScreenShare,
762
+ req.width,
763
+ req.height,
764
+ opts,
771
765
  );
772
- opts.videoCodec = backupCodec.codec;
773
- opts.videoEncoding = backupCodec.encoding;
774
- encodings = simEncodings;
775
766
  }
776
767
  }
777
768
 
@@ -785,20 +776,19 @@ export default class LocalParticipant extends Participant {
785
776
  }
786
777
  log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });
787
778
 
788
- // store RTPSender
789
779
  track.sender = await this.engine.createSender(track, opts, encodings);
790
780
 
791
781
  if (encodings) {
792
782
  if (isFireFox() && track.kind === Track.Kind.Audio) {
793
783
  /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
794
- livekit-server uses maxaveragebitrate=510000in the answer sdp to permit client to
784
+ livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
795
785
  publish high quality audio track. But firefox always uses this value as the actual
796
786
  bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
797
787
  So the client need to modify maxaverragebitrates in answer sdp to user provided value to
798
788
  fix the issue.
799
789
  */
800
790
  let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
801
- for (const transceiver of this.engine.publisher.pc.getTransceivers()) {
791
+ for (const transceiver of this.engine.publisher.getTransceivers()) {
802
792
  if (transceiver.sender === track.sender) {
803
793
  trackTransceiver = transceiver;
804
794
  break;
@@ -888,7 +878,6 @@ export default class LocalParticipant extends Participant {
888
878
  {
889
879
  codec: opts.videoCodec,
890
880
  cid: simulcastTrack.mediaStreamTrack.id,
891
- enableSimulcastLayers: opts.simulcast,
892
881
  },
893
882
  ],
894
883
  });
@@ -946,11 +935,11 @@ export default class LocalParticipant extends Participant {
946
935
  track.sender = undefined;
947
936
  if (
948
937
  this.engine.publisher &&
949
- this.engine.publisher.pc.connectionState !== 'closed' &&
938
+ this.engine.publisher.getConnectionState() !== 'closed' &&
950
939
  trackSender
951
940
  ) {
952
941
  try {
953
- for (const transceiver of this.engine.publisher.pc.getTransceivers()) {
942
+ for (const transceiver of this.engine.publisher.getTransceivers()) {
954
943
  // if sender is not currently sending (after replaceTrack(null))
955
944
  // removeTrack would have no effect.
956
945
  // to ensure we end up successfully removing the track, manually set
@@ -138,7 +138,7 @@ export default class LocalAudioTrack extends LocalTrack {
138
138
  this.prevStats = stats;
139
139
  };
140
140
 
141
- async setProcessor(processor: TrackProcessor<typeof this.kind>) {
141
+ async setProcessor(processor: TrackProcessor<this['kind']>) {
142
142
  const unlock = await this.processorLock.lock();
143
143
  try {
144
144
  if (!this.audioContext) {