livekit-client 2.8.1 → 2.9.0
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.
- package/README.md +18 -7
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +1 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2565 -1849
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +6 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +50 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/StreamReader.d.ts +56 -0
- package/dist/src/room/StreamReader.d.ts.map +1 -0
- package/dist/src/room/StreamWriter.d.ts +16 -0
- package/dist/src/room/StreamWriter.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +23 -36
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +1 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrackPublication.d.ts +1 -0
- package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +1 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -0
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +2 -1
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/facingMode.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +18 -2
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +43 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +26 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +7 -5
- package/dist/ts4.2/src/room/RTCEngine.d.ts +6 -0
- package/dist/ts4.2/src/room/Room.d.ts +49 -0
- package/dist/ts4.2/src/room/StreamReader.d.ts +56 -0
- package/dist/ts4.2/src/room/StreamWriter.d.ts +25 -0
- package/dist/ts4.2/src/room/errors.d.ts +3 -1
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +23 -36
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -0
- package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +2 -1
- package/dist/ts4.2/src/room/track/options.d.ts +18 -2
- package/dist/ts4.2/src/room/types.d.ts +43 -0
- package/dist/ts4.2/src/room/utils.d.ts +26 -0
- package/package.json +3 -3
- package/src/api/SignalClient.ts +5 -2
- package/src/e2ee/E2eeManager.ts +2 -2
- package/src/index.ts +17 -1
- package/src/room/RTCEngine.ts +69 -2
- package/src/room/Room.ts +311 -23
- package/src/room/StreamReader.ts +177 -0
- package/src/room/StreamWriter.ts +32 -0
- package/src/room/errors.ts +16 -2
- package/src/room/participant/LocalParticipant.ts +320 -165
- package/src/room/participant/Participant.ts +2 -5
- package/src/room/participant/RemoteParticipant.ts +3 -2
- package/src/room/{participant/LocalParticipant.test.ts → rpc.test.ts} +22 -29
- package/src/room/track/LocalAudioTrack.ts +4 -3
- package/src/room/track/LocalTrack.ts +12 -4
- package/src/room/track/LocalTrackPublication.ts +6 -1
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteTrack.ts +4 -0
- package/src/room/track/RemoteTrackPublication.ts +8 -4
- package/src/room/track/Track.ts +2 -0
- package/src/room/track/TrackPublication.ts +6 -3
- package/src/room/track/create.ts +4 -3
- package/src/room/track/facingMode.ts +2 -1
- package/src/room/track/options.ts +20 -2
- package/src/room/track/utils.ts +1 -1
- package/src/room/types.ts +50 -0
- package/src/room/utils.ts +77 -0
package/src/room/Room.ts
CHANGED
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
ConnectionQualityUpdate,
|
|
5
5
|
type DataPacket,
|
|
6
6
|
DataPacket_Kind,
|
|
7
|
+
DataStream_Chunk,
|
|
8
|
+
DataStream_Header,
|
|
9
|
+
DataStream_Trailer,
|
|
7
10
|
DisconnectReason,
|
|
8
11
|
JoinResponse,
|
|
9
12
|
LeaveRequest,
|
|
@@ -45,6 +48,12 @@ import { getBrowser } from '../utils/browserParser';
|
|
|
45
48
|
import DeviceManager from './DeviceManager';
|
|
46
49
|
import RTCEngine from './RTCEngine';
|
|
47
50
|
import { RegionUrlProvider } from './RegionUrlProvider';
|
|
51
|
+
import {
|
|
52
|
+
type ByteStreamHandler,
|
|
53
|
+
ByteStreamReader,
|
|
54
|
+
type TextStreamHandler,
|
|
55
|
+
TextStreamReader,
|
|
56
|
+
} from './StreamReader';
|
|
48
57
|
import {
|
|
49
58
|
audioDefaults,
|
|
50
59
|
publishDefaults,
|
|
@@ -58,6 +67,7 @@ import LocalParticipant from './participant/LocalParticipant';
|
|
|
58
67
|
import type Participant from './participant/Participant';
|
|
59
68
|
import type { ConnectionQuality } from './participant/Participant';
|
|
60
69
|
import RemoteParticipant from './participant/RemoteParticipant';
|
|
70
|
+
import { MAX_PAYLOAD_BYTES, RpcError, type RpcInvocationData, byteLength } from './rpc';
|
|
61
71
|
import CriticalTimers from './timers';
|
|
62
72
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
|
63
73
|
import type LocalTrack from './track/LocalTrack';
|
|
@@ -70,14 +80,18 @@ import type { TrackPublication } from './track/TrackPublication';
|
|
|
70
80
|
import type { TrackProcessor } from './track/processor/types';
|
|
71
81
|
import type { AdaptiveStreamSettings } from './track/types';
|
|
72
82
|
import { getNewAudioContext, sourceToKind } from './track/utils';
|
|
73
|
-
import
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
import {
|
|
84
|
+
type ByteStreamInfo,
|
|
85
|
+
type ChatMessage,
|
|
86
|
+
type SimulationOptions,
|
|
87
|
+
type SimulationScenario,
|
|
88
|
+
type StreamController,
|
|
89
|
+
type TextStreamInfo,
|
|
90
|
+
type TranscriptionSegment,
|
|
78
91
|
} from './types';
|
|
79
92
|
import {
|
|
80
93
|
Future,
|
|
94
|
+
bigIntToNumber,
|
|
81
95
|
createDummyVideoStreamTrack,
|
|
82
96
|
extractChatMessage,
|
|
83
97
|
extractTranscriptionSegments,
|
|
@@ -85,9 +99,14 @@ import {
|
|
|
85
99
|
getEmptyAudioStreamTrack,
|
|
86
100
|
isBrowserSupported,
|
|
87
101
|
isCloud,
|
|
102
|
+
isLocalAudioTrack,
|
|
103
|
+
isLocalParticipant,
|
|
88
104
|
isReactNative,
|
|
105
|
+
isRemotePub,
|
|
89
106
|
isSafari,
|
|
90
107
|
isWeb,
|
|
108
|
+
numberToBigInt,
|
|
109
|
+
sleep,
|
|
91
110
|
supportsSetSinkId,
|
|
92
111
|
toHttpUrl,
|
|
93
112
|
unpackStreamId,
|
|
@@ -180,6 +199,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
180
199
|
*/
|
|
181
200
|
private transcriptionReceivedTimes: Map<string, number>;
|
|
182
201
|
|
|
202
|
+
private byteStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
|
|
203
|
+
|
|
204
|
+
private textStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
|
|
205
|
+
|
|
206
|
+
private byteStreamHandlers = new Map<string, ByteStreamHandler>();
|
|
207
|
+
|
|
208
|
+
private textStreamHandlers = new Map<string, TextStreamHandler>();
|
|
209
|
+
|
|
210
|
+
private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
|
|
211
|
+
|
|
183
212
|
/**
|
|
184
213
|
* Creates a new Room, the primary construct for a LiveKit session.
|
|
185
214
|
* @param options
|
|
@@ -211,7 +240,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
211
240
|
|
|
212
241
|
this.disconnectLock = new Mutex();
|
|
213
242
|
|
|
214
|
-
this.localParticipant = new LocalParticipant(
|
|
243
|
+
this.localParticipant = new LocalParticipant(
|
|
244
|
+
'',
|
|
245
|
+
'',
|
|
246
|
+
this.engine,
|
|
247
|
+
this.options,
|
|
248
|
+
this.rpcHandlers,
|
|
249
|
+
);
|
|
215
250
|
|
|
216
251
|
if (this.options.videoCaptureDefaults.deviceId) {
|
|
217
252
|
this.localParticipant.activeDeviceMap.set(
|
|
@@ -252,6 +287,135 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
252
287
|
}
|
|
253
288
|
}
|
|
254
289
|
|
|
290
|
+
registerTextStreamHandler(topic: string, callback: TextStreamHandler) {
|
|
291
|
+
if (this.textStreamHandlers.has(topic)) {
|
|
292
|
+
throw new TypeError(`A text stream handler for topic "${topic}" has already been set.`);
|
|
293
|
+
}
|
|
294
|
+
this.textStreamHandlers.set(topic, callback);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
unregisterTextStreamHandler(topic: string) {
|
|
298
|
+
this.textStreamHandlers.delete(topic);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
registerByteStreamHandler(topic: string, callback: ByteStreamHandler) {
|
|
302
|
+
if (this.byteStreamHandlers.has(topic)) {
|
|
303
|
+
throw new TypeError(`A byte stream handler for topic "${topic}" has already been set.`);
|
|
304
|
+
}
|
|
305
|
+
this.byteStreamHandlers.set(topic, callback);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
unregisterByteStreamHandler(topic: string) {
|
|
309
|
+
this.byteStreamHandlers.delete(topic);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Establishes the participant as a receiver for calls of the specified RPC method.
|
|
314
|
+
* Will overwrite any existing callback for the same method.
|
|
315
|
+
*
|
|
316
|
+
* @param method - The name of the indicated RPC method
|
|
317
|
+
* @param handler - Will be invoked when an RPC request for this method is received
|
|
318
|
+
* @returns A promise that resolves when the method is successfully registered
|
|
319
|
+
* @throws {Error} if the handler for a specific method has already been registered already
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* room.localParticipant?.registerRpcMethod(
|
|
324
|
+
* 'greet',
|
|
325
|
+
* async (data: RpcInvocationData) => {
|
|
326
|
+
* console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`);
|
|
327
|
+
* return `Hello, ${data.callerIdentity}!`;
|
|
328
|
+
* }
|
|
329
|
+
* );
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* The handler should return a Promise that resolves to a string.
|
|
333
|
+
* If unable to respond within `responseTimeout`, the request will result in an error on the caller's side.
|
|
334
|
+
*
|
|
335
|
+
* You may throw errors of type `RpcError` with a string `message` in the handler,
|
|
336
|
+
* and they will be received on the caller's side with the message intact.
|
|
337
|
+
* Other errors thrown in your handler will not be transmitted as-is, and will instead arrive to the caller as `1500` ("Application Error").
|
|
338
|
+
*/
|
|
339
|
+
registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
|
|
340
|
+
if (this.rpcHandlers.has(method)) {
|
|
341
|
+
throw Error(
|
|
342
|
+
`RPC handler already registered for method ${method}, unregisterRpcMethod before trying to register again`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
this.rpcHandlers.set(method, handler);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Unregisters a previously registered RPC method.
|
|
350
|
+
*
|
|
351
|
+
* @param method - The name of the RPC method to unregister
|
|
352
|
+
*/
|
|
353
|
+
unregisterRpcMethod(method: string) {
|
|
354
|
+
this.rpcHandlers.delete(method);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private async handleIncomingRpcRequest(
|
|
358
|
+
callerIdentity: string,
|
|
359
|
+
requestId: string,
|
|
360
|
+
method: string,
|
|
361
|
+
payload: string,
|
|
362
|
+
responseTimeout: number,
|
|
363
|
+
version: number,
|
|
364
|
+
) {
|
|
365
|
+
await this.engine.publishRpcAck(callerIdentity, requestId);
|
|
366
|
+
|
|
367
|
+
if (version !== 1) {
|
|
368
|
+
await this.engine.publishRpcResponse(
|
|
369
|
+
callerIdentity,
|
|
370
|
+
requestId,
|
|
371
|
+
null,
|
|
372
|
+
RpcError.builtIn('UNSUPPORTED_VERSION'),
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const handler = this.rpcHandlers.get(method);
|
|
378
|
+
|
|
379
|
+
if (!handler) {
|
|
380
|
+
await this.engine.publishRpcResponse(
|
|
381
|
+
callerIdentity,
|
|
382
|
+
requestId,
|
|
383
|
+
null,
|
|
384
|
+
RpcError.builtIn('UNSUPPORTED_METHOD'),
|
|
385
|
+
);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let responseError: RpcError | null = null;
|
|
390
|
+
let responsePayload: string | null = null;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const response = await handler({
|
|
394
|
+
requestId,
|
|
395
|
+
callerIdentity,
|
|
396
|
+
payload,
|
|
397
|
+
responseTimeout,
|
|
398
|
+
});
|
|
399
|
+
if (byteLength(response) > MAX_PAYLOAD_BYTES) {
|
|
400
|
+
responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
|
|
401
|
+
console.warn(`RPC Response payload too large for ${method}`);
|
|
402
|
+
} else {
|
|
403
|
+
responsePayload = response;
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error instanceof RpcError) {
|
|
407
|
+
responseError = error;
|
|
408
|
+
} else {
|
|
409
|
+
console.warn(
|
|
410
|
+
`Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
|
|
411
|
+
error,
|
|
412
|
+
);
|
|
413
|
+
responseError = RpcError.builtIn('APPLICATION_ERROR');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
await this.engine.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
|
|
417
|
+
}
|
|
418
|
+
|
|
255
419
|
/**
|
|
256
420
|
* @experimental
|
|
257
421
|
*/
|
|
@@ -276,7 +440,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
276
440
|
this.e2eeManager.on(
|
|
277
441
|
EncryptionEvent.ParticipantEncryptionStatusChanged,
|
|
278
442
|
(enabled, participant) => {
|
|
279
|
-
if (participant
|
|
443
|
+
if (isLocalParticipant(participant)) {
|
|
280
444
|
this.isE2EEEnabled = enabled;
|
|
281
445
|
}
|
|
282
446
|
this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant);
|
|
@@ -960,7 +1124,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
960
1124
|
req = new SimulateScenario({
|
|
961
1125
|
scenario: {
|
|
962
1126
|
case: 'subscriberBandwidth',
|
|
963
|
-
value:
|
|
1127
|
+
value: numberToBigInt(arg),
|
|
964
1128
|
},
|
|
965
1129
|
});
|
|
966
1130
|
break;
|
|
@@ -1593,9 +1757,136 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1593
1757
|
this.handleChatMessage(participant, packet.value.value);
|
|
1594
1758
|
} else if (packet.value.case === 'metrics') {
|
|
1595
1759
|
this.handleMetrics(packet.value.value, participant);
|
|
1760
|
+
} else if (packet.value.case === 'streamHeader') {
|
|
1761
|
+
this.handleStreamHeader(packet.value.value, packet.participantIdentity);
|
|
1762
|
+
} else if (packet.value.case === 'streamChunk') {
|
|
1763
|
+
this.handleStreamChunk(packet.value.value);
|
|
1764
|
+
} else if (packet.value.case === 'streamTrailer') {
|
|
1765
|
+
this.handleStreamTrailer(packet.value.value);
|
|
1766
|
+
} else if (packet.value.case === 'rpcRequest') {
|
|
1767
|
+
const rpc = packet.value.value;
|
|
1768
|
+
this.handleIncomingRpcRequest(
|
|
1769
|
+
packet.participantIdentity,
|
|
1770
|
+
rpc.id,
|
|
1771
|
+
rpc.method,
|
|
1772
|
+
rpc.payload,
|
|
1773
|
+
rpc.responseTimeoutMs,
|
|
1774
|
+
rpc.version,
|
|
1775
|
+
);
|
|
1596
1776
|
}
|
|
1597
1777
|
};
|
|
1598
1778
|
|
|
1779
|
+
private async handleStreamHeader(streamHeader: DataStream_Header, participantIdentity: string) {
|
|
1780
|
+
if (streamHeader.contentHeader.case === 'byteHeader') {
|
|
1781
|
+
const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
|
|
1782
|
+
|
|
1783
|
+
if (!streamHandlerCallback) {
|
|
1784
|
+
this.log.debug(
|
|
1785
|
+
'ignoring incoming byte stream due to no handler for topic',
|
|
1786
|
+
streamHeader.topic,
|
|
1787
|
+
);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
|
|
1791
|
+
const info: ByteStreamInfo = {
|
|
1792
|
+
id: streamHeader.streamId,
|
|
1793
|
+
name: streamHeader.contentHeader.value.name ?? 'unknown',
|
|
1794
|
+
mimeType: streamHeader.mimeType,
|
|
1795
|
+
size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
|
|
1796
|
+
topic: streamHeader.topic,
|
|
1797
|
+
timestamp: bigIntToNumber(streamHeader.timestamp),
|
|
1798
|
+
attributes: streamHeader.attributes,
|
|
1799
|
+
};
|
|
1800
|
+
const stream = new ReadableStream({
|
|
1801
|
+
start: (controller) => {
|
|
1802
|
+
streamController = controller;
|
|
1803
|
+
this.byteStreamControllers.set(streamHeader.streamId, {
|
|
1804
|
+
info,
|
|
1805
|
+
controller: streamController,
|
|
1806
|
+
startTime: Date.now(),
|
|
1807
|
+
});
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
streamHandlerCallback(
|
|
1811
|
+
new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
|
|
1812
|
+
{
|
|
1813
|
+
identity: participantIdentity,
|
|
1814
|
+
},
|
|
1815
|
+
);
|
|
1816
|
+
} else if (streamHeader.contentHeader.case === 'textHeader') {
|
|
1817
|
+
const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
|
|
1818
|
+
|
|
1819
|
+
if (!streamHandlerCallback) {
|
|
1820
|
+
this.log.debug(
|
|
1821
|
+
'ignoring incoming text stream due to no handler for topic',
|
|
1822
|
+
streamHeader.topic,
|
|
1823
|
+
);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
|
|
1827
|
+
const info: TextStreamInfo = {
|
|
1828
|
+
id: streamHeader.streamId,
|
|
1829
|
+
mimeType: streamHeader.mimeType,
|
|
1830
|
+
size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
|
|
1831
|
+
topic: streamHeader.topic,
|
|
1832
|
+
timestamp: Number(streamHeader.timestamp),
|
|
1833
|
+
attributes: streamHeader.attributes,
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
const stream = new ReadableStream<DataStream_Chunk>({
|
|
1837
|
+
start: (controller) => {
|
|
1838
|
+
streamController = controller;
|
|
1839
|
+
this.textStreamControllers.set(streamHeader.streamId, {
|
|
1840
|
+
info,
|
|
1841
|
+
controller: streamController,
|
|
1842
|
+
startTime: Date.now(),
|
|
1843
|
+
});
|
|
1844
|
+
},
|
|
1845
|
+
});
|
|
1846
|
+
streamHandlerCallback(
|
|
1847
|
+
new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
|
|
1848
|
+
{ identity: participantIdentity },
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
private handleStreamChunk(chunk: DataStream_Chunk) {
|
|
1854
|
+
const fileBuffer = this.byteStreamControllers.get(chunk.streamId);
|
|
1855
|
+
if (fileBuffer) {
|
|
1856
|
+
if (chunk.content.length > 0) {
|
|
1857
|
+
fileBuffer.controller.enqueue(chunk);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
const textBuffer = this.textStreamControllers.get(chunk.streamId);
|
|
1861
|
+
if (textBuffer) {
|
|
1862
|
+
if (chunk.content.length > 0) {
|
|
1863
|
+
textBuffer.controller.enqueue(chunk);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
private handleStreamTrailer(trailer: DataStream_Trailer) {
|
|
1869
|
+
const textBuffer = this.textStreamControllers.get(trailer.streamId);
|
|
1870
|
+
|
|
1871
|
+
if (textBuffer) {
|
|
1872
|
+
textBuffer.info.attributes = {
|
|
1873
|
+
...textBuffer.info.attributes,
|
|
1874
|
+
...trailer.attributes,
|
|
1875
|
+
};
|
|
1876
|
+
textBuffer.controller.close();
|
|
1877
|
+
this.byteStreamControllers.delete(trailer.streamId);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
|
|
1881
|
+
if (fileBuffer) {
|
|
1882
|
+
{
|
|
1883
|
+
fileBuffer.info.attributes = { ...fileBuffer.info.attributes, ...trailer.attributes };
|
|
1884
|
+
fileBuffer.controller.close();
|
|
1885
|
+
this.byteStreamControllers.delete(trailer.streamId);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1599
1890
|
private handleUserPacket = (
|
|
1600
1891
|
participant: RemoteParticipant | undefined,
|
|
1601
1892
|
userPacket: UserPacket,
|
|
@@ -1771,24 +2062,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1771
2062
|
this.audioContext = getNewAudioContext() ?? undefined;
|
|
1772
2063
|
}
|
|
1773
2064
|
|
|
2065
|
+
if (this.options.webAudioMix) {
|
|
2066
|
+
this.remoteParticipants.forEach((participant) =>
|
|
2067
|
+
participant.setAudioContext(this.audioContext),
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
this.localParticipant.setAudioContext(this.audioContext);
|
|
2072
|
+
|
|
1774
2073
|
if (this.audioContext && this.audioContext.state === 'suspended') {
|
|
1775
2074
|
// for iOS a newly created AudioContext is always in `suspended` state.
|
|
1776
2075
|
// we try our best to resume the context here, if that doesn't work, we just continue with regular processing
|
|
1777
2076
|
try {
|
|
1778
|
-
await this.audioContext.resume();
|
|
2077
|
+
await Promise.race([this.audioContext.resume(), sleep(200)]);
|
|
1779
2078
|
} catch (e: any) {
|
|
1780
2079
|
this.log.warn('Could not resume audio context', { ...this.logContext, error: e });
|
|
1781
2080
|
}
|
|
1782
2081
|
}
|
|
1783
2082
|
|
|
1784
|
-
if (this.options.webAudioMix) {
|
|
1785
|
-
this.remoteParticipants.forEach((participant) =>
|
|
1786
|
-
participant.setAudioContext(this.audioContext),
|
|
1787
|
-
);
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
this.localParticipant.setAudioContext(this.audioContext);
|
|
1791
|
-
|
|
1792
2083
|
const newContextIsRunning = this.audioContext?.state === 'running';
|
|
1793
2084
|
if (newContextIsRunning !== this.canPlaybackAudio) {
|
|
1794
2085
|
this.audioEnabled = newContextIsRunning;
|
|
@@ -1877,9 +2168,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1877
2168
|
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
|
|
1878
2169
|
},
|
|
1879
2170
|
)
|
|
1880
|
-
.on(ParticipantEvent.TrackSubscriptionFailed, (sid: string) => {
|
|
1881
|
-
this.emit(RoomEvent.TrackSubscriptionFailed, sid, participant);
|
|
1882
|
-
})
|
|
1883
2171
|
.on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
|
|
1884
2172
|
this.emitWhenConnected(RoomEvent.TrackMuted, pub, participant);
|
|
1885
2173
|
})
|
|
@@ -1950,7 +2238,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1950
2238
|
private updateSubscriptions() {
|
|
1951
2239
|
for (const p of this.remoteParticipants.values()) {
|
|
1952
2240
|
for (const pub of p.videoTrackPublications.values()) {
|
|
1953
|
-
if (pub.isSubscribed && pub
|
|
2241
|
+
if (pub.isSubscribed && isRemotePub(pub)) {
|
|
1954
2242
|
pub.emitTrackUpdate();
|
|
1955
2243
|
}
|
|
1956
2244
|
}
|
|
@@ -2072,7 +2360,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2072
2360
|
|
|
2073
2361
|
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
|
2074
2362
|
|
|
2075
|
-
if (pub.track
|
|
2363
|
+
if (isLocalAudioTrack(pub.track)) {
|
|
2076
2364
|
const trackIsSilent = await pub.track.checkForSilence();
|
|
2077
2365
|
if (trackIsSilent) {
|
|
2078
2366
|
this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
|
|
@@ -2300,7 +2588,7 @@ function mapArgs(args: unknown[]): any {
|
|
|
2300
2588
|
return mapArgs(arg);
|
|
2301
2589
|
}
|
|
2302
2590
|
if (typeof arg === 'object') {
|
|
2303
|
-
return 'logContext' in arg
|
|
2591
|
+
return 'logContext' in arg ? arg.logContext : undefined;
|
|
2304
2592
|
}
|
|
2305
2593
|
return arg;
|
|
2306
2594
|
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { DataStream_Chunk } from '@livekit/protocol';
|
|
2
|
+
import type { BaseStreamInfo, ByteStreamInfo, TextStreamChunk, TextStreamInfo } from './types';
|
|
3
|
+
import { bigIntToNumber } from './utils';
|
|
4
|
+
|
|
5
|
+
abstract class BaseStreamReader<T extends BaseStreamInfo> {
|
|
6
|
+
protected reader: ReadableStream<DataStream_Chunk>;
|
|
7
|
+
|
|
8
|
+
protected totalByteSize?: number;
|
|
9
|
+
|
|
10
|
+
protected _info: T;
|
|
11
|
+
|
|
12
|
+
protected bytesReceived: number;
|
|
13
|
+
|
|
14
|
+
get info() {
|
|
15
|
+
return this._info;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor(info: T, stream: ReadableStream<DataStream_Chunk>, totalByteSize?: number) {
|
|
19
|
+
this.reader = stream;
|
|
20
|
+
this.totalByteSize = totalByteSize;
|
|
21
|
+
this._info = info;
|
|
22
|
+
this.bytesReceived = 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected abstract handleChunkReceived(chunk: DataStream_Chunk): void;
|
|
26
|
+
|
|
27
|
+
onProgress?: (progress: number | undefined) => void;
|
|
28
|
+
|
|
29
|
+
abstract readAll(): Promise<string | Array<Uint8Array>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ByteStreamReader extends BaseStreamReader<ByteStreamInfo> {
|
|
33
|
+
protected handleChunkReceived(chunk: DataStream_Chunk) {
|
|
34
|
+
this.bytesReceived += chunk.content.byteLength;
|
|
35
|
+
const currentProgress = this.totalByteSize
|
|
36
|
+
? this.bytesReceived / this.totalByteSize
|
|
37
|
+
: undefined;
|
|
38
|
+
this.onProgress?.(currentProgress);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onProgress?: (progress: number | undefined) => void;
|
|
42
|
+
|
|
43
|
+
[Symbol.asyncIterator]() {
|
|
44
|
+
const reader = this.reader.getReader();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
next: async (): Promise<IteratorResult<Uint8Array>> => {
|
|
48
|
+
try {
|
|
49
|
+
const { done, value } = await reader.read();
|
|
50
|
+
if (done) {
|
|
51
|
+
return { done: true, value: undefined as any };
|
|
52
|
+
} else {
|
|
53
|
+
this.handleChunkReceived(value);
|
|
54
|
+
return { done: false, value: value.content };
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// TODO handle errors
|
|
58
|
+
return { done: true, value: undefined };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
return(): IteratorResult<Uint8Array> {
|
|
63
|
+
reader.releaseLock();
|
|
64
|
+
return { done: true, value: undefined };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async readAll(): Promise<Array<Uint8Array>> {
|
|
70
|
+
let chunks: Set<Uint8Array> = new Set();
|
|
71
|
+
for await (const chunk of this) {
|
|
72
|
+
chunks.add(chunk);
|
|
73
|
+
}
|
|
74
|
+
return Array.from(chunks);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A class to read chunks from a ReadableStream and provide them in a structured format.
|
|
80
|
+
*/
|
|
81
|
+
export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
|
|
82
|
+
private receivedChunks: Map<number, DataStream_Chunk>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A TextStreamReader instance can be used as an AsyncIterator that returns the entire string
|
|
86
|
+
* that has been received up to the current point in time.
|
|
87
|
+
*/
|
|
88
|
+
constructor(
|
|
89
|
+
info: TextStreamInfo,
|
|
90
|
+
stream: ReadableStream<DataStream_Chunk>,
|
|
91
|
+
totalChunkCount?: number,
|
|
92
|
+
) {
|
|
93
|
+
super(info, stream, totalChunkCount);
|
|
94
|
+
this.receivedChunks = new Map();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected handleChunkReceived(chunk: DataStream_Chunk) {
|
|
98
|
+
const index = bigIntToNumber(chunk.chunkIndex);
|
|
99
|
+
const previousChunkAtIndex = this.receivedChunks.get(index);
|
|
100
|
+
if (previousChunkAtIndex && previousChunkAtIndex.version > chunk.version) {
|
|
101
|
+
// we have a newer version already, dropping the old one
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.receivedChunks.set(index, chunk);
|
|
105
|
+
this.bytesReceived += chunk.content.byteLength;
|
|
106
|
+
const currentProgress = this.totalByteSize
|
|
107
|
+
? this.bytesReceived / this.totalByteSize
|
|
108
|
+
: undefined;
|
|
109
|
+
this.onProgress?.(currentProgress);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param progress - progress of the stream between 0 and 1. Undefined for streams of unknown size
|
|
114
|
+
*/
|
|
115
|
+
onProgress?: (progress: number | undefined) => void;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Async iterator implementation to allow usage of `for await...of` syntax.
|
|
119
|
+
* Yields structured chunks from the stream.
|
|
120
|
+
*
|
|
121
|
+
*/
|
|
122
|
+
[Symbol.asyncIterator]() {
|
|
123
|
+
const reader = this.reader.getReader();
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
next: async (): Promise<IteratorResult<TextStreamChunk>> => {
|
|
128
|
+
try {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done) {
|
|
131
|
+
return { done: true, value: undefined };
|
|
132
|
+
} else {
|
|
133
|
+
this.handleChunkReceived(value);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
done: false,
|
|
137
|
+
value: {
|
|
138
|
+
index: bigIntToNumber(value.chunkIndex),
|
|
139
|
+
current: decoder.decode(value.content),
|
|
140
|
+
collected: Array.from(this.receivedChunks.values())
|
|
141
|
+
.sort((a, b) => bigIntToNumber(a.chunkIndex) - bigIntToNumber(b.chunkIndex))
|
|
142
|
+
.map((chunk) => decoder.decode(chunk.content))
|
|
143
|
+
.join(''),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// TODO handle errors
|
|
149
|
+
return { done: true, value: undefined };
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
return(): IteratorResult<TextStreamChunk> {
|
|
154
|
+
reader.releaseLock();
|
|
155
|
+
return { done: true, value: undefined };
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async readAll(): Promise<string> {
|
|
161
|
+
let latestString: string = '';
|
|
162
|
+
for await (const { collected } of this) {
|
|
163
|
+
latestString = collected;
|
|
164
|
+
}
|
|
165
|
+
return latestString;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ByteStreamHandler = (
|
|
170
|
+
reader: ByteStreamReader,
|
|
171
|
+
participantInfo: { identity: string },
|
|
172
|
+
) => void;
|
|
173
|
+
|
|
174
|
+
export type TextStreamHandler = (
|
|
175
|
+
reader: TextStreamReader,
|
|
176
|
+
participantInfo: { identity: string },
|
|
177
|
+
) => void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from './types';
|
|
2
|
+
|
|
3
|
+
class BaseStreamWriter<T, InfoType extends BaseStreamInfo> {
|
|
4
|
+
protected writableStream: WritableStream<[T, number?]>;
|
|
5
|
+
|
|
6
|
+
protected defaultWriter: WritableStreamDefaultWriter<[T, number?]>;
|
|
7
|
+
|
|
8
|
+
protected onClose?: () => void;
|
|
9
|
+
|
|
10
|
+
readonly info: InfoType;
|
|
11
|
+
|
|
12
|
+
constructor(writableStream: WritableStream<[T, number?]>, info: InfoType, onClose?: () => void) {
|
|
13
|
+
this.writableStream = writableStream;
|
|
14
|
+
this.defaultWriter = writableStream.getWriter();
|
|
15
|
+
this.onClose = onClose;
|
|
16
|
+
this.info = info;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
write(chunk: T): Promise<void> {
|
|
20
|
+
return this.defaultWriter.write([chunk]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async close() {
|
|
24
|
+
await this.defaultWriter.close();
|
|
25
|
+
this.defaultWriter.releaseLock();
|
|
26
|
+
this.onClose?.();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class TextStreamWriter extends BaseStreamWriter<string, TextStreamInfo> {}
|
|
31
|
+
|
|
32
|
+
export class BinaryStreamWriter extends BaseStreamWriter<Uint8Array, ByteStreamInfo> {}
|