livekit-client 2.21.0 → 2.22.1

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 (116) hide show
  1. package/README.md +10 -10
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +189 -2
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +2243 -270
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +189 -2
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/api/SignalClient.d.ts +25 -1
  15. package/dist/src/api/SignalClient.d.ts.map +1 -1
  16. package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
  17. package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
  18. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  19. package/dist/src/api/utils.d.ts.map +1 -1
  20. package/dist/src/index.d.ts +3 -2
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/logger.d.ts +2 -1
  23. package/dist/src/logger.d.ts.map +1 -1
  24. package/dist/src/options.d.ts +22 -0
  25. package/dist/src/options.d.ts.map +1 -1
  26. package/dist/src/room/PCTransport.d.ts +51 -3
  27. package/dist/src/room/PCTransport.d.ts.map +1 -1
  28. package/dist/src/room/RTCEngine.d.ts +27 -3
  29. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  30. package/dist/src/room/Room.d.ts +10 -1
  31. package/dist/src/room/Room.d.ts.map +1 -1
  32. package/dist/src/room/errors.d.ts.map +1 -1
  33. package/dist/src/room/events.d.ts +1 -1
  34. package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
  35. package/dist/src/room/statsSummary.d.ts +13 -0
  36. package/dist/src/room/statsSummary.d.ts.map +1 -0
  37. package/dist/src/room/token-source/TokenSource.d.ts +19 -10
  38. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  39. package/dist/src/room/token-source/utils.d.ts.map +1 -1
  40. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  41. package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
  42. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  43. package/dist/src/room/track/RemoteTrack.d.ts +3 -0
  44. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  45. package/dist/src/room/track/Track.d.ts +10 -0
  46. package/dist/src/room/track/Track.d.ts.map +1 -1
  47. package/dist/src/room/utils.d.ts +15 -0
  48. package/dist/src/room/utils.d.ts.map +1 -1
  49. package/dist/src/test/promiseState.d.ts +12 -0
  50. package/dist/src/test/promiseState.d.ts.map +1 -0
  51. package/dist/src/test/signalToken.d.ts.map +1 -1
  52. package/dist/src/utils/AsyncQueue.d.ts +3 -3
  53. package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
  54. package/dist/src/utils/machineInspector.d.ts +54 -0
  55. package/dist/src/utils/machineInspector.d.ts.map +1 -0
  56. package/dist/ts4.2/api/SignalClient.d.ts +25 -1
  57. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
  58. package/dist/ts4.2/index.d.ts +3 -2
  59. package/dist/ts4.2/logger.d.ts +2 -1
  60. package/dist/ts4.2/options.d.ts +22 -0
  61. package/dist/ts4.2/room/PCTransport.d.ts +51 -3
  62. package/dist/ts4.2/room/RTCEngine.d.ts +27 -3
  63. package/dist/ts4.2/room/Room.d.ts +10 -1
  64. package/dist/ts4.2/room/events.d.ts +1 -1
  65. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
  66. package/dist/ts4.2/room/statsSummary.d.ts +13 -0
  67. package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
  68. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
  69. package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
  70. package/dist/ts4.2/room/track/Track.d.ts +10 -0
  71. package/dist/ts4.2/room/utils.d.ts +15 -0
  72. package/dist/ts4.2/test/promiseState.d.ts +12 -0
  73. package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
  74. package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
  75. package/package.json +19 -12
  76. package/src/api/SignalClient.e2e.test.ts +16 -9
  77. package/src/api/SignalClient.test.ts +320 -8
  78. package/src/api/SignalClient.ts +260 -82
  79. package/src/api/SignalClientStateMachine.test.ts +472 -0
  80. package/src/api/SignalClientStateMachine.ts +180 -0
  81. package/src/api/WebSocketStream.ts +19 -3
  82. package/src/api/utils.test.ts +20 -1
  83. package/src/api/utils.ts +5 -0
  84. package/src/e2ee/utils.ts +1 -1
  85. package/src/index.ts +5 -0
  86. package/src/logger.ts +1 -0
  87. package/src/options.ts +24 -0
  88. package/src/room/PCTransport.test.ts +243 -1
  89. package/src/room/PCTransport.ts +183 -81
  90. package/src/room/RTCEngine.test.ts +339 -2
  91. package/src/room/RTCEngine.ts +168 -18
  92. package/src/room/Room.test.ts +134 -3
  93. package/src/room/Room.ts +122 -22
  94. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
  95. package/src/room/data-stream/incoming/StreamReader.ts +1 -1
  96. package/src/room/errors.ts +1 -2
  97. package/src/room/events.ts +1 -1
  98. package/src/room/statsSummary.ts +187 -0
  99. package/src/room/token-source/TokenSource.ts +25 -12
  100. package/src/room/token-source/test-tokens.ts +20 -0
  101. package/src/room/token-source/utils.test.ts +27 -0
  102. package/src/room/token-source/utils.ts +12 -5
  103. package/src/room/track/LocalAudioTrack.ts +9 -3
  104. package/src/room/track/LocalVideoTrack.test.ts +105 -2
  105. package/src/room/track/LocalVideoTrack.ts +36 -10
  106. package/src/room/track/RemoteTrack.test.ts +144 -0
  107. package/src/room/track/RemoteTrack.ts +39 -12
  108. package/src/room/track/Track.ts +29 -1
  109. package/src/room/utils.test.ts +94 -2
  110. package/src/room/utils.ts +53 -1
  111. package/src/test/promiseState.ts +23 -0
  112. package/src/test/signalServerSetup.ts +2 -1
  113. package/src/test/signalToken.ts +17 -13
  114. package/src/type-polyfills/header-extensions.d.ts +13 -0
  115. package/src/utils/AsyncQueue.ts +3 -3
  116. package/src/utils/machineInspector.ts +90 -0
