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
@@ -1,4 +1,4 @@
1
- import type { MediaAttributes, MediaDescription } from 'sdp-transform';
1
+ import type { MediaAttributes, MediaDescription, SessionDescription } from 'sdp-transform';
2
2
  import type TypedEmitter from 'typed-emitter';
3
3
  import type { LoggerOptions } from './types';
4
4
  /** @internal */
@@ -9,6 +9,18 @@ interface TrackBitrateInfo {
9
9
  maxbr: number;
10
10
  isScreenShare?: boolean;
11
11
  }
12
+ /**
13
+ * Applies the configured start bitrate when this media section belongs to `cid`.
14
+ * This SDP munging is used for a bitrate setting that cannot be applied through
15
+ * `RTCRtpEncodingParameters`.
16
+ *
17
+ * Returns `undefined` when the section does not belong to the track, `0` when
18
+ * it does but does not offer the requested codec, and the codec payload when the
19
+ * requested codec is present (whether the bitrate was added or already set).
20
+ *
21
+ * @internal
22
+ */
23
+ export declare function applyVideoStartBitrate(media: MediaDescription, cid: string, codec: string, maxbr: number, isScreenShare?: boolean): number | undefined;
12
24
  export declare const PCEvents: {
13
25
  readonly NegotiationStarted: "negotiationStarted";
14
26
  readonly NegotiationComplete: "negotiationComplete";
@@ -79,13 +91,44 @@ export default class PCTransport extends PCTransport_base {
79
91
  getSenders(): RTCRtpSender[];
80
92
  getLocalDescription(): RTCSessionDescription | null | undefined;
81
93
  getRemoteDescription(): RTCSessionDescription | null;
82
- getStats(): Promise<RTCStatsReport>;
94
+ /** stats of the underlying connection, `undefined` when there is none */
95
+ getStats(): Promise<RTCStatsReport> | undefined;
83
96
  getMaxMessageSize(): number | undefined;
84
97
  getConnectedAddress(): Promise<string | undefined>;
85
98
  close: () => void;
86
99
  private setMungedSDP;
87
- private ensureVideoDDExtensionForSVC;
88
100
  }
101
+ /**
102
+ * Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
103
+ * returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
104
+ * been chosen yet).
105
+ *
106
+ * A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
107
+ * `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
108
+ * sections it can send on, and an earlier offer may have munged it into others.
109
+ * @internal
110
+ */
111
+ export declare function ensureVideoDDExtension(media: {
112
+ type: string;
113
+ port: number;
114
+ protocol: string;
115
+ payloads?: string | undefined;
116
+ } & MediaDescription, sdp: SessionDescription, ddExtID: number): number;
117
+ /**
118
+ * Checks whether an fmtp config declares `param` as an exact, `;`-delimited
119
+ * token. A plain substring check conflates distinct opus parameters — e.g.
120
+ * `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
121
+ * parameter, not appear anywhere within the config string.
122
+ * @internal
123
+ */
124
+ export declare function fmtpConfigHasParam(config: string, param: string): boolean;
125
+ /** @internal */
126
+ export declare function ensureAudioNackAndStereo(media: {
127
+ type: string;
128
+ port: number;
129
+ protocol: string;
130
+ payloads?: string | undefined;
131
+ } & MediaDescription, stereoMids: string[], nackMids: string[]): void;
89
132
  /**
90
133
  * Returns the mids of transceivers that carry no outgoing track: the
91
134
  * pre-populated placeholders added by `RTCEngine.applyInitialPublisherLayout`,
@@ -117,6 +160,11 @@ export declare function placeholderMidsFromTransceivers(transceivers: readonly R
117
160
  * @internal
118
161
  */
119
162
  export declare function conformBundledCodecFmtp(media: MediaDescription[], isPlaceholder: (media: MediaDescription) => boolean): void;
163
+ /** @internal */
164
+ export declare function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
165
+ stereoMids: string[];
166
+ nackMids: string[];
167
+ };
120
168
  type PCTransportEventCallbacks = {
121
169
  negotiationStarted: () => void;
122
170
  negotiationComplete: () => void;
@@ -1,5 +1,5 @@
1
1
  import type { AddTrackRequest, ConnectionQualityUpdate, JoinResponse, RegionSettings, StreamStateUpdate, SubscriptionPermissionUpdate, SubscriptionResponse } from '@livekit/protocol';
2
- import { DataPacket, DataTrackSubscriberHandles, DisconnectReason, Encryption_Type, ParticipantInfo, PublishDataTrackResponse, RequestResponse, Room as RoomModel, RoomMovedResponse, ServerInfo, SpeakerInfo, SubscribedQualityUpdate, TrackInfo, TrackUnpublishedResponse, Transcription, UnpublishDataTrackResponse } from '@livekit/protocol';
2
+ import { DataPacket, DataTrackSubscriberHandles, DisconnectReason, Encryption_Type, ParticipantInfo, PublishDataTrackResponse, ReconnectReason, RequestResponse, Room as RoomModel, RoomMovedResponse, ServerInfo, SpeakerInfo, SubscribedQualityUpdate, TrackInfo, TrackUnpublishedResponse, Transcription, UnpublishDataTrackResponse } from '@livekit/protocol';
3
3
  import type TypedEventEmitter from 'typed-emitter';
4
4
  import type { SignalOptions } from '../api/SignalClient';
5
5
  import { SignalClient } from '../api/SignalClient';
@@ -80,6 +80,10 @@ export default class RTCEngine extends RTCEngine_base {
80
80
  private midToTrackId;
81
81
  /** used to indicate whether the browser is currently waiting to reconnect */
82
82
  private isWaitingForNetworkReconnect;
83
+ /** set while the local participant's connection quality is `LOST`; forces a full reconnect on timeout */
84
+ private lostQualityTimeout?;
85
+ /** timestamp (ms) the primary transport entered `CONNECTING`, used to bound how long we tolerate it */
86
+ private transportConnectingSince?;
83
87
  constructor(options: InternalRoomOptions);
84
88
  /** @internal */
85
89
  get logContext(): {
@@ -94,10 +98,15 @@ export default class RTCEngine extends RTCEngine_base {
94
98
  joinResponse: JoinResponse;
95
99
  serverInfo: Partial<ServerInfo>;
96
100
  }>;
97
- close(): Promise<void>;
101
+ /**
102
+ * @param reason why the session is ending, recorded by the signal lifecycle. Worth passing
103
+ * wherever the caller knows more than "someone called close" — the server's leave reason, or
104
+ * having given up on reconnecting.
105
+ */
106
+ close(reason?: string): Promise<void>;
98
107
  cleanupPeerConnections(): Promise<void>;
99
108
  cleanupLossyDataStats(): void;
100
- cleanupClient(): Promise<void>;
109
+ cleanupClient(reason?: string): Promise<void>;
101
110
  addTrack(req: AddTrackRequest): Promise<TrackInfo>;
102
111
  /**
103
112
  * Removes sender from PeerConnection, returning true if it was removed successfully
@@ -137,6 +146,21 @@ export default class RTCEngine extends RTCEngine_base {
137
146
  private createSimulcastTransceiverSender;
138
147
  private createRTCRtpSender;
139
148
  private handleDisconnect;
149
+ /**
150
+ * A sustained local `LOST` while connected and publishing means the server isn't receiving
151
+ * our media, so force a full reconnect; any non-`LOST` value cancels a pending trigger.
152
+ */
153
+ private handleLocalConnectionQuality;
154
+ private scheduleLostQualityReconnect;
155
+ private clearLostQualityTimeout;
156
+ /** Whether the publisher currently has any sender with a live track. */
157
+ private hasActivePublisherSenders;
158
+ /**
159
+ * Forces a full reconnect while keeping the engine (and its saved credentials) alive. Used by
160
+ * Room's connection-reconcile safety net when the transport silently died but we looked connected.
161
+ * @internal
162
+ */
163
+ reconnect(reason?: ReconnectReason): void;
140
164
  private attemptReconnect;
141
165
  private getNextRetryDelay;
142
166
  private restartConnection;
@@ -72,12 +72,15 @@ declare class Room extends Room_base {
72
72
  private frameMetadataManager;
73
73
  private e2eeStateMutex;
74
74
  private connectionReconcileInterval?;
75
+ private statsLogInterval?;
75
76
  private regionUrlProvider?;
76
77
  private regionUrl?;
77
78
  private isVideoPlaybackBlocked;
78
79
  private log;
80
+ private statsLog;
79
81
  private bufferedEvents;
80
82
  private isResuming;
83
+ private pendingTrackAddedCallbacks;
81
84
  /**
82
85
  * map to store first point in time when a particular transcription segment was received
83
86
  */
@@ -226,6 +229,7 @@ declare class Room extends Room_base {
226
229
  private setupLocalParticipantEvents;
227
230
  private recreateEngine;
228
231
  private onTrackAdded;
232
+ private cancelPendingTrackAdded;
229
233
  private handleLocalTrackSubscribed;
230
234
  private emitLocalTrackSubscribed;
231
235
  private handleRestarting;
@@ -274,6 +278,11 @@ declare class Room extends Room_base {
274
278
  private getRemoteParticipantCapabilities;
275
279
  private getAllRemoteParticipantIdentities;
276
280
  private registerRpcDataStreamHandler;
281
+ private setStatsLogging;
282
+ /**
283
+ * Dumps stats of both peer connections.
284
+ */
285
+ private logWebRTCStats;
277
286
  private registerConnectionReconcile;
278
287
  private clearConnectionReconcile;
279
288
  private setAndEmitConnectionState;
@@ -311,7 +320,7 @@ export type RoomEventCallbacks = {
311
320
  moved: (name: string) => void;
312
321
  mediaDevicesChanged: () => void;
313
322
  participantConnected: (participant: RemoteParticipant) => void;
314
- participantDisconnected: (participant: RemoteParticipant) => void;
323
+ participantDisconnected: (participant: RemoteParticipant, disconnectReason?: DisconnectReason) => void;
315
324
  trackPublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
316
325
  trackSubscribed: (track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
317
326
  trackSubscriptionFailed: (trackSid: string, participant: RemoteParticipant, reason?: SubscriptionError) => void;
@@ -70,7 +70,7 @@ export declare enum RoomEvent {
70
70
  * When a [[RemoteParticipant]] leaves *after* the local
71
71
  * participant has joined.
72
72
  *
73
- * args: ([[RemoteParticipant]])
73
+ * args: ([[RemoteParticipant]], [[DisconnectReason]] | undefined)
74
74
  */
75
75
  ParticipantDisconnected = "participantDisconnected",
76
76
  /**
@@ -222,7 +222,7 @@ export default class LocalParticipant extends Participant {
222
222
  *
223
223
  * @param options.topic Topic identifier used to route the stream to appropriate handlers.
224
224
  */
225
- streamBytes(options?: StreamBytesOptions): Promise<import("../data-stream/outgoing/StreamWriter").ByteStreamWriter>;
225
+ streamBytes(options?: StreamBytesOptions): Promise<import("../..").ByteStreamWriter>;
226
226
  /**
227
227
  * Initiate an RPC call to a remote participant
228
228
  * @param params - Parameters for initiating the RPC call, see {@link PerformRpcParams}
@@ -0,0 +1,13 @@
1
+ /** one summarised stats entry; keys without a value are dropped */
2
+ type Summary = Record<string, unknown>;
3
+ /**
4
+ * Picks the interesting fields out of a `getStats()` report and groups them by
5
+ * RTP stream, so a stats dump can be read without unfolding the raw report.
6
+ */
7
+ export declare function summarizeStatsReport(report: RTCStatsReport): {
8
+ connection: Summary | undefined;
9
+ outbound: Summary[] | undefined;
10
+ inbound: Summary[] | undefined;
11
+ };
12
+ export {};
13
+ //# sourceMappingURL=statsSummary.d.ts.map
@@ -34,13 +34,18 @@ declare class TokenSourceEndpoint extends TokenSourceCached {
34
34
  private createRequestFromOptions;
35
35
  protected update(options: TokenSourceFetchOptions): Promise<TokenSourceResponse>;
36
36
  }
37
- export type SandboxTokenServerOptions = {
37
+ export type DevelopmentTokenServerOptions = {
38
38
  baseUrl?: string;
39
39
  };
40
- declare class TokenSourceSandboxTokenServer extends TokenSourceEndpoint {
41
- constructor(sandboxId: string, options: SandboxTokenServerOptions);
40
+ declare class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
41
+ constructor(tokenServerId: string, options: DevelopmentTokenServerOptions);
42
42
  }
43
- export type { TokenSourceLiteral, TokenSourceCustom, TokenSourceEndpoint, TokenSourceSandboxTokenServer };
43
+ /** @deprecated use {@link DevelopmentTokenServerOptions} instead */
44
+ export type SandboxTokenServerOptions = DevelopmentTokenServerOptions;
45
+ /** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
46
+ declare class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {
47
+ }
48
+ export type { TokenSourceLiteral, TokenSourceCustom, TokenSourceEndpoint, TokenSourceSandboxTokenServer, TokenSourceDevelopmentTokenServer };
44
49
  export { decodeTokenPayload, areTokenSourceFetchOptionsEqual };
45
50
  export declare const TokenSource: {
46
51
  /** TokenSource.literal contains a single, literal set of {@link TokenSourceResponseObject}
@@ -56,18 +61,22 @@ export declare const TokenSource: {
56
61
  /**
57
62
  * TokenSource.endpoint creates a token source that fetches credentials from a given URL using
58
63
  * the standard endpoint format:
59
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
64
+ * @see https://docs.livekit.io/frontends/build/authentication/endpoint/
60
65
  */
61
66
  endpoint(url: string, options?: EndpointOptions): TokenSourceEndpoint;
62
67
  /**
63
- * TokenSource.sandboxTokenServer queries a sandbox token server for credentials,
68
+ * @deprecated Use {@link TokenSource.developmentTokenServer} instead
69
+ */
70
+ sandboxTokenServer(sandboxId: string, options?: SandboxTokenServerOptions): TokenSourceSandboxTokenServer;
71
+ /**
72
+ * TokenSource.developmentTokenServer queries a development token server for credentials,
64
73
  * which supports quick prototyping / getting started types of use cases.
65
74
  *
66
75
  * This token provider is INSECURE and should NOT be used in production.
67
76
  *
68
77
  * For more info:
69
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
78
+ * @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
70
79
  */
71
- sandboxTokenServer(sandboxId: string, options?: SandboxTokenServerOptions): TokenSourceSandboxTokenServer;
80
+ developmentTokenServer(tokenServerId: string, options?: DevelopmentTokenServerOptions): TokenSourceDevelopmentTokenServer;
72
81
  };
73
82
  //# sourceMappingURL=TokenSource.d.ts.map
@@ -56,8 +56,19 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
56
56
  private applyEncodingsToSender;
57
57
  setProcessor(processor: TrackProcessor<Track.Kind.Video>, showProcessedStreamLocally?: boolean): Promise<void>;
58
58
  setDegradationPreference(preference: RTCDegradationPreference): Promise<void>;
59
+ /**
60
+ * Degradation preference is a property of the sender, not of the track, so every sender
61
+ * publishing this track needs it applied separately. A backup codec publishes over its
62
+ * own sender, which would otherwise let the browser resolve a preference implicitly and
63
+ * diverge from the primary encoder.
64
+ *
65
+ * Callers apply this sequentially rather than concurrently: `setParameters` is only valid
66
+ * against the parameters most recently returned by `getParameters`, which is why this file
67
+ * serializes other sender parameter updates through `senderLock`.
68
+ */
69
+ private applyDegradationPreference;
59
70
  addSimulcastTrack(codec: VideoCodec, encodings?: RTCRtpEncodingParameters[]): SimulcastTrackInfo | undefined;
60
- setSimulcastTrackSender(codec: VideoCodec, sender: RTCRtpSender): void;
71
+ setSimulcastTrackSender(codec: VideoCodec, sender: RTCRtpSender): Promise<void>;
61
72
  /**
62
73
  * @internal
63
74
  * Sets codecs that should be publishing, returns new codecs that have not yet
@@ -29,7 +29,10 @@ export default abstract class RemoteTrack<TrackKind extends Track.Kind = Track.K
29
29
  */
30
30
  getPlayoutDelay(): number;
31
31
  startMonitor(): void;
32
+ stopMonitor(): void;
32
33
  protected abstract monitorReceiver(): void;
34
+ private timeSyncLoop;
35
+ private onTimeSyncListenerAdded;
33
36
  registerTimeSyncUpdate(): void;
34
37
  }
35
38
  //# sourceMappingURL=RemoteTrack.d.ts.map
@@ -42,6 +42,7 @@ export declare abstract class Track<TrackKind extends Track.Kind = Track.Kind> e
42
42
  protected timeSyncHandle: number | undefined;
43
43
  protected _currentBitrate: number;
44
44
  protected monitorInterval?: ReturnType<typeof setInterval>;
45
+ private finalStatsLogged;
45
46
  protected log: StructuredLogger;
46
47
  protected constructor(mediaTrack: MediaStreamTrack, kind: TrackKind, loggerOptions?: LoggerOptions);
47
48
  protected get logContext(): {
@@ -78,7 +79,16 @@ export declare abstract class Track<TrackKind extends Track.Kind = Track.Kind> e
78
79
  protected enable(): void;
79
80
  protected disable(): void;
80
81
  abstract startMonitor(signalClient?: SignalClient): void;
82
+ /**
83
+ * Raw stats of the sender or receiver this track is attached to.
84
+ */
85
+ abstract getRTCStatsReport(): Promise<RTCStatsReport | undefined>;
81
86
  stopMonitor(): void;
87
+ /**
88
+ * Dumps the raw stats of the track as it ends, once: a track that stops
89
+ * between two of the room's stats dumps is gone by the time the next one runs.
90
+ */
91
+ private logFinalStats;
82
92
  /** @internal */
83
93
  updateLoggerOptions(loggerOptions: LoggerOptions): void;
84
94
  private recycleElement;
@@ -32,6 +32,20 @@ export declare function supportsAV1(): boolean;
32
32
  export declare function supportsVP9(): boolean;
33
33
  export declare function supportsH265(): boolean;
34
34
  export declare function isSVCCodec(codec?: string): boolean;
35
+ /**
36
+ * Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
37
+ *
38
+ * Chrome only offers the extension on transceivers that can send, so one we create to receive on
39
+ * never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
40
+ * assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
41
+ * transceiver rather than munging the extension into the SDP leaves the browser owning the
42
+ * extension id, which is what keeps that id consistent across the bundle and across
43
+ * renegotiations.
44
+ *
45
+ * A no-op where the browser offers no such control, or does not know the extension at all.
46
+ * @internal
47
+ */
48
+ export declare function negotiateDependencyDescriptor(transceiver: RTCRtpTransceiver): boolean;
35
49
  export declare function supportsSetSinkId(elm?: HTMLMediaElement): boolean;
36
50
  /**
37
51
  * Checks whether or not setting an audio output via {@link Room#setActiveDevice}
@@ -162,4 +176,5 @@ export declare function decodeBase64(base64: string): Uint8Array;
162
176
  export declare function extractMaxAgeFromRequestHeaders(headers: Headers): number | undefined;
163
177
  export declare function isCompressionStreamSupported(): boolean;
164
178
  export declare function isPublisherOfferWithJoinSupported(): boolean;
179
+ export declare function extractTrackSid(mediaTrack: MediaStreamTrack, stream: MediaStream): Track.SID | undefined;
165
180
  //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Test helper: resolves `true` if `promise` is still pending after `ms`
3
+ * (default: one macrotask), `false` if it has already settled — regardless of
4
+ * whether it resolved or rejected.
5
+ *
6
+ * Use to assert that something has *not* happened yet (e.g. an onClose callback
7
+ * that must stay silent), optionally within a timeout window. Preferable to
8
+ * `Promise.race([p, Promise.resolve(sentinel)])` when the "not yet" needs to
9
+ * hold for a duration rather than just the current microtask.
10
+ */
11
+ export declare function isPending(promise: Promise<unknown>, ms?: number): Promise<boolean>;
12
+ //# sourceMappingURL=promiseState.d.ts.map
@@ -1,8 +1,8 @@
1
1
  type QueueTask<T> = () => PromiseLike<T>;
2
2
  declare enum QueueTaskStatus {
3
- 'WAITING' = 0,
4
- 'RUNNING' = 1,
5
- 'COMPLETED' = 2
3
+ WAITING = 0,
4
+ RUNNING = 1,
5
+ COMPLETED = 2
6
6
  }
7
7
  type QueueTaskInfo = {
8
8
  id: number;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Opt-in registry that lets a development tool observe the state machines driving the connection
3
+ * layer.
4
+ *
5
+ * The machines are private to the objects that own them and, more importantly, do not outlive them:
6
+ * a full reconnect replaces the whole engine along with its `SignalClient`, so anything watching a
7
+ * single machine reference goes blind exactly when the interesting part starts. Machines therefore
8
+ * announce themselves here as they are constructed, and a subscriber sees the whole succession.
9
+ *
10
+ * Nothing is recorded until {@link enableMachineInspector} is called, which no shipping code does —
11
+ * with the registry off, announcing is a comparison and a return.
12
+ */
13
+ /**
14
+ * The slice of machina's `Fsm` surface an inspector needs. Structural rather than machina's own
15
+ * type so that any machine satisfies it regardless of its state and input unions.
16
+ */
17
+ export interface InspectableMachine {
18
+ readonly id: string;
19
+ readonly initialState: string;
20
+ readonly states: Record<string, Record<string, unknown>>;
21
+ readonly context?: unknown;
22
+ currentState(): string;
23
+ canHandle(input: string): boolean;
24
+ on(eventName: string, callback: (data: any) => void): {
25
+ off(): void;
26
+ };
27
+ }
28
+ export interface MachineAnnouncement {
29
+ /** What the machine drives, e.g. `signal`. Not unique: each new `SignalClient` announces one. */
30
+ label: string;
31
+ /** How many machines have been announced before this one, so successive instances are tellable apart. */
32
+ seq: number;
33
+ /**
34
+ * When the machine was constructed. Carried on the announcement because subscribers receive the
35
+ * recorded ones on subscribe, and stamping those at replay time would date them all to whenever
36
+ * the tool happened to open.
37
+ */
38
+ at: number;
39
+ machine: InspectableMachine;
40
+ }
41
+ /**
42
+ * Starts recording machine announcements. Call before the machines of interest are constructed —
43
+ * for the signal machine that means before the `Room` is created.
44
+ */
45
+ export declare function enableMachineInspector(): void;
46
+ export declare function isMachineInspectorEnabled(): boolean;
47
+ /** Announces a machine to whatever is watching. A no-op unless the inspector was enabled. */
48
+ export declare function announceMachine(label: string, machine: InspectableMachine): void;
49
+ /**
50
+ * Subscribes to machine announcements, replaying the ones already recorded so that a panel opened
51
+ * after connecting still sees the machine currently in charge. Returns an unsubscribe function.
52
+ */
53
+ export declare function onMachineAnnounced(callback: (announcement: MachineAnnouncement) => void): () => void;
54
+ //# sourceMappingURL=machineInspector.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.21.0",
3
+ "version": "2.22.1",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -54,6 +54,7 @@
54
54
  "events": "^3.3.0",
55
55
  "jose": "^6.1.0",
56
56
  "loglevel": "^1.9.2",
57
+ "machina": "^7.0.0",
57
58
  "sdp-transform": "^2.15.0",
58
59
  "tslib": "2.8.1",
59
60
  "typed-emitter": "^2.1.0",
@@ -66,12 +67,12 @@
66
67
  "@babel/core": "7.29.7",
67
68
  "@babel/preset-env": "7.29.7",
68
69
  "@bufbuild/protoc-gen-es": "^1.10.0",
69
- "@changesets/cli": "2.31.0",
70
+ "@changesets/cli": "2.31.1",
70
71
  "@eslint/js": "10.0.1",
71
- "@livekit/changesets-changelog-github": "^0.0.4",
72
+ "@livekit/changesets-changelog-github": "^0.2.0",
72
73
  "@livekit/throws-transformer": "^0.1.3",
73
- "@rollup/plugin-babel": "7.0.0",
74
- "@rollup/plugin-commonjs": "29.0.2",
74
+ "@rollup/plugin-babel": "7.1.0",
75
+ "@rollup/plugin-commonjs": "29.0.3",
75
76
  "@rollup/plugin-json": "6.1.0",
76
77
  "@rollup/plugin-node-resolve": "16.0.3",
77
78
  "@rollup/plugin-terser": "^1.0.0",
@@ -82,34 +83,39 @@
82
83
  "@types/events": "^3.0.3",
83
84
  "@types/sdp-transform": "2.15.0",
84
85
  "@types/ua-parser-js": "0.7.39",
85
- "@typescript-eslint/eslint-plugin": "8.59.4",
86
- "@typescript-eslint/parser": "8.59.4",
86
+ "@typescript-eslint/eslint-plugin": "8.64.0",
87
+ "@typescript-eslint/parser": "8.64.0",
87
88
  "@vitest/browser": "^4.1.10",
88
89
  "@vitest/browser-playwright": "^4.1.10",
89
90
  "downlevel-dts": "^0.11.0",
90
- "eslint": "10.4.0",
91
+ "eslint": "10.7.0",
91
92
  "eslint-config-airbnb-extended": "^2.3.2",
92
93
  "eslint-config-prettier": "10.1.8",
93
94
  "eslint-plugin-compat": "^6.0.2",
94
95
  "eslint-plugin-import-x": "^4.16.1",
96
+ "eslint-plugin-machina": "^0.1.4",
95
97
  "eslint-plugin-prettier": "^5.5.4",
96
98
  "gh-pages": "6.3.0",
97
99
  "glob": "^13.0.6",
98
100
  "happy-dom": "^20.0.0",
99
101
  "jsdom": "^26.1.0",
102
+ "livekit-server-sdk": "^2.17.0",
103
+ "machina-inspect": "^4.0.0",
104
+ "machina-test": "^3.0.0",
105
+ "mermaid": "^11.16.1",
100
106
  "playwright": "^1.61.1",
101
107
  "prettier": "^3.4.2",
102
108
  "publint": "^0.3.21",
103
- "rollup": "4.60.4",
109
+ "rollup": "4.62.2",
104
110
  "rollup-plugin-delete": "^2.1.0",
105
111
  "rollup-plugin-typescript2": "0.37.0",
106
- "size-limit": "^11.2.0",
112
+ "size-limit": "^12.0.0",
107
113
  "tsx": "^4.21.0",
108
- "typedoc": "0.28.19",
114
+ "typedoc": "0.28.20",
109
115
  "typedoc-plugin-no-inherit": "1.6.1",
110
116
  "typescript": "5.9.3",
111
117
  "typescript-eslint": "^8.47.0",
112
- "vite": "7.3.5",
118
+ "vite": "8.1.5",
113
119
  "vitest": "^4.1.10"
114
120
  },
115
121
  "scripts": {
@@ -120,6 +126,7 @@
120
126
  "build-docs": "typedoc && mkdir -p docs/assets/github && cp .github/*.png docs/assets/github/ && find docs -name '*.html' -type f -exec sed -i.bak 's|=\"/.github/|=\"assets/github/|g' {} + && find docs -name '*.bak' -delete",
121
127
  "proto": "protoc --es_out src/proto --es_opt target=ts -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto",
122
128
  "examples:demo": "vite examples/demo -c vite.config.mjs",
129
+ "examples:machine": "vite examples/state-machine -c vite.config.mjs",
123
130
  "dev": "pnpm examples:demo",
124
131
  "lint": "eslint src",
125
132
  "test": "vitest run src",
@@ -1,6 +1,8 @@
1
1
  import { type LeaveRequest, LeaveRequest_Action } from '@livekit/protocol';
2
- import { afterEach, beforeEach, describe, expect, inject, it, vi } from 'vitest';
2
+ import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest';
3
3
  import { ConnectionErrorReason } from '../room/errors';
4
+ import { sleep } from '../room/utils';
5
+ import { isPending } from '../test/promiseState';
4
6
  import { createInvalidToken, createToken } from '../test/signalToken';
5
7
  import { SignalClient, SignalConnectionState, type SignalOptions } from './SignalClient';
6
8
 
@@ -15,8 +17,6 @@ const defaultOpts = (): SignalOptions => ({
15
17
  websocketTimeout: 5_000,
16
18
  });
17
19
 
18
- const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
19
-
20
20
  function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
21
21
  return Promise.race([
22
22
  p,
@@ -70,12 +70,11 @@ describe.skipIf(!!unavailable)('SignalClient e2e', () => {
70
70
  });
71
71
 
72
72
  it('stays connected past the ping timeout while the server pongs', async () => {
73
- const onClose = vi.fn();
74
73
  await join('happy');
75
- client.onClose = onClose;
74
+ const closed = captureClose(client);
76
75
  // Server ping timeout is 3s; a healthy pong loop must keep us alive.
77
76
  await sleep(4_000);
78
- expect(onClose).not.toHaveBeenCalled();
77
+ expect(await isPending(closed)).toBe(true);
79
78
  expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
80
79
  });
81
80
 
@@ -151,15 +150,19 @@ describe.skipIf(!!unavailable)('SignalClient e2e', () => {
151
150
  (e) => e as Error,
152
151
  );
153
152
  expect(err).toBeInstanceOf(Error);
153
+ expect((err as { reason?: ConnectionErrorReason }).reason).toBe(
154
+ ConnectionErrorReason.LeaveRequest,
155
+ );
156
+ expect((err as Error).message).toContain('Received leave request');
154
157
  });
155
158
 
156
159
  it('closes gracefully via close() without firing onClose', async () => {
157
- const onClose = vi.fn();
158
160
  await join('happy');
159
- client.onClose = onClose;
161
+ const closed = captureClose(client);
160
162
  await client.close();
161
163
  expect(client.isDisconnected).toBe(true);
162
- expect(onClose).not.toHaveBeenCalled();
164
+ // Give any stray onClose a macrotask to land before asserting it stayed silent.
165
+ expect(await isPending(closed, 100)).toBe(true);
163
166
  });
164
167
 
165
168
  it('reconnects (resume) back to CONNECTED', async () => {
@@ -181,6 +184,10 @@ describe.skipIf(!!unavailable)('SignalClient e2e', () => {
181
184
  (e) => e as Error,
182
185
  );
183
186
  expect(err).toBeInstanceOf(Error);
187
+ expect((err as { reason?: ConnectionErrorReason }).reason).toBe(
188
+ ConnectionErrorReason.LeaveRequest,
189
+ );
190
+ expect((err as Error).message).toContain('Received leave request');
184
191
  });
185
192
 
186
193
  // --- validate-endpoint classification -------------------------------