livekit-client 2.8.0 → 2.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +3685 -2966
- 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/RemoteAudioTrack.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/RemoteVideoTrack.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 +317 -25
- 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/RemoteAudioTrack.ts +1 -0
- package/src/room/track/RemoteTrack.ts +4 -0
- package/src/room/track/RemoteTrackPublication.ts +8 -4
- package/src/room/track/RemoteVideoTrack.ts +1 -0
- 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;
|
@@ -1184,8 +1348,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1184
1348
|
throw e;
|
1185
1349
|
}
|
1186
1350
|
}
|
1187
|
-
if (needsUpdateWithoutTracks) {
|
1188
|
-
|
1351
|
+
if (needsUpdateWithoutTracks || kind === 'audiooutput') {
|
1352
|
+
// if there are not active tracks yet or we're switching audiooutput, we need to manually update the active device map here as changing audio output won't result in a track restart
|
1353
|
+
this.localParticipant.activeDeviceMap.set(
|
1354
|
+
kind,
|
1355
|
+
(kind === 'audiooutput' && this.options.audioOutput?.deviceId) || deviceId,
|
1356
|
+
);
|
1189
1357
|
this.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
|
1190
1358
|
}
|
1191
1359
|
|
@@ -1589,9 +1757,136 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1589
1757
|
this.handleChatMessage(participant, packet.value.value);
|
1590
1758
|
} else if (packet.value.case === 'metrics') {
|
1591
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
|
+
);
|
1592
1776
|
}
|
1593
1777
|
};
|
1594
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
|
+
|
1595
1890
|
private handleUserPacket = (
|
1596
1891
|
participant: RemoteParticipant | undefined,
|
1597
1892
|
userPacket: UserPacket,
|
@@ -1767,24 +2062,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1767
2062
|
this.audioContext = getNewAudioContext() ?? undefined;
|
1768
2063
|
}
|
1769
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
|
+
|
1770
2073
|
if (this.audioContext && this.audioContext.state === 'suspended') {
|
1771
2074
|
// for iOS a newly created AudioContext is always in `suspended` state.
|
1772
2075
|
// we try our best to resume the context here, if that doesn't work, we just continue with regular processing
|
1773
2076
|
try {
|
1774
|
-
await this.audioContext.resume();
|
2077
|
+
await Promise.race([this.audioContext.resume(), sleep(200)]);
|
1775
2078
|
} catch (e: any) {
|
1776
2079
|
this.log.warn('Could not resume audio context', { ...this.logContext, error: e });
|
1777
2080
|
}
|
1778
2081
|
}
|
1779
2082
|
|
1780
|
-
if (this.options.webAudioMix) {
|
1781
|
-
this.remoteParticipants.forEach((participant) =>
|
1782
|
-
participant.setAudioContext(this.audioContext),
|
1783
|
-
);
|
1784
|
-
}
|
1785
|
-
|
1786
|
-
this.localParticipant.setAudioContext(this.audioContext);
|
1787
|
-
|
1788
2083
|
const newContextIsRunning = this.audioContext?.state === 'running';
|
1789
2084
|
if (newContextIsRunning !== this.canPlaybackAudio) {
|
1790
2085
|
this.audioEnabled = newContextIsRunning;
|
@@ -1873,9 +2168,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1873
2168
|
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
|
1874
2169
|
},
|
1875
2170
|
)
|
1876
|
-
.on(ParticipantEvent.TrackSubscriptionFailed, (sid: string) => {
|
1877
|
-
this.emit(RoomEvent.TrackSubscriptionFailed, sid, participant);
|
1878
|
-
})
|
1879
2171
|
.on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
|
1880
2172
|
this.emitWhenConnected(RoomEvent.TrackMuted, pub, participant);
|
1881
2173
|
})
|
@@ -1946,7 +2238,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1946
2238
|
private updateSubscriptions() {
|
1947
2239
|
for (const p of this.remoteParticipants.values()) {
|
1948
2240
|
for (const pub of p.videoTrackPublications.values()) {
|
1949
|
-
if (pub.isSubscribed && pub
|
2241
|
+
if (pub.isSubscribed && isRemotePub(pub)) {
|
1950
2242
|
pub.emitTrackUpdate();
|
1951
2243
|
}
|
1952
2244
|
}
|
@@ -2068,7 +2360,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
2068
2360
|
|
2069
2361
|
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
2070
2362
|
|
2071
|
-
if (pub.track
|
2363
|
+
if (isLocalAudioTrack(pub.track)) {
|
2072
2364
|
const trackIsSilent = await pub.track.checkForSilence();
|
2073
2365
|
if (trackIsSilent) {
|
2074
2366
|
this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
|
@@ -2296,7 +2588,7 @@ function mapArgs(args: unknown[]): any {
|
|
2296
2588
|
return mapArgs(arg);
|
2297
2589
|
}
|
2298
2590
|
if (typeof arg === 'object') {
|
2299
|
-
return 'logContext' in arg
|
2591
|
+
return 'logContext' in arg ? arg.logContext : undefined;
|
2300
2592
|
}
|
2301
2593
|
return arg;
|
2302
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> {}
|