livekit-client 2.13.1 → 2.13.2

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 (44) hide show
  1. package/dist/livekit-client.esm.mjs +298 -73
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/index.d.ts +1 -0
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/room/RTCEngine.d.ts +1 -1
  8. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  9. package/dist/src/room/Room.d.ts +5 -1
  10. package/dist/src/room/Room.d.ts.map +1 -1
  11. package/dist/src/room/defaults.d.ts.map +1 -1
  12. package/dist/src/room/events.d.ts +5 -1
  13. package/dist/src/room/events.d.ts.map +1 -1
  14. package/dist/src/room/participant/LocalParticipant.d.ts +9 -0
  15. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  16. package/dist/src/room/track/LocalTrack.d.ts +10 -0
  17. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  18. package/dist/src/room/track/Track.d.ts +1 -0
  19. package/dist/src/room/track/Track.d.ts.map +1 -1
  20. package/dist/src/room/track/options.d.ts +8 -0
  21. package/dist/src/room/track/options.d.ts.map +1 -1
  22. package/dist/src/room/track/record.d.ts +6 -0
  23. package/dist/src/room/track/record.d.ts.map +1 -0
  24. package/dist/ts4.2/src/index.d.ts +1 -0
  25. package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -1
  26. package/dist/ts4.2/src/room/Room.d.ts +4 -0
  27. package/dist/ts4.2/src/room/events.d.ts +5 -1
  28. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +9 -0
  29. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +10 -0
  30. package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
  31. package/dist/ts4.2/src/room/track/options.d.ts +8 -0
  32. package/dist/ts4.2/src/room/track/record.d.ts +6 -0
  33. package/package.json +3 -2
  34. package/src/e2ee/worker/tsconfig.json +9 -1
  35. package/src/index.ts +2 -0
  36. package/src/room/RTCEngine.ts +2 -5
  37. package/src/room/Room.ts +14 -2
  38. package/src/room/defaults.ts +1 -0
  39. package/src/room/events.ts +5 -0
  40. package/src/room/participant/LocalParticipant.ts +179 -16
  41. package/src/room/track/LocalTrack.ts +47 -2
  42. package/src/room/track/Track.ts +1 -0
  43. package/src/room/track/options.ts +9 -0
  44. package/src/room/track/record.ts +51 -0
package/src/room/Room.ts CHANGED
@@ -65,7 +65,7 @@ import { ConnectionError, ConnectionErrorReason, UnsupportedServer } from './err
65
65
  import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
66
66
  import LocalParticipant from './participant/LocalParticipant';
67
67
  import type Participant from './participant/Participant';
68
- import type { ConnectionQuality } from './participant/Participant';
68
+ import { type ConnectionQuality, ParticipantKind } from './participant/Participant';
69
69
  import RemoteParticipant from './participant/RemoteParticipant';
70
70
  import { MAX_PAYLOAD_BYTES, RpcError, type RpcInvocationData, byteLength } from './rpc';
71
71
  import CriticalTimers from './timers';
@@ -1992,7 +1992,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1992
1992
  }
1993
1993
  };
1994
1994
 
1995
- private handleDeviceChange = async () => {
1995
+ /**
1996
+ * attempt to select the default devices if the previously selected devices are no longer available after a device change event
1997
+ */
1998
+ private async selectDefaultDevices() {
1996
1999
  const previousDevices = DeviceManager.getInstance().previousDevices;
1997
2000
  // check for available devices, but don't request permissions in order to avoid prompts for kinds that haven't been used before
1998
2001
  const availableDevices = await DeviceManager.getInstance().getDevices(undefined, false);
@@ -2055,7 +2058,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2055
2058
  await this.switchActiveDevice(kind, devicesOfKind[0].deviceId);
2056
2059
  }
2057
2060
  }
2061
+ }
2058
2062
 
2063
+ private handleDeviceChange = async () => {
2064
+ if (getBrowser()?.os !== 'iOS') {
2065
+ // default devices are non deterministic on iOS, so we don't attempt to select them here
2066
+ await this.selectDefaultDevices();
2067
+ }
2059
2068
  this.emit(RoomEvent.MediaDevicesChanged);
2060
2069
  };
2061
2070
 
@@ -2247,6 +2256,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2247
2256
  })
