livekit-client 2.18.7 → 2.18.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) 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 +2 -2
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +408 -255
  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.map +1 -1
  10. package/dist/src/logger.d.ts +11 -1
  11. package/dist/src/logger.d.ts.map +1 -1
  12. package/dist/src/room/PCTransport.d.ts +13 -3
  13. package/dist/src/room/PCTransport.d.ts.map +1 -1
  14. package/dist/src/room/PCTransportManager.d.ts +3 -1
  15. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/data-track/LocalDataTrack.d.ts +32 -0
  19. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  20. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  21. package/dist/src/room/data-track/handle.d.ts +1 -0
  22. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  23. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  24. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  25. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  26. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  27. package/dist/src/room/data-track/outgoing/types.d.ts +7 -0
  28. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  29. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  30. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  31. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
  32. package/dist/ts4.2/logger.d.ts +11 -1
  33. package/dist/ts4.2/room/PCTransport.d.ts +13 -3
  34. package/dist/ts4.2/room/PCTransportManager.d.ts +3 -1
  35. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +32 -0
  36. package/dist/ts4.2/room/data-track/handle.d.ts +1 -0
  37. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  38. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  39. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/src/api/SignalClient.ts +19 -31
  42. package/src/logger.test.ts +61 -0
  43. package/src/logger.ts +38 -4
  44. package/src/room/PCTransport.ts +26 -3
  45. package/src/room/PCTransportManager.test.ts +281 -0
  46. package/src/room/PCTransportManager.ts +45 -31
  47. package/src/room/RTCEngine.ts +34 -52
  48. package/src/room/Room.ts +37 -59
  49. package/src/room/data-track/LocalDataTrack.ts +60 -1
  50. package/src/room/data-track/RemoteDataTrack.ts +4 -1
  51. package/src/room/data-track/handle.ts +4 -0
  52. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +72 -2
  53. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +5 -3
  54. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +387 -1
  55. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +51 -3
  56. package/src/room/data-track/outgoing/types.ts +5 -0
  57. package/src/room/participant/LocalParticipant.ts +59 -144
  58. package/src/room/participant/Participant.ts +4 -1
  59. package/src/utils/subscribeToEvents.ts +11 -8
package/src/room/Room.ts CHANGED
@@ -233,7 +233,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
233
233
  this.sidToIdentity = new Map();
234
234
  this.options = { ...roomOptionDefaults, ...options };
235
235
 
236
- this.log = getLogger(this.options.loggerName ?? LoggerNames.Room);
236
+ this.log = getLogger(this.options.loggerName ?? LoggerNames.Room, () => this.logContext);
237
237
  this.transcriptionReceivedTimes = new Map();
238
238
 
