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/utils.ts CHANGED
@@ -58,7 +58,7 @@ export function supportsAddTrack() {
58
58
  }
59
59
 
60
60
  export function supportsAdaptiveStream() {
61
- return typeof ResizeObserver !== undefined && typeof IntersectionObserver !== undefined;
61
+ return typeof ResizeObserver !== 'undefined' && typeof IntersectionObserver !== 'undefined';
62
62
  }
63
63
 
64
64
  export function supportsDynacast() {
@@ -146,6 +146,45 @@ export function isSVCCodec(codec?: string): boolean {
146
146
  return codec === 'av1' || codec === 'vp9';
147
147
  }
148
148
 
149
+ /**
150
+ * Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
151
+ *
152
+ * Chrome only offers the extension on transceivers that can send, so one we create to receive on
153
+ * never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
154
+ * assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
155
+ * transceiver rather than munging the extension into the SDP leaves the browser owning the
156
+ * extension id, which is what keeps that id consistent across the bundle and across
157
+ * renegotiations.
158
+ *
159
+ * A no-op where the browser offers no such control, or does not know the extension at all.
160
+ * @internal
161
+ */
162
+ export function negotiateDependencyDescriptor(transceiver: RTCRtpTransceiver): boolean {
163
+ const extensions = transceiver.getHeaderExtensionsToNegotiate?.();
164
+ if (!extensions || !transceiver.setHeaderExtensionsToNegotiate) {
165
+ return false;
166
+ }
167
+ const dd = extensions.find((ext) => ext.uri === ddExtensionURI);
168
+ if (!dd) {
169
+ return false;
170
+ }
171
+ if (dd.direction !== 'stopped') {
172
+ return true;
173
+ }
174
+ // sendrecv rather than recvonly, so that the extension is written as a plain `a=extmap` line —
175
+ // the form the server already emits where it is the one offering — rather than one carrying a
176
+ // `/recvonly` suffix that its parser may not expect
177
+ dd.direction = 'sendrecv';
178
+ try {
179
+ transceiver.setHeaderExtensionsToNegotiate(extensions);
180
+ return true;
181
+ } catch (e) {
182
+ // a rejected direction throws. Negotiating without the extension is what happened before this
183
+ // existed, so it is not worth failing the connection over
184
+ return false;
185
+ }
186
+ }
187
+
149
188
  export function supportsSetSinkId(elm?: HTMLMediaElement): boolean {
150
189
  if (!document || isSafariBased()) {
151
190
  return false;
@@ -867,3 +906,16 @@ export function isPublisherOfferWithJoinSupported() {
867
906
  // we have connectivity issue about publisher offer with join on firefox #1919
868
907
  return isCompressionStreamSupported() && !isFireFox();
869
908
  }
909
+
910
+ export function extractTrackSid(
911
+ mediaTrack: MediaStreamTrack,
912
+ stream: MediaStream,
913
+ ): Track.SID | undefined {
914
+ const [, streamId] = unpackStreamId(stream.id);
915
+ if (streamId?.startsWith('TR')) {
916
+ return streamId;
917
+ }
918
+ if (mediaTrack.id.startsWith('TR')) {
919
+ return mediaTrack.id;
920
+ }
921
+ }
@@ -0,0 +1,23 @@
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 async function isPending(promise: Promise<unknown>, ms = 0): Promise<boolean> {
12
+ const pending = Symbol('pending');
13
+ // Swallow settlement (incl. rejection) so racing it can't raise unhandled
14
+ // rejections when the timer wins.
15
+ const settled = promise.then(
16
+ () => 'settled' as const,
17
+ () => 'settled' as const,
18
+ );
19
+ const timer = new Promise<typeof pending>((resolve) => {
20
+ setTimeout(() => resolve(pending), ms);
21
+ });
22
+ return (await Promise.race([settled, timer])) === pending;
23
+ }
@@ -2,6 +2,7 @@ import { type ChildProcess, execFileSync, spawn } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { Socket, createServer } from 'node:net';
4
4
  import { join } from 'node:path';
5
+ import { sleep } from '../room/utils';
5
6
  import { createToken } from './signalToken';
6
7
 
7
8
  /**
@@ -47,7 +48,7 @@ async function waitReady(serverUrl: string): Promise<boolean> {
47
48
  } catch {
48
49
  // not up yet
49
50
  }
50
- await new Promise((r) => setTimeout(r, 200));
51
+ await sleep(200);
51
52
  }
52
53
  return false;
53
54
  }
@@ -1,4 +1,4 @@
1
- import { SignJWT } from 'jose';
1
+ import { AccessToken } from 'livekit-server-sdk';
2
2
 
3
3
  /**
4
4
  * Mint a LiveKit access token for the mock test-server (HS256, dev secret).
@@ -33,24 +33,28 @@ export async function createToken(opts: TokenOptions = {}): Promise<string> {
33
33
  secret = 'secret',
34
34
  ttlSeconds = 600,
35
35
  } = opts;
36
- const key = new TextEncoder().encode(secret);
37
- const payload: Record<string, unknown> = {
38
- video: { room, roomJoin: true, canPublish: true, canSubscribe: true, canPublishData: true },
39
- };
36
+ const attributes: Record<string, string> = {};
40
37
  if (signal) {
41
38
  const control: Record<string, unknown> = { signal };
42
39
  if (leaveAction !== undefined) {
43
40
  control.leaveAction = leaveAction;
44
41
  }
45
- payload.attributes = { 'lk.mock': JSON.stringify(control) };
42
+ attributes['lk.mock'] = JSON.stringify(control);
46
43
  }
47
- return new SignJWT(payload)
48
- .setProtectedHeader({ alg: 'HS256' })
49
- .setIssuer(apiKey)
50
- .setSubject(identity)
51
- .setIssuedAt()
52
- .setExpirationTime(`${ttlSeconds}s`)
53
- .sign(key);
44
+ const token = new AccessToken(apiKey, secret, {
45
+ ttl: ttlSeconds,
46
+ identity,
47
+ attributes,
48
+ });
49
+ token.addGrant({
50
+ room,
51
+ roomJoin: true,
52
+ canPublish: true,
53
+ canSubscribe: true,
54
+ canPublishData: true,
55
+ });
56
+
57
+ return token.toJwt();
54
58
  }
55
59
 
56
60
  /** A syntactically-valid token signed with the WRONG secret — for 401 tests. */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Per-transceiver control over which RTP header extensions get negotiated. Declared as optional
3
+ * because it is not in lib.dom yet and not every browser implements it.
4
+ * https://w3c.github.io/webrtc-extensions/#rtcrtptransceiver-interface-extensions
5
+ */
6
+ interface RTCRtpTransceiver {
7
+ getHeaderExtensionsToNegotiate?(): RTCRtpHeaderExtensionCapability[];
8
+ setHeaderExtensionsToNegotiate?(extensions: RTCRtpHeaderExtensionCapability[]): void;
9
+ }
10
+
11
+ interface RTCRtpHeaderExtensionCapability {
12
+ direction?: 'sendrecv' | 'sendonly' | 'recvonly' | 'stopped';
13
+ }
@@ -3,9 +3,9 @@ import { Mutex } from '@livekit/mutex';
3
3
  type QueueTask<T> = () => PromiseLike<T>;
4
4
 
5
5
  enum QueueTaskStatus {
6
- 'WAITING',
7
- 'RUNNING',
8
- 'COMPLETED',
6
+ WAITING,
7
+ RUNNING,
8
+ COMPLETED,
9
9
  }
10
10
 
11
11
  type QueueTaskInfo = {
@@ -0,0 +1,90 @@
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
+ /**
15
+ * The slice of machina's `Fsm` surface an inspector needs. Structural rather than machina's own
16
+ * type so that any machine satisfies it regardless of its state and input unions.
17
+ */
18
+ export interface InspectableMachine {
19
+ readonly id: string;
20
+ readonly initialState: string;
21
+ readonly states: Record<string, Record<string, unknown>>;
22
+ readonly context?: unknown;
23
+ currentState(): string;
24
+ canHandle(input: string): boolean;
25
+ on(eventName: string, callback: (data: any) => void): { 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
+ /** Kept small: enough for a panel opened mid-session to see how the connection got where it is. */
43
+ const HISTORY_LIMIT = 16;
44
+
45
+ let enabled = false;
46
+ let announced: MachineAnnouncement[] = [];
47
+ let subscribers: Array<(announcement: MachineAnnouncement) => void> = [];
48
+
49
+ /**
50
+ * Starts recording machine announcements. Call before the machines of interest are constructed —
51
+ * for the signal machine that means before the `Room` is created.
52
+ */
53
+ export function enableMachineInspector() {
54
+ enabled = true;
55
+ }
56
+
57
+ export function isMachineInspectorEnabled() {
58
+ return enabled;
59
+ }
60
+
61
+ /** Announces a machine to whatever is watching. A no-op unless the inspector was enabled. */
62
+ export function announceMachine(label: string, machine: InspectableMachine) {
63
+ if (!enabled) {
64
+ return;
65
+ }
66
+ const announcement: MachineAnnouncement = {
67
+ label,
68
+ seq: announced.length,
69
+ at: Date.now(),
70
+ machine,
71
+ };
72
+ announced = [...announced.slice(-(HISTORY_LIMIT - 1)), announcement];
73
+ for (const subscriber of subscribers) {
74
+ subscriber(announcement);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Subscribes to machine announcements, replaying the ones already recorded so that a panel opened
80
+ * after connecting still sees the machine currently in charge. Returns an unsubscribe function.
81
+ */
82
+ export function onMachineAnnounced(callback: (announcement: MachineAnnouncement) => void) {
83
+ subscribers = [...subscribers, callback];
84
+ for (const announcement of announced) {
85
+ callback(announcement);
86
+ }
87
+ return () => {
88
+ subscribers = subscribers.filter((subscriber) => subscriber !== callback);
89
+ };
90
+ }