@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.
Files changed (141) hide show
  1. package/dist/async_queue.d.ts +2 -3
  2. package/dist/audio_filter.d.ts +2 -3
  3. package/dist/audio_frame.d.ts +5 -6
  4. package/dist/audio_mixer.d.ts +3 -6
  5. package/dist/audio_resampler.d.ts +3 -6
  6. package/dist/audio_source.d.ts +5 -6
  7. package/dist/{audio_stream-4M8XlO4-.d.cts → audio_stream-BNVaONuH.d.cts} +36 -10
  8. package/dist/audio_stream.d.cts +2 -2
  9. package/dist/audio_stream.d.ts +61 -18
  10. package/dist/data_streams/index.d.cts +1 -1
  11. package/dist/data_streams/index.d.ts +4 -3
  12. package/dist/data_streams/stream_reader.cjs +97 -58
  13. package/dist/data_streams/stream_reader.cjs.map +1 -1
  14. package/dist/data_streams/stream_reader.d.cts +1 -1
  15. package/dist/data_streams/stream_reader.d.ts +65 -2
  16. package/dist/data_streams/stream_reader.d.ts.map +1 -1
  17. package/dist/data_streams/stream_writer.d.cts +1 -1
  18. package/dist/data_streams/stream_writer.d.ts +5 -7
  19. package/dist/data_streams/types.cjs.map +1 -1
  20. package/dist/data_streams/types.d.cts +1 -1
  21. package/dist/data_streams/types.d.ts +47 -2
  22. package/dist/data_streams/types.d.ts.map +1 -1
  23. package/dist/e2ee.cjs +5 -1
  24. package/dist/e2ee.cjs.map +1 -1
  25. package/dist/e2ee.d.cts +3 -1
  26. package/dist/e2ee.d.ts +11 -11
  27. package/dist/e2ee.d.ts.map +1 -1
  28. package/dist/ffi_client.d.ts +8 -10
  29. package/dist/frame_processor.d.ts +8 -11
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +2 -2
  32. package/dist/index.d.ts +22 -17
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -107
  35. package/dist/log.d.ts +5 -7
  36. package/dist/participant.cjs +179 -192
  37. package/dist/participant.cjs.map +1 -1
  38. package/dist/participant.d.cts +2 -2
  39. package/dist/participant.d.ts +169 -18
  40. package/dist/participant.d.ts.map +1 -1
  41. package/dist/room.cjs +166 -120
  42. package/dist/room.cjs.map +1 -1
  43. package/dist/room.d.cts +2 -2
  44. package/dist/room.d.ts +228 -18
  45. package/dist/room.d.ts.map +1 -1
  46. package/dist/rpc.d.ts +7 -9
  47. package/dist/tests/e2e_common.cjs +157 -0
  48. package/dist/tests/e2e_common.cjs.map +1 -0
  49. package/dist/tests/e2e_common.d.cts +56 -0
  50. package/dist/tests/e2e_common.d.ts +37 -0
  51. package/dist/tests/e2e_common.d.ts.map +1 -0
  52. package/dist/track.d.cts +2 -2
  53. package/dist/track.d.ts +67 -18
  54. package/dist/track_publication.d.cts +2 -2
  55. package/dist/track_publication.d.ts +45 -18
  56. package/dist/transcription.d.ts +3 -4
  57. package/dist/{types-_PdPVish.d.ts → types-f7wTlFjs.d.cts} +34 -11
  58. package/dist/types.d.ts +2 -3
  59. package/dist/utils.cjs +27 -2
  60. package/dist/utils.cjs.map +1 -1
  61. package/dist/utils.d.cts +8 -1
  62. package/dist/utils.d.ts +10 -5
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/version.cjs +1 -1
  65. package/dist/version.cjs.map +1 -1
  66. package/dist/version.d.cts +1 -1
  67. package/dist/version.d.ts +2 -3
  68. package/dist/video_frame.d.ts +4 -5
  69. package/dist/video_source.d.ts +6 -8
  70. package/dist/video_stream.d.cts +2 -2
  71. package/dist/video_stream.d.ts +5 -22
  72. package/package.json +6 -7
  73. package/src/audio_stream_room_lifecycle.test.ts +4 -1
  74. package/src/data_streams/stream_reader.ts +132 -82
  75. package/src/data_streams/types.ts +6 -0
  76. package/src/e2ee.ts +7 -0
  77. package/src/index.ts +8 -1
  78. package/src/participant.ts +252 -223
  79. package/src/room.ts +233 -134
  80. package/src/tests/e2e.test.ts +50 -139
  81. package/src/tests/e2e_common.ts +163 -0
  82. package/src/tests/e2e_data_streams.test.ts +639 -0
  83. package/src/utils.ts +30 -0
  84. package/src/version.ts +1 -1
  85. package/dist/async_queue.js +0 -56
  86. package/dist/async_queue.js.map +0 -1
  87. package/dist/audio_filter.js +0 -24
  88. package/dist/audio_filter.js.map +0 -1
  89. package/dist/audio_frame.js +0 -78
  90. package/dist/audio_frame.js.map +0 -1
  91. package/dist/audio_mixer.js +0 -269
  92. package/dist/audio_mixer.js.map +0 -1
  93. package/dist/audio_resampler.js +0 -154
  94. package/dist/audio_resampler.js.map +0 -1
  95. package/dist/audio_source.js +0 -134
  96. package/dist/audio_source.js.map +0 -1
  97. package/dist/audio_stream-CTyN1Ldy.d.ts +0 -523
  98. package/dist/audio_stream.js +0 -122
  99. package/dist/audio_stream.js.map +0 -1
  100. package/dist/data_streams/index.js +0 -3
  101. package/dist/data_streams/index.js.map +0 -1
  102. package/dist/data_streams/stream_reader.js +0 -124
  103. package/dist/data_streams/stream_reader.js.map +0 -1
  104. package/dist/data_streams/stream_writer.js +0 -26
  105. package/dist/data_streams/stream_writer.js.map +0 -1
  106. package/dist/data_streams/types.js +0 -1
  107. package/dist/data_streams/types.js.map +0 -1
  108. package/dist/e2ee.js +0 -251
  109. package/dist/e2ee.js.map +0 -1
  110. package/dist/ffi_client.js +0 -89
  111. package/dist/ffi_client.js.map +0 -1
  112. package/dist/frame_processor.js +0 -29
  113. package/dist/frame_processor.js.map +0 -1
  114. package/dist/index.js.map +0 -1
  115. package/dist/log.js +0 -9
  116. package/dist/log.js.map +0 -1
  117. package/dist/participant.js +0 -715
  118. package/dist/participant.js.map +0 -1
  119. package/dist/room.js +0 -900
  120. package/dist/room.js.map +0 -1
  121. package/dist/rpc.js +0 -65
  122. package/dist/rpc.js.map +0 -1
  123. package/dist/track.js +0 -234
  124. package/dist/track.js.map +0 -1
  125. package/dist/track_publication.js +0 -100
  126. package/dist/track_publication.js.map +0 -1
  127. package/dist/transcription.js +0 -1
  128. package/dist/transcription.js.map +0 -1
  129. package/dist/types-_PdPVish.d.cts +0 -87
  130. package/dist/types.js +0 -1
  131. package/dist/types.js.map +0 -1
  132. package/dist/utils.js +0 -35
  133. package/dist/utils.js.map +0 -1
  134. package/dist/version.js +0 -5
  135. package/dist/version.js.map +0 -1
  136. package/dist/video_frame.js +0 -207
  137. package/dist/video_frame.js.map +0 -1
  138. package/dist/video_source.js +0 -53
  139. package/dist/video_source.js.map +0 -1
  140. package/dist/video_stream.js +0 -70
  141. package/dist/video_stream.js.map +0 -1