239
239
  this.options.audioCaptureDefaults = {
@@ -290,8 +290,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
290
290
  .on('trackUnpublished', (event) => {
291
291
  this.emit(RoomEvent.LocalDataTrackUnpublished, event.sid);
292
292
  })
293
- .on('packetAvailable', ({ bytes }) => {
294
- this.engine.sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait');
293
+ .on('packetAvailable', ({ handle, bytes }) => {
294
+ this.engine
295
+ .sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait')
296
+ .finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
295
297
  });
296
298
 
297
299
  this.disconnectLock = new Mutex();
@@ -329,7 +331,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
329
331
  this.switchActiveDevice(
330
332
  'audiooutput',
331
333
  unwrapConstraint(this.options.audioOutput.deviceId),
332
- ).catch((e) => this.log.warn(`Could not set audio output: ${e.message}`, this.logContext));
334
+ ).catch((e) => this.log.warn(`Could not set audio output: ${e.message}`));
333
335
  }
334
336
 
335
337
  if (isWeb()) {
@@ -467,8 +469,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
467
469
  return {
468
470
  room: this.name,
469
471
  roomID: this.roomInfo?.sid,
470
- participant: this.localParticipant.identity,
471
- participantID: this.localParticipant.sid,
472
+ participant: this.localParticipant?.identity,
473
+ participantID: this.localParticipant?.sid,
472
474
  };
473
475
  }
474
476
 
@@ -555,7 +557,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
555
557
  .on(EngineEvent.Resuming, () => {
556
558
  this.clearConnectionReconcile();
557
559
  this.isResuming = true;
558
- this.log.info('Resuming signal connection', this.logContext);
560
+ this.log.debug('Resuming signal connection');
559
561
  if (this.setAndEmitConnectionState(ConnectionState.SignalReconnecting)) {
560
562
  this.emit(RoomEvent.SignalReconnecting);
561
563
  }
@@ -563,7 +565,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
563
565
  .on(EngineEvent.Resumed, () => {
564
566
  this.registerConnectionReconcile();
565
567
  this.isResuming = false;
566
- this.log.info('Resumed signal connection', this.logContext);
568
+ this.log.debug('Resumed signal connection');
567
569
  this.updateSubscriptions();
568
570
  this.emitBufferedEvents();
569
571
  if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
@@ -613,7 +615,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
613
615
  if (!event.info) {
614
616
  this.log.warn(
615
617
  `received PublishDataTrackResponse, but event.info was ${event.info}, so skipping.`,
616
- this.logContext,
617
618
  );
618
619
  return;
619
620
  }
@@ -632,7 +633,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
632
633
  if (!event.info) {
633
634
  this.log.warn(
634
635
  `received UnPublishDataTrackResponse, but event.info was ${event.info}, so skipping.`,
635
- this.logContext,
636
636
  );
637
637
  return;
638
638
  }
@@ -732,7 +732,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
732
732
  if (this.state !== ConnectionState.Disconnected) {
733
733
  return;
734
734
  }
735
- this.log.debug(`prepareConnection to ${url}`, this.logContext);
735
+ this.log.debug(`prepareConnection to ${url}`);
736
736
  try {
737
737
  if (isCloud(new URL(url)) && token) {
738
738
  this.regionUrlProvider = new RegionUrlProvider(url, token);
@@ -742,13 +742,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
742
742
  if (regionUrl && this.state === ConnectionState.Disconnected) {
743
743
  this.regionUrl = regionUrl;
744
744
  await fetch(toHttpUrl(regionUrl), { method: 'HEAD' });
745
- this.log.debug(`prepared connection to ${regionUrl}`, this.logContext);
745
+ this.log.debug(`prepared connection to ${regionUrl}`);
746
746
  }
747
747
  } else {
748
748
  await fetch(toHttpUrl(url), { method: 'HEAD' });
749
749
  }
750
750
  } catch (e) {
751
- this.log.warn('could not prepare connection', { ...this.logContext, error: e });
751
+ this.log.warn('could not prepare connection', { error: e });
752
752
  }
753
753
  }
754
754
 
@@ -768,7 +768,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
768
768
 
769
769
  if (this.state === ConnectionState.Connected) {
770
770
  // when the state is reconnecting or connected, this function returns immediately
771
- this.log.info(`already connected to room ${this.name}`, this.logContext);
771
+ this.log.info(`already connected to room ${this.name}`);
772
772
  unlockDisconnect();
773
773
  return Promise.resolve();
774
774
  }
@@ -798,7 +798,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
798
798
  this.regionUrlProvider?.setServerReportedRegions(settings);
799
799
  })
800
800
  .catch((e) => {
801
- this.log.warn('could not fetch region settings', { ...this.logContext, error: e });
801
+ this.log.warn('could not fetch region settings', { error: e });
802
802
  });
803
803
  }
804
804
 
@@ -864,7 +864,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
864
864
  if (nextUrl && !this.abortController?.signal.aborted) {
865
865
  this.log.info(
866
866
  `Initial connection failed with ConnectionError: ${error.message}. Retrying with another region: ${nextUrl}`,
867
- this.logContext,
868
867
  );
869
868
  this.recreateEngine(true);
870
869
  await connectFn(resolve, reject, nextUrl);
@@ -930,7 +929,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
930
929
  }
931
930
 
932
931
  if (serverInfo.version === '0.15.1' && this.options.dynacast) {
933
- this.log.debug('disabling dynacast due to server version', this.logContext);
932
+ this.log.debug('disabling dynacast due to server version');
934
933
  // dynacast has a bug in 0.15.1, so we cannot use it then
935
934
  roomOptions.dynacast = false;
936
935
  }
@@ -950,7 +949,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
950
949
  this.e2eeManager.setSifTrailer(joinResponse.sifTrailer);
951
950
  } catch (e: any) {
952
951
  this.log.error(e instanceof Error ? e.message : 'Could not set SifTrailer', {
953
- ...this.logContext,
954
952
  error: e,
955
953
  });
956
954
  }
