livekit-client 2.8.0 → 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 +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> {}
|