package/src/room/Room.ts CHANGED
@@ -91,6 +91,7 @@ import {
91
91
  type RpcInvocationData,
92
92
  RpcServerManager,
93
93
  } from './rpc';
94
+ import { summarizeStatsReport } from './statsSummary';
94
95
  import CriticalTimers from './timers';
95
96
  import LocalAudioTrack from './track/LocalAudioTrack';
96
97
  import type LocalTrack from './track/LocalTrack';
@@ -113,6 +114,7 @@ import {
113
114
  Future,
114
115
  createDummyVideoStreamTrack,
115
116
  extractChatMessage,
117
+ extractTrackSid,
116
118
  extractTranscriptionSegments,
117
119
  getDisconnectReasonFromConnectionError,
118
120
  getEmptyAudioStreamTrack,
@@ -142,6 +144,7 @@ export enum ConnectionState {
142
144
  }
143
145
 
144
146
  const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
147
+ const STATS_LOG_FREQUENCY_MS = 30 * 1000;
145
148
 
146
149
  /**
147
150
  * In LiveKit, a room is the logical grouping for a list of participants.
@@ -206,6 +209,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
206
209
 
207
210
  private connectionReconcileInterval?: ReturnType<typeof setInterval>;
208
211
 
212
+ private statsLogInterval?: ReturnType<typeof setInterval>;
213
+
209
214
  private regionUrlProvider?: RegionUrlProvider;
210
215
 
211
216
  private regionUrl?: string;
@@ -214,10 +219,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
214
219
 
215
220
  private log = log;
216
221
 
222
+ private statsLog = log;
223
+
217
224
  private bufferedEvents: Array<any> = [];
218
225
 
219
226
  private isResuming: boolean = false;
220
227
 
228
+ private pendingTrackAddedCallbacks = new Map<Track.SID, Set<() => void>>();
229
+
221
230
  /**
222
231
  * map to store first point in time when a particular transcription segment was received
223
232
  */
@@ -251,6 +260,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
251
260
  this.options = { ...roomOptionDefaults, ...options };
252
261
 
253
262
  this.log = getLogger(this.options.loggerName ?? LoggerNames.Room, () => this.logContext);
263
+ // its own logger name, so the stats dumps can be silenced or routed
264
+ // separately from the rest of the room's logs
265
+ this.statsLog = getLogger(LoggerNames.Stats, () => this.logContext);
254
266
  this.transcriptionReceivedTimes = new Map();
255
267
 
256
268
  this.options.audioCaptureDefaults = {
@@ -268,7 +280,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
268
280
 
269
281
  this.maybeCreateEngine();
270
282
 
271
- this.incomingDataStreamManager = new IncomingDataStreamManager();
283
+ this.incomingDataStreamManager = new IncomingDataStreamManager(
284
+ this.options.dataStream?.maxPayloadByteLength,
285
+ );
272
286
  this.outgoingDataStreamManager = new OutgoingDataStreamManager(
273
287
  this.engine,
274
288
  this.log,
@@ -636,10 +650,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
636
650
  this.emitBufferedEvents();
637
651
  })
638
652
  .on(EngineEvent.SignalResumed, () => {
639
- this.bufferedEvents = [];
640
653
  if (this.state === ConnectionState.Reconnecting || this.isResuming) {
641
654
  this.sendSyncState();
642
655
  }
656
+ this.emitBufferedEvents();
643
657
  })
644
658
  .on(EngineEvent.Restarting, this.handleRestarting)
645
659
  .on(EngineEvent.Restarted, this.handleRestarted)
@@ -1588,24 +1602,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1588
1602
  // at that time, ICE connectivity has not been established so the track is not
1589
1603
  // technically subscribed.
1590
1604
  // We'll defer these events until when the room is connected or eventually disconnected.
1591
- if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
1605
+ if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
1606
+ const pendingTrackSid = extractTrackSid(mediaTrack, stream);
1607
+ this.log.debug('deferring on track for later', {
1608
+ mediaTrackId: mediaTrack.id,
1609
+ mediaStreamId: stream.id,
1610
+ tracksInStream: stream.getTracks().map((track) => track.id),
1611
+ });
1592
1612
  const reconnectedHandler = () => {
1593
- this.log.debug('deferring on track for later', {
1594
- mediaTrackId: mediaTrack.id,
1595
- mediaStreamId: stream.id,
1596
- tracksInStream: stream.getTracks().map((track) => track.id),
1597
- });
1598
- this.onTrackAdded(mediaTrack, stream, receiver);
1599
1613
  cleanup();
1614
+ this.onTrackAdded(mediaTrack, stream, receiver);
1600
1615
  };
1601
1616
  const cleanup = () => {
1602
1617
  this.off(RoomEvent.Reconnected, reconnectedHandler);
1603
1618
  this.off(RoomEvent.Connected, reconnectedHandler);
1604
1619
  this.off(RoomEvent.Disconnected, cleanup);
1620
+ if (pendingTrackSid) {
1621
+ const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
1622
+ pendingCallbacks?.delete(cleanup);
1623
+ if (pendingCallbacks?.size === 0) {
1624
+ this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
1625
+ }
1626
+ }
1605
1627
  };
1606
1628
  this.once(RoomEvent.Reconnected, reconnectedHandler);
1607
1629
  this.once(RoomEvent.Connected, reconnectedHandler);
1608
1630
  this.once(RoomEvent.Disconnected, cleanup);
1631
+ if (pendingTrackSid) {
1632
+ const pendingCallbacks =
1633
+ this.pendingTrackAddedCallbacks.get(pendingTrackSid) ?? new Set<() => void>();
1634
+ pendingCallbacks.add(cleanup);
1635
+ this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
1636
+ }
1609
1637
  return;
1610
1638
  }