@@ -975,7 +973,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
975
973
  this.isResuming ||
976
974
  this.engine?.pendingReconnect
977
975
  ) {
978
- this.log.info('Reconnection attempt replaced by new connection attempt', this.logContext);
976
+ this.log.info('Reconnection attempt replaced by new connection attempt');
979
977
  // make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
980
978
  this.recreateEngine(true);
981
979
  } else {
@@ -1027,7 +1025,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1027
1025
  resultingError.status = err.status;
1028
1026
  }
1029
1027
  this.log.debug(`error trying to establish signal connection`, {
1030
- ...this.logContext,
1031
1028
  error: err,
1032
1029
  });
1033
1030
  throw resultingError;
@@ -1077,12 +1074,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1077
1074
  const unlock = await this.disconnectLock.lock();
1078
1075
  try {
1079
1076
  if (this.state === ConnectionState.Disconnected) {
1080
- this.log.debug('already disconnected', this.logContext);
1077
+ this.log.debug('already disconnected');
1081
1078
  return;
1082
1079
  }
1083
- this.log.info('disconnect from room', {
1084
- ...this.logContext,
1085
- });
1080
+ this.log.info('disconnect from room');
1086
1081
  if (
1087
1082
  this.state === ConnectionState.Connecting ||
1088
1083
  this.state === ConnectionState.Reconnecting ||
@@ -1090,7 +1085,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1090
1085
  ) {
1091
1086
  // try aborting pending connection attempt
1092
1087
  const msg = 'Abort connection attempt due to user initiated disconnect';
1093
- this.log.warn(msg, this.logContext);
1088
+ this.log.warn(msg);
1094
1089
  this.abortController?.abort(msg);
1095
1090
  // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
1096
1091
  this.connectFuture?.reject?.(ConnectionError.cancelled('Client initiated disconnect'));
@@ -1256,7 +1251,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1256
1251
  }
1257
1252
 
1258
1253
  private onPageLeave = async () => {
1259
- this.log.info('Page leave detected, disconnecting', this.logContext);
1254
+ this.log.info('Page leave detected, disconnecting');
1260
1255
  await this.disconnect();
1261
1256
  };
1262
1257
 
@@ -1299,7 +1294,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1299
1294
  if (!document.hidden) {
1300
1295
  this.log.debug(
1301
1296
  'page visible again, triggering startAudio to resume playback and update playback status',
1302
- this.logContext,
1303
1297
  );
1304
1298
  this.startAudio();
1305
1299
  }
@@ -1359,7 +1353,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1359
1353
  } else {
1360
1354
  this.log.warn(
1361
1355
  'Resuming video playback failed, make sure you call `startVideo` directly in a user gesture handler',
1362
- this.logContext,
1363
1356
  );
1364
1357
  }
1365
1358
  });
@@ -1545,11 +1538,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1545
1538
  return;
1546
1539
  }
1547
1540
  if (this.state === ConnectionState.Disconnected) {
1548
- this.log.warn('skipping incoming track after Room disconnected', this.logContext);
1541
+ this.log.warn('skipping incoming track after Room disconnected');
1549
1542
  return;
1550
1543
  }
1551
1544
  if (mediaTrack.readyState === 'ended') {
1552
- this.log.info('skipping incoming track as it already ended', this.logContext);
1545
+ this.log.debug('skipping incoming track as it already ended');
1553
1546
  return;
1554
1547
  }
1555
1548
  const parts = unpackStreamId(stream.id);
@@ -1561,7 +1554,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1561
1554
  if (streamId && streamId.startsWith('TR')) trackId = streamId;
1562
1555
 
1563
1556
  if (participantSid === this.localParticipant.sid) {
1564
- this.log.warn('tried to create RemoteParticipant for local participant', this.logContext);
1557
+ this.log.warn('tried to create RemoteParticipant for local participant');
1565
1558
  return;
1566
1559
  }
1567
1560
 
