livekit-client 2.5.9 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. package/README.md +54 -0
  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 +500 -5114
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +519 -127
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/room/PCTransport.d.ts +2 -0
  14. package/dist/src/room/PCTransport.d.ts.map +1 -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/participant/LocalParticipant.d.ts +56 -0
  19. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  20. package/dist/src/room/rpc.d.ts +96 -0
  21. package/dist/src/room/rpc.d.ts.map +1 -0
  22. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  23. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  24. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/utils.d.ts +2 -2
  26. package/dist/src/room/track/utils.d.ts.map +1 -1
  27. package/dist/src/room/utils.d.ts +0 -10
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/ts4.2/src/index.d.ts +4 -1
  30. package/dist/ts4.2/src/room/PCTransport.d.ts +2 -0
  31. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +56 -0
  32. package/dist/ts4.2/src/room/rpc.d.ts +96 -0
  33. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -1
  34. package/dist/ts4.2/src/room/track/utils.d.ts +2 -2
  35. package/dist/ts4.2/src/room/utils.d.ts +0 -10
  36. package/package.json +3 -2
  37. package/src/api/SignalClient.ts +2 -1
  38. package/src/index.ts +3 -1
  39. package/src/room/PCTransport.ts +42 -29
  40. package/src/room/PCTransportManager.ts +2 -1
  41. package/src/room/RTCEngine.ts +3 -1
  42. package/src/room/Room.ts +2 -1
  43. package/src/room/participant/LocalParticipant.test.ts +304 -0
  44. package/src/room/participant/LocalParticipant.ts +340 -1
  45. package/src/room/rpc.ts +172 -0
  46. package/src/room/track/LocalTrack.ts +2 -1
  47. package/src/room/track/LocalVideoTrack.ts +2 -1
  48. package/src/room/track/options.ts +5 -5
  49. package/src/room/track/utils.ts +1 -6
  50. package/src/room/utils.ts +0 -38
  51. package/src/utils/AsyncQueue.test.ts +2 -2
  52. package/src/utils/AsyncQueue.ts +1 -1