1611
1639
  if (this.state === ConnectionState.Disconnected) {
@@ -1618,11 +1646,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1618
1646
  }
1619
1647
  const parts = unpackStreamId(stream.id);
1620
1648
  const participantSid = parts[0];
1621
- let streamId = parts[1];
1622
- let trackId = mediaTrack.id;
1649
+ const streamId = parts[1];
1623
1650
  // firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
1624
1651
  // and generates its own track id instead of infer from sdp track id.
1625
- if (streamId && streamId.startsWith('TR')) trackId = streamId;
1652
+ let trackId = extractTrackSid(mediaTrack, stream) ?? mediaTrack.id;
1626
1653
 
1627
1654
  if (participantSid === this.localParticipant.sid) {
1628
1655
  this.log.warn('tried to create RemoteParticipant for local participant');
@@ -1691,6 +1718,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1691
1718
  }
1692
1719
  }
1693
1720
 
1721
+ private cancelPendingTrackAdded(trackSid: Track.SID) {
1722
+ this.pendingTrackAddedCallbacks.get(trackSid)?.forEach((cleanup) => cleanup());
1723
+ }
1724
+
1694
1725
  private handleLocalTrackSubscribed(subscribedSid: string) {
1695
1726
  const findPublication = () =>
1696
1727
  this.localParticipant
@@ -1893,7 +1924,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1893
1924
 
1894
1925
  // when it's disconnected, send updates
1895
1926
  if (info.state === ParticipantInfo_State.DISCONNECTED) {
1896
- this.handleParticipantDisconnected(info.identity, remoteParticipant);
1927
+ this.handleParticipantDisconnected(
1928
+ info.identity,
1929
+ remoteParticipant,
1930
+ info.disconnectReason === DisconnectReason.UNKNOWN_REASON
1931
+ ? undefined
1932
+ : info.disconnectReason,
1933
+ );
1897
1934
  } else {
1898
1935
  // create participant if doesn't exist
1899
1936
  this.getOrCreateParticipant(info.identity, info);
@@ -1911,7 +1948,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1911
1948
  this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
1912
1949
  };
1913
1950
 
1914
- private handleParticipantDisconnected(identity: string, participant?: RemoteParticipant) {
1951
+ private handleParticipantDisconnected(
1952
+ identity: string,
1953
+ participant?: RemoteParticipant,
1954
+ disconnectReason?: DisconnectReason,
1955
+ ) {
1915
1956
  // remove and send event
1916
1957
  this.remoteParticipants.delete(identity);
1917
1958
  if (!participant) {
@@ -1924,7 +1965,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1924
1965
  participant.trackPublications.forEach((publication) => {
1925
1966
  participant.unpublishTrack(publication.trackSid, true);
1926
1967
  });
1927
- this.emit(RoomEvent.ParticipantDisconnected, participant);
1968
+ this.emit(RoomEvent.ParticipantDisconnected, participant, disconnectReason);
1928
1969
  participant.setDisconnected();
1929
1970
  this.rpcClientManager.handleParticipantDisconnected(participant.identity);
1930
1971
  }
@@ -2008,8 +2049,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2008
2049
  return;
2009
2050
  }
2010
2051
  const newStreamState = Track.streamStateFromProto(streamState.state);
2052
+ const prevStreamState = pub.track.streamState;
2011
2053
  pub.track.setStreamState(newStreamState);
2012
- if (newStreamState !== pub.track.streamState) {
2054
+ if (newStreamState !== prevStreamState) {
2013
2055
  participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
2014
2056
  this.emitWhenConnected(
2015
2057
  RoomEvent.TrackStreamStateChanged,
@@ -2035,6 +2077,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2035
2077
  };
2036
2078
 
2037
2079
  private handleSubscriptionError = (update: SubscriptionResponse) => {
2080
+ this.cancelPendingTrackAdded(update.trackSid);
2081
+
2038
2082
  const participant = Array.from(this.remoteParticipants.values()).find((p) =>
2039
2083
  p.trackPublications.has(update.trackSid),
2040
2084
  );
@@ -2409,6 +2453,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2409
2453
  },
2410
2454
  )
2411
2455
  .on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
2456
+ this.cancelPendingTrackAdded(publication.trackSid);
2412
2457
  this.emit(RoomEvent.TrackUnpublished, publication, participant);
2413
2458
  })
2414
2459
  .on(
@@ -2558,6 +2603,46 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2558
2603
  );
2559
2604
  }