2248
2257
  .on(ParticipantEvent.Active, () => {
2249
2258
  this.emitWhenConnected(RoomEvent.ParticipantActive, participant);
2259
+ if (participant.kind === ParticipantKind.AGENT) {
2260
+ this.localParticipant.setActiveAgent(participant);
2261
+ }
2250
2262
  });
2251
2263
 
2252
2264
  // update info at the end after callbacks have been set up
@@ -19,6 +19,7 @@ export const publishDefaults: TrackPublishDefaults = {
19
19
  stopMicTrackOnMute: false,
20
20
  videoCodec: defaultVideoCodec,
21
21
  backupCodec: true,
22
+ preConnectBuffer: false,
22
23
  } as const;
23
24
 
24
25
  export const audioDefaults: AudioCaptureOptions = {
@@ -666,4 +666,9 @@ export enum TrackEvent {
666
666
  * @experimental
667
667
  */
668
668
  TimeSyncUpdate = 'timeSyncUpdate',
669
+
670
+ /**
671
+ * @internal
672
+ */
673
+ PreConnectBufferFlushed = 'preConnectBufferFlushed',
669
674
  }
@@ -1,6 +1,7 @@
1
1
  import { Mutex } from '@livekit/mutex';
2
2
  import {
3
3
  AddTrackRequest,
4
+ AudioTrackFeature,
4
5
  BackupCodecPolicy,
5
6
  ChatMessage as ChatMessageModel,
6
7
  Codec,
@@ -13,6 +14,7 @@ import {
13
14
  DataStream_TextHeader,
14
15
  DataStream_Trailer,
15
16
  Encryption_Type,
17
+ JoinResponse,
16
18
  ParticipantInfo,
17
19
  ParticipantPermission,
18
20
  RequestResponse,
@@ -103,6 +105,7 @@ import {
103
105
  import Participant from './Participant';
104
106
  import type { ParticipantTrackPermission } from './ParticipantTrackPermission';
105
107
  import { trackPermissionToProto } from './ParticipantTrackPermission';
108
+ import type RemoteParticipant from './RemoteParticipant';
106
109
  import {
107
110
  computeTrackBackupEncodings,
108
111
  computeVideoEncodings,
@@ -146,6 +149,12 @@ export default class LocalParticipant extends Participant {
146
149
 
147
150
  private reconnectFuture?: Future<void>;
148
151
 
152
+ private signalConnectedFuture?: Future<void>;
153
+
154
+ private activeAgentFuture?: Future<RemoteParticipant>;
155
+
156
+ private firstActiveAgent?: RemoteParticipant;
157
+
149
158
  private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>>;
150
159
 
151
160
  private pendingSignalRequests: Map<
@@ -241,6 +250,7 @@ export default class LocalParticipant extends Participant {
241
250
 
242
251
  this.engine
243
252
  .on(EngineEvent.Connected, this.handleReconnected)
253
+ .on(EngineEvent.SignalConnected, this.handleSignalConnected)
244
254
  .on(EngineEvent.SignalRestarted, this.handleReconnected)
245
255
  .on(EngineEvent.SignalResumed, this.handleReconnected)
246
256
  .on(EngineEvent.Restarting, this.handleReconnecting)
@@ -270,6 +280,25 @@ export default class LocalParticipant extends Participant {
270
280
  this.reconnectFuture?.reject?.('Got disconnected during reconnection attempt');
271
281
  this.reconnectFuture = undefined;
272
282
  }
283
+ if (this.signalConnectedFuture) {
284
+ this.signalConnectedFuture.reject?.('Got disconnected without signal connected');
285
+ this.signalConnectedFuture = undefined;
286
+ }
287
+
288
+ this.activeAgentFuture?.reject?.('Got disconnected without active agent present');
289
+ this.activeAgentFuture = undefined;
290
+ this.firstActiveAgent = undefined;
291
+ };
292
+
293
+ private handleSignalConnected = (joinResponse: JoinResponse) => {
294
+ if (joinResponse.participant) {
295
+ this.updateInfo(joinResponse.participant);
296
+ }
297
+ if (!this.signalConnectedFuture) {
298
+ this.signalConnectedFuture = new Future<void>();
299
+ }
300
+
301
+ this.signalConnectedFuture.resolve?.();
273
302
  };
274
303
 
275
304
  private handleSignalRequestResponse = (response: RequestResponse) => {
@@ -523,6 +552,20 @@ export default class LocalParticipant extends Participant {
523
552
  this.pendingPublishing.delete(source);
524
553
  throw e;
525
554
  }
555
+
556
+ for (const localTrack of localTracks) {
557
+ if (
558
+ source === Track.Source.Microphone &&
559
+ isAudioTrack(localTrack) &&
560
+ publishOptions?.preConnectBuffer
561
+ ) {
562
+ this.log.info('starting preconnect buffer for microphone', {
563
+ ...this.logContext,
564
+ });
565
+ localTrack.startPreConnectBuffer();
566
+ }
567
+ }
568
+
526
569
  try {
527
570
  const publishPromises: Array<Promise<LocalTrackPublication>> = [];
528
571
  for (const localTrack of localTracks) {
@@ -849,16 +892,8 @@ export default class LocalParticipant extends Participant {
849
892
  ...this.logContext,
850
893
  track: getLogContextFromTrack(track),
851
894
  });
852
- const onSignalConnected = async () => {
853
- try {
854
- const publication = await this.publish(track, opts, isStereo);
855
- resolve(publication);
856
- } catch (e) {
857
- reject(e);
858
- }
859
- };
860
- setTimeout(() => {
861
- this.engine.off(EngineEvent.SignalConnected, onSignalConnected);
895
+
896
+ const timeout = setTimeout(() => {
862
897
  reject(
863
898
  new PublishTrackError(
864
899
  'publishing rejected as engine not connected within timeout',
@@ -866,11 +901,10 @@ export default class LocalParticipant extends Participant {
866
901
  ),
867
902
  );
868
903
  }, 15_000);
869
- this.engine.once(EngineEvent.SignalConnected, onSignalConnected);
870
- this.engine.on(EngineEvent.Closing, () => {
871
- this.engine.off(EngineEvent.SignalConnected, onSignalConnected);
872
- reject(new PublishTrackError('publishing rejected as engine closed', 499));
873
- });
904
+ await this.waitUntilEngineConnected();
905
+ clearTimeout(timeout);
906
+ const publication = await this.publish(track, opts, isStereo);
907
+ resolve(publication);
874
908
  } else {
875
909
  try {
876
910
  const publication = await this.publish(track, opts, isStereo);
@@ -894,6 +928,13 @@ export default class LocalParticipant extends Participant {
894
928
  }
895
929
  }
896
930
 
931
+ private waitUntilEngineConnected() {
932
+ if (!this.signalConnectedFuture) {
933
+ this.signalConnectedFuture = new Future<void>();
934
+ }
935
+ return this.signalConnectedFuture.promise;
936
+ }
937
+
897
938
  private hasPermissionsToPublish(track: LocalTrack): boolean {
898
939
  if (!this.permissions) {
899
940
  this.log.warn('no permissions present for publishing track', {
@@ -971,6 +1012,30 @@ export default class LocalParticipant extends Participant {
971
1012
  track.on(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
972
1013
  track.on(TrackEvent.AudioTrackFeatureUpdate, this.onTrackFeatureUpdate);
973
1014
 
1015
+ const audioFeatures: AudioTrackFeature[] = [];
1016
+ const disableDtx = !(opts.dtx ?? true);
1017
+
1018
+ const settings = track.getSourceTrackSettings();
1019
+
1020
+ if (settings.autoGainControl) {
1021
+ audioFeatures.push(AudioTrackFeature.TF_AUTO_GAIN_CONTROL);
1022
+ }
1023
+ if (settings.echoCancellation) {
1024
+ audioFeatures.push(AudioTrackFeature.TF_ECHO_CANCELLATION);
1025
+ }
1026
+ if (settings.noiseSuppression) {
1027
+ audioFeatures.push(AudioTrackFeature.TF_NOISE_SUPPRESSION);
1028
+ }
1029
+ if (settings.channelCount && settings.channelCount > 1) {
1030
+ audioFeatures.push(AudioTrackFeature.TF_STEREO);
1031
+ }
1032
+ if (disableDtx) {
1033
+ audioFeatures.push(AudioTrackFeature.TF_NO_DTX);
1034
+ }
1035
+ if (isLocalAudioTrack(track) && track.hasPreConnectBuffer) {
1036
+ audioFeatures.push(AudioTrackFeature.TF_PRECONNECT_BUFFER);
1037
+ }
1038
+
974
1039
  // create track publication from track
975
1040
  const req = new AddTrackRequest({
976
1041
  // get local track id for use during publishing
@@ -979,12 +1044,13 @@ export default class LocalParticipant extends Participant {
979
1044
  type: Track.kindToProto(track.kind),
980
1045
  muted: track.isMuted,
981
1046
  source: Track.sourceToProto(track.source),
982
- disableDtx: !(opts.dtx ?? true),
1047
+ disableDtx,
983
1048
  encryption: this.encryptionType,
984
1049
  stereo: isStereo,
985
1050
  disableRed: this.isE2EEEnabled || !(opts.red ?? true),
986
1051
  stream: opts?.stream,
987
1052
  backupCodecPolicy: opts?.backupCodecPolicy as BackupCodecPolicy,
1053
+ audioFeatures,
988
1054
  });
989
1055
 
990
1056
  // compute encodings and layers for video
@@ -1222,6 +1288,79 @@ export default class LocalParticipant extends Participant {
1222
1288
  this.addTrackPublication(publication);
1223
1289
  // send event for publication
1224
1290
  this.emit(ParticipantEvent.LocalTrackPublished, publication);
1291
+
1292
+ if (
1293
+ isLocalAudioTrack(track) &&
1294
+ ti.audioFeatures.includes(AudioTrackFeature.TF_PRECONNECT_BUFFER)
1295
+ ) {
1296
+ const stream = track.getPreConnectBuffer();
1297
+ // TODO: we're registering the listener after negotiation, so there might be a race
1298
+ this.on(ParticipantEvent.LocalTrackSubscribed, (pub) => {
1299
+ if (pub.trackSid === ti.sid) {
1300
+ if (!track.hasPreConnectBuffer) {
1301
+ this.log.warn('subscribe event came to late, buffer already closed', this.logContext);
1302
+ return;
1303
+ }
1304
+ this.log.debug('finished recording preconnect buffer', {
1305
+ ...this.logContext,
1306
+ ...getLogContextFromTrack(track),
1307
+ });
1308
+ track.stopPreConnectBuffer();
1309
+ }
1310
+ });
1311
+
1312
+ if (stream) {
1313
+ const bufferStreamPromise = new Promise<void>(async (resolve, reject) => {
1314
+ try {
1315
+ this.log.debug('waiting for agent', {
1316
+ ...this.logContext,
1317
+ ...getLogContextFromTrack(track),
1318
+ });
1319
+ const agentActiveTimeout = setTimeout(() => {
1320
+ reject(new Error('agent not active within 10 seconds'));
1321
+ }, 10_000);
1322
+ const agent = await this.waitUntilActiveAgentPresent();
1323
+ clearTimeout(agentActiveTimeout);
1324
+ this.log.debug('sending preconnect buffer', {
1325
+ ...this.logContext,
1326
+ ...getLogContextFromTrack(track),
1327
+ });
1328
+ const writer = await this.streamBytes({
1329
+ name: 'preconnect-buffer',
1330
+ mimeType: 'audio/opus',
1331
+ topic: 'lk.agent.pre-connect-audio-buffer',
1332
+ destinationIdentities: [agent.identity],
1333
+ attributes: {
1334
+ trackId: publication.trackSid,
1335
+ sampleRate: String(settings.sampleRate ?? '48000'),
1336
+ channels: String(settings.channelCount ?? '1'),
1337
+ },
1338
+ });
1339
+ for await (const chunk of stream) {
1340
+ await writer.write(chunk);
1341
+ }
1342
+ await writer.close();
1343
+ resolve();
1344
+ } catch (e) {
1345
+ reject(e);
1346
+ }
1347
+ });
1348
+ bufferStreamPromise
1349
+ .then(() => {
1350
+ this.log.debug('preconnect buffer sent successfully', {
1351
+ ...this.logContext,
1352
+ ...getLogContextFromTrack(track),
1353
+ });
1354
+ })
1355
+ .catch((e) => {
1356
+ this.log.error('error sending preconnect buffer', {
1357
+ ...this.logContext,
1358
+ ...getLogContextFromTrack(track),
1359
+ error: e,
1360
+ });
1361
+ });
1362
+ }
1363
+ }
1225
1364
  return publication;
1226
1365
  }
1227
1366
 
@@ -2113,6 +2252,30 @@ export default class LocalParticipant extends Participant {
2113
2252
  );
2114
2253
  };
2115
2254
 
2255
+ /** @internal */
2256
+ setActiveAgent(agent: RemoteParticipant | undefined) {
2257
+ this.firstActiveAgent = agent;
2258
+ if (agent && !this.firstActiveAgent) {
2259
+ this.firstActiveAgent = agent;
2260
+ }
2261
+ if (agent) {
2262
+ this.activeAgentFuture?.resolve?.(agent);
2263
+ } else {
2264
+ this.activeAgentFuture?.reject?.('Agent disconnected');
2265
+ }
2266
+ this.activeAgentFuture = undefined;
2267
+ }
2268
+
2269
+ private waitUntilActiveAgentPresent() {
2270
+ if (this.firstActiveAgent) {
2271
+ return Promise.resolve(this.firstActiveAgent);
2272
+ }
2273
+ if (!this.activeAgentFuture) {
2274
+ this.activeAgentFuture = new Future<RemoteParticipant>();
2275
+ }
2276
+ return this.activeAgentFuture.promise;
2277
+ }
2278
+
2116
2279
  /** @internal */
2117
2280
  private onTrackUnmuted = (track: LocalTrack) => {
2118
2281
  this.onTrackMuted(track, track.isUpstreamPaused);
@@ -9,15 +9,19 @@ import { compareVersions, isMobile, sleep, unwrapConstraint } from '../utils';
9
9
  import { Track, attachToElement, detachTrack } from './Track';
10
10
  import type { VideoCodec } from './options';
11
11
  import type { TrackProcessor } from './processor/types';
12
+ import { LocalTrackRecorder } from './record';
12
13
  import type { ReplaceTrackOptions } from './types';
13
14
 
14
- const defaultDimensionsTimeout = 1000;
15
+ const DEFAULT_DIMENSIONS_TIMEOUT = 1000;
16
+ const PRE_CONNECT_BUFFER_TIMEOUT = 10_000;
15
17
 
16
18
  export default abstract class LocalTrack<
17
19
  TrackKind extends Track.Kind = Track.Kind,
18
20
  > extends Track<TrackKind> {
19
21
  protected _sender?: RTCRtpSender;
20
22
 
23
+ private autoStopPreConnectBuffer: ReturnType<typeof setTimeout> | undefined;
24
+
21
25
  /** @internal */
22
26
  get sender(): RTCRtpSender | undefined {
23
27
  return this._sender;
@@ -35,6 +39,10 @@ export default abstract class LocalTrack<
35
39
  return this._constraints;
36
40
  }
37
41
 
42
+ get hasPreConnectBuffer() {
43
+ return !!this.localTrackRecorder;
44
+ }
45
+
38
46
  protected _constraints: MediaTrackConstraints;
39
47
 
40
48
  protected reacquireTrack: boolean;
@@ -55,6 +63,8 @@ export default abstract class LocalTrack<
55
63
 
56
64
  protected manuallyStopped: boolean = false;
57
65
 
66
+ protected localTrackRecorder: LocalTrackRecorder<typeof this> | undefined;
67
+
58
68
  private restartLock: Mutex;
59
69
 
60
70
  /**
@@ -203,7 +213,7 @@ export default abstract class LocalTrack<
203
213
  }
204
214
  }
205
215
 
206
- async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
216
+ async waitForDimensions(timeout = DEFAULT_DIMENSIONS_TIMEOUT): Promise<Track.Dimensions> {
207
217
  if (this.kind === Track.Kind.Audio) {
208
218
  throw new Error('cannot get dimensions for audio tracks');
209
219
  }
@@ -585,5 +595,40 @@ export default abstract class LocalTrack<
585
595
  this.emit(TrackEvent.TrackProcessorUpdate);
586
596
  }
587
597
 
598
+ /** @internal */
599
+ startPreConnectBuffer(timeslice: number = 100) {
600
+ if (!this.localTrackRecorder) {
601
+ this.localTrackRecorder = new LocalTrackRecorder(this, {
602
+ mimeType: 'audio/webm;codecs=opus',
603
+ });
604
+ } else {
605
+ this.log.warn('preconnect buffer already started');
606
+ return;
607
+ }
608
+
609
+ this.localTrackRecorder.start(timeslice);
610
+ this.autoStopPreConnectBuffer = setTimeout(() => {
611
+ this.log.warn(
612
+ 'preconnect buffer timed out, stopping recording automatically',
613
+ this.logContext,
614
+ );
615
+ this.stopPreConnectBuffer();
616
+ }, PRE_CONNECT_BUFFER_TIMEOUT);
617
+ }
618
+
619
+ /** @internal */
620
+ stopPreConnectBuffer() {
621
+ clearTimeout(this.autoStopPreConnectBuffer);
622
+ if (this.localTrackRecorder) {
623
+ this.localTrackRecorder.stop();
624
+ this.localTrackRecorder = undefined;
625
+ }
626
+ }
627
+
628
+ /** @internal */
629
+ getPreConnectBuffer() {
630
+ return this.localTrackRecorder?.byteStream;
631
+ }
632
+
588
633
  protected abstract monitorSender(): void;
589
634
  }
@@ -528,4 +528,5 @@ export type TrackEventCallbacks = {
528
528
  trackProcessorUpdate: (processor?: TrackProcessor<Track.Kind, any>) => void;
529
529
  audioTrackFeatureUpdate: (track: any, feature: AudioTrackFeature, enabled: boolean) => void;
530
530
  timeSyncUpdate: (update: { timestamp: number; rtpTimestamp: number }) => void;
531
+ preConnectBufferFlushed: (buffer: Uint8Array[]) => void;
531
532
  };
@@ -119,6 +119,15 @@ export interface TrackPublishDefaults {
119
119
  * defaults to false
120
120
  */
121
121
  stopMicTrackOnMute?: boolean;
122
+
123
+ /**
124
+ * Enables preconnect buffer for a user's microphone track.
125
+ * This is useful for reducing perceived latency when the user starts to speak before the connection is established.
126
+ * Only works for agent use cases.
127
+ *
128
+ * Defaults to false.
129
+ */
130
+ preConnectBuffer?: boolean;
122
131
  }
123
132
 
124
133
  /**
@@ -0,0 +1,51 @@
1
+ import type LocalTrack from './LocalTrack';
2
+
3
+ export class LocalTrackRecorder<T extends LocalTrack> extends MediaRecorder {
4
+ byteStream: ReadableStream<Uint8Array>;
5
+
6
+ constructor(track: T, options?: MediaRecorderOptions) {
7
+ super(new MediaStream([track.mediaStreamTrack]), options);
8
+
9
+ let dataListener: (event: BlobEvent) => void;
10
+
11
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
12
+
13
+ const isClosed = () => streamController === undefined;
14
+
15
+ const onStop = () => {
16
+ this.removeEventListener('dataavailable', dataListener);
17
+ this.removeEventListener('stop', onStop);
18
+ this.removeEventListener('error', onError);
19
+ streamController?.close();
20
+ streamController = undefined;
21
+ };
22
+
23
+ const onError = (event: Event) => {
24
+ streamController?.error(event);
25
+ this.removeEventListener('dataavailable', dataListener);
26
+ this.removeEventListener('stop', onStop);
27
+ this.removeEventListener('error', onError);
28
+ streamController = undefined;
29
+ };
30
+
31
+ this.byteStream = new ReadableStream({
32
+ start: (controller) => {
33
+ streamController = controller;
34
+ dataListener = async (event: BlobEvent) => {
35
+ const arrayBuffer = await event.data.arrayBuffer();
36
+ if (isClosed()) {
37
+ return;
38
+ }
39
+ controller.enqueue(new Uint8Array(arrayBuffer));
40
+ };
41
+ this.addEventListener('dataavailable', dataListener);
42
+ },
43
+ cancel: () => {
44
+ onStop();
45
+ },
46
+ });
47
+
48
+ this.addEventListener('stop', onStop);
49
+ this.addEventListener('error', onError);
50
+ }
51
+ }