@livekit/rtc-node 0.13.32 → 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/async_queue.d.ts +2 -3
- package/dist/audio_filter.d.ts +2 -3
- package/dist/audio_frame.d.ts +5 -6
- package/dist/audio_mixer.d.ts +3 -6
- package/dist/audio_resampler.d.ts +3 -6
- package/dist/audio_source.d.ts +5 -6
- package/dist/{audio_stream-4M8XlO4-.d.cts → audio_stream-BNVaONuH.d.cts} +36 -10
- package/dist/audio_stream.d.cts +2 -2
- package/dist/audio_stream.d.ts +61 -18
- package/dist/data_streams/index.d.cts +1 -1
- package/dist/data_streams/index.d.ts +4 -3
- 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 +65 -2
- 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/stream_writer.d.ts +5 -7
- 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 +47 -2
- 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 +11 -11
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/ffi_client.d.ts +8 -10
- package/dist/frame_processor.d.ts +8 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +22 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -107
- package/dist/log.d.ts +5 -7
- package/dist/participant.cjs +179 -192
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -2
- package/dist/participant.d.ts +169 -18
- package/dist/participant.d.ts.map +1 -1
- package/dist/room.cjs +166 -120
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +2 -2
- package/dist/room.d.ts +228 -18
- package/dist/room.d.ts.map +1 -1
- package/dist/rpc.d.ts +7 -9
- 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.d.ts +67 -18
- package/dist/track_publication.d.cts +2 -2
- package/dist/track_publication.d.ts +45 -18
- package/dist/transcription.d.ts +3 -4
- package/dist/{types-_PdPVish.d.ts → types-f7wTlFjs.d.cts} +34 -11
- package/dist/types.d.ts +2 -3
- 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 +10 -5
- 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 +2 -3
- package/dist/video_frame.d.ts +4 -5
- package/dist/video_source.d.ts +6 -8
- package/dist/video_stream.d.cts +2 -2
- package/dist/video_stream.d.ts +5 -22
- package/package.json +6 -7
- 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/index.ts +8 -1
- package/src/participant.ts +252 -223
- package/src/room.ts +233 -134
- 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/dist/async_queue.js +0 -56
- package/dist/async_queue.js.map +0 -1
- package/dist/audio_filter.js +0 -24
- package/dist/audio_filter.js.map +0 -1
- package/dist/audio_frame.js +0 -78
- package/dist/audio_frame.js.map +0 -1
- package/dist/audio_mixer.js +0 -269
- package/dist/audio_mixer.js.map +0 -1
- package/dist/audio_resampler.js +0 -154
- package/dist/audio_resampler.js.map +0 -1
- package/dist/audio_source.js +0 -134
- package/dist/audio_source.js.map +0 -1
- package/dist/audio_stream-CTyN1Ldy.d.ts +0 -523
- package/dist/audio_stream.js +0 -122
- package/dist/audio_stream.js.map +0 -1
- package/dist/data_streams/index.js +0 -3
- package/dist/data_streams/index.js.map +0 -1
- package/dist/data_streams/stream_reader.js +0 -124
- package/dist/data_streams/stream_reader.js.map +0 -1
- package/dist/data_streams/stream_writer.js +0 -26
- package/dist/data_streams/stream_writer.js.map +0 -1
- package/dist/data_streams/types.js +0 -1
- package/dist/data_streams/types.js.map +0 -1
- package/dist/e2ee.js +0 -251
- package/dist/e2ee.js.map +0 -1
- package/dist/ffi_client.js +0 -89
- package/dist/ffi_client.js.map +0 -1
- package/dist/frame_processor.js +0 -29
- package/dist/frame_processor.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/log.js +0 -9
- package/dist/log.js.map +0 -1
- package/dist/participant.js +0 -715
- package/dist/participant.js.map +0 -1
- package/dist/room.js +0 -900
- package/dist/room.js.map +0 -1
- package/dist/rpc.js +0 -65
- package/dist/rpc.js.map +0 -1
- package/dist/track.js +0 -234
- package/dist/track.js.map +0 -1
- package/dist/track_publication.js +0 -100
- package/dist/track_publication.js.map +0 -1
- package/dist/transcription.js +0 -1
- package/dist/transcription.js.map +0 -1
- package/dist/types-_PdPVish.d.cts +0 -87
- package/dist/types.js +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js +0 -35
- package/dist/utils.js.map +0 -1
- package/dist/version.js +0 -5
- package/dist/version.js.map +0 -1
- package/dist/video_frame.js +0 -207
- package/dist/video_frame.js.map +0 -1
- package/dist/video_source.js +0 -53
- package/dist/video_source.js.map +0 -1
- package/dist/video_stream.js +0 -70
- package/dist/video_stream.js.map +0 -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) {
|
|
@@ -278,92 +294,70 @@ export class LocalParticipant extends Participant {
|
|
|
278
294
|
streamId?: string;
|
|
279
295
|
senderIdentity?: string;
|
|
280
296
|
}): Promise<TextStreamWriter> {
|
|
281
|
-
const
|
|
282
|
-
const streamId = options?.streamId ?? crypto.randomUUID();
|
|
283
|
-
const destinationIdentities = options?.destinationIdentities;
|
|
284
|
-
|
|
285
|
-
const info: TextStreamInfo = {
|
|
286
|
-
streamId: streamId,
|
|
287
|
-
mimeType: 'text/plain',
|
|
288
|
-
topic: options?.topic ?? '',
|
|
289
|
-
timestamp: Date.now(),
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const headerReq = new SendStreamHeaderRequest({
|
|
293
|
-
senderIdentity,
|
|
294
|
-
destinationIdentities,
|
|
297
|
+
const req = new TextStreamOpenRequest({
|
|
295
298
|
localParticipantHandle: this.ffi_handle.handle,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
mimeType: info.mimeType,
|
|
299
|
-
topic: info.topic,
|
|
300
|
-
timestamp: numberToBigInt(info.timestamp),
|
|
299
|
+
options: new StreamTextOptions({
|
|
300
|
+
topic: options?.topic ?? '',
|
|
301
301
|
attributes: options?.attributes,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
operationType: DataStream_OperationType.CREATE,
|
|
306
|
-
version: 0,
|
|
307
|
-
replyToStreamId: '',
|
|
308
|
-
generated: false,
|
|
309
|
-
}),
|
|
310
|
-
},
|
|
302
|
+
destinationIdentities: options?.destinationIdentities,
|
|
303
|
+
id: options?.streamId,
|
|
304
|
+
senderIdentity: options?.senderIdentity ?? this.identity,
|
|
311
305
|
}),
|
|
312
306
|
});
|
|
313
307
|
|
|
314
|
-
|
|
308
|
+
const res = FfiClient.instance.request<TextStreamOpenResponse>({
|
|
309
|
+
message: { case: 'textStreamOpen', value: req },
|
|
310
|
+
});
|
|
311
|
+
|
|
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
|
+
);
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
};
|
|
320
334
|
|
|
321
335
|
const writableStream = new WritableStream<string>({
|
|
322
336
|
// Implement the sink
|
|
323
|
-
async
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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.
|
|
334
350
|
});
|
|
335
|
-
|
|
336
|
-
await sendChunk(chunkRequest);
|
|
337
|
-
nextChunkId += 1;
|
|
351
|
+
throw err;
|
|
338
352
|
}
|
|
339
353
|
},
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
senderIdentity,
|
|
343
|
-
localParticipantHandle: localHandle,
|
|
344
|
-
destinationIdentities,
|
|
345
|
-
trailer: new DataStream_Trailer({
|
|
346
|
-
streamId,
|
|
347
|
-
reason: '',
|
|
348
|
-
}),
|
|
349
|
-
});
|
|
350
|
-
await sendTrailer(trailerReq);
|
|
351
|
-
},
|
|
352
|
-
// 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
|
|
353
356
|
// controller is closed instead of waiting for data that won't arrive.
|
|
354
|
-
async
|
|
357
|
+
abort: async (err) => {
|
|
355
358
|
log.error(err, 'Sink Error');
|
|
356
359
|
try {
|
|
357
|
-
|
|
358
|
-
senderIdentity,
|
|
359
|
-
localParticipantHandle: localHandle,
|
|
360
|
-
destinationIdentities,
|
|
361
|
-
trailer: new DataStream_Trailer({
|
|
362
|
-
streamId,
|
|
363
|
-
reason: err instanceof Error ? err.message : String(err ?? ''),
|
|
364
|
-
}),
|
|
365
|
-
});
|
|
366
|
-
await sendTrailer(trailerReq);
|
|
360
|
+
await closeWriter(err instanceof Error ? err.message : String(err ?? ''));
|
|
367
361
|
} catch {
|
|
368
362
|
// Best-effort: the connection may already be gone.
|
|
369
363
|
}
|
|
@@ -382,12 +376,41 @@ export class LocalParticipant extends Participant {
|
|
|
382
376
|
attributes?: Record<string, string>;
|
|
383
377
|
destinationIdentities?: Array<string>;
|
|
384
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;
|
|
385
385
|
},
|
|
386
386
|
) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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,
|
|
398
|
+
});
|
|
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);
|
|
391
414
|
}
|
|
392
415
|
|
|
393
416
|
async streamBytes(options?: {
|
|
@@ -399,102 +422,72 @@ export class LocalParticipant extends Participant {
|
|
|
399
422
|
mimeType?: string;
|
|
400
423
|
totalSize?: number;
|
|
401
424
|
}) {
|
|
402
|
-
const
|
|
403
|
-
const streamId = options?.streamId ?? crypto.randomUUID();
|
|
404
|
-
const destinationIdentities = options?.destinationIdentities;
|
|
405
|
-
|
|
406
|
-
const info: ByteStreamInfo = {
|
|
407
|
-
streamId: streamId,
|
|
408
|
-
mimeType: options?.mimeType ?? 'application/octet-stream',
|
|
409
|
-
topic: options?.topic ?? '',
|
|
410
|
-
timestamp: Date.now(),
|
|
411
|
-
attributes: options?.attributes,
|
|
412
|
-
totalSize: options?.totalSize,
|
|
413
|
-
name: options?.name ?? 'unknown',
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const headerReq = new SendStreamHeaderRequest({
|
|
417
|
-
senderIdentity,
|
|
418
|
-
destinationIdentities,
|
|
425
|
+
const req = new ByteStreamOpenRequest({
|
|
419
426
|
localParticipantHandle: this.ffi_handle.handle,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
value: new DataStream_ByteHeader({
|
|
430
|
-
name: info.name,
|
|
431
|
-
}),
|
|
432
|
-
},
|
|
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,
|
|
433
436
|
}),
|
|
434
437
|
});
|
|
435
438
|
|
|
436
|
-
|
|
439
|
+
const res = FfiClient.instance.request<ByteStreamOpenResponse>({
|
|
440
|
+
message: { case: 'byteStreamOpen', value: req },
|
|
441
|
+
});
|
|
437
442
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
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
|
+
);
|
|
443
447
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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);
|
|
447
455
|
|
|
448
|
-
|
|
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) => {
|
|
449
468
|
try {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await sendChunk(chunkRequest);
|
|
464
|
-
chunkId += 1;
|
|
465
|
-
byteOffset += subChunk.byteLength;
|
|
466
|
-
}
|
|
467
|
-
} finally {
|
|
468
|
-
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;
|
|
469
482
|
}
|
|
470
483
|
},
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
senderIdentity,
|
|
474
|
-
localParticipantHandle: localHandle,
|
|
475
|
-
destinationIdentities,
|
|
476
|
-
trailer: new DataStream_Trailer({
|
|
477
|
-
streamId,
|
|
478
|
-
reason: '',
|
|
479
|
-
}),
|
|
480
|
-
});
|
|
481
|
-
await sendTrailer(trailerReq);
|
|
482
|
-
},
|
|
483
|
-
// 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
|
|
484
486
|
// controller is closed instead of waiting for data that won't arrive.
|
|
485
|
-
async
|
|
487
|
+
abort: async (err) => {
|
|
486
488
|
log.error(err, 'Sink error');
|
|
487
489
|
try {
|
|
488
|
-
|
|
489
|
-
senderIdentity,
|
|
490
|
-
localParticipantHandle: localHandle,
|
|
491
|
-
destinationIdentities,
|
|
492
|
-
trailer: new DataStream_Trailer({
|
|
493
|
-
streamId,
|
|
494
|
-
reason: err instanceof Error ? err.message : String(err ?? ''),
|
|
495
|
-
}),
|
|
496
|
-
});
|
|
497
|
-
await sendTrailer(trailerReq);
|
|
490
|
+
await closeWriter(err instanceof Error ? err.message : String(err ?? ''));
|
|
498
491
|
} catch {
|
|
499
492
|
// Best-effort: the connection may already be gone.
|
|
500
493
|
}
|
|
@@ -508,80 +501,116 @@ export class LocalParticipant extends Participant {
|
|
|
508
501
|
|
|
509
502
|
/** Sends a file provided as PathLike to specified recipients */
|
|
510
503
|
async sendFile(path: PathLike, options?: ByteStreamOptions) {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const writer = await this.streamBytes({
|
|
519
|
-
streamId: streamId,
|
|
520
|
-
name: options?.name,
|
|
521
|
-
totalSize: fileStats.size,
|
|
522
|
-
destinationIdentities,
|
|
523
|
-
topic: options?.topic,
|
|
524
|
-
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 ?? '',
|
|
525
510
|
attributes: options?.attributes,
|
|
526
|
-
|
|
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
|
+
});
|
|
527
519
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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);
|
|
534
531
|
}
|
|
535
532
|
}
|
|
536
533
|
|
|
537
|
-
private async
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
message: { case: type, value: req },
|
|
534
|
+
private textStreamWriterWrite = async (req: TextStreamWriterWriteRequest) => {
|
|
535
|
+
const res = FfiClient.instance.request<TextStreamWriterWriteResponse>({
|
|
536
|
+
message: { case: 'textStreamWrite', value: req },
|
|
541
537
|
});
|
|
542
538
|
|
|
543
|
-
const cb = await FfiClient.instance.waitFor<
|
|
544
|
-
(ev) => ev.message.case ==
|
|
539
|
+
const cb = await FfiClient.instance.waitFor<TextStreamWriterWriteCallback>(
|
|
540
|
+
(ev) => ev.message.case == 'textStreamWriterWrite' && ev.message.value.asyncId == res.asyncId,
|
|
545
541
|
{ signal: this.disconnectSignal },
|
|
546
542
|
);
|
|
547
543
|
|
|
548
544
|
if (cb.error) {
|
|
549
|
-
throw new Error(cb.error);
|
|
545
|
+
throw new Error(cb.error.description);
|
|
550
546
|
}
|
|
551
|
-
}
|
|
547
|
+
};
|
|
552
548
|
|
|
553
|
-
private
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
message: { case: type, value: req },
|
|
549
|
+
private textStreamWriterClose = async (req: TextStreamWriterCloseRequest) => {
|
|
550
|
+
const res = FfiClient.instance.request<TextStreamWriterCloseResponse>({
|
|
551
|
+
message: { case: 'textStreamClose', value: req },
|
|
557
552
|
});
|
|
558
553
|
|
|
559
|
-
const cb = await FfiClient.instance.waitFor<
|
|
560
|
-
(ev) => ev.message.case ==
|
|
554
|
+
const cb = await FfiClient.instance.waitFor<TextStreamWriterCloseCallback>(
|
|
555
|
+
(ev) => ev.message.case == 'textStreamWriterClose' && ev.message.value.asyncId == res.asyncId,
|
|
561
556
|
{ signal: this.disconnectSignal },
|
|
562
557
|
);
|
|
563
558
|
|
|
564
559
|
if (cb.error) {
|
|
565
|
-
throw new Error(cb.error);
|
|
560
|
+
throw new Error(cb.error.description);
|
|
566
561
|
}
|
|
567
562
|
};
|
|
568
563
|
|
|
569
|
-
private
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
message: { case: type, value: req },
|
|
564
|
+
private byteStreamWriterWrite = async (req: ByteStreamWriterWriteRequest) => {
|
|
565
|
+
const res = FfiClient.instance.request<ByteStreamWriterWriteResponse>({
|
|
566
|
+
message: { case: 'byteStreamWrite', value: req },
|
|
573
567
|
});
|
|
574
568
|
|
|
575
|
-
const cb = await FfiClient.instance.waitFor<
|
|
576
|
-
(ev) => ev.message.case ==
|
|
569
|
+
const cb = await FfiClient.instance.waitFor<ByteStreamWriterWriteCallback>(
|
|
570
|
+
(ev) => ev.message.case == 'byteStreamWriterWrite' && ev.message.value.asyncId == res.asyncId,
|
|
577
571
|
{ signal: this.disconnectSignal },
|
|
578
572
|
);
|
|
579
573
|
|
|
580
574
|
if (cb.error) {
|
|
581
|
-
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);
|
|
582
591
|
}
|
|
583
592
|
};
|
|
584
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
|
+
|
|
585
614
|
/**
|
|
586
615
|
* Sends a chat message to participants in the room
|
|
587
616
|
*
|