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.
- package/dist/livekit-client.esm.mjs +298 -73
- 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/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +5 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +5 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +9 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +10 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +8 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/record.d.ts +6 -0
- package/dist/src/room/track/record.d.ts.map +1 -0
- package/dist/ts4.2/src/index.d.ts +1 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -1
- package/dist/ts4.2/src/room/Room.d.ts +4 -0
- package/dist/ts4.2/src/room/events.d.ts +5 -1
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +9 -0
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +10 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/src/room/track/options.d.ts +8 -0
- package/dist/ts4.2/src/room/track/record.d.ts +6 -0
- package/package.json +3 -2
- package/src/e2ee/worker/tsconfig.json +9 -1
- package/src/index.ts +2 -0
- package/src/room/RTCEngine.ts +2 -5
- package/src/room/Room.ts +14 -2
- package/src/room/defaults.ts +1 -0
- package/src/room/events.ts +5 -0
- package/src/room/participant/LocalParticipant.ts +179 -16
- package/src/room/track/LocalTrack.ts +47 -2
- package/src/room/track/Track.ts +1 -0
- package/src/room/track/options.ts +9 -0
- 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
|
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
|
-
|
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
|
package/src/room/defaults.ts
CHANGED
package/src/room/events.ts
CHANGED
@@ -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
|
-
|
853
|
-
|
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.
|
870
|
-
|
871
|
-
|
872
|
-
|
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
|
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
|
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 =
|
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
|
}
|
package/src/room/track/Track.ts
CHANGED
@@ -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
|
+
}
|