package/dist/log.d.ts CHANGED
@@ -1,11 +1,9 @@
1
- import * as pino from 'pino';
2
- import { LevelWithSilent } from 'pino';
3
-
1
+ import type { LevelWithSilent } from 'pino';
4
2
  /**
5
3
  * temporarily disabling the transport option as there might be a memory leak in some corner cases related to it
6
4
  * see https://github.com/pinojs/pino/issues/2370
7
5
  */
8
- declare const log: pino.Logger<never, boolean>;
9
- type LogLevel = LevelWithSilent;
10
-
11
- export { type LogLevel, log };
6
+ declare const log: import("pino").Logger<never, boolean>;
7
+ export type LogLevel = LevelWithSilent;
8
+ export { log };
9
+ //# sourceMappingURL=log.d.ts.map
@@ -27,14 +27,14 @@ var import_mutex = require("@livekit/mutex");
27
27
  var import_rtc_ffi_bindings = require("@livekit/rtc-ffi-bindings");
28
28
  var import_rtc_ffi_bindings2 = require("@livekit/rtc-ffi-bindings");
29
29
  var import_rtc_ffi_bindings3 = require("@livekit/rtc-ffi-bindings");
30
- var import_promises = require("node:fs/promises");
30
+ var import_node_url = require("node:url");
31
31
  var import_data_streams = require("./data_streams/index.cjs");