@@ -1574,7 +1567,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1574
1567
  if (participantSid.startsWith('PA')) {
1575
1568
  this.log.error(
1576
1569
  `Tried to add a track for a participant, that's not present. Sid: ${participantSid}`,
1577
- this.logContext,
1578
1570
  );
1579
1571
  }
1580
1572
  return;
@@ -1588,7 +1580,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1588
1580
  if (!id) {
1589
1581
  this.log.error(
1590
1582
  `Tried to add a track whose 'sid' could not be found for a participant, that's not present. Sid: ${participantSid}`,
1591
- this.logContext,
1592
1583
  );
1593
1584
  return;
1594
1585
  }
@@ -1598,7 +1589,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1598
1589
  if (!trackId.startsWith('TR')) {
1599
1590
  this.log.warn(
1600
1591
  `Tried to add a track whose 'sid' could not be determined for a participant, that's not present. Sid: ${participantSid}, streamId: ${streamId}, trackId: ${trackId}`,
1601
- { ...this.logContext, remoteParticipantID: participantSid, streamId, trackId },
1592
+ { remoteParticipantID: participantSid, streamId, trackId },
1602
1593
  );
1603
1594
  }
1604
1595
 
@@ -1645,7 +1636,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1645
1636
  // the subscription before publishTrack has finished adding the publication.
1646
1637
  // defer with a timeout until LocalTrackPublished fires for the matching trackSid
1647
1638
  this.log.debug('deferring LocalTrackSubscribed, publication not yet available', {
1648
- ...this.logContext,
1649
1639
  subscribedSid,
1650
1640
  });
1651
1641
 
@@ -1677,7 +1667,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1677
1667
  } else {
1678
1668
  this.log.warn(
1679
1669
  'could not find local track publication for LocalTrackSubscribed event after timeout',
1680
- { ...this.logContext, subscribedSid },
1670
+ { subscribedSid },
1681
1671
  );
1682
1672
  }
1683
1673
  }, TIMEOUT_MS);
@@ -1710,7 +1700,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1710
1700
 
1711
1701
  private handleSignalRestarted = async (joinResponse: JoinResponse) => {
1712
1702
  this.log.debug(`signal reconnected to server, region ${joinResponse.serverRegion}`, {
1713
- ...this.logContext,
1714
1703
  region: joinResponse.serverRegion,
1715
1704
  });
1716
1705
  this.bufferedEvents = [];
@@ -1721,18 +1710,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1721
1710
  // unpublish & republish tracks
1722
1711
  await this.localParticipant.republishAllTracks(undefined, true);
1723
1712
  } catch (error) {
1724
- this.log.error('error trying to re-publish tracks after reconnection', {
1725
- ...this.logContext,
1726
- error,
1727
- });
1713
+ this.log.error('error trying to re-publish tracks after reconnection', { error });
1728
1714
  }
1729
1715
 
1730
1716
  try {
1731
1717
  await this.engine.waitForRestarted();
1732
- this.log.debug(`fully reconnected to server`, {
1733
- ...this.logContext,
1734
- region: joinResponse.serverRegion,
1735
- });
1718
+ this.log.debug(`fully reconnected to server`, { region: joinResponse.serverRegion });
1736
1719
  } catch {
1737
1720
  // reconnection failed, handleDisconnect is being invoked already, just return here
1738
1721
  return;
@@ -1749,6 +1732,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1749
1732
  this.bufferedEvents = [];
1750
1733
  this.transcriptionReceivedTimes.clear();
1751
1734
  this.incomingDataStreamManager.clearControllers();
1735
+ this.incomingDataTrackManager.reset();
1736
+ this.outgoingDataTrackManager.reset();
1752
1737
  if (this.state === ConnectionState.Disconnected) {
1753
1738
  return;
1754
1739
  }
@@ -2158,7 +2143,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2158
2143
  };
2159
2144
 
2160
2145
  private handleAudioPlaybackFailed = (e: any) => {
2161
- this.log.warn('could not playback audio', { ...this.logContext, error: e });
2146
+ this.log.warn('could not playback audio', { error: e });
2162
2147
  if (!this.canPlaybackAudio) {
2163
2148
  return;
2164
2149
  }
@@ -2304,7 +2289,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2304
2289
  try {
2305
2290
  await Promise.race([this.audioContext.resume(), sleep(200)]);
2306
2291
  } catch (e: any) {
2307
- this.log.warn('Could not resume audio context', { ...this.logContext, error: e });
2292
+ this.log.warn('Could not resume audio context', { error: e });
2308
2293
  }
2309
2294
  }
