livekit-client 2.15.4 → 2.15.6
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/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +373 -164
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +982 -643
- 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/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +0 -47
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +27 -0
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -0
- package/dist/src/e2ee/worker/sifPayload.d.ts +22 -0
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +6 -10
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +20 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -0
- package/dist/{ts4.2/src/room → src/room/data-stream/incoming}/StreamReader.d.ts +82 -56
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -0
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +27 -0
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -0
- package/dist/src/room/{StreamWriter.d.ts → data-stream/outgoing/StreamWriter.d.ts} +1 -1
- package/dist/src/room/data-stream/outgoing/StreamWriter.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +13 -0
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +32 -19
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +7 -2
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +4 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -1
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +8 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +0 -47
- package/dist/ts4.2/src/e2ee/worker/naluUtils.d.ts +27 -0
- package/dist/ts4.2/src/e2ee/worker/sifPayload.d.ts +22 -0
- package/dist/ts4.2/src/index.d.ts +2 -2
- package/dist/ts4.2/src/room/Room.d.ts +6 -10
- package/dist/ts4.2/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +20 -0
- package/dist/{src/room → ts4.2/src/room/data-stream/incoming}/StreamReader.d.ts +82 -56
- package/dist/ts4.2/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +27 -0
- package/dist/ts4.2/src/room/{StreamWriter.d.ts → data-stream/outgoing/StreamWriter.d.ts} +1 -1
- package/dist/ts4.2/src/room/errors.d.ts +13 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +32 -19
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +7 -2
- package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +4 -1
- package/dist/ts4.2/src/room/types.d.ts +17 -1
- package/dist/ts4.2/src/room/utils.d.ts +8 -0
- package/package.json +7 -7
- package/src/e2ee/E2eeManager.ts +18 -1
- package/src/e2ee/worker/FrameCryptor.ts +56 -157
- package/src/e2ee/worker/e2ee.worker.ts +6 -1
- package/src/e2ee/worker/naluUtils.ts +328 -0
- package/src/e2ee/worker/sifPayload.ts +75 -0
- package/src/index.ts +2 -2
- package/src/room/Room.ts +104 -208
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +247 -0
- package/src/room/data-stream/incoming/StreamReader.ts +317 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +316 -0
- package/src/room/{StreamWriter.ts → data-stream/outgoing/StreamWriter.ts} +1 -1
- package/src/room/errors.ts +34 -0
- package/src/room/participant/LocalParticipant.ts +39 -295
- package/src/room/track/LocalAudioTrack.ts +2 -2
- package/src/room/track/LocalTrack.ts +70 -50
- package/src/room/track/RemoteVideoTrack.ts +12 -2
- package/src/room/track/Track.ts +10 -1
- package/src/room/types.ts +22 -1
- package/src/room/utils.ts +14 -5
- package/dist/src/e2ee/worker/SifGuard.d.ts +0 -11
- package/dist/src/e2ee/worker/SifGuard.d.ts.map +0 -1
- package/dist/src/room/StreamReader.d.ts.map +0 -1
- package/dist/src/room/StreamWriter.d.ts.map +0 -1
- package/dist/ts4.2/src/e2ee/worker/SifGuard.d.ts +0 -11
- package/src/e2ee/worker/SifGuard.ts +0 -47
- package/src/room/StreamReader.ts +0 -170
@@ -0,0 +1,317 @@
|
|
1
|
+
import type { DataStream_Chunk } from '@livekit/protocol';
|
2
|
+
import { DataStreamError, DataStreamErrorReason } from '../../errors';
|
3
|
+
import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from '../../types';
|
4
|
+
import { Future, bigIntToNumber } from '../../utils';
|
5
|
+
|
6
|
+
export type BaseStreamReaderReadAllOpts = {
|
7
|
+
/** An AbortSignal can be used to terminate reads early. */
|
8
|
+
signal?: AbortSignal;
|
9
|
+
};
|
10
|
+
|
11
|
+
abstract class BaseStreamReader<T extends BaseStreamInfo> {
|
12
|
+
protected reader: ReadableStream<DataStream_Chunk>;
|
13
|
+
|
14
|
+
protected totalByteSize?: number;
|
15
|
+
|
16
|
+
protected _info: T;
|
17
|
+
|
18
|
+
protected bytesReceived: number;
|
19
|
+
|
20
|
+
protected outOfBandFailureRejectingFuture?: Future<never>;
|
21
|
+
|
22
|
+
get info() {
|
23
|
+
return this._info;
|
24
|
+
}
|
25
|
+
|
26
|
+
/** @internal */
|
27
|
+
protected validateBytesReceived(doneReceiving: boolean = false) {
|
28
|
+
if (typeof this.totalByteSize !== 'number' || this.totalByteSize === 0) {
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
if (doneReceiving && this.bytesReceived < this.totalByteSize) {
|
33
|
+
throw new DataStreamError(
|
34
|
+
`Not enough chunk(s) received - expected ${this.totalByteSize} bytes of data total, only received ${this.bytesReceived} bytes`,
|
35
|
+
DataStreamErrorReason.Incomplete,
|
36
|
+
);
|
37
|
+
} else if (this.bytesReceived > this.totalByteSize) {
|
38
|
+
throw new DataStreamError(
|
39
|
+
`Extra chunk(s) received - expected ${this.totalByteSize} bytes of data total, received ${this.bytesReceived} bytes`,
|
40
|
+
DataStreamErrorReason.LengthExceeded,
|
41
|
+
);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
constructor(
|
46
|
+
info: T,
|
47
|
+
stream: ReadableStream<DataStream_Chunk>,
|
48
|
+
totalByteSize?: number,
|
49
|
+
outOfBandFailureRejectingFuture?: Future<never>,
|
50
|
+
) {
|
51
|
+
this.reader = stream;
|
52
|
+
this.totalByteSize = totalByteSize;
|
53
|
+
this._info = info;
|
54
|
+
this.bytesReceived = 0;
|
55
|
+
this.outOfBandFailureRejectingFuture = outOfBandFailureRejectingFuture;
|
56
|
+
}
|
57
|
+
|
58
|
+
protected abstract handleChunkReceived(chunk: DataStream_Chunk): void;
|
59
|
+
|
60
|
+
onProgress?: (progress: number | undefined) => void;
|
61
|
+
|
62
|
+
abstract readAll(opts?: BaseStreamReaderReadAllOpts): Promise<string | Array<Uint8Array>>;
|
63
|
+
}
|
64
|
+
|
65
|
+
export class ByteStreamReader extends BaseStreamReader<ByteStreamInfo> {
|
66
|
+
protected handleChunkReceived(chunk: DataStream_Chunk) {
|
67
|
+
this.bytesReceived += chunk.content.byteLength;
|
68
|
+
this.validateBytesReceived();
|
69
|
+
|
70
|
+
const currentProgress = this.totalByteSize
|
71
|
+
? this.bytesReceived / this.totalByteSize
|
72
|
+
: undefined;
|
73
|
+
this.onProgress?.(currentProgress);
|
74
|
+
}
|
75
|
+
|
76
|
+
onProgress?: (progress: number | undefined) => void;
|
77
|
+
|
78
|
+
signal?: AbortSignal;
|
79
|
+
|
80
|
+
[Symbol.asyncIterator]() {
|
81
|
+
const reader = this.reader.getReader();
|
82
|
+
|
83
|
+
let rejectingSignalFuture = new Future<never>();
|
84
|
+
let activeSignal: AbortSignal | null = null;
|
85
|
+
let onAbort: (() => void) | null = null;
|
86
|
+
if (this.signal) {
|
87
|
+
const signal = this.signal;
|
88
|
+
onAbort = () => {
|
89
|
+
rejectingSignalFuture.reject?.(signal.reason);
|
90
|
+
};
|
91
|
+
signal.addEventListener('abort', onAbort);
|
92
|
+
activeSignal = signal;
|
93
|
+
}
|
94
|
+
|
95
|
+
const cleanup = () => {
|
96
|
+
reader.releaseLock();
|
97
|
+
|
98
|
+
if (activeSignal && onAbort) {
|
99
|
+
activeSignal.removeEventListener('abort', onAbort);
|
100
|
+
}
|
101
|
+
|
102
|
+
this.signal = undefined;
|
103
|
+
};
|
104
|
+
|
105
|
+
return {
|
106
|
+
next: async (): Promise<IteratorResult<Uint8Array>> => {
|
107
|
+
try {
|
108
|
+
const { done, value } = await Promise.race([
|
109
|
+
reader.read(),
|
110
|
+
// Rejects if this.signal is aborted
|
111
|
+
rejectingSignalFuture.promise,
|
112
|
+
// Rejects if something external says it should, like a participant disconnecting, etc
|
113
|
+
this.outOfBandFailureRejectingFuture?.promise ??
|
114
|
+
new Promise<never>(() => {
|
115
|
+
/* never resolves */
|
116
|
+
}),
|
117
|
+
]);
|
118
|
+
if (done) {
|
119
|
+
this.validateBytesReceived(true);
|
120
|
+
return { done: true, value: undefined as any };
|
121
|
+
} else {
|
122
|
+
this.handleChunkReceived(value);
|
123
|
+
return { done: false, value: value.content };
|
124
|
+
}
|
125
|
+
} catch (err) {
|
126
|
+
cleanup();
|
127
|
+
throw err;
|
128
|
+
}
|
129
|
+
},
|
130
|
+
|
131
|
+
// note: `return` runs only for premature exits, see:
|
132
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#errors_during_iteration
|
133
|
+
async return(): Promise<IteratorResult<Uint8Array>> {
|
134
|
+
cleanup();
|
135
|
+
return { done: true, value: undefined };
|
136
|
+
},
|
137
|
+
};
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Injects an AbortSignal, which if aborted, will terminate the currently active
|
142
|
+
* stream iteration operation.
|
143
|
+
*
|
144
|
+
* Note that when using AbortSignal.timeout(...), the timeout applies across
|
145
|
+
* the whole iteration operation, not just one individual chunk read.
|
146
|
+
*/
|
147
|
+
withAbortSignal(signal: AbortSignal) {
|
148
|
+
this.signal = signal;
|
149
|
+
return this;
|
150
|
+
}
|
151
|
+
|
152
|
+
async readAll(opts: BaseStreamReaderReadAllOpts = {}): Promise<Array<Uint8Array>> {
|
153
|
+
let chunks: Set<Uint8Array> = new Set();
|
154
|
+
const iterator = opts.signal ? this.withAbortSignal(opts.signal) : this;
|
155
|
+
for await (const chunk of iterator) {
|
156
|
+
chunks.add(chunk);
|
157
|
+
}
|
158
|
+
return Array.from(chunks);
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
/**
|
163
|
+
* A class to read chunks from a ReadableStream and provide them in a structured format.
|
164
|
+
*/
|
165
|
+
export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
|
166
|
+
private receivedChunks: Map<number, DataStream_Chunk>;
|
167
|
+
|
168
|
+
signal?: AbortSignal;
|
169
|
+
|
170
|
+
/**
|
171
|
+
* A TextStreamReader instance can be used as an AsyncIterator that returns the entire string
|
172
|
+
* that has been received up to the current point in time.
|
173
|
+
*/
|
174
|
+
constructor(
|
175
|
+
info: TextStreamInfo,
|
176
|
+
stream: ReadableStream<DataStream_Chunk>,
|
177
|
+
totalChunkCount?: number,
|
178
|
+
outOfBandFailureRejectingFuture?: Future<never>,
|
179
|
+
) {
|
180
|
+
super(info, stream, totalChunkCount, outOfBandFailureRejectingFuture);
|
181
|
+
this.receivedChunks = new Map();
|
182
|
+
}
|
183
|
+
|
184
|
+
protected handleChunkReceived(chunk: DataStream_Chunk) {
|
185
|
+
const index = bigIntToNumber(chunk.chunkIndex);
|
186
|
+
const previousChunkAtIndex = this.receivedChunks.get(index);
|
187
|
+
if (previousChunkAtIndex && previousChunkAtIndex.version > chunk.version) {
|
188
|
+
// we have a newer version already, dropping the old one
|
189
|
+
return;
|
190
|
+
}
|
191
|
+
this.receivedChunks.set(index, chunk);
|
192
|
+
|
193
|
+
this.bytesReceived += chunk.content.byteLength;
|
194
|
+
this.validateBytesReceived();
|
195
|
+
|
196
|
+
const currentProgress = this.totalByteSize
|
197
|
+
? this.bytesReceived / this.totalByteSize
|
198
|
+
: undefined;
|
199
|
+
this.onProgress?.(currentProgress);
|
200
|
+
}
|
201
|
+
|
202
|
+
/**
|
203
|
+
* @param progress - progress of the stream between 0 and 1. Undefined for streams of unknown size
|
204
|
+
*/
|
205
|
+
onProgress?: (progress: number | undefined) => void;
|
206
|
+
|
207
|
+
/**
|
208
|
+
* Async iterator implementation to allow usage of `for await...of` syntax.
|
209
|
+
* Yields structured chunks from the stream.
|
210
|
+
*
|
211
|
+
*/
|
212
|
+
[Symbol.asyncIterator]() {
|
213
|
+
const reader = this.reader.getReader();
|
214
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
215
|
+
|
216
|
+
let rejectingSignalFuture = new Future<never>();
|
217
|
+
let activeSignal: AbortSignal | null = null;
|
218
|
+
let onAbort: (() => void) | null = null;
|
219
|
+
if (this.signal) {
|
220
|
+
const signal = this.signal;
|
221
|
+
onAbort = () => {
|
222
|
+
rejectingSignalFuture.reject?.(signal.reason);
|
223
|
+
};
|
224
|
+
signal.addEventListener('abort', onAbort);
|
225
|
+
activeSignal = signal;
|
226
|
+
}
|
227
|
+
|
228
|
+
const cleanup = () => {
|
229
|
+
reader.releaseLock();
|
230
|
+
|
231
|
+
if (activeSignal && onAbort) {
|
232
|
+
activeSignal.removeEventListener('abort', onAbort);
|
233
|
+
}
|
234
|
+
|
235
|
+
this.signal = undefined;
|
236
|
+
};
|
237
|
+
|
238
|
+
return {
|
239
|
+
next: async (): Promise<IteratorResult<string>> => {
|
240
|
+
try {
|
241
|
+
const { done, value } = await Promise.race([
|
242
|
+
reader.read(),
|
243
|
+
// Rejects if this.signal is aborted
|
244
|
+
rejectingSignalFuture.promise,
|
245
|
+
// Rejects if something external says it should, like a participant disconnecting, etc
|
246
|
+
this.outOfBandFailureRejectingFuture?.promise ??
|
247
|
+
new Promise<never>(() => {
|
248
|
+
/* never resolves */
|
249
|
+
}),
|
250
|
+
]);
|
251
|
+
if (done) {
|
252
|
+
this.validateBytesReceived(true);
|
253
|
+
return { done: true, value: undefined };
|
254
|
+
} else {
|
255
|
+
this.handleChunkReceived(value);
|
256
|
+
|
257
|
+
let decodedResult;
|
258
|
+
try {
|
259
|
+
decodedResult = decoder.decode(value.content);
|
260
|
+
} catch (err) {
|
261
|
+
throw new DataStreamError(
|
262
|
+
`Cannot decode datastream chunk ${value.chunkIndex} as text: ${err}`,
|
263
|
+
DataStreamErrorReason.DecodeFailed,
|
264
|
+
);
|
265
|
+
}
|
266
|
+
|
267
|
+
return {
|
268
|
+
done: false,
|
269
|
+
value: decodedResult,
|
270
|
+
};
|
271
|
+
}
|
272
|
+
} catch (err) {
|
273
|
+
cleanup();
|
274
|
+
throw err;
|
275
|
+
}
|
276
|
+
},
|
277
|
+
|
278
|
+
// note: `return` runs only for premature exits, see:
|
279
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#errors_during_iteration
|
280
|
+
async return(): Promise<IteratorResult<string>> {
|
281
|
+
cleanup();
|
282
|
+
return { done: true, value: undefined };
|
283
|
+
},
|
284
|
+
};
|
285
|
+
}
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Injects an AbortSignal, which if aborted, will terminate the currently active
|
289
|
+
* stream iteration operation.
|
290
|
+
*
|
291
|
+
* Note that when using AbortSignal.timeout(...), the timeout applies across
|
292
|
+
* the whole iteration operation, not just one individual chunk read.
|
293
|
+
*/
|
294
|
+
withAbortSignal(signal: AbortSignal) {
|
295
|
+
this.signal = signal;
|
296
|
+
return this;
|
297
|
+
}
|
298
|
+
|
299
|
+
async readAll(opts: BaseStreamReaderReadAllOpts = {}): Promise<string> {
|
300
|
+
let finalString: string = '';
|
301
|
+
const iterator = opts.signal ? this.withAbortSignal(opts.signal) : this;
|
302
|
+
for await (const chunk of iterator) {
|
303
|
+
finalString += chunk;
|
304
|
+
}
|
305
|
+
return finalString;
|
306
|
+
}
|
307
|
+
}
|
308
|
+
|
309
|
+
export type ByteStreamHandler = (
|
310
|
+
reader: ByteStreamReader,
|
311
|
+
participantInfo: { identity: string },
|
312
|
+
) => void;
|
313
|
+
|
314
|
+
export type TextStreamHandler = (
|
315
|
+
reader: TextStreamReader,
|
316
|
+
participantInfo: { identity: string },
|
317
|
+
) => void;
|
@@ -0,0 +1,316 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
2
|
+
import {
|
3
|
+
DataPacket,
|
4
|
+
DataPacket_Kind,
|
5
|
+
DataStream_ByteHeader,
|
6
|
+
DataStream_Chunk,
|
7
|
+
DataStream_Header,
|
8
|
+
DataStream_OperationType,
|
9
|
+
DataStream_TextHeader,
|
10
|
+
DataStream_Trailer,
|
11
|
+
} from '@livekit/protocol';
|
12
|
+
import { type StructuredLogger } from '../../../logger';
|
13
|
+
import type RTCEngine from '../../RTCEngine';
|
14
|
+
import { EngineEvent } from '../../events';
|
15
|
+
import type {
|
16
|
+
ByteStreamInfo,
|
17
|
+
SendFileOptions,
|
18
|
+
SendTextOptions,
|
19
|
+
StreamBytesOptions,
|
20
|
+
StreamTextOptions,
|
21
|
+
TextStreamInfo,
|
22
|
+
} from '../../types';
|
23
|
+
import { numberToBigInt, splitUtf8 } from '../../utils';
|
24
|
+
import { ByteStreamWriter, TextStreamWriter } from './StreamWriter';
|
25
|
+
|
26
|
+
const STREAM_CHUNK_SIZE = 15_000;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Manages sending custom user data via data channels.
|
30
|
+
* @internal
|
31
|
+
*/
|
32
|
+
export default class OutgoingDataStreamManager {
|
33
|
+
protected engine: RTCEngine;
|
34
|
+
|
35
|
+
protected log: StructuredLogger;
|
36
|
+
|
37
|
+
constructor(engine: RTCEngine, log: StructuredLogger) {
|
38
|
+
this.engine = engine;
|
39
|
+
this.log = log;
|
40
|
+
}
|
41
|
+
|
42
|
+
setupEngine(engine: RTCEngine) {
|
43
|
+
this.engine = engine;
|
44
|
+
}
|
45
|
+
|
46
|
+
/** {@inheritDoc LocalParticipant.sendText} */
|
47
|
+
async sendText(text: string, options?: SendTextOptions): Promise<TextStreamInfo> {
|
48
|
+
const streamId = crypto.randomUUID();
|
49
|
+
const textInBytes = new TextEncoder().encode(text);
|
50
|
+
const totalTextLength = textInBytes.byteLength;
|
51
|
+
|
52
|
+
const fileIds = options?.attachments?.map(() => crypto.randomUUID());
|
53
|
+
|
54
|
+
const progresses = new Array<number>(fileIds ? fileIds.length + 1 : 1).fill(0);
|
55
|
+
|
56
|
+
const handleProgress = (progress: number, idx: number) => {
|
57
|
+
progresses[idx] = progress;
|
58
|
+
const totalProgress = progresses.reduce((acc, val) => acc + val, 0);
|
59
|
+
options?.onProgress?.(totalProgress);
|
60
|
+
};
|
61
|
+
|
62
|
+
const writer = await this.streamText({
|
63
|
+
streamId,
|
64
|
+
totalSize: totalTextLength,
|
65
|
+
destinationIdentities: options?.destinationIdentities,
|
66
|
+
topic: options?.topic,
|
67
|
+
attachedStreamIds: fileIds,
|
68
|
+
attributes: options?.attributes,
|
69
|
+
});
|
70
|
+
|
71
|
+
await writer.write(text);
|
72
|
+
// set text part of progress to 1
|
73
|
+
handleProgress(1, 0);
|
74
|
+
|
75
|
+
await writer.close();
|
76
|
+
|
77
|
+
if (options?.attachments && fileIds) {
|
78
|
+
await Promise.all(
|
79
|
+
options.attachments.map(async (file, idx) =>
|
80
|
+
this._sendFile(fileIds[idx], file, {
|
81
|
+
topic: options.topic,
|
82
|
+
mimeType: file.type,
|
83
|
+
onProgress: (progress) => {
|
84
|
+
handleProgress(progress, idx + 1);
|
85
|
+
},
|
86
|
+
}),
|
87
|
+
),
|
88
|
+
);
|
89
|
+
}
|
90
|
+
return writer.info;
|
91
|
+
}
|
92
|
+
|
93
|
+
/**
|
94
|
+
* @internal
|
95
|
+
* @experimental CAUTION, might get removed in a minor release
|
96
|
+
*/
|
97
|
+
async streamText(options?: StreamTextOptions): Promise<TextStreamWriter> {
|
98
|
+
const streamId = options?.streamId ?? crypto.randomUUID();
|
99
|
+
|
100
|
+
const info: TextStreamInfo = {
|
101
|
+
id: streamId,
|
102
|
+
mimeType: 'text/plain',
|
103
|
+
timestamp: Date.now(),
|
104
|
+
topic: options?.topic ?? '',
|
105
|
+
size: options?.totalSize,
|
106
|
+
attributes: options?.attributes,
|
107
|
+
};
|
108
|
+
const header = new DataStream_Header({
|
109
|
+
streamId,
|
110
|
+
mimeType: info.mimeType,
|
111
|
+
topic: info.topic,
|
112
|
+
timestamp: numberToBigInt(info.timestamp),
|
113
|
+
totalLength: numberToBigInt(options?.totalSize),
|
114
|
+
attributes: info.attributes,
|
115
|
+
contentHeader: {
|
116
|
+
case: 'textHeader',
|
117
|
+
value: new DataStream_TextHeader({
|
118
|
+
version: options?.version,
|
119
|
+
attachedStreamIds: options?.attachedStreamIds,
|
120
|
+
replyToStreamId: options?.replyToStreamId,
|
121
|
+
operationType:
|
122
|
+
options?.type === 'update'
|
123
|
+
? DataStream_OperationType.UPDATE
|
124
|
+
: DataStream_OperationType.CREATE,
|
125
|
+
}),
|
126
|
+
},
|
127
|
+
});
|
128
|
+
const destinationIdentities = options?.destinationIdentities;
|
129
|
+
const packet = new DataPacket({
|
130
|
+
destinationIdentities,
|
131
|
+
value: {
|
132
|
+
case: 'streamHeader',
|
133
|
+
value: header,
|
134
|
+
},
|
135
|
+
});
|
136
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
137
|
+
|
138
|
+
let chunkId = 0;
|
139
|
+
const engine = this.engine;
|
140
|
+
|
141
|
+
const writableStream = new WritableStream<string>({
|
142
|
+
// Implement the sink
|
143
|
+
async write(text) {
|
144
|
+
for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE)) {
|
145
|
+
await engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
|
146
|
+
const chunk = new DataStream_Chunk({
|
147
|
+
content: textByteChunk,
|
148
|
+
streamId,
|
149
|
+
chunkIndex: numberToBigInt(chunkId),
|
150
|
+
});
|
151
|
+
const chunkPacket = new DataPacket({
|
152
|
+
destinationIdentities,
|
153
|
+
value: {
|
154
|
+
case: 'streamChunk',
|
155
|
+
value: chunk,
|
156
|
+
},
|
157
|
+
});
|
158
|
+
await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
|
159
|
+
|
160
|
+
chunkId += 1;
|
161
|
+
}
|
162
|
+
},
|
163
|
+
async close() {
|
164
|
+
const trailer = new DataStream_Trailer({
|
165
|
+
streamId,
|
166
|
+
});
|
167
|
+
const trailerPacket = new DataPacket({
|
168
|
+
destinationIdentities,
|
169
|
+
value: {
|
170
|
+
case: 'streamTrailer',
|
171
|
+
value: trailer,
|
172
|
+
},
|
173
|
+
});
|
174
|
+
await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
|
175
|
+
},
|
176
|
+
abort(err) {
|
177
|
+
console.log('Sink error:', err);
|
178
|
+
// TODO handle aborts to signal something to receiver side
|
179
|
+
},
|
180
|
+
});
|
181
|
+
|
182
|
+
let onEngineClose = async () => {
|
183
|
+
await writer.close();
|
184
|
+
};
|
185
|
+
|
186
|
+
engine.once(EngineEvent.Closing, onEngineClose);
|
187
|
+
|
188
|
+
const writer = new TextStreamWriter(writableStream, info, () =>
|
189
|
+
this.engine.off(EngineEvent.Closing, onEngineClose),
|
190
|
+
);
|
191
|
+
|
192
|
+
return writer;
|
193
|
+
}
|
194
|
+
|
195
|
+
async sendFile(file: File, options?: SendFileOptions): Promise<{ id: string }> {
|
196
|
+
const streamId = crypto.randomUUID();
|
197
|
+
await this._sendFile(streamId, file, options);
|
198
|
+
return { id: streamId };
|
199
|
+
}
|
200
|
+
|
201
|
+
private async _sendFile(streamId: string, file: File, options?: SendFileOptions) {
|
202
|
+
const writer = await this.streamBytes({
|
203
|
+
streamId,
|
204
|
+
totalSize: file.size,
|
205
|
+
name: file.name,
|
206
|
+
mimeType: options?.mimeType ?? file.type,
|
207
|
+
topic: options?.topic,
|
208
|
+
destinationIdentities: options?.destinationIdentities,
|
209
|
+
});
|
210
|
+
const reader = file.stream().getReader();
|
211
|
+
while (true) {
|
212
|
+
const { done, value } = await reader.read();
|
213
|
+
if (done) {
|
214
|
+
break;
|
215
|
+
}
|
216
|
+
await writer.write(value);
|
217
|
+
}
|
218
|
+
await writer.close();
|
219
|
+
return writer.info;
|
220
|
+
}
|
221
|
+
|
222
|
+
async streamBytes(options?: StreamBytesOptions) {
|
223
|
+
const streamId = options?.streamId ?? crypto.randomUUID();
|
224
|
+
const destinationIdentities = options?.destinationIdentities;
|
225
|
+
|
226
|
+
const info: ByteStreamInfo = {
|
227
|
+
id: streamId,
|
228
|
+
mimeType: options?.mimeType ?? 'application/octet-stream',
|
229
|
+
topic: options?.topic ?? '',
|
230
|
+
timestamp: Date.now(),
|
231
|
+
attributes: options?.attributes,
|
232
|
+
size: options?.totalSize,
|
233
|
+
name: options?.name ?? 'unknown',
|
234
|
+
};
|
235
|
+
|
236
|
+
const header = new DataStream_Header({
|
237
|
+
totalLength: numberToBigInt(info.size ?? 0),
|
238
|
+
mimeType: info.mimeType,
|
239
|
+
streamId,
|
240
|
+
topic: info.topic,
|
241
|
+
timestamp: numberToBigInt(Date.now()),
|
242
|
+
attributes: info.attributes,
|
243
|
+
contentHeader: {
|
244
|
+
case: 'byteHeader',
|
245
|
+
value: new DataStream_ByteHeader({
|
246
|
+
name: info.name,
|
247
|
+
}),
|
248
|
+
},
|
249
|
+
});
|
250
|
+
|
251
|
+
const packet = new DataPacket({
|
252
|
+
destinationIdentities,
|
253
|
+
value: {
|
254
|
+
case: 'streamHeader',
|
255
|
+
value: header,
|
256
|
+
},
|
257
|
+
});
|
258
|
+
|
259
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
260
|
+
|
261
|
+
let chunkId = 0;
|
262
|
+
const writeMutex = new Mutex();
|
263
|
+
const engine = this.engine;
|
264
|
+
const logLocal = this.log;
|
265
|
+
|
266
|
+
const writableStream = new WritableStream<Uint8Array>({
|
267
|
+
async write(chunk) {
|
268
|
+
const unlock = await writeMutex.lock();
|
269
|
+
|
270
|
+
let byteOffset = 0;
|
271
|
+
try {
|
272
|
+
while (byteOffset < chunk.byteLength) {
|
273
|
+
const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
|
274
|
+
await engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
|
275
|
+
const chunkPacket = new DataPacket({
|
276
|
+
destinationIdentities,
|
277
|
+
value: {
|
278
|
+
case: 'streamChunk',
|
279
|
+
value: new DataStream_Chunk({
|
280
|
+
content: subChunk,
|
281
|
+
streamId,
|
282
|
+
chunkIndex: numberToBigInt(chunkId),
|
283
|
+
}),
|
284
|
+
},
|
285
|
+
});
|
286
|
+
await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
|
287
|
+
chunkId += 1;
|
288
|
+
byteOffset += subChunk.byteLength;
|
289
|
+
}
|
290
|
+
} finally {
|
291
|
+
unlock();
|
292
|
+
}
|
293
|
+
},
|
294
|
+
async close() {
|
295
|
+
const trailer = new DataStream_Trailer({
|
296
|
+
streamId,
|
297
|
+
});
|
298
|
+
const trailerPacket = new DataPacket({
|
299
|
+
destinationIdentities,
|
300
|
+
value: {
|
301
|
+
case: 'streamTrailer',
|
302
|
+
value: trailer,
|
303
|
+
},
|
304
|
+
});
|
305
|
+
await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
|
306
|
+
},
|
307
|
+
abort(err) {
|
308
|
+
logLocal.error('Sink error:', err);
|
309
|
+
},
|
310
|
+
});
|
311
|
+
|
312
|
+
const byteWriter = new ByteStreamWriter(writableStream, info);
|
313
|
+
|
314
|
+
return byteWriter;
|
315
|
+
}
|
316
|
+
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from '
|
1
|
+
import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from '../../types';
|
2
2
|
|
3
3
|
class BaseStreamWriter<T, InfoType extends BaseStreamInfo> {
|
4
4
|
protected writableStream: WritableStream<T>;
|
package/src/room/errors.ts
CHANGED
@@ -111,6 +111,40 @@ export class SignalRequestError extends LivekitError {
|
|
111
111
|
}
|
112
112
|
}
|
113
113
|
|
114
|
+
// NOTE: matches with https://github.com/livekit/client-sdk-swift/blob/f37bbd260d61e165084962db822c79f995f1a113/Sources/LiveKit/DataStream/StreamError.swift#L17
|
115
|
+
export enum DataStreamErrorReason {
|
116
|
+
// Unable to open a stream with the same ID more than once.
|
117
|
+
AlreadyOpened = 0,
|
118
|
+
|
119
|
+
// Stream closed abnormally by remote participant.
|
120
|
+
AbnormalEnd = 1,
|
121
|
+
|
122
|
+
// Incoming chunk data could not be decoded.
|
123
|
+
DecodeFailed = 2,
|
124
|
+
|
125
|
+
// Read length exceeded total length specified in stream header.
|
126
|
+
LengthExceeded = 3,
|
127
|
+
|
128
|
+
// Read length less than total length specified in stream header.
|
129
|
+
Incomplete = 4,
|
130
|
+
|
131
|
+
// Unable to register a stream handler more than once.
|
132
|
+
HandlerAlreadyRegistered = 7,
|
133
|
+
}
|
134
|
+
|
135
|
+
export class DataStreamError extends LivekitError {
|
136
|
+
reason: DataStreamErrorReason;
|
137
|
+
|
138
|
+
reasonName: string;
|
139
|
+
|
140
|
+
constructor(message: string, reason: DataStreamErrorReason) {
|
141
|
+
super(16, message);
|
142
|
+
this.name = 'DataStreamError';
|
143
|
+
this.reason = reason;
|
144
|
+
this.reasonName = DataStreamErrorReason[reason];
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
114
148
|
export enum MediaDeviceFailure {
|
115
149
|
// user rejected permissions
|
116
150
|
PermissionDenied = 'PermissionDenied',
|