@@ -9,6 +9,9 @@ import {
9
9
  ParticipantPermission,
10
10
  RequestResponse,
11
11
  RequestResponse_Reason,
12
+ RpcAck,
13
+ RpcRequest,
14
+ RpcResponse,
12
15
  SimulcastCodec,
13
16
  SipDTMF,
14
17
  SubscribedQualityUpdate,
@@ -29,6 +32,13 @@ import {
29
32
  UnexpectedConnectionState,
30
33
  } from '../errors';
31
34
  import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
35
+ import {
36
+ MAX_PAYLOAD_BYTES,
37
+ type PerformRpcParams,
38
+ RpcError,
39
+ type RpcInvocationData,
40
+ byteLength,
41
+ } from '../rpc';
32
42
  import LocalAudioTrack from '../track/LocalAudioTrack';
33
43
  import LocalTrack from '../track/LocalTrack';
34
44
  import LocalTrackPublication from '../track/LocalTrackPublication';
@@ -54,6 +64,7 @@ import {
54
64
  import type { ChatMessage, DataPublishOptions } from '../types';
55
65
  import {
56
66
  Future,
67
+ compareVersions,
57
68
  isE2EESimulcastSupported,
58
69
  isFireFox,
59
70
  isSVCCodec,
@@ -119,6 +130,18 @@ export default class LocalParticipant extends Participant {
119
130
 
120
131
  private enabledPublishVideoCodecs: Codec[] = [];
121
132
 
133
+ private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
134
+
135
+ private pendingAcks = new Map<string, { resolve: () => void; participantIdentity: string }>();
136
+
137
+ private pendingResponses = new Map<
138
+ string,
139
+ {
140
+ resolve: (payload: string | null, error: RpcError | null) => void;
141
+ participantIdentity: string;
142
+ }
143
+ >();
144
+
122
145
  /** @internal */
123
146
  constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
124
147
  super(sid, identity, undefined, undefined, {
@@ -187,7 +210,8 @@ export default class LocalParticipant extends Participant {
187
210
  .on(EngineEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
188
211
  .on(EngineEvent.SubscribedQualityUpdate, this.handleSubscribedQualityUpdate)
189
212
  .on(EngineEvent.Disconnected, this.handleDisconnected)
190
- .on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse);
213
+ .on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse)
214
+ .on(EngineEvent.DataPacketReceived, this.handleDataPacket);
191
215
  }
192
216
 
193
217
  private handleReconnecting = () => {
@@ -221,6 +245,38 @@ export default class LocalParticipant extends Participant {
221
245
  }
222
246
  };
223
247
 
248
+ private handleDataPacket = (packet: DataPacket) => {
249
+ switch (packet.value.case) {
250
+ case 'rpcRequest':
251
+ let rpcRequest = packet.value.value as RpcRequest;
252
+ this.handleIncomingRpcRequest(
253
+ packet.participantIdentity,
254
+ rpcRequest.id,
255
+ rpcRequest.method,
256
+ rpcRequest.payload,
257
+ rpcRequest.responseTimeoutMs,
258
+ rpcRequest.version,
259
+ );
260
+ break;
261
+ case 'rpcResponse':
262
+ let rpcResponse = packet.value.value as RpcResponse;
263
+ let payload: string | null = null;
264
+ let error: RpcError | null = null;
265
+
266
+ if (rpcResponse.value.case === 'payload') {
267
+ payload = rpcResponse.value.value;
268
+ } else if (rpcResponse.value.case === 'error') {
269
+ error = RpcError.fromProto(rpcResponse.value.value);
270
+ }
271
+ this.handleIncomingRpcResponse(rpcResponse.requestId, payload, error);
272
+ break;
273
+ case 'rpcAck':
274
+ let rpcAck = packet.value.value as RpcAck;
275
+ this.handleIncomingRpcAck(rpcAck.requestId);
276
+ break;
277
+ }
278
+ };
279
+
224
280
  /**
225
281
  * Sets and updates the metadata of the local participant.
226
282
  * Note: this requires `canUpdateOwnMetadata` permission.
@@ -1415,6 +1471,121 @@ export default class LocalParticipant extends Participant {
1415
1471
  return msg;
1416
1472
  }
1417
1473
 
1474
+ /**
1475
+ * Initiate an RPC call to a remote participant
1476
+ * @param params - Parameters for initiating the RPC call, see {@link PerformRpcParams}
1477
+ * @returns A promise that resolves with the response payload or rejects with an error.
1478
+ * @throws Error on failure. Details in `message`.
1479
+ */
1480
+ async performRpc({
1481
+ destinationIdentity,
1482
+ method,
1483
+ payload,
1484
+ responseTimeout = 10000,
1485
+ }: PerformRpcParams): Promise<string> {
1486
+ const maxRoundTripLatency = 2000;
1487
+
1488
+ return new Promise(async (resolve, reject) => {
1489
+ if (byteLength(payload) > MAX_PAYLOAD_BYTES) {
1490
+ reject(RpcError.builtIn('REQUEST_PAYLOAD_TOO_LARGE'));
1491
+ return;
1492
+ }
1493
+
1494
+ if (
1495
+ this.engine.latestJoinResponse?.serverInfo?.version &&
1496
+ compareVersions(this.engine.latestJoinResponse?.serverInfo?.version, '1.8.0') < 0
1497
+ ) {
1498
+ reject(RpcError.builtIn('UNSUPPORTED_SERVER'));
1499
+ return;
1500
+ }
1501
+
1502
+ const id = crypto.randomUUID();
1503
+ await this.publishRpcRequest(
1504
+ destinationIdentity,
1505
+ id,
1506
+ method,
1507
+ payload,
1508
+ responseTimeout - maxRoundTripLatency,
1509
+ );
1510
+
1511
+ const ackTimeoutId = setTimeout(() => {
1512
+ this.pendingAcks.delete(id);
1513
+ reject(RpcError.builtIn('CONNECTION_TIMEOUT'));
1514
+ this.pendingResponses.delete(id);
1515
+ clearTimeout(responseTimeoutId);
1516
+ }, maxRoundTripLatency);
1517
+
1518
+ this.pendingAcks.set(id, {
1519
+ resolve: () => {
1520
+ clearTimeout(ackTimeoutId);
1521
+ },
1522
+ participantIdentity: destinationIdentity,
1523
+ });
1524
+
1525
+ const responseTimeoutId = setTimeout(() => {
1526
+ this.pendingResponses.delete(id);
1527
+ reject(RpcError.builtIn('RESPONSE_TIMEOUT'));
1528
+ }, responseTimeout);
1529
+
1530
+ this.pendingResponses.set(id, {
1531
+ resolve: (responsePayload: string | null, responseError: RpcError | null) => {
1532
+ clearTimeout(responseTimeoutId);
1533
+ if (this.pendingAcks.has(id)) {
1534
+ console.warn('RPC response received before ack', id);
1535
+ this.pendingAcks.delete(id);
1536
+ clearTimeout(ackTimeoutId);
1537
+ }
1538
+
1539
+ if (responseError) {
1540
+ reject(responseError);
1541
+ } else {
1542
+ resolve(responsePayload ?? '');
1543
+ }
1544
+ },
1545
+ participantIdentity: destinationIdentity,
1546
+ });
1547
+ });
1548
+ }
1549
+
1550
+ /**
1551
+ * Establishes the participant as a receiver for calls of the specified RPC method.
1552
+ * Will overwrite any existing callback for the same method.
1553
+ *
1554
+ * @param method - The name of the indicated RPC method
1555
+ * @param handler - Will be invoked when an RPC request for this method is received
1556
+ * @returns A promise that resolves when the method is successfully registered
1557
+ *
1558
+ * @example
1559
+ * ```typescript
1560
+ * room.localParticipant?.registerRpcMethod(
1561
+ * 'greet',
1562
+ * async (data: RpcInvocationData) => {
1563
+ * console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`);
1564
+ * return `Hello, ${data.callerIdentity}!`;
1565
+ * }
1566
+ * );
1567
+ * ```
1568
+ *
1569
+ * The handler should return a Promise that resolves to a string.
1570
+ * If unable to respond within `responseTimeout`, the request will result in an error on the caller's side.
1571
+ *
1572
+ * You may throw errors of type `RpcError` with a string `message` in the handler,
1573
+ * and they will be received on the caller's side with the message intact.
1574
+ * Other errors thrown in your handler will not be transmitted as-is, and will instead arrive to the caller as `1500` ("Application Error").
1575
+ */
1576
+ registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
1577
+ this.rpcHandlers.set(method, handler);
1578
+ }
1579
+
1580
+ /**
1581
+ * Unregisters a previously registered RPC method.
1582
+ *
1583
+ * @param method - The name of the RPC method to unregister
1584
+ */
1585
+ unregisterRpcMethod(method: string) {
1586
+ this.rpcHandlers.delete(method);
1587
+ }
1588
+
1418
1589
  /**
1419
1590
  * Control who can subscribe to LocalParticipant's published tracks.
1420
1591
  *
@@ -1443,6 +1614,174 @@ export default class LocalParticipant extends Participant {
1443
1614
  }
1444
1615
  }
1445
1616
 
1617
+ private handleIncomingRpcAck(requestId: string) {
1618
+ const handler = this.pendingAcks.get(requestId);
1619
+ if (handler) {
1620
+ handler.resolve();
1621
+ this.pendingAcks.delete(requestId);
1622
+ } else {
1623
+ console.error('Ack received for unexpected RPC request', requestId);
1624
+ }
1625
+ }
1626
+
1627
+ private handleIncomingRpcResponse(
1628
+ requestId: string,
1629
+ payload: string | null,
1630
+ error: RpcError | null,
1631
+ ) {
1632
+ const handler = this.pendingResponses.get(requestId);
1633
+ if (handler) {
1634
+ handler.resolve(payload, error);
1635
+ this.pendingResponses.delete(requestId);
1636
+ } else {
1637
+ console.error('Response received for unexpected RPC request', requestId);
1638
+ }
1639
+ }
1640
+
1641
+ private async handleIncomingRpcRequest(
1642
+ callerIdentity: string,
1643
+ requestId: string,
1644
+ method: string,
1645
+ payload: string,
1646
+ responseTimeout: number,
1647
+ version: number,
1648
+ ) {
1649
+ await this.publishRpcAck(callerIdentity, requestId);
1650
+
1651
+ if (version !== 1) {
1652
+ await this.publishRpcResponse(
1653
+ callerIdentity,
1654
+ requestId,
1655
+ null,
1656
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
1657
+ );
1658
+ return;
1659
+ }
1660
+
1661
+ const handler = this.rpcHandlers.get(method);
1662
+
1663
+ if (!handler) {
1664
+ await this.publishRpcResponse(
1665
+ callerIdentity,
1666
+ requestId,
1667
+ null,
1668
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
1669
+ );
1670
+ return;
1671
+ }
1672
+
1673
+ let responseError: RpcError | null = null;
1674
+ let responsePayload: string | null = null;
1675
+
1676
+ try {
1677
+ const response = await handler({
1678
+ requestId,
1679
+ callerIdentity,
1680
+ payload,
1681
+ responseTimeout,
1682
+ });
1683
+ if (byteLength(response) > MAX_PAYLOAD_BYTES) {
1684
+ responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
1685
+ console.warn(`RPC Response payload too large for ${method}`);
1686
+ } else {
1687
+ responsePayload = response;
1688
+ }
1689
+ } catch (error) {
1690
+ if (error instanceof RpcError) {
1691
+ responseError = error;
1692
+ } else {
1693
+ console.warn(
1694
+ `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
1695
+ error,
1696
+ );
1697
+ responseError = RpcError.builtIn('APPLICATION_ERROR');
1698
+ }
1699
+ }
1700
+ await this.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
1701
+ }
1702
+
1703
+ /** @internal */
1704
+ private async publishRpcRequest(
1705
+ destinationIdentity: string,
1706
+ requestId: string,
1707
+ method: string,
1708
+ payload: string,
1709
+ responseTimeout: number,
1710
+ ) {
1711
+ const packet = new DataPacket({
1712
+ destinationIdentities: [destinationIdentity],
1713
+ kind: DataPacket_Kind.RELIABLE,
1714
+ value: {
1715
+ case: 'rpcRequest',
1716
+ value: new RpcRequest({
1717
+ id: requestId,
1718
+ method,
1719
+ payload,
1720
+ responseTimeoutMs: responseTimeout,
1721
+ version: 1,
1722
+ }),
1723
+ },
1724
+ });
1725
+
1726
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1727
+ }
1728
+
1729
+ /** @internal */
1730
+ private async publishRpcResponse(
1731
+ destinationIdentity: string,
1732
+ requestId: string,
1733
+ payload: string | null,
1734
+ error: RpcError | null,
1735
+ ) {
1736
+ const packet = new DataPacket({
1737
+ destinationIdentities: [destinationIdentity],
1738
+ kind: DataPacket_Kind.RELIABLE,
1739
+ value: {
1740
+ case: 'rpcResponse',
1741
+ value: new RpcResponse({
1742
+ requestId,
1743
+ value: error
1744
+ ? { case: 'error', value: error.toProto() }
1745
+ : { case: 'payload', value: payload ?? '' },
1746
+ }),
1747
+ },
1748
+ });
1749
+
1750
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1751
+ }
1752
+
1753
+ /** @internal */
1754
+ private async publishRpcAck(destinationIdentity: string, requestId: string) {
1755
+ const packet = new DataPacket({
1756
+ destinationIdentities: [destinationIdentity],
1757
+ kind: DataPacket_Kind.RELIABLE,
1758
+ value: {
1759
+ case: 'rpcAck',
1760
+ value: new RpcAck({
1761
+ requestId,
1762
+ }),
1763
+ },
1764
+ });
1765
+
1766
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1767
+ }
1768
+
1769
+ /** @internal */
1770
+ handleParticipantDisconnected(participantIdentity: string) {
1771
+ for (const [id, { participantIdentity: pendingIdentity }] of this.pendingAcks) {
1772
+ if (pendingIdentity === participantIdentity) {
1773
+ this.pendingAcks.delete(id);
1774
+ }
1775
+ }
1776
+
1777
+ for (const [id, { participantIdentity: pendingIdentity, resolve }] of this.pendingResponses) {
1778
+ if (pendingIdentity === participantIdentity) {
1779
+ resolve(null, RpcError.builtIn('RECIPIENT_DISCONNECTED'));
1780
+ this.pendingResponses.delete(id);
1781
+ }
1782
+ }
1783
+ }
1784
+
1446
1785
  /** @internal */
1447
1786
  setEnabledPublishCodecs(codecs: Codec[]) {
1448
1787
  this.enabledPublishVideoCodecs = codecs.filter(
@@ -0,0 +1,172 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { RpcError as RpcError_Proto } from '@livekit/protocol';
5
+
6
+ /** Parameters for initiating an RPC call */
7
+ export interface PerformRpcParams {
8
+ /** The `identity` of the destination participant */
9
+ destinationIdentity: string;
10
+ /** The method name to call */
11
+ method: string;
12
+ /** The method payload */
13
+ payload: string;
14
+ /** Timeout for receiving a response after initial connection (milliseconds). Default: 10000 */
15
+ responseTimeout?: number;
16
+ }
17
+
18
+ /**
19
+ * Data passed to method handler for incoming RPC invocations
20
+ */
21
+ export interface RpcInvocationData {
22
+ /**
23
+ * The unique request ID. Will match at both sides of the call, useful for debugging or logging.
24
+ */
25
+ requestId: string;
26
+
27
+ /**
28
+ * The unique participant identity of the caller.
29
+ */
30
+ callerIdentity: string;
31
+
32
+ /**
33
+ * The payload of the request. User-definable format, typically JSON.
34
+ */
35
+ payload: string;
36
+
37
+ /**
38
+ * The maximum time the caller will wait for a response.
39
+ */
40
+ responseTimeout: number;
41
+ }
42
+
43
+ /**
44
+ * Specialized error handling for RPC methods.
45
+ *
46
+ * Instances of this type, when thrown in a method handler, will have their `message`
47
+ * serialized and sent across the wire. The sender will receive an equivalent error on the other side.
48
+ *
49
+ * Built-in types are included but developers may use any string, with a max length of 256 bytes.
50
+ */
51
+
52
+ export class RpcError extends Error {
53
+ static MAX_MESSAGE_BYTES = 256;
54
+
55
+ static MAX_DATA_BYTES = 15360; // 15 KB
56
+
57
+ code: number;
58
+
59
+ data?: string;
60
+
61
+ /**
62
+ * Creates an error object with the given code and message, plus an optional data payload.
63
+ *
64
+ * If thrown in an RPC method handler, the error will be sent back to the caller.
65
+ *
66
+ * Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings).
67
+ */
68
+ constructor(code: number, message: string, data?: string) {
69
+ super(message);
70
+ this.code = code;
71
+ this.message = truncateBytes(message, RpcError.MAX_MESSAGE_BYTES);
72
+ this.data = data ? truncateBytes(data, RpcError.MAX_DATA_BYTES) : undefined;
73
+ }
74
+
75
+ /**
76
+ * @internal
77
+ */
78
+ static fromProto(proto: RpcError_Proto) {
79
+ return new RpcError(proto.code, proto.message, proto.data);
80
+ }
81
+
82
+ /**
83
+ * @internal
84
+ */
85
+ toProto() {
86
+ return new RpcError_Proto({
87
+ code: this.code as number,
88
+ message: this.message,
89
+ data: this.data,
90
+ });
91
+ }
92
+
93
+ static ErrorCode = {
94
+ APPLICATION_ERROR: 1500,
95
+ CONNECTION_TIMEOUT: 1501,
96
+ RESPONSE_TIMEOUT: 1502,
97
+ RECIPIENT_DISCONNECTED: 1503,
98
+ RESPONSE_PAYLOAD_TOO_LARGE: 1504,
99
+ SEND_FAILED: 1505,
100
+
101
+ UNSUPPORTED_METHOD: 1400,
102
+ RECIPIENT_NOT_FOUND: 1401,
103
+ REQUEST_PAYLOAD_TOO_LARGE: 1402,
104
+ UNSUPPORTED_SERVER: 1403,
105
+ UNSUPPORTED_VERSION: 1404,
106
+ } as const;
107
+
108
+ /**
109
+ * @internal
110
+ */
111
+ static ErrorMessage: Record<keyof typeof RpcError.ErrorCode, string> = {
112
+ APPLICATION_ERROR: 'Application error in method handler',
113
+ CONNECTION_TIMEOUT: 'Connection timeout',
114
+ RESPONSE_TIMEOUT: 'Response timeout',
115
+ RECIPIENT_DISCONNECTED: 'Recipient disconnected',
116
+ RESPONSE_PAYLOAD_TOO_LARGE: 'Response payload too large',
117
+ SEND_FAILED: 'Failed to send',
118
+
119
+ UNSUPPORTED_METHOD: 'Method not supported at destination',
120
+ RECIPIENT_NOT_FOUND: 'Recipient not found',
121
+ REQUEST_PAYLOAD_TOO_LARGE: 'Request payload too large',
122
+ UNSUPPORTED_SERVER: 'RPC not supported by server',
123
+ UNSUPPORTED_VERSION: 'Unsupported RPC version',
124
+ } as const;
125
+
126
+ /**
127
+ * Creates an error object from the code, with an auto-populated message.
128
+ *
129
+ * @internal
130
+ */
131
+ static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError {
132
+ return new RpcError(RpcError.ErrorCode[key], RpcError.ErrorMessage[key], data);
133
+ }
134
+ }
135
+
136
+ /*
137
+ * Maximum payload size for RPC requests and responses. If a payload exceeds this size,
138
+ * the RPC call will fail with a REQUEST_PAYLOAD_TOO_LARGE(1402) or RESPONSE_PAYLOAD_TOO_LARGE(1504) error.
139
+ */
140
+ export const MAX_PAYLOAD_BYTES = 15360; // 15 KB
141
+
142
+ /**
143
+ * @internal
144
+ */
145
+ export function byteLength(str: string): number {
146
+ const encoder = new TextEncoder();
147
+ return encoder.encode(str).length;
148
+ }
149
+
150
+ /**
151
+ * @internal
152
+ */
153
+ export function truncateBytes(str: string, maxBytes: number): string {
154
+ if (byteLength(str) <= maxBytes) {
155
+ return str;
156
+ }
157
+
158
+ let low = 0;
159
+ let high = str.length;
160
+ const encoder = new TextEncoder();
161
+
162
+ while (low < high) {
163
+ const mid = Math.floor((low + high + 1) / 2);
164
+ if (encoder.encode(str.slice(0, mid)).length <= maxBytes) {
165
+ low = mid;
166
+ } else {
167
+ high = mid - 1;
168
+ }
169
+ }
170
+
171
+ return str.slice(0, low);
172
+ }
@@ -1,10 +1,11 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import { debounce } from 'ts-debounce';
2
3
  import { getBrowser } from '../../utils/browserParser';
3
4
  import DeviceManager from '../DeviceManager';
4
5
  import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
5
6
  import { TrackEvent } from '../events';
6
7
  import type { LoggerOptions } from '../types';
7
- import { Mutex, compareVersions, isMobile, sleep } from '../utils';
8
+ import { compareVersions, isMobile, sleep } from '../utils';
8
9
  import { Track, attachToElement, detachTrack } from './Track';
9
10
  import type { VideoCodec } from './options';
10
11
  import type { TrackProcessor } from './processor/types';
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import {
2
3
  VideoQuality as ProtoVideoQuality,
3
4
  SubscribedCodec,
@@ -10,7 +11,7 @@ import { ScalabilityMode } from '../participant/publishUtils';
10
11
  import type { VideoSenderStats } from '../stats';
11
12
  import { computeBitrate, monitorFrequency } from '../stats';
12
13
  import type { LoggerOptions } from '../types';
13
- import { Mutex, isFireFox, isMobile, isWeb, unwrapConstraint } from '../utils';
14
+ import { isFireFox, isMobile, isWeb, unwrapConstraint } from '../utils';
14
15
  import LocalTrack from './LocalTrack';
15
16
  import { Track, VideoQuality } from './Track';
16
17
  import type { VideoCaptureOptions, VideoCodec } from './options';
@@ -398,19 +398,19 @@ export namespace AudioPresets {
398
398
  maxBitrate: 12_000,
399
399
  };
400
400
  export const speech: AudioPreset = {
401
- maxBitrate: 20_000,
401
+ maxBitrate: 24_000,
402
402
  };
403
403
  export const music: AudioPreset = {
404
- maxBitrate: 32_000,
404
+ maxBitrate: 48_000,
405
405
  };
406
406
  export const musicStereo: AudioPreset = {
407
- maxBitrate: 48_000,
407
+ maxBitrate: 64_000,
408
408
  };
409
409
  export const musicHighQuality: AudioPreset = {
410
- maxBitrate: 64_000,
410
+ maxBitrate: 96_000,
411
411
  };
412
412
  export const musicHighQualityStereo: AudioPreset = {
413
- maxBitrate: 96_000,
413
+ maxBitrate: 128_000,
414
414
  };
415
415
  }
416
416
 
@@ -9,7 +9,6 @@ import {
9
9
  type ScreenShareCaptureOptions,
10
10
  type VideoCaptureOptions,
11
11
  type VideoCodec,
12
- videoCodecs,
13
12
  } from './options';
14
13
  import type { AudioTrack } from './types';
15
14
 
@@ -188,11 +187,7 @@ export function screenCaptureToDisplayMediaStreamOptions(
188
187
  }
189
188
 
190
189
  export function mimeTypeToVideoCodecString(mimeType: string) {
191
- const codec = mimeType.split('/')[1].toLowerCase() as VideoCodec;
192
- if (!videoCodecs.includes(codec)) {
193
- throw Error(`Video codec not supported: ${codec}`);
194
- }
195
- return codec;
190
+ return mimeType.split('/')[1].toLowerCase() as VideoCodec;
196
191
  }
197
192
 
198
193
  export function getTrackPublicationInfo<T extends TrackPublication>(
package/src/room/utils.ts CHANGED
@@ -454,44 +454,6 @@ export function createAudioAnalyser(
454
454
  return { calculateVolume, analyser, cleanup };
455
455
  }
456
456
 
457
- /**
458
- * @internal
459
- */
460
- export class Mutex {
461
- private _locking: Promise<void>;
462
-
463
- private _locks: number;
464
-
465
- constructor() {
466
- this._locking = Promise.resolve();
467
- this._locks = 0;
468
- }
469
-
470
- isLocked() {
471
- return this._locks > 0;
472
- }
473
-
474
- lock() {
475
- this._locks += 1;
476
-
477
- let unlockNext: () => void;
478
-
479
- const willLock = new Promise<void>(
480
- (resolve) =>
481
- (unlockNext = () => {
482
- this._locks -= 1;
483
- resolve();
484
- }),
485
- );
486
-
487
- const willUnlock = this._locking.then(() => unlockNext);
488
-
489
- this._locking = this._locking.then(() => willLock);
490
-
491
- return willUnlock;
492
- }
493
- }
494
-
495
457
  export function isVideoCodec(maybeCodec: string): maybeCodec is VideoCodec {
496
458
  return videoCodecs.includes(maybeCodec as VideoCodec);
497
459
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { assert, describe, expect, it } from 'vitest';
2
2
  import { sleep } from '../room/utils';
3
3
  import { AsyncQueue } from './AsyncQueue';
4
4
 
@@ -49,7 +49,7 @@ describe('asyncQueue', () => {
49
49
  task2Executed = true;
50
50
  })
51
51
  .catch(() => {
52
- fail('task 2 should not have thrown');
52
+ assert.fail('task 2 should not have thrown');
53
53
  });
54
54
 
55
55
  expect(task1threw).toBeTruthy();
@@ -1,4 +1,4 @@
1
- import { Mutex } from '../room/utils';
1
+ import { Mutex } from '@livekit/mutex';
2
2
 
3
3
  type QueueTask<T> = () => PromiseLike<T>;
4
4