2560
2605
 
2606
+ private setStatsLogging(enabled: boolean) {
2607
+ if (enabled) {
2608
+ if (!this.statsLogInterval) {
2609
+ this.statsLogInterval = CriticalTimers.setInterval(() => {
2610
+ // logWebRTCStats handles its own errors, nothing to await here
2611
+ this.logWebRTCStats();
2612
+ }, STATS_LOG_FREQUENCY_MS);
2613
+ }
2614
+ } else if (this.statsLogInterval) {
2615
+ CriticalTimers.clearInterval(this.statsLogInterval);
2616
+ this.statsLogInterval = undefined;
2617
+ }
2618
+ }
2619
+
2620
+ /**
2621
+ * Dumps stats of both peer connections.
2622
+ */
2623
+ private logWebRTCStats = async () => {
2624
+ const pcManager = this.engine?.pcManager;
2625
+ if (!pcManager) {
2626
+ return;
2627
+ }
2628
+ try {
2629
+ const [publisher, subscriber] = await Promise.all([
2630
+ pcManager.publisher.getStats(),
2631
+ pcManager.subscriber?.getStats(),
2632
+ ]);
2633
+ const publisherStats = publisher && summarizeStatsReport(publisher);
2634
+ const subscriberStats = subscriber && summarizeStatsReport(subscriber);
2635
+ this.statsLog.info(`webrtc stats`, {
2636
+ publisher: publisherStats?.connection,
2637
+ subscriber: subscriberStats?.connection,
2638
+ inbound: [...(publisherStats?.inbound ?? []), ...(subscriberStats?.inbound ?? [])],
2639
+ outbound: publisherStats?.outbound,
2640
+ });
2641
+ } catch (error) {
2642
+ this.statsLog.debug('could not collect webrtc stats', { error });
2643
+ }
2644
+ };
2645
+
2561
2646
  private registerConnectionReconcile() {
2562
2647
  this.clearConnectionReconcile();
2563
2648
  let consecutiveFailures = 0;
@@ -2581,11 +2666,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2581
2666
  : undefined,
2582
2667
  });
2583
2668
  if (consecutiveFailures >= 3) {
2584
- this.recreateEngine();
2585
- this.handleDisconnect(
2586
- this.options.stopLocalTrackOnUnpublish,
2587
- DisconnectReason.STATE_MISMATCH,
2588
- );
2669
+ this.clearConnectionReconcile();
2670
+ if (this.engine && !this.engine.isClosed) {
2671
+ // The transport silently died while we still looked connected. Try a full reconnect
2672
+ // (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
2673
+ this.log.warn('detected connection state mismatch, attempting full reconnect');
2674
+ this.engine.reconnect();
2675
+ } else {
2676
+ // No usable engine to reconnect with; tear down.
2677
+ this.recreateEngine();
2678
+ this.handleDisconnect(
2679
+ this.options.stopLocalTrackOnUnpublish,
2680
+ DisconnectReason.STATE_MISMATCH,
2681
+ );
2682
+ }
2589
2683
  }