2310
2295
 
@@ -2347,7 +2332,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2347
2332
  if (this.options.audioOutput?.deviceId) {
2348
2333
  participant
2349
2334
  .setAudioOutput(this.options.audioOutput)
2350
- .catch((e) => this.log.warn(`Could not set audio output: ${e.message}`, this.logContext));
2335
+ .catch((e) => this.log.warn(`Could not set audio output: ${e.message}`));
2351
2336
  }
2352
2337
  return participant;
2353
2338
  }
@@ -2506,7 +2491,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2506
2491
  ) {
2507
2492
  consecutiveFailures++;
2508
2493
  this.log.warn('detected connection state mismatch', {
2509
- ...this.logContext,
2510
2494
  numFailures: consecutiveFailures,
2511
2495
  engine: this.engine
2512
2496
  ? {
@@ -2539,6 +2523,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2539
2523
  // unchanged
2540
2524
  return false;
2541
2525
  }
2526
+ this.log.info(`connection state changed: ${this.state} -> ${state}`);
2542
2527
  this.state = state;
2543
2528
  this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
2544
2529
  this.emit(RoomEvent.ConnectionStateChanged, this.state);
@@ -2633,10 +2618,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2633
2618
  deviceId &&
2634
2619
  deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
2635
2620
  ) {
2636
- this.log.debug(
2637
- `local track restarted, setting ${deviceKind} ${deviceId} active`,
2638
- this.logContext,
2639
- );
2621
+ this.log.debug(`local track restarted, setting ${deviceKind} ${deviceId} active`);
2640
2622
  this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
2641
2623
  this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
2642
2624
  }
@@ -2815,13 +2797,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2815
2797
  // only extract logContext from arguments in order to avoid logging the whole object tree
2816
2798
  const minimizedArgs = mapArgs(args).filter((arg: unknown) => arg !== undefined);
2817
2799
  if (event === RoomEvent.TrackSubscribed || event === RoomEvent.TrackUnsubscribed) {
2818
- this.log.trace(`subscribe trace: ${event}`, {
2819
- ...this.logContext,
2820
- event,
2821
- args: minimizedArgs,
2822
- });
2800
+ this.log.trace(`subscribe trace: ${event}`, { event, args: minimizedArgs });
2823
2801
  }
2824
- this.log.debug(`room event ${event}`, { ...this.logContext, event, args: minimizedArgs });
2802
+ this.log.debug(`room event ${event}`, { event, args: minimizedArgs });
2825
2803
  }
2826
2804
  return super.emit(event, ...args);
2827
2805
  }
@@ -1,9 +1,10 @@
1
1
  import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
2
+ import { Future } from '../utils';
2
3
  import { type DataTrackFrame, DataTrackFrameInternal } from './frame';
3
4
  import type { DataTrackHandle } from './handle';
4
5
  import type OutgoingDataTrackManager from './outgoing/OutgoingDataTrackManager';
5
6
  import { DataTrackPushFrameError } from './outgoing/errors';