32
32
  var import_ffi_client = require("./ffi_client.cjs");
33
33
  var import_log = require("./log.cjs");
34
34
  var import_rpc = require("./rpc.cjs");
35
35
  var import_track_publication = require("./track_publication.cjs");
36
36
  var import_utils = require("./utils.cjs");
37
- const STREAM_CHUNK_SIZE = 15e3;
37
+ var import_utils2 = require("./utils.cjs");
38
38
  class Participant {
39
39
  constructor(owned_info) {
40
40
  this.trackPublications = /* @__PURE__ */ new Map();
@@ -73,31 +73,60 @@ class LocalParticipant extends Participant {
73
73
  constructor(info, ffiEventLock, disconnectSignal) {
74
74
  super(info);
75
75
  this.rpcHandlers = /* @__PURE__ */ new Map();
76
+ // Handle ids of data stream writers that have been opened but not yet
77
+ // closed. The FFI close request consumes the handle (take_handle), so an
78
+ // entry is removed as soon as a close is sent for it; whatever is left when
79
+ // the room disconnects is dropped by disposeOpenStreamWriters(). Only the
80
+ // ids are kept — wrapping them in FfiHandle would make GC drop a handle the
81
+ // close request already consumed.
82
+ this.openStreamWriters = /* @__PURE__ */ new Set();
76
83
  this.trackPublications = /* @__PURE__ */ new Map();
77
- this.sendStreamChunk = async (req) => {
78
- const type = "sendStreamChunk";
84
+ this.textStreamWriterWrite = async (req) => {
79
85
  const res = import_ffi_client.FfiClient.instance.request({
80
- message: { case: type, value: req }
86
+ message: { case: "textStreamWrite", value: req }
81
87
  });
82
88
  const cb = await import_ffi_client.FfiClient.instance.waitFor(
83
- (ev) => ev.message.case == type && ev.message.value.asyncId == res.asyncId,
89
+ (ev) => ev.message.case == "textStreamWriterWrite" && ev.message.value.asyncId == res.asyncId,
84
90
  { signal: this.disconnectSignal }
85
91
  );
86
92
  if (cb.error) {
87
- throw new Error(cb.error);
93
+ throw new Error(cb.error.description);
88
94
  }
89
95
  };
90
- this.sendStreamTrailer = async (req) => {
91
- const type = "sendStreamTrailer";
96
+ this.textStreamWriterClose = async (req) => {
92
97
  const res = import_ffi_client.FfiClient.instance.request({
93
- message: { case: type, value: req }
98
+ message: { case: "textStreamClose", value: req }
94
99
  });
95
100
  const cb = await import_ffi_client.FfiClient.instance.waitFor(
96
- (ev) => ev.message.case == type && ev.message.value.asyncId == res.asyncId,
101
+ (ev) => ev.message.case == "textStreamWriterClose" && ev.message.value.asyncId == res.asyncId,
97
102
  { signal: this.disconnectSignal }
98
103
  );
99
104
  if (cb.error) {
100
- throw new Error(cb.error);
105
+ throw new Error(cb.error.description);
106
+ }
107
+ };
108
+ this.byteStreamWriterWrite = async (req) => {
109
+ const res = import_ffi_client.FfiClient.instance.request({
110
+ message: { case: "byteStreamWrite", value: req }
111
+ });
112
+ const cb = await import_ffi_client.FfiClient.instance.waitFor(
113
+ (ev) => ev.message.case == "byteStreamWriterWrite" && ev.message.value.asyncId == res.asyncId,
114
+ { signal: this.disconnectSignal }
115
+ );
116
+ if (cb.error) {
117
+ throw new Error(cb.error.description);
118
+ }
119
+ };
120
+ this.byteStreamWriterClose = async (req) => {
121
+ const res = import_ffi_client.FfiClient.instance.request({
122
+ message: { case: "byteStreamClose", value: req }
123
+ });
124
+ const cb = await import_ffi_client.FfiClient.instance.waitFor(
125
+ (ev) => ev.message.case == "byteStreamWriterClose" && ev.message.value.asyncId == res.asyncId,
126
+ { signal: this.disconnectSignal }
127
+ );
128
+ if (cb.error) {
129
+ throw new Error(cb.error.description);
101
130
  }
102
131
  };
103
132
  this.ffiEventLock = ffiEventLock;
@@ -185,86 +214,55 @@ class LocalParticipant extends Participant {
185
214
  * Well suited for TTS and/or streaming LLM output. If you want to simply send a text then use sendText() instead
186
215
  */
187
216
  async streamText(options) {
188
- const senderIdentity = (options == null ? void 0 : options.senderIdentity) ?? this.identity;
189
- const streamId = (options == null ? void 0 : options.streamId) ?? crypto.randomUUID();
190
- const destinationIdentities = options == null ? void 0 : options.destinationIdentities;
191
- const info = {
192
- streamId,
193
- mimeType: "text/plain",
194
- topic: (options == null ? void 0 : options.topic) ?? "",
195
- timestamp: Date.now()
196
- };
197
- const headerReq = new import_rtc_ffi_bindings2.SendStreamHeaderRequest({
198
- senderIdentity,
199
- destinationIdentities,
217
+ const req = new import_rtc_ffi_bindings2.TextStreamOpenRequest({
200
218
  localParticipantHandle: this.ffi_handle.handle,
201
- header: new import_rtc_ffi_bindings2.DataStream_Header({
202
- streamId,
203
- mimeType: info.mimeType,
204
- topic: info.topic,
205
- timestamp: (0, import_utils.numberToBigInt)(info.timestamp),
219
+ options: new import_rtc_ffi_bindings2.StreamTextOptions({
220
+ topic: (options == null ? void 0 : options.topic) ?? "",
206
221
  attributes: options == null ? void 0 : options.attributes,
207
- contentHeader: {
208
- case: "textHeader",
209
- value: new import_rtc_ffi_bindings2.DataStream_TextHeader({
210
- operationType: import_rtc_ffi_bindings2.DataStream_OperationType.CREATE,
211
- version: 0,
212
- replyToStreamId: "",
213
- generated: false
214
- })
215
- }
222
+ destinationIdentities: options == null ? void 0 : options.destinationIdentities,
223
+ id: options == null ? void 0 : options.streamId,
224
+ senderIdentity: (options == null ? void 0 : options.senderIdentity) ?? this.identity
216
225
  })
217
226
  });
218
- await this.sendStreamHeader(headerReq);
219
- let nextChunkId = 0;
220
- const localHandle = this.ffi_handle.handle;
221
- const sendTrailer = this.sendStreamTrailer;
222
- const sendChunk = this.sendStreamChunk;
227
+ const res = import_ffi_client.FfiClient.instance.request({
228
+ message: { case: "textStreamOpen", value: req }
229
+ });
230
+ const cb = await import_ffi_client.FfiClient.instance.waitFor(
231
+ (ev) => ev.message.case == "textStreamOpen" && ev.message.value.asyncId == res.asyncId,
232
+ { signal: this.disconnectSignal }
233
+ );
234
+ if (cb.result.case !== "writer") {
235
+ throw new Error(cb.result.case === "error" ? cb.result.value.description : "unknown error");
236
+ }
237
+ const writerHandle = cb.result.value.handle.id;
238
+ const info = (0, import_utils.textStreamInfoFromProto)(cb.result.value.info);
239
+ this.openStreamWriters.add(writerHandle);
240
+ const closeWriter = async (reason) => {
241
+ if (!this.openStreamWriters.delete(writerHandle)) {
242
+ return;
243
+ }
244
+ await this.textStreamWriterClose(new import_rtc_ffi_bindings2.TextStreamWriterCloseRequest({ writerHandle, reason }));
245
+ };
223
246
  const writableStream = new WritableStream({
224
247
  // Implement the sink
225
- async write(text) {
226
- for (const textByteChunk of (0, import_utils.splitUtf8)(text, STREAM_CHUNK_SIZE)) {
227
- const chunkRequest = new import_rtc_ffi_bindings2.SendStreamChunkRequest({
228
- senderIdentity,
229
- localParticipantHandle: localHandle,
230
- destinationIdentities,
231
- chunk: new import_rtc_ffi_bindings2.DataStream_Chunk({
232
- content: textByteChunk,
233
- streamId,
234
- chunkIndex: (0, import_utils.numberToBigInt)(nextChunkId)
235
- })
248
+ write: async (text) => {
249
+ try {
250
+ await this.textStreamWriterWrite(
251
+ new import_rtc_ffi_bindings2.TextStreamWriterWriteRequest({ writerHandle, text })
252
+ );
253
+ } catch (err) {
254
+ await closeWriter(err instanceof Error ? err.message : String(err ?? "")).catch(() => {
236
255
  });
237
- await sendChunk(chunkRequest);
238
- nextChunkId += 1;
256
+ throw err;
239
257
  }
240
258
  },
241
- async close() {
242
- const trailerReq = new import_rtc_ffi_bindings2.SendStreamTrailerRequest({
243
- senderIdentity,
244
- localParticipantHandle: localHandle,
245
- destinationIdentities,
246
- trailer: new import_rtc_ffi_bindings2.DataStream_Trailer({
247
- streamId,
248
- reason: ""
249
- })
250
- });
251
- await sendTrailer(trailerReq);
252
- },
253
- // Send a trailer with the error reason so the remote side's stream
259
+ close: () => closeWriter(),
260
+ // Close the stream with the error reason so the remote side's stream
254
261
  // controller is closed instead of waiting for data that won't arrive.
255
- async abort(err) {
262
+ abort: async (err) => {
256
263
  import_log.log.error(err, "Sink Error");
257
264
  try {
258
- const trailerReq = new import_rtc_ffi_bindings2.SendStreamTrailerRequest({
259
- senderIdentity,
260
- localParticipantHandle: localHandle,
261
- destinationIdentities,
262
- trailer: new import_rtc_ffi_bindings2.DataStream_Trailer({
263
- streamId,
264
- reason: err instanceof Error ? err.message : String(err ?? "")
265
- })
266
- });
267
- await sendTrailer(trailerReq);
265
+ await closeWriter(err instanceof Error ? err.message : String(err ?? ""));
268
266
  } catch {
269
267
  }
270
268
  }
@@ -273,101 +271,82 @@ class LocalParticipant extends Participant {
273
271
  return writer;
274
272
  }
275
273
  async sendText(text, options) {
276
- const writer = await this.streamText(options);
277
- await writer.write(text);
278
- await writer.close();
279
- return writer.info;
274
+ const req = new import_rtc_ffi_bindings2.StreamSendTextRequest({
275
+ localParticipantHandle: this.ffi_handle.handle,
276
+ options: new import_rtc_ffi_bindings2.StreamTextOptions({
277
+ topic: (options == null ? void 0 : options.topic) ?? "",
278
+ attributes: options == null ? void 0 : options.attributes,
279
+ destinationIdentities: options == null ? void 0 : options.destinationIdentities,
280
+ id: options == null ? void 0 : options.streamId,
281
+ senderIdentity: this.identity,
282
+ compress: options == null ? void 0 : options.compress
283
+ }),
284
+ text
285
+ });
286
+ const res = import_ffi_client.FfiClient.instance.request({
287
+ message: { case: "sendText", value: req }
288
+ });
289
+ const cb = await import_ffi_client.FfiClient.instance.waitFor(
290
+ (ev) => ev.message.case == "sendText" && ev.message.value.asyncId == res.asyncId,
291
+ { signal: this.disconnectSignal }
292
+ );
293
+ if (cb.result.case !== "info") {
294
+ throw new Error(cb.result.case === "error" ? cb.result.value.description : "unknown error");
295
+ }
296
+ return (0, import_utils.textStreamInfoFromProto)(cb.result.value);
280
297
  }
281
298
  async streamBytes(options) {
282
- const senderIdentity = this.identity;
283
- const streamId = (options == null ? void 0 : options.streamId) ?? crypto.randomUUID();
284
- const destinationIdentities = options == null ? void 0 : options.destinationIdentities;
285
- const info = {
286
- streamId,
287
- mimeType: (options == null ? void 0 : options.mimeType) ?? "application/octet-stream",
288
- topic: (options == null ? void 0 : options.topic) ?? "",
289
- timestamp: Date.now(),
290
- attributes: options == null ? void 0 : options.attributes,
291
- totalSize: options == null ? void 0 : options.totalSize,
292
- name: (options == null ? void 0 : options.name) ?? "unknown"
293
- };
294
- const headerReq = new import_rtc_ffi_bindings2.SendStreamHeaderRequest({
295
- senderIdentity,
296
- destinationIdentities,
299
+ const req = new import_rtc_ffi_bindings2.ByteStreamOpenRequest({
297
300
  localParticipantHandle: this.ffi_handle.handle,
298
- header: new import_rtc_ffi_bindings2.DataStream_Header({
299
- streamId,
300
- mimeType: info.mimeType,
301
- topic: info.topic,
302
- timestamp: (0, import_utils.numberToBigInt)(info.timestamp),
303
- attributes: info.attributes,
304
- totalLength: (0, import_utils.numberToBigInt)(info.totalSize),
305
- contentHeader: {
306
- case: "byteHeader",
307
- value: new import_rtc_ffi_bindings2.DataStream_ByteHeader({
308
- name: info.name
309
- })
310
- }
301
+ options: new import_rtc_ffi_bindings2.StreamByteOptions({
302
+ topic: (options == null ? void 0 : options.topic) ?? "",
303
+ attributes: options == null ? void 0 : options.attributes,
304
+ destinationIdentities: options == null ? void 0 : options.destinationIdentities,
305
+ id: options == null ? void 0 : options.streamId,
306
+ name: (options == null ? void 0 : options.name) ?? "unknown",
307
+ mimeType: (options == null ? void 0 : options.mimeType) ?? "application/octet-stream",
308
+ totalLength: (0, import_utils2.numberToBigInt)(options == null ? void 0 : options.totalSize),
309
+ senderIdentity: this.identity
311
310
  })
312
311
  });
313
- await this.sendStreamHeader(headerReq);
314
- let chunkId = 0;
315
- const localHandle = this.ffi_handle.handle;
316
- const sendTrailer = this.sendStreamTrailer;
317
- const sendChunk = this.sendStreamChunk;
318
- const writeMutex = new import_mutex.Mutex();
312
+ const res = import_ffi_client.FfiClient.instance.request({
313
+ message: { case: "byteStreamOpen", value: req }
314
+ });
315
+ const cb = await import_ffi_client.FfiClient.instance.waitFor(
316
+ (ev) => ev.message.case == "byteStreamOpen" && ev.message.value.asyncId == res.asyncId,
317
+ { signal: this.disconnectSignal }
318
+ );
319
+ if (cb.result.case !== "writer") {
320
+ throw new Error(cb.result.case === "error" ? cb.result.value.description : "unknown error");
321
+ }
322
+ const writerHandle = cb.result.value.handle.id;
323
+ const info = (0, import_utils.byteStreamInfoFromProto)(cb.result.value.info);
324
+ this.openStreamWriters.add(writerHandle);
325
+ const closeWriter = async (reason) => {
326
+ if (!this.openStreamWriters.delete(writerHandle)) {
327
+ return;
328
+ }
329
+ await this.byteStreamWriterClose(new import_rtc_ffi_bindings2.ByteStreamWriterCloseRequest({ writerHandle, reason }));
330
+ };
319
331
  const writableStream = new WritableStream({
320
- async write(chunk) {
321
- const unlock = await writeMutex.lock();
322
- let byteOffset = 0;
332
+ write: async (chunk) => {
323
333
  try {
324
- while (byteOffset < chunk.byteLength) {
325
- const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
326
- const chunkRequest = new import_rtc_ffi_bindings2.SendStreamChunkRequest({
327
- senderIdentity,
328
- localParticipantHandle: localHandle,
329
- destinationIdentities,
330
- chunk: new import_rtc_ffi_bindings2.DataStream_Chunk({
331
- content: subChunk,
332
- streamId,
333
- chunkIndex: (0, import_utils.numberToBigInt)(chunkId)
334
- })
335
- });
336
- await sendChunk(chunkRequest);
337
- chunkId += 1;
338
- byteOffset += subChunk.byteLength;
339
- }
340
- } finally {
341
- unlock();
334
+ await this.byteStreamWriterWrite(
335
+ new import_rtc_ffi_bindings2.ByteStreamWriterWriteRequest({ writerHandle, bytes: chunk })
336
+ );
337
+ } catch (err) {
338
+ await closeWriter(err instanceof Error ? err.message : String(err ?? "")).catch(() => {
339
+ });
340
+ throw err;
342
341
  }
343
342
  },
344
- async close() {
345
- const trailerReq = new import_rtc_ffi_bindings2.SendStreamTrailerRequest({
346
- senderIdentity,
347
- localParticipantHandle: localHandle,
348
- destinationIdentities,
349
- trailer: new import_rtc_ffi_bindings2.DataStream_Trailer({
350
- streamId,
351
- reason: ""
352
- })
353
- });
354
- await sendTrailer(trailerReq);
355
- },
356
- // Send a trailer with the error reason so the remote side's stream
343
+ close: () => closeWriter(),
344
+ // Close the stream with the error reason so the remote side's stream
357
345
  // controller is closed instead of waiting for data that won't arrive.
358
- async abort(err) {
346
+ abort: async (err) => {
359
347
  import_log.log.error(err, "Sink error");
360
348
  try {
361
- const trailerReq = new import_rtc_ffi_bindings2.SendStreamTrailerRequest({
362
- senderIdentity,
363
- localParticipantHandle: localHandle,
364
- destinationIdentities,
365
- trailer: new import_rtc_ffi_bindings2.DataStream_Trailer({
366
- streamId,
367
- reason: err instanceof Error ? err.message : String(err ?? "")
368
- })
369
- });
370
- await sendTrailer(trailerReq);
349
+ await closeWriter(err instanceof Error ? err.message : String(err ?? ""));
371
350
  } catch {
372
351
  }
373
352
  }
@@ -377,41 +356,49 @@ class LocalParticipant extends Participant {
377
356
  }
378
357
  /** Sends a file provided as PathLike to specified recipients */
379
358
  async sendFile(path, options) {
380
- const fileStats = await (0, import_promises.stat)(path);
381
- const file = await (0, import_promises.open)(path);
382
- try {
383
- const stream = file.readableWebStream();
384
- const streamId = crypto.randomUUID();
385
- const destinationIdentities = options == null ? void 0 : options.destinationIdentities;
386
- const writer = await this.streamBytes({
387
- streamId,
388
- name: options == null ? void 0 : options.name,
389
- totalSize: fileStats.size,
390
- destinationIdentities,
391
- topic: options == null ? void 0 : options.topic,
392
- mimeType: options == null ? void 0 : options.mimeType,
393
- attributes: options == null ? void 0 : options.attributes
394
- });
395
- for await (const chunk of stream) {
396
- await writer.write(chunk);
397
- }
398
- await writer.close();
399
- } finally {
400
- await file.close();
401
- }
402
- }
403
- async sendStreamHeader(req) {
404
- const type = "sendStreamHeader";
359
+ const filePath = path instanceof URL ? (0, import_node_url.fileURLToPath)(path) : path.toString();
360
+ const req = new import_rtc_ffi_bindings2.StreamSendFileRequest({
361
+ localParticipantHandle: this.ffi_handle.handle,
362
+ options: new import_rtc_ffi_bindings2.StreamByteOptions({
363
+ topic: (options == null ? void 0 : options.topic) ?? "",
364
+ attributes: options == null ? void 0 : options.attributes,
365
+ destinationIdentities: options == null ? void 0 : options.destinationIdentities,
366
+ name: (options == null ? void 0 : options.name) ?? "unknown",
367
+ mimeType: (options == null ? void 0 : options.mimeType) ?? "application/octet-stream",
368
+ senderIdentity: this.identity,
369
+ compress: options == null ? void 0 : options.compress
370
+ }),
371
+ filePath
372
+ });
405
373
  const res = import_ffi_client.FfiClient.instance.request({
406
- message: { case: type, value: req }
374
+ message: { case: "sendFile", value: req }
407
375
  });
408
376
  const cb = await import_ffi_client.FfiClient.instance.waitFor(
409
- (ev) => ev.message.case == type && ev.message.value.asyncId == res.asyncId,
377
+ (ev) => ev.message.case == "sendFile" && ev.message.value.asyncId == res.asyncId,
410
378
  { signal: this.disconnectSignal }
411
379
  );
412
- if (cb.error) {
413
- throw new Error(cb.error);
380
+ if (cb.result.case === "error") {
381
+ throw new Error(cb.result.value.description);
382
+ }
383
+ }
384
+ /**
385
+ * Drops the native writers of any data streams that were opened but never
386
+ * closed — a writer the caller abandoned, or one whose write failed. Their
387
+ * handles would otherwise be held by the FFI server for the lifetime of the
388
+ * process. The room is already gone at this point, so the handles are dropped
389
+ * directly rather than closed: there is no end-of-stream left to deliver.
390
+ *
391
+ * @internal
392
+ */
393
+ disposeOpenStreamWriters() {
394
+ for (const writerHandle of this.openStreamWriters) {
395
+ try {
396
+ new import_ffi_client.FfiHandle(writerHandle).dispose();
397
+ } catch (err) {
398
+ import_log.log.warn("failed to dispose data stream writer handle: %s", err);
399
+ }
414
400
  }
401
+ this.openStreamWriters.clear();
415
402
  }
416
403
  /**
417
404
  * Sends a chat message to participants in the room