@livekit/rtc-node 0.13.33 → 0.13.34
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/dist/{audio_stream-CnODFAmo.d.cts → audio_stream-BNVaONuH.d.cts} +36 -11
- package/dist/audio_stream.d.cts +2 -2
- package/dist/data_streams/index.d.cts +1 -1
- package/dist/data_streams/stream_reader.cjs +97 -58
- package/dist/data_streams/stream_reader.cjs.map +1 -1
- package/dist/data_streams/stream_reader.d.cts +1 -1
- package/dist/data_streams/stream_reader.d.ts +28 -11
- package/dist/data_streams/stream_reader.d.ts.map +1 -1
- package/dist/data_streams/stream_writer.d.cts +1 -1
- package/dist/data_streams/types.cjs.map +1 -1
- package/dist/data_streams/types.d.cts +1 -1
- package/dist/data_streams/types.d.ts +6 -0
- package/dist/data_streams/types.d.ts.map +1 -1
- package/dist/e2ee.cjs +5 -1
- package/dist/e2ee.cjs.map +1 -1
- package/dist/e2ee.d.cts +3 -1
- package/dist/e2ee.d.ts +3 -1
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/participant.cjs +178 -196
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -2
- package/dist/participant.d.ts +22 -5
- package/dist/participant.d.ts.map +1 -1
- package/dist/room.cjs +166 -126
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +3 -3
- package/dist/room.d.ts +16 -8
- package/dist/room.d.ts.map +1 -1
- package/dist/tests/e2e_common.cjs +157 -0
- package/dist/tests/e2e_common.cjs.map +1 -0
- package/dist/tests/e2e_common.d.cts +56 -0
- package/dist/tests/e2e_common.d.ts +37 -0
- package/dist/tests/e2e_common.d.ts.map +1 -0
- package/dist/track.d.cts +2 -2
- package/dist/track_publication.d.cts +2 -2
- package/dist/{types-_PdPVish.d.cts → types-f7wTlFjs.d.cts} +34 -11
- package/dist/utils.cjs +27 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +8 -1
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/video_stream.d.cts +2 -2
- package/package.json +2 -2
- package/src/audio_stream_room_lifecycle.test.ts +4 -1
- package/src/data_streams/stream_reader.ts +132 -82
- package/src/data_streams/types.ts +6 -0
- package/src/e2ee.ts +7 -0
- package/src/participant.ts +251 -228
- package/src/room.ts +233 -144
- package/src/tests/e2e.test.ts +50 -139
- package/src/tests/e2e_common.ts +163 -0
- package/src/tests/e2e_data_streams.test.ts +639 -0
- package/src/utils.ts +30 -0
- package/src/version.ts +1 -1
package/src/participant.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import { Mutex } from '@livekit/mutex';
|
|
4
|
+
import { type Mutex } from '@livekit/mutex';
|
|
5
5
|
import {
|
|
6
6
|
DisconnectReason,
|
|
7
7
|
type OwnedParticipant,
|
|
@@ -10,13 +10,16 @@ import {
|
|
|
10
10
|
type ParticipantKindDetail,
|
|
11
11
|
} from '@livekit/rtc-ffi-bindings';
|
|
12
12
|
import {
|
|
13
|
+
type ByteStreamOpenCallback,
|
|
14
|
+
ByteStreamOpenRequest,
|
|
15
|
+
type ByteStreamOpenResponse,
|
|
16
|
+
type ByteStreamWriterCloseCallback,
|
|
17
|
+
ByteStreamWriterCloseRequest,
|
|
18
|
+
type ByteStreamWriterCloseResponse,
|
|
19
|
+
type ByteStreamWriterWriteCallback,
|
|
20
|
+
ByteStreamWriterWriteRequest,
|
|
21
|
+
type ByteStreamWriterWriteResponse,
|
|
13
22
|
ChatMessage as ChatMessageModel,
|
|
14
|
-
DataStream_ByteHeader,
|
|
15
|
-
DataStream_Chunk,
|
|
16
|
-
DataStream_Header,
|
|
17
|
-
DataStream_OperationType,
|
|
18
|
-
DataStream_TextHeader,
|
|
19
|
-
DataStream_Trailer,
|
|
20
23
|
EditChatMessageRequest,
|
|
21
24
|
TranscriptionSegment as ProtoTranscriptionSegment,
|
|
22
25
|
type PublishDataCallback,
|
|
@@ -34,15 +37,6 @@ import {
|
|
|
34
37
|
type SendChatMessageCallback,
|
|
35
38
|
SendChatMessageRequest,
|
|
36
39
|
type SendChatMessageResponse,
|
|
37
|
-
type SendStreamChunkCallback,
|
|
38
|
-
SendStreamChunkRequest,
|
|
39
|
-
type SendStreamChunkResponse,
|
|
40
|
-
type SendStreamHeaderCallback,
|
|
41
|
-
SendStreamHeaderRequest,
|
|
42
|
-
type SendStreamHeaderResponse,
|
|
43
|
-
type SendStreamTrailerCallback,
|
|
44
|
-
SendStreamTrailerRequest,
|
|
45
|
-
type SendStreamTrailerResponse,
|
|
46
40
|
type SetLocalAttributesCallback,
|
|
47
41
|
SetLocalAttributesRequest,
|
|
48
42
|
type SetLocalAttributesResponse,
|
|
@@ -52,6 +46,23 @@ import {
|
|
|
52
46
|
type SetLocalNameCallback,
|
|
53
47
|
SetLocalNameRequest,
|
|
54
48
|
type SetLocalNameResponse,
|
|
49
|
+
StreamByteOptions,
|
|
50
|
+
type StreamSendFileCallback,
|
|
51
|
+
StreamSendFileRequest,
|
|
52
|
+
type StreamSendFileResponse,
|
|
53
|
+
type StreamSendTextCallback,
|
|
54
|
+
StreamSendTextRequest,
|
|
55
|
+
type StreamSendTextResponse,
|
|
56
|
+
StreamTextOptions,
|
|
57
|
+
type TextStreamOpenCallback,
|
|
58
|
+
TextStreamOpenRequest,
|
|
59
|
+
type TextStreamOpenResponse,
|
|
60
|
+
type TextStreamWriterCloseCallback,
|
|
61
|
+
TextStreamWriterCloseRequest,
|
|
62
|
+
type TextStreamWriterCloseResponse,
|
|
63
|
+
type TextStreamWriterWriteCallback,
|
|
64
|
+
TextStreamWriterWriteRequest,
|
|
65
|
+
type TextStreamWriterWriteResponse,
|
|
55
66
|
type TrackPublishOptions,
|
|
56
67
|
type UnpublishTrackCallback,
|
|
57
68
|
UnpublishTrackRequest,
|
|
@@ -71,12 +82,10 @@ import {
|
|
|
71
82
|
UnregisterRpcMethodRequest,
|
|
72
83
|
} from '@livekit/rtc-ffi-bindings';
|
|
73
84
|
import type { PathLike } from 'node:fs';
|
|
74
|
-
import {
|
|
85
|
+
import { fileURLToPath } from 'node:url';
|
|
75
86
|
import {
|
|
76
|
-
type ByteStreamInfo,
|
|
77
87
|
type ByteStreamOptions,
|
|
78
88
|
ByteStreamWriter,
|
|
79
|
-
type TextStreamInfo,
|
|
80
89
|
TextStreamWriter,
|
|
81
90
|
} from './data_streams/index.js';
|
|
82
91
|
import { FfiClient, FfiHandle } from './ffi_client.js';
|
|
@@ -87,9 +96,8 @@ import type { RemoteTrackPublication, TrackPublication } from './track_publicati
|
|
|
87
96
|
import { LocalTrackPublication } from './track_publication.js';
|
|
88
97
|
import type { Transcription } from './transcription.js';
|
|
89
98
|
import type { ChatMessage } from './types.js';
|
|
90
|
-
import {
|
|
91
|
-
|
|
92
|
-
const STREAM_CHUNK_SIZE = 15_000;
|
|
99
|
+
import { byteStreamInfoFromProto, textStreamInfoFromProto } from './utils.js';
|
|
100
|
+
import { numberToBigInt } from './utils.js';
|
|
93
101
|
|
|
94
102
|
export abstract class Participant {
|
|
95
103
|
/** @internal */
|
|
@@ -166,6 +174,14 @@ export class LocalParticipant extends Participant {
|
|
|
166
174
|
// pending FfiClient.waitFor() listeners so they don't leak.
|
|
167
175
|
private disconnectSignal: AbortSignal;
|
|
168
176
|
|
|
177
|
+
// Handle ids of data stream writers that have been opened but not yet
|
|
178
|
+
// closed. The FFI close request consumes the handle (take_handle), so an
|
|
179
|
+
// entry is removed as soon as a close is sent for it; whatever is left when
|
|
180
|
+
// the room disconnects is dropped by disposeOpenStreamWriters(). Only the
|
|
181
|
+
// ids are kept — wrapping them in FfiHandle would make GC drop a handle the
|
|
182
|
+
// close request already consumed.
|
|
183
|
+
private openStreamWriters = new Set<bigint>();
|
|
184
|
+
|
|
169
185
|
trackPublications: Map<string, LocalTrackPublication> = new Map();
|
|
170
186
|
|
|
171
187
|
constructor(info: OwnedParticipant, ffiEventLock: Mutex, disconnectSignal: AbortSignal) {
|
|
@@ -277,96 +293,71 @@ export class LocalParticipant extends Participant {
|
|
|
277
293
|
destinationIdentities?: Array<string>;
|
|
278
294
|
streamId?: string;
|
|
279
295
|
senderIdentity?: string;
|
|
280
|
-
totalSize?: number;
|
|
281
296
|
}): Promise<TextStreamWriter> {
|
|
282
|
-
const
|
|
283
|
-
const streamId = options?.streamId ?? crypto.randomUUID();
|
|
284
|
-
const destinationIdentities = options?.destinationIdentities;
|
|
285
|
-
|
|
286
|
-
const info: TextStreamInfo = {
|
|
287
|
-
streamId: streamId,
|
|
288
|
-
mimeType: 'text/plain',
|
|
289
|
-
topic: options?.topic ?? '',
|
|
290
|
-
timestamp: Date.now(),
|
|
291
|
-
totalSize: options?.totalSize,
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const headerReq = new SendStreamHeaderRequest({
|
|
295
|
-
senderIdentity,
|
|
296
|
-
destinationIdentities,
|
|
297
|
+
const req = new TextStreamOpenRequest({
|
|
297
298
|
localParticipantHandle: this.ffi_handle.handle,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
mimeType: info.mimeType,
|
|
301
|
-
topic: info.topic,
|
|
302
|
-
timestamp: numberToBigInt(info.timestamp),
|
|
303
|
-
totalLength: numberToBigInt(info.totalSize),
|
|
299
|
+
options: new StreamTextOptions({
|
|
300
|
+
topic: options?.topic ?? '',
|
|
304
301
|
attributes: options?.attributes,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
operationType: DataStream_OperationType.CREATE,
|
|
309
|
-
version: 0,
|
|
310
|
-
replyToStreamId: '',
|
|
311
|
-
generated: false,
|
|
312
|
-
}),
|
|
313
|
-
},
|
|
302
|
+
destinationIdentities: options?.destinationIdentities,
|
|
303
|
+
id: options?.streamId,
|
|
304
|
+
senderIdentity: options?.senderIdentity ?? this.identity,
|
|
314
305
|
}),
|
|
315
306
|
});
|
|
316
307
|
|
|
317
|
-
|
|
308
|
+
const res = FfiClient.instance.request<TextStreamOpenResponse>({
|
|
309
|
+
message: { case: 'textStreamOpen', value: req },
|
|
310
|
+
});
|
|
318
311
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
312
|
+
const cb = await FfiClient.instance.waitFor<TextStreamOpenCallback>(
|
|
313
|
+
(ev) => ev.message.case == 'textStreamOpen' && ev.message.value.asyncId == res.asyncId,
|
|
314
|
+
{ signal: this.disconnectSignal },
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
if (cb.result.case !== 'writer') {
|
|
318
|
+
throw new Error(cb.result.case === 'error' ? cb.result.value.description : 'unknown error');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const writerHandle = cb.result.value.handle!.id!;
|
|
322
|
+
const info = textStreamInfoFromProto(cb.result.value.info!);
|
|
323
|
+
this.openStreamWriters.add(writerHandle);
|
|
324
|
+
|
|
325
|
+
// Sends the close request at most once: it consumes the writer handle on
|
|
326
|
+
// the native side, so a second one would fail and, worse, the handle must
|
|
327
|
+
// no longer be dropped by disconnect cleanup.
|
|
328
|
+
const closeWriter = async (reason?: string) => {
|
|
329
|
+
if (!this.openStreamWriters.delete(writerHandle)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await this.textStreamWriterClose(new TextStreamWriterCloseRequest({ writerHandle, reason }));
|
|
333
|
+
};
|
|
323
334
|
|
|
324
335
|
const writableStream = new WritableStream<string>({
|
|
325
336
|
// Implement the sink
|
|
326
|
-
async
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
write: async (text) => {
|
|
338
|
+
try {
|
|
339
|
+
await this.textStreamWriterWrite(
|
|
340
|
+
new TextStreamWriterWriteRequest({ writerHandle, text }),
|
|
341
|
+
);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
// A rejected write leaves the writable stream errored, and an errored
|
|
344
|
+
// stream never runs the sink's close()/abort() — a later close() just
|
|
345
|
+
// rejects with this error. Close the native writer here so its handle
|
|
346
|
+
// isn't held for the lifetime of the process and peers get an
|
|
347
|
+
// end-of-stream instead of a stream that never terminates.
|
|
348
|
+
await closeWriter(err instanceof Error ? err.message : String(err ?? '')).catch(() => {
|
|
349
|
+
// Best-effort: the connection may already be gone.
|
|
337
350
|
});
|
|
338
|
-
|
|
339
|
-
await sendChunk(chunkRequest);
|
|
340
|
-
nextChunkId += 1;
|
|
351
|
+
throw err;
|
|
341
352
|
}
|
|
342
353
|
},
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
senderIdentity,
|
|
346
|
-
localParticipantHandle: localHandle,
|
|
347
|
-
destinationIdentities,
|
|
348
|
-
trailer: new DataStream_Trailer({
|
|
349
|
-
streamId,
|
|
350
|
-
reason: '',
|
|
351
|
-
}),
|
|
352
|
-
});
|
|
353
|
-
await sendTrailer(trailerReq);
|
|
354
|
-
},
|
|
355
|
-
// Send a trailer with the error reason so the remote side's stream
|
|
354
|
+
close: () => closeWriter(),
|
|
355
|
+
// Close the stream with the error reason so the remote side's stream
|
|
356
356
|
// controller is closed instead of waiting for data that won't arrive.
|
|
357
|
-
async
|
|
357
|
+
abort: async (err) => {
|
|
358
358
|
log.error(err, 'Sink Error');
|
|
359
359
|
try {
|
|
360
|
-
|
|
361
|
-
senderIdentity,
|
|
362
|
-
localParticipantHandle: localHandle,
|
|
363
|
-
destinationIdentities,
|
|
364
|
-
trailer: new DataStream_Trailer({
|
|
365
|
-
streamId,
|
|
366
|
-
reason: err instanceof Error ? err.message : String(err ?? ''),
|
|
367
|
-
}),
|
|
368
|
-
});
|
|
369
|
-
await sendTrailer(trailerReq);
|
|
360
|
+
await closeWriter(err instanceof Error ? err.message : String(err ?? ''));
|
|
370
361
|
} catch {
|
|
371
362
|
// Best-effort: the connection may already be gone.
|
|
372
363
|
}
|
|
@@ -385,15 +376,41 @@ export class LocalParticipant extends Participant {
|
|
|
385
376
|
attributes?: Record<string, string>;
|
|
386
377
|
destinationIdentities?: Array<string>;
|
|
387
378
|
streamId?: string;
|
|
379
|
+
/**
|
|
380
|
+
* Whether the payload may be compressed on the wire. Defaults to true;
|
|
381
|
+
* compression is only applied when it actually reduces the payload size
|
|
382
|
+
* and every recipient supports it.
|
|
383
|
+
*/
|
|
384
|
+
compress?: boolean;
|
|
388
385
|
},
|
|
389
386
|
) {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
387
|
+
const req = new StreamSendTextRequest({
|
|
388
|
+
localParticipantHandle: this.ffi_handle.handle,
|
|
389
|
+
options: new StreamTextOptions({
|
|
390
|
+
topic: options?.topic ?? '',
|
|
391
|
+
attributes: options?.attributes,
|
|
392
|
+
destinationIdentities: options?.destinationIdentities,
|
|
393
|
+
id: options?.streamId,
|
|
394
|
+
senderIdentity: this.identity,
|
|
395
|
+
compress: options?.compress,
|
|
396
|
+
}),
|
|
397
|
+
text,
|
|
393
398
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
399
|
+
|
|
400
|
+
const res = FfiClient.instance.request<StreamSendTextResponse>({
|
|
401
|
+
message: { case: 'sendText', value: req },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const cb = await FfiClient.instance.waitFor<StreamSendTextCallback>(
|
|
405
|
+
(ev) => ev.message.case == 'sendText' && ev.message.value.asyncId == res.asyncId,
|
|
406
|
+
{ signal: this.disconnectSignal },
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (cb.result.case !== 'info') {
|
|
410
|
+
throw new Error(cb.result.case === 'error' ? cb.result.value.description : 'unknown error');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return textStreamInfoFromProto(cb.result.value);
|
|
397
414
|
}
|
|
398
415
|
|
|
399
416
|
async streamBytes(options?: {
|
|
@@ -405,102 +422,72 @@ export class LocalParticipant extends Participant {
|
|
|
405
422
|
mimeType?: string;
|
|
406
423
|
totalSize?: number;
|
|
407
424
|
}) {
|
|
408
|
-
const
|
|
409
|
-
const streamId = options?.streamId ?? crypto.randomUUID();
|
|
410
|
-
const destinationIdentities = options?.destinationIdentities;
|
|
411
|
-
|
|
412
|
-
const info: ByteStreamInfo = {
|
|
413
|
-
streamId: streamId,
|
|
414
|
-
mimeType: options?.mimeType ?? 'application/octet-stream',
|
|
415
|
-
topic: options?.topic ?? '',
|
|
416
|
-
timestamp: Date.now(),
|
|
417
|
-
attributes: options?.attributes,
|
|
418
|
-
totalSize: options?.totalSize,
|
|
419
|
-
name: options?.name ?? 'unknown',
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const headerReq = new SendStreamHeaderRequest({
|
|
423
|
-
senderIdentity,
|
|
424
|
-
destinationIdentities,
|
|
425
|
+
const req = new ByteStreamOpenRequest({
|
|
425
426
|
localParticipantHandle: this.ffi_handle.handle,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
value: new DataStream_ByteHeader({
|
|
436
|
-
name: info.name,
|
|
437
|
-
}),
|
|
438
|
-
},
|
|
427
|
+
options: new StreamByteOptions({
|
|
428
|
+
topic: options?.topic ?? '',
|
|
429
|
+
attributes: options?.attributes,
|
|
430
|
+
destinationIdentities: options?.destinationIdentities,
|
|
431
|
+
id: options?.streamId,
|
|
432
|
+
name: options?.name ?? 'unknown',
|
|
433
|
+
mimeType: options?.mimeType ?? 'application/octet-stream',
|
|
434
|
+
totalLength: numberToBigInt(options?.totalSize),
|
|
435
|
+
senderIdentity: this.identity,
|
|
439
436
|
}),
|
|
440
437
|
});
|
|
441
438
|
|
|
442
|
-
|
|
439
|
+
const res = FfiClient.instance.request<ByteStreamOpenResponse>({
|
|
440
|
+
message: { case: 'byteStreamOpen', value: req },
|
|
441
|
+
});
|
|
443
442
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const writeMutex = new Mutex();
|
|
443
|
+
const cb = await FfiClient.instance.waitFor<ByteStreamOpenCallback>(
|
|
444
|
+
(ev) => ev.message.case == 'byteStreamOpen' && ev.message.value.asyncId == res.asyncId,
|
|
445
|
+
{ signal: this.disconnectSignal },
|
|
446
|
+
);
|
|
449
447
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
448
|
+
if (cb.result.case !== 'writer') {
|
|
449
|
+
throw new Error(cb.result.case === 'error' ? cb.result.value.description : 'unknown error');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const writerHandle = cb.result.value.handle!.id!;
|
|
453
|
+
const info = byteStreamInfoFromProto(cb.result.value.info!);
|
|
454
|
+
this.openStreamWriters.add(writerHandle);
|
|
453
455
|
|
|
454
|
-
|
|
456
|
+
// Sends the close request at most once: it consumes the writer handle on
|
|
457
|
+
// the native side, so a second one would fail and, worse, the handle must
|
|
458
|
+
// no longer be dropped by disconnect cleanup.
|
|
459
|
+
const closeWriter = async (reason?: string) => {
|
|
460
|
+
if (!this.openStreamWriters.delete(writerHandle)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
await this.byteStreamWriterClose(new ByteStreamWriterCloseRequest({ writerHandle, reason }));
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const writableStream = new WritableStream<Uint8Array>({
|
|
467
|
+
write: async (chunk) => {
|
|
455
468
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
await sendChunk(chunkRequest);
|
|
470
|
-
chunkId += 1;
|
|
471
|
-
byteOffset += subChunk.byteLength;
|
|
472
|
-
}
|
|
473
|
-
} finally {
|
|
474
|
-
unlock();
|
|
469
|
+
await this.byteStreamWriterWrite(
|
|
470
|
+
new ByteStreamWriterWriteRequest({ writerHandle, bytes: chunk }),
|
|
471
|
+
);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
// A rejected write leaves the writable stream errored, and an errored
|
|
474
|
+
// stream never runs the sink's close()/abort() — a later close() just
|
|
475
|
+
// rejects with this error. Close the native writer here so its handle
|
|
476
|
+
// isn't held for the lifetime of the process and peers get an
|
|
477
|
+
// end-of-stream instead of a stream that never terminates.
|
|
478
|
+
await closeWriter(err instanceof Error ? err.message : String(err ?? '')).catch(() => {
|
|
479
|
+
// Best-effort: the connection may already be gone.
|
|
480
|
+
});
|
|
481
|
+
throw err;
|
|
475
482
|
}
|
|
476
483
|
},
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
senderIdentity,
|
|
480
|
-
localParticipantHandle: localHandle,
|
|
481
|
-
destinationIdentities,
|
|
482
|
-
trailer: new DataStream_Trailer({
|
|
483
|
-
streamId,
|
|
484
|
-
reason: '',
|
|
485
|
-
}),
|
|
486
|
-
});
|
|
487
|
-
await sendTrailer(trailerReq);
|
|
488
|
-
},
|
|
489
|
-
// Send a trailer with the error reason so the remote side's stream
|
|
484
|
+
close: () => closeWriter(),
|
|
485
|
+
// Close the stream with the error reason so the remote side's stream
|
|
490
486
|
// controller is closed instead of waiting for data that won't arrive.
|
|
491
|
-
async
|
|
487
|
+
abort: async (err) => {
|
|
492
488
|
log.error(err, 'Sink error');
|
|
493
489
|
try {
|
|
494
|
-
|
|
495
|
-
senderIdentity,
|
|
496
|
-
localParticipantHandle: localHandle,
|
|
497
|
-
destinationIdentities,
|
|
498
|
-
trailer: new DataStream_Trailer({
|
|
499
|
-
streamId,
|
|
500
|
-
reason: err instanceof Error ? err.message : String(err ?? ''),
|
|
501
|
-
}),
|
|
502
|
-
});
|
|
503
|
-
await sendTrailer(trailerReq);
|
|
490
|
+
await closeWriter(err instanceof Error ? err.message : String(err ?? ''));
|
|
504
491
|
} catch {
|
|
505
492
|
// Best-effort: the connection may already be gone.
|
|
506
493
|
}
|
|
@@ -514,80 +501,116 @@ export class LocalParticipant extends Participant {
|
|
|
514
501
|
|
|
515
502
|
/** Sends a file provided as PathLike to specified recipients */
|
|
516
503
|
async sendFile(path: PathLike, options?: ByteStreamOptions) {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const writer = await this.streamBytes({
|
|
525
|
-
streamId: streamId,
|
|
526
|
-
name: options?.name,
|
|
527
|
-
totalSize: fileStats.size,
|
|
528
|
-
destinationIdentities,
|
|
529
|
-
topic: options?.topic,
|
|
530
|
-
mimeType: options?.mimeType,
|
|
504
|
+
const filePath = path instanceof URL ? fileURLToPath(path) : path.toString();
|
|
505
|
+
|
|
506
|
+
const req = new StreamSendFileRequest({
|
|
507
|
+
localParticipantHandle: this.ffi_handle.handle,
|
|
508
|
+
options: new StreamByteOptions({
|
|
509
|
+
topic: options?.topic ?? '',
|
|
531
510
|
attributes: options?.attributes,
|
|
532
|
-
|
|
511
|
+
destinationIdentities: options?.destinationIdentities,
|
|
512
|
+
name: options?.name ?? 'unknown',
|
|
513
|
+
mimeType: options?.mimeType ?? 'application/octet-stream',
|
|
514
|
+
senderIdentity: this.identity,
|
|
515
|
+
compress: options?.compress,
|
|
516
|
+
}),
|
|
517
|
+
filePath,
|
|
518
|
+
});
|
|
533
519
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
520
|
+
const res = FfiClient.instance.request<StreamSendFileResponse>({
|
|
521
|
+
message: { case: 'sendFile', value: req },
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const cb = await FfiClient.instance.waitFor<StreamSendFileCallback>(
|
|
525
|
+
(ev) => ev.message.case == 'sendFile' && ev.message.value.asyncId == res.asyncId,
|
|
526
|
+
{ signal: this.disconnectSignal },
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (cb.result.case === 'error') {
|
|
530
|
+
throw new Error(cb.result.value.description);
|
|
540
531
|
}
|
|
541
532
|
}
|
|
542
533
|
|
|
543
|
-
private async
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
message: { case: type, value: req },
|
|
534
|
+
private textStreamWriterWrite = async (req: TextStreamWriterWriteRequest) => {
|
|
535
|
+
const res = FfiClient.instance.request<TextStreamWriterWriteResponse>({
|
|
536
|
+
message: { case: 'textStreamWrite', value: req },
|
|
547
537
|
});
|
|
548
538
|
|
|
549
|
-
const cb = await FfiClient.instance.waitFor<
|
|
550
|
-
(ev) => ev.message.case ==
|
|
539
|
+
const cb = await FfiClient.instance.waitFor<TextStreamWriterWriteCallback>(
|
|
540
|
+
(ev) => ev.message.case == 'textStreamWriterWrite' && ev.message.value.asyncId == res.asyncId,
|
|
551
541
|
{ signal: this.disconnectSignal },
|
|
552
542
|
);
|
|
553
543
|
|
|
554
544
|
if (cb.error) {
|
|
555
|
-
throw new Error(cb.error);
|
|
545
|
+
throw new Error(cb.error.description);
|
|
556
546
|
}
|
|
557
|
-
}
|
|
547
|
+
};
|
|
558
548
|
|
|
559
|
-
private
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
message: { case: type, value: req },
|
|
549
|
+
private textStreamWriterClose = async (req: TextStreamWriterCloseRequest) => {
|
|
550
|
+
const res = FfiClient.instance.request<TextStreamWriterCloseResponse>({
|
|
551
|
+
message: { case: 'textStreamClose', value: req },
|
|
563
552
|
});
|
|
564
553
|
|
|
565
|
-
const cb = await FfiClient.instance.waitFor<
|
|
566
|
-
(ev) => ev.message.case ==
|
|
554
|
+
const cb = await FfiClient.instance.waitFor<TextStreamWriterCloseCallback>(
|
|
555
|
+
(ev) => ev.message.case == 'textStreamWriterClose' && ev.message.value.asyncId == res.asyncId,
|
|
567
556
|
{ signal: this.disconnectSignal },
|
|
568
557
|
);
|
|
569
558
|
|
|
570
559
|
if (cb.error) {
|
|
571
|
-
throw new Error(cb.error);
|
|
560
|
+
throw new Error(cb.error.description);
|
|
572
561
|
}
|
|
573
562
|
};
|
|
574
563
|
|
|
575
|
-
private
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
message: { case: type, value: req },
|
|
564
|
+
private byteStreamWriterWrite = async (req: ByteStreamWriterWriteRequest) => {
|
|
565
|
+
const res = FfiClient.instance.request<ByteStreamWriterWriteResponse>({
|
|
566
|
+
message: { case: 'byteStreamWrite', value: req },
|
|
579
567
|
});
|
|
580
568
|
|
|
581
|
-
const cb = await FfiClient.instance.waitFor<
|
|
582
|
-
(ev) => ev.message.case ==
|
|
569
|
+
const cb = await FfiClient.instance.waitFor<ByteStreamWriterWriteCallback>(
|
|
570
|
+
(ev) => ev.message.case == 'byteStreamWriterWrite' && ev.message.value.asyncId == res.asyncId,
|
|
583
571
|
{ signal: this.disconnectSignal },
|
|
584
572
|
);
|
|
585
573
|
|
|
586
574
|
if (cb.error) {
|
|
587
|
-
throw new Error(cb.error);
|
|
575
|
+
throw new Error(cb.error.description);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
private byteStreamWriterClose = async (req: ByteStreamWriterCloseRequest) => {
|
|
580
|
+
const res = FfiClient.instance.request<ByteStreamWriterCloseResponse>({
|
|
581
|
+
message: { case: 'byteStreamClose', value: req },
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const cb = await FfiClient.instance.waitFor<ByteStreamWriterCloseCallback>(
|
|
585
|
+
(ev) => ev.message.case == 'byteStreamWriterClose' && ev.message.value.asyncId == res.asyncId,
|
|
586
|
+
{ signal: this.disconnectSignal },
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
if (cb.error) {
|
|
590
|
+
throw new Error(cb.error.description);
|
|
588
591
|
}
|
|
589
592
|
};
|
|
590
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Drops the native writers of any data streams that were opened but never
|
|
596
|
+
* closed — a writer the caller abandoned, or one whose write failed. Their
|
|
597
|
+
* handles would otherwise be held by the FFI server for the lifetime of the
|
|
598
|
+
* process. The room is already gone at this point, so the handles are dropped
|
|
599
|
+
* directly rather than closed: there is no end-of-stream left to deliver.
|
|
600
|
+
*
|
|
601
|
+
* @internal
|
|
602
|
+
*/
|
|
603
|
+
disposeOpenStreamWriters() {
|
|
604
|
+
for (const writerHandle of this.openStreamWriters) {
|
|
605
|
+
try {
|
|
606
|
+
new FfiHandle(writerHandle).dispose();
|
|
607
|
+
} catch (err) {
|
|
608
|
+
log.warn('failed to dispose data stream writer handle: %s', err);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
this.openStreamWriters.clear();
|
|
612
|
+
}
|
|
613
|
+
|
|
591
614
|
/**
|
|
592
615
|
* Sends a chat message to participants in the room
|
|
593
616
|
*
|