6
- import type { DataTrackOptions } from './outgoing/types';
7
+ import type { DataTrackOptions, EventPacketsFlushedChange } from './outgoing/types';
7
8
  import {
8
9
  DataTrackSymbol,
9
10
  type IDataTrack,
@@ -28,14 +29,40 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
28
29
 
29
30
  protected log: StructuredLogger = log;
30
31
 
32
+ /** Resolves once the data track has sent all pending packets the rtc data channel buffer. */
33
+ protected flushedFuture = new Future<void, never>();
34
+
35
+ protected isFlushed = true;
36
+
31
37
  /** @internal */
32
38
  constructor(options: DataTrackOptions, manager: OutgoingDataTrackManager) {
33
39
  this.options = options;
34
40
  this.manager = manager;
35
41
 
36
42
  this.log = getLogger(LoggerNames.DataTracks);
43
+
44
+ this.manager.on('packetsFlushedChange', this.handleManagerPacketsFlushedChange);
45
+ this.manager.on('reset', this.handleManagerReset);
37
46
  }
38
47
 
48
+ private handleManagerReset = () => {
49
+ // When the associated manager resets, mark any in flight flushes as complete
50
+ // There's nothing actionable a user can do to get these to complete so no
51
+ // error is being thrown.
52
+ this.flushedFuture.resolve?.();
53
+
54
+ this.manager.off('packetsFlushedChange', this.handleManagerPacketsFlushedChange);
55
+ this.manager.off('reset', this.handleManagerReset);
56
+ };
57
+
58
+ private handleManagerPacketsFlushedChange = (event: EventPacketsFlushedChange) => {
59
+ this.isFlushed = event.isFlushed;
60
+ if (event.isFlushed) {
61
+ this.flushedFuture.resolve?.();
62
+ this.flushedFuture = new Future();
63
+ }
64
+ };
65
+
39
66
  /** @internal */
40
67
  static withExplicitHandle(
41
68
  options: DataTrackOptions,
@@ -104,6 +131,38 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
104
131
  }
105
132
  }
106
133
 
134
+ /**
135
+ * When called, waits for all in flight packets to be sent before resolving.
136
+ *
137
+ * Use this to:
138
+ *
139
+ * 1. Send frames exactly in order:
140
+ * ```ts
141
+ * await track.tryPush(/* ... *\/);
142
+ * await track.flush();
143
+ * await track.tryPush(/* ... *\/);
144
+ * await track.flush();
145
+ * // ... etc ...
146
+ * ```
147
+ *
148
+ * 2. Wait for frames to all be delivered before unpublishing a local data track:
149
+ *
150
+ * ```ts
151
+ * await track.tryPush(/* ... *\/);
152
+ * await track.tryPush(/* ... *\/);
153
+ * await track.tryPush(/* ... *\/);
154
+ * // ... etc ...
155
+ * await track.flush();
156
+ * await track.unpublish();
157
+ * ```
158
+ **/
159
+ async flush(): Promise<void> {
160
+ if (this.isFlushed) {
161
+ return;
162
+ }
163
+ return this.flushedFuture.promise;
164
+ }
165
+
107
166
  /**
108
167
  * Unpublish the track from the SFU. Once this is called, any further calls to {@link tryPush}
109
168
  * will fail.
@@ -66,11 +66,14 @@ export default class RemoteDataTrack implements IRemoteTrack, IDataTrack {
66
66
  */