2590
2684
  } else {
2591
2685
  consecutiveFailures = 0;
@@ -2607,7 +2701,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2607
2701
  this.log.info(`connection state changed: ${this.state} -> ${state}`);
2608
2702
  this.state = state;
2609
2703
  this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
2704
+ this.setStatsLogging(state === ConnectionState.Connected);
2705
+
2610
2706
  this.emit(RoomEvent.ConnectionStateChanged, this.state);
2707
+
2611
2708
  return true;
2612
2709
  }
2613
2710
 
@@ -2913,7 +3010,10 @@ export type RoomEventCallbacks = {
2913
3010
  moved: (name: string) => void;
2914
3011
  mediaDevicesChanged: () => void;
2915
3012
  participantConnected: (participant: RemoteParticipant) => void;
2916
- participantDisconnected: (participant: RemoteParticipant) => void;
3013
+ participantDisconnected: (
3014
+ participant: RemoteParticipant,
3015
+ disconnectReason?: DisconnectReason,
3016
+ ) => void;
2917
3017
  trackPublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
2918
3018
  trackSubscribed: (
2919
3019
  track: RemoteTrack,
@@ -535,7 +535,7 @@ function bytesToChunks(streamId: string): TransformStream<Uint8Array, DataStream
535
535
  * synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
536
536
  */
537
537
  function bytesToDecodedUtf8(streamId: string): TransformStream<Uint8Array, DataStream_Chunk> {
538
- const decoder = new TextDecoder('utf-8', { fatal: true });
538
+ const decoder = new TextDecoder('utf-8');
539
539
  const encoder = new TextEncoder();
540
540
 
541
541
  let outIndex = 0;
@@ -204,7 +204,7 @@ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
204
204
  // Suppress unhandled rejection on reader.closed — errors are
205
205
  // already propagated through reader.read() to the consumer.
206
206
  reader.closed.catch(() => {});
207
- const decoder = new TextDecoder('utf-8', { fatal: true });
207
+ const decoder = new TextDecoder('utf-8');
208
208
  const signal = this.signal;
209
209
 
210
210
  const cleanup = () => {
@@ -248,8 +248,7 @@ export class PublishTrackError extends LivekitError {
248
248
  }
249
249
 
250
250
  export type RequestErrorReason =
251
- | Exclude<RequestResponse_Reason, RequestResponse_Reason.OK>
252
- | 'TimeoutError';
251
+ Exclude<RequestResponse_Reason, RequestResponse_Reason.OK> | 'TimeoutError';
253
252
 
254
253
  export class SignalRequestError extends LivekitReasonedError<RequestErrorReason> {
255
254
  readonly name = 'SignalRequestError';
@@ -80,7 +80,7 @@ export enum RoomEvent {
80
80
  * When a [[RemoteParticipant]] leaves *after* the local
81
81
  * participant has joined.
82
82
  *
83
- * args: ([[RemoteParticipant]])
83
+ * args: ([[RemoteParticipant]], [[DisconnectReason]] | undefined)
84
84
  */
85
85
  ParticipantDisconnected = 'participantDisconnected',
86
86
 
@@ -0,0 +1,187 @@
1
+ /** one summarised stats entry; keys without a value are dropped */
2
+ type Summary = Record<string, unknown>;
3
+
4
+ function compact<T extends Summary>(summary: T): Summary {
5
+ const compacted: Summary = {};
6
+ for (const [key, value] of Object.entries(summary)) {
7
+ if (value !== undefined) {
8
+ compacted[key] = value;
9
+ }
10
+ }
11
+ return compacted;
12
+ }
13
+
14
+ function resolution(width?: number, height?: number): string | undefined {
15
+ return width && height ? `${width}x${height}` : undefined;
16
+ }
17
+
18
+ /** keeps the derived durations readable; reported values are logged as they are */
19
+ function round(seconds: number): number {
20
+ return Math.round(seconds * 10_000) / 10_000;
21
+ }
22
+
23
+ /**
24
+ * Average time media spent in the jitter buffer, in s. Both counters are
25
+ * cumulative, so this is the average over the lifetime of the stream.
26
+ */
27
+ function jitterBuffer(stat: Summary): number | undefined {
28
+ const delay = stat.jitterBufferDelay as number | undefined;
29
+ const emitted = stat.jitterBufferEmittedCount as number | undefined;
30
+ return delay !== undefined && emitted ? round(delay / emitted) : undefined;
31
+ }
32
+
33
+ /**
34
+ * Playout delay in s. Video receive reports it directly, the audio path keeps it
35
+ * in the linked `media-playout` stats summed over the samples played out. Pairs
36
+ * with `RemoteTrack.setPlayoutDelay`.
37
+ */
38
+ function playoutDelay(stat: Summary, playout?: Summary): number | undefined {
39
+ if (stat.playoutDelay !== undefined) {
40
+ return round(stat.playoutDelay as number);
41
+ }
42
+ const total = playout?.totalPlayoutDelay as number | undefined;
43
+ const samples = playout?.totalSamplesCount as number | undefined;
44
+ return total !== undefined && samples ? round(total / samples) : undefined;
45
+ }
46
+
47
+ /**
48
+ * Picks the interesting fields out of a `getStats()` report and groups them by
49
+ * RTP stream, so a stats dump can be read without unfolding the raw report.
50
+ */
51
+ export function summarizeStatsReport(report: RTCStatsReport) {
52
+ const byId = new Map<string, Summary>();
53
+ const candidatePairs: Summary[] = [];
54
+ const inbound: Summary[] = [];
55
+ const outbound: Summary[] = [];
56
+ let transport: Summary | undefined;
57
+
58
+ report.forEach((stat) => byId.set(stat.id, stat));
59
+
60
+ const codecOf = (stat: Summary) =>
61
+ stat.codecId ? byId.get(stat.codecId as string)?.mimeType : undefined;
62
+ const relatedOf = (stat: Summary, key: 'remoteId' | 'mediaSourceId' | 'playoutId') =>
63
+ stat[key] ? byId.get(stat[key] as string) : undefined;
64
+
65
+ report.forEach((stat) => {
66
+ switch (stat.type) {
67
+ case 'inbound-rtp': {
68
+ const playout = relatedOf(stat, 'playoutId');
69
+ inbound.push(
70
+ compact({
71
+ kind: stat.kind,
72
+ ssrc: stat.ssrc,
73
+ mid: stat.mid,
74
+ // matches `streamTrackID` in the track's own log context
75
+ trackId: stat.trackIdentifier,
76
+ codec: codecOf(stat),
77
+ decoder: stat.decoderImplementation,
78
+ resolution: resolution(stat.frameWidth, stat.frameHeight),
79
+ fps: stat.framesPerSecond,
80
+ bytesReceived: stat.bytesReceived,
81
+ packetsReceived: stat.packetsReceived,
82
+ packetsLost: stat.packetsLost,
83
+ packetsDiscarded: stat.packetsDiscarded,
84
+ // frames received without frames decoded is a decode failure
85
+ framesReceived: stat.framesReceived,
86
+ framesDecoded: stat.framesDecoded,
87
+ framesDropped: stat.framesDropped,
88
+ keyFramesDecoded: stat.keyFramesDecoded,
89
+ freezeCount: stat.freezeCount,
90
+ totalFreezesDuration: stat.totalFreezesDuration,
91
+ pauseCount: stat.pauseCount,
92
+ nackCount: stat.nackCount,
93
+ pliCount: stat.pliCount,
94
+ firCount: stat.firCount,
95
+ jitter: stat.jitter,
96
+ jitterBuffer: jitterBuffer(stat),
97
+ playoutDelay: playoutDelay(stat, playout),
98
+ audioLevel: stat.audioLevel,
99
+ totalSamplesReceived: stat.totalSamplesReceived,
100
+ concealedSamples: stat.concealedSamples,
101
+ }),
102
+ );
103
+ break;
104
+ }
105
+ case 'outbound-rtp': {
106
+ const remote = relatedOf(stat, 'remoteId');
107
+ const source = relatedOf(stat, 'mediaSourceId');
108
+ outbound.push(
109
+ compact({
110
+ kind: stat.kind,
111
+ ssrc: stat.ssrc,
112
+ mid: stat.mid,
113
+ rid: stat.rid,
114
+ trackId: source?.trackIdentifier,
115
+ active: stat.active,
116
+ codec: codecOf(stat),
117
+ encoder: stat.encoderImplementation,
118
+ resolution: resolution(stat.frameWidth, stat.frameHeight),
119
+ fps: stat.framesPerSecond,
120
+ // what the source produces, to tell a stalled capture from a stalled encoder
121
+ captureResolution: resolution(
122
+ source?.width as number | undefined,
123
+ source?.height as number | undefined,
124
+ ),
125
+ captureFps: source?.framesPerSecond,
126
+ audioLevel: source?.audioLevel,
127
+ targetBitrate: stat.targetBitrate,
128
+ bytesSent: stat.bytesSent,
129
+ packetsSent: stat.packetsSent,
130
+ retransmittedPacketsSent: stat.retransmittedPacketsSent,
131
+ framesEncoded: stat.framesEncoded,
132
+ keyFramesEncoded: stat.keyFramesEncoded,
133
+ limitedBy:
134
+ stat.qualityLimitationReason === 'none' ? undefined : stat.qualityLimitationReason,
135
+ nackCount: stat.nackCount,
136
+ pliCount: stat.pliCount,
137
+ firCount: stat.firCount,
138
+ // loss, jitter and RTT are only known from what the remote reports
139
+ remotePacketsLost: remote?.packetsLost,
140
+ remoteFractionLost: remote?.fractionLost,
141
+ remoteJitter: remote?.jitter,
142
+ remoteRoundTripTime: remote?.roundTripTime,
143
+ }),
144
+ );
145
+ break;
146
+ }
147
+ case 'transport':
148
+ transport = stat;
149
+ break;
150
+ case 'candidate-pair':
151
+ candidatePairs.push(stat);
152
+ break;
153
+ default:
154
+ }
155
+ });
156
+
157
+ const selectedPairId = transport?.selectedCandidatePairId as string | undefined;
158
+ const pair =
159
+ (selectedPairId ? byId.get(selectedPairId) : undefined) ??
160
+ candidatePairs.find((candidate) => candidate.selected) ??
161
+ candidatePairs.find((candidate) => candidate.nominated);
162
+ const local = pair?.localCandidateId ? byId.get(pair.localCandidateId as string) : undefined;
163
+ const remote = pair?.remoteCandidateId ? byId.get(pair.remoteCandidateId as string) : undefined;
164
+
165
+ const connection = compact({
166
+ ice: transport?.iceState,
167
+ dtls: transport?.dtlsState,
168
+ route:
169
+ local && remote
170
+ ? `${local.candidateType}/${local.protocol} -> ${remote.candidateType}`
171
+ : undefined,
172
+ network: local?.networkType,
173
+ currentRoundTripTime: pair?.currentRoundTripTime,
174
+ // the send bandwidth estimate; no RTP stream reports it
175
+ availableOutgoingBitrate: pair?.availableOutgoingBitrate,
176
+ availableIncomingBitrate: pair?.availableIncomingBitrate,
177
+ bytesSent: pair?.bytesSent,
178
+ bytesReceived: pair?.bytesReceived,
179
+ candidatePairChanges: transport?.selectedCandidatePairChanges,
180
+ });
181
+
182
+ return {
183
+ connection: Object.keys(connection).length > 0 ? connection : undefined,
184
+ outbound: outbound.length > 0 ? outbound : undefined,
185
+ inbound: inbound.length > 0 ? inbound : undefined,
186
+ };
187
+ }
@@ -213,29 +213,34 @@ class TokenSourceEndpoint extends TokenSourceCached {
213
213
  const body = await response.json();
214
214
  return TokenSourceResponse.fromJson(body, {
215
215
  // NOTE: it could be possible that the response body could contain more fields than just
216
- // what's in TokenSourceResponse depending on the implementation (ie, SandboxTokenServer)
216
+ // what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
217
217
  ignoreUnknownFields: true,
218
218
  });
219
219
  }
220
220
  }
221
221
 
222
- export type SandboxTokenServerOptions = {
222
+ export type DevelopmentTokenServerOptions = {
223
223
  baseUrl?: string;
224
224
  };
225
-
226
- class TokenSourceSandboxTokenServer extends TokenSourceEndpoint {
227
- constructor(sandboxId: string, options: SandboxTokenServerOptions) {
225
+ class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
226
+ constructor(tokenServerId: string, options: DevelopmentTokenServerOptions) {
228
227
  const { baseUrl = 'https://cloud-api.livekit.io', ...rest } = options;
229
228
 
230
229
  super(`${baseUrl}/api/v2/sandbox/connection-details`, {
231
230
  ...rest,
232
231
  headers: {
233
- 'X-Sandbox-ID': sandboxId,
232
+ 'X-Sandbox-ID': tokenServerId,
234
233
  },
235
234
  });
236
235
  }
237
236
  }
238
237
 
238
+ /** @deprecated use {@link DevelopmentTokenServerOptions} instead */
239
+ export type SandboxTokenServerOptions = DevelopmentTokenServerOptions;
240
+
241
+ /** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
242
+ class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
243
+
239
244
  export {
240
245
  /** The return type of {@link TokenSource.literal} */
241
246
  type TokenSourceLiteral,
@@ -243,8 +248,9 @@ export {
243
248
  type TokenSourceCustom,
244
249
  /** The return type of {@link TokenSource.endpoint} */
245
250
  type TokenSourceEndpoint,
246
- /** The return type of {@link TokenSource.sandboxTokenServer} */
247
251
  type TokenSourceSandboxTokenServer,
252
+ /** The return type of {@link TokenSource.developmentTokenServer} */
253
+ type TokenSourceDevelopmentTokenServer,
248
254
  decodeTokenPayload,
249
255
  areTokenSourceFetchOptionsEqual,
250
256
  };
@@ -269,22 +275,29 @@ export const TokenSource = {
269
275
  /**
270
276
  * TokenSource.endpoint creates a token source that fetches credentials from a given URL using
271
277
  * the standard endpoint format:
272
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
278
+ * @see https://docs.livekit.io/frontends/build/authentication/endpoint/
273
279
  */
274
280
  endpoint(url: string, options: EndpointOptions = {}) {
275
281
  return new TokenSourceEndpoint(url, options);
276
282
  },
277
283
 
278
284
  /**
279
- * TokenSource.sandboxTokenServer queries a sandbox token server for credentials,
285
+ * @deprecated Use {@link TokenSource.developmentTokenServer} instead
286
+ */
287
+ sandboxTokenServer(sandboxId: string, options: SandboxTokenServerOptions = {}) {
288
+ return new TokenSourceSandboxTokenServer(sandboxId, options);
289
+ },
290
+
291
+ /**
292
+ * TokenSource.developmentTokenServer queries a development token server for credentials,
280
293
  * which supports quick prototyping / getting started types of use cases.
281
294
  *
282
295
  * This token provider is INSECURE and should NOT be used in production.
283
296
  *
284
297
  * For more info:
285
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
298
+ * @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
286
299
  */
287
- sandboxTokenServer(sandboxId: string, options: SandboxTokenServerOptions = {}) {
288
- return new TokenSourceSandboxTokenServer(sandboxId, options);
300
+ developmentTokenServer(tokenServerId: string, options: DevelopmentTokenServerOptions = {}) {
301
+ return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
289
302
  },
290
303
  };
@@ -1,3 +1,11 @@
1
+ // Builds an unsigned (`alg: none`) JWT. These aren't signed at all, so they can only be used in
2
+ // tests which don't care about the signature.
3
+ function unsignedToken(payload: Record<string, unknown>) {
4
+ const encode = (value: Record<string, unknown>) =>
5
+ btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
6
+ return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
7
+ }
8
+
1
9
  // Test JWTs created for test purposes only.
2
10
  // None of these actually auth against anything.
3
11
  export const TOKENS = {
@@ -25,4 +33,16 @@ export const TOKENS = {
25
33
  // A dummy roomConfig value is also set, with room_config.name = "test room name", room_config.extraField = "extra field value", and room_config.agents = [{"agentName": "test agent name","metadata":"test agent metadata","extraField":"extra field value"}]
26
34
  EXTRA_FIELDS:
27
35
  'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjEwLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwicm9vbUNvbmZpZyI6eyJuYW1lIjoidGVzdCByb29tIG5hbWUiLCJlbXB0eVRpbWVvdXQiOjAsImRlcGFydHVyZVRpbWVvdXQiOjAsIm1heFBhcnRpY2lwYW50cyI6MCwibWluUGxheW91dERlbGF5IjowLCJtYXhQbGF5b3V0RGVsYXkiOjAsInN5bmNTdHJlYW1zIjpmYWxzZSwiYWdlbnRzIjpbeyJhZ2VudE5hbWUiOiJ0ZXN0IGFnZW50IG5hbWUiLCJtZXRhZGF0YSI6InRlc3QgYWdlbnQgbWV0YWRhdGEiLCJleHRyYUZpZWxkIjoiZXh0cmEgZmllbGQgdmFsdWUifV0sIm1ldGFkYXRhIjoiIiwiZXh0cmFGaWVsZCI6ImV4dHJhIGZpZWxkIHZhbHVlIn19Cg.EDetpHG8cSubaApzgWJaQrpCiSy9KDBlfCfVdIydbQ-_CHiNnXOK_f_mCJbTf9A-duT1jmvPOkLrkkWFT60XPQ',
36
+
37
+ // Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
38
+ // No exp date set at all
39
+ NO_EXP: unsignedToken({ sub: '1234567890', nbf: 1234567890, iat: 1234567890 }),
40
+
41
+ // No nbf date set at all
42
+ // Exp date set at 1234567891 seconds (Fri Feb 13 2009 23:31:31 GMT+0000)
43
+ EXP_IN_PAST_NO_NBF: unsignedToken({ sub: '1234567890', exp: 1234567891, iat: 1234567890 }),
44
+
45
+ // No nbf date set at all
46
+ // Exp date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
47
+ VALID_NO_NBF: unsignedToken({ sub: '1234567890', exp: 9876543210, iat: 1234567890 }),
28
48
  };