67
67
  subscribe(options?: DataTrackSubscribeOptions): ReadableStream<DataTrackFrame> {
68
68
  try {
69
- const [stream] = this.manager.openSubscriptionStream(
69
+ const [stream, sfuSubscriptionComplete] = this.manager.openSubscriptionStream(
70
70
  this.info.sid,
71
71
  options?.signal,
72
72
  options?.bufferSize,
73
73
  );
74
+ // Prevent uncaught promise rejections from bubbling up if rejections occur after the
75
+ // readable stream is discarded.
76
+ sfuSubscriptionComplete.catch(() => {});
74
77
  return stream;
75
78
  } catch (err) {
76
79
  // NOTE: Rethrow errors to break Throws<...> type boundary
@@ -66,4 +66,8 @@ export class DataTrackHandleAllocator {
66
66
  }
67
67
  return this.value;
68
68
  }
69
+
70
+ reset() {
71
+ this.value = 0;
72
+ }
69
73
  }
@@ -327,7 +327,7 @@ describe('DataTrackIncomingManager', () => {
327
327
  expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(false);
328
328
 
329
329
  // 7. Make sure shutting down the manager doesn't throw errors
330
- manager.shutdown();
330
+ manager.reset();
331
331
  });
332
332
 
333
333
  it('should NOT terminate the sfu subscription if the abortsignal is triggered on one of two active subscriptions', async () => {
@@ -675,7 +675,7 @@ describe('DataTrackIncomingManager', () => {
675
675
  );
676
676
 
677
677
  // 4. Shutdown the manager, and make sure it doesn't throw
678
- manager.shutdown();
678
+ manager.reset();
679
679
 
680
680
  // 5. Make sure the trackUnpublished event fires for the descriptor
681
681
  const trackUnpublishedEvent = await managerEvents.waitFor('trackUnpublished');
@@ -866,5 +866,75 @@ describe('DataTrackIncomingManager', () => {
866
866
  // 8. Make sure the in flight stream is now complete
867
867
  await expect(reader.read()).resolves.toStrictEqual({ value: undefined, done: true });
868
868
  });
869
+
870
+ it(`should not produce an unhandled promise rejection when RemoteDataTrack.subscribe()'s signal is aborted`, async () => {
871
+ const manager = new IncomingDataTrackManager();
872
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
873
+ 'sfuUpdateSubscription',
874
+ 'trackPublished',
875
+ ]);
876
+
877
+ const sid = 'data track sid';
878
+
879
+ // 1. Register the data track so we can get a RemoteDataTrack via trackPublished.
880
+ await manager.receiveSfuPublicationUpdates(
881
+ new Map([
882
+ [
883
+ 'identity',
884
+ [{ sid, pubHandle: DataTrackHandle.fromNumber(5), name: 'test', usesE2ee: false }],
885
+ ],
886
+ ]),
887
+ );
888
+ const { track } = await managerEvents.waitFor('trackPublished');
889
+
890
+ // 2. Listen for unhandled rejections (coming from sfuSubscriptionComplete) and throw
891
+ // them so the test will terminate.
892
+ const onUnhandled = (reason: unknown) => {
893
+ throw reason;
894
+ };
895
+ process.on('unhandledRejection', onUnhandled);
896
+
897
+ try {
898
+ const controller = new AbortController();
899
+ const stream = track.subscribe({ signal: controller.signal });
900
+
901
+ // 3. Consume the stream the way a user would, catching the rejection.
902
+ const caughtByUser: unknown[] = [];
903
+ const consumerDone = (async () => {
904
+ try {
905
+ const reader = stream.getReader();
906
+ while (true) {
907
+ const { done } = await reader.read();
908
+ if (done) {
909
+ return;
910
+ }
911
+ }
912
+ } catch (err) {
913
+ caughtByUser.push(err);
914
+ }
915
+ })();
916
+
917
+ // Wait until subscribeRequest has kicked off so we abort during the
918
+ // 'pending' state — the path that rejects sfuSubscriptionComplete.
919
+ await managerEvents.waitFor('sfuUpdateSubscription');
920
+
921
+ // 4. Abort the subscription
922
+ controller.abort();
923
+ await consumerDone;
924
+
925
+ // Drain microtasks so any unhandledRejection has a chance to fire.
926
+ await new Promise((resolve) => setTimeout(resolve, 0));
927
+ await new Promise((resolve) => setTimeout(resolve, 0));
928
+
929
+ // 5. Make sure that no `unhandledrejection`s occur and get bubbled up as user
930
+ // facing errors.
931
+
932
+ // But, the error should still get raised by the user so they can catch it / do with it as
933
+ // they please.
934
+ expect(caughtByUser).toHaveLength(1);
935
+ } finally {
936
+ process.off('unhandledRejection', onUnhandled);
937
+ }
938
+ });
869
939
  });
870
940
  });
@@ -92,7 +92,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
92
92
  *
93
93
  * This is an index that allows track descriptors to be looked up
94
94
  * by subscriber handle in O(1) time, to make routing incoming packets
95
- * a (hot code path) faster.
95
+ * (a hot code path) faster.
96
96
  */
97
97
  private subscriptionHandles = new Map<DataTrackHandle, DataTrackSid>();
98
98
 
@@ -626,8 +626,9 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
626
626
  }
627
627
  }
628
628
 
629
- /** Shutdown the manager, ending any subscriptions. */
630
- shutdown() {
629
+ /** Resets the manager, ending any subscriptions, and getting it ready for the next room
630
+ * connection. */
631
+ reset() {
631
632
  for (const descriptor of this.descriptors.values()) {
632
633
  this.emit('trackUnpublished', {
633
634
  sid: descriptor.info.sid,
@@ -643,5 +644,6 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
643
644
  }
644
645
  }
645
646
  this.descriptors.clear();
647
+ this.subscriptionHandles.clear();
646
648
  }
647
649
  }