livekit-client 1.7.0 → 1.8.0
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/README.md +20 -1
- package/dist/livekit-client.esm.mjs +2240 -1067
- 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 +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +32 -0
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +315 -75
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +9 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/ReconnectPolicy.d.ts +1 -0
- package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts +14 -0
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
- package/dist/src/room/Room.d.ts +6 -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/errors.d.ts +2 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +15 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -2
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +3 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +4 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +4 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +3 -1
- package/dist/ts4.2/src/options.d.ts +5 -0
- package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
- package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
- package/dist/ts4.2/src/room/RTCEngine.d.ts +9 -1
- package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
- package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
- package/dist/ts4.2/src/room/Room.d.ts +6 -1
- package/dist/ts4.2/src/room/errors.d.ts +2 -1
- package/dist/ts4.2/src/room/events.d.ts +15 -2
- package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
- package/dist/ts4.2/src/room/types.d.ts +4 -0
- package/dist/ts4.2/src/room/utils.d.ts +4 -0
- package/package.json +19 -19
- package/src/api/SignalClient.ts +4 -4
- package/src/index.ts +3 -0
- package/src/options.ts +6 -0
- package/src/proto/google/protobuf/timestamp.ts +15 -6
- package/src/proto/livekit_models.ts +903 -222
- package/src/proto/livekit_rtc.ts +1053 -279
- package/src/room/RTCEngine.ts +168 -56
- package/src/room/ReconnectPolicy.ts +2 -0
- package/src/room/RegionUrlProvider.ts +73 -0
- package/src/room/Room.ts +212 -133
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.ts +1 -0
- package/src/room/events.ts +15 -0
- package/src/room/track/LocalAudioTrack.ts +14 -6
- package/src/room/track/LocalTrack.ts +22 -8
- package/src/room/track/LocalVideoTrack.ts +12 -6
- package/src/room/track/RemoteTrackPublication.ts +10 -4
- package/src/room/track/RemoteVideoTrack.test.ts +2 -0
- package/src/room/track/RemoteVideoTrack.ts +53 -9
- package/src/room/track/Track.ts +46 -31
- package/src/room/track/utils.ts +3 -2
- package/src/room/types.ts +6 -0
- package/src/room/utils.ts +53 -0
package/src/room/Room.ts
CHANGED
@@ -37,14 +37,13 @@ import {
|
|
37
37
|
videoDefaults,
|
38
38
|
} from './defaults';
|
39
39
|
import DeviceManager from './DeviceManager';
|
40
|
-
import { ConnectionError, UnsupportedServer } from './errors';
|
40
|
+
import { ConnectionError, ConnectionErrorReason, UnsupportedServer } from './errors';
|
41
41
|
import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
|
42
42
|
import LocalParticipant from './participant/LocalParticipant';
|
43
43
|
import type Participant from './participant/Participant';
|
44
44
|
import type { ConnectionQuality } from './participant/Participant';
|
45
45
|
import RemoteParticipant from './participant/RemoteParticipant';
|
46
46
|
import RTCEngine from './RTCEngine';
|
47
|
-
import CriticalTimers from './timers';
|
48
47
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
49
48
|
import LocalTrackPublication from './track/LocalTrackPublication';
|
50
49
|
import LocalVideoTrack from './track/LocalVideoTrack';
|
@@ -59,11 +58,13 @@ import {
|
|
59
58
|
createDummyVideoStreamTrack,
|
60
59
|
Future,
|
61
60
|
getEmptyAudioStreamTrack,
|
61
|
+
isCloud,
|
62
62
|
isWeb,
|
63
63
|
Mutex,
|
64
64
|
supportsSetSinkId,
|
65
65
|
unpackStreamId,
|
66
66
|
} from './utils';
|
67
|
+
import { RegionUrlProvider } from './RegionUrlProvider';
|
67
68
|
|
68
69
|
export enum ConnectionState {
|
69
70
|
Disconnected = 'disconnected',
|
@@ -206,7 +207,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
206
207
|
}
|
207
208
|
})
|
208
209
|
.on(EngineEvent.Restarting, this.handleRestarting)
|
209
|
-
.on(EngineEvent.Restarted, this.handleRestarted)
|
210
|
+
.on(EngineEvent.Restarted, this.handleRestarted)
|
211
|
+
.on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
|
212
|
+
this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
|
213
|
+
});
|
210
214
|
|
211
215
|
if (this.localParticipant) {
|
212
216
|
this.localParticipant.setupEngine(this.engine);
|
@@ -258,153 +262,219 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
258
262
|
|
259
263
|
this.setAndEmitConnectionState(ConnectionState.Connecting);
|
260
264
|
|
261
|
-
const
|
262
|
-
|
263
|
-
|
265
|
+
const urlProvider = new RegionUrlProvider(url, token);
|
266
|
+
|
267
|
+
const connectFn = async (
|
268
|
+
resolve: () => void,
|
269
|
+
reject: (reason: any) => void,
|
270
|
+
regionUrl?: string,
|
271
|
+
) => {
|
272
|
+
if (this.abortController) {
|
273
|
+
this.abortController.abort();
|
264
274
|
}
|
275
|
+
this.abortController = new AbortController();
|
276
|
+
|
265
277
|
// at this point the intention to connect has been signalled so we can allow cancelling of the connection via disconnect() again
|
266
|
-
unlockDisconnect();
|
278
|
+
unlockDisconnect?.();
|
267
279
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
}
|
273
|
-
|
274
|
-
|
280
|
+
try {
|
281
|
+
await this.attemptConnection(regionUrl ?? url, token, opts, this.abortController);
|
282
|
+
this.abortController = undefined;
|
283
|
+
resolve();
|
284
|
+
} catch (e) {
|
285
|
+
if (
|
286
|
+
isCloud(new URL(url)) &&
|
287
|
+
e instanceof ConnectionError &&
|
288
|
+
e.reason !== ConnectionErrorReason.Cancelled
|
289
|
+
) {
|
290
|
+
let nextUrl: string | null = null;
|
291
|
+
try {
|
292
|
+
nextUrl = await urlProvider.getNextBestRegionUrl(this.abortController?.signal);
|
293
|
+
} catch (error) {
|
294
|
+
if (
|
295
|
+
error instanceof ConnectionError &&
|
296
|
+
(error.status === 401 || error.reason === ConnectionErrorReason.Cancelled)
|
297
|
+
) {
|
298
|
+
reject(error);
|
299
|
+
return;
|
300
|
+
}
|
301
|
+
}
|
302
|
+
if (nextUrl) {
|
303
|
+
log.debug('initial connection failed, retrying with another region');
|
304
|
+
await connectFn(resolve, reject, nextUrl);
|
305
|
+
} else {
|
306
|
+
reject(e);
|
307
|
+
}
|
308
|
+
} else {
|
309
|
+
reject(e);
|
310
|
+
}
|
275
311
|
}
|
312
|
+
};
|
313
|
+
this.connectFuture = new Future(connectFn, () => {
|
314
|
+
this.clearConnectionFutures();
|
315
|
+
});
|
276
316
|
|
277
|
-
|
278
|
-
|
279
|
-
this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
|
317
|
+
return this.connectFuture.promise;
|
318
|
+
};
|
280
319
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
320
|
+
private connectSignal = async (
|
321
|
+
url: string,
|
322
|
+
token: string,
|
323
|
+
engine: RTCEngine,
|
324
|
+
connectOptions: InternalRoomConnectOptions,
|
325
|
+
roomOptions: InternalRoomOptions,
|
326
|
+
abortController: AbortController,
|
327
|
+
): Promise<JoinResponse> => {
|
328
|
+
const joinResponse = await engine.join(
|
329
|
+
url,
|
330
|
+
token,
|
331
|
+
{
|
332
|
+
autoSubscribe: connectOptions.autoSubscribe,
|
333
|
+
publishOnly: connectOptions.publishOnly,
|
334
|
+
adaptiveStream:
|
335
|
+
typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
336
|
+
maxRetries: connectOptions.maxRetries,
|
337
|
+
},
|
338
|
+
abortController.signal,
|
339
|
+
);
|
287
340
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
{
|
293
|
-
autoSubscribe: this.connOptions.autoSubscribe,
|
294
|
-
publishOnly: this.connOptions.publishOnly,
|
295
|
-
adaptiveStream:
|
296
|
-
typeof this.options.adaptiveStream === 'object' ? true : this.options.adaptiveStream,
|
297
|
-
maxRetries: this.connOptions.maxRetries,
|
298
|
-
},
|
299
|
-
this.abortController.signal,
|
300
|
-
);
|
341
|
+
let serverInfo: Partial<ServerInfo> | undefined = joinResponse.serverInfo;
|
342
|
+
if (!serverInfo) {
|
343
|
+
serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
|
344
|
+
}
|
301
345
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
}
|
346
|
+
log.debug(
|
347
|
+
`connected to Livekit Server ${Object.entries(serverInfo)
|
348
|
+
.map(([key, value]) => `${key}: ${value}`)
|
349
|
+
.join(', ')}`,
|
350
|
+
);
|
306
351
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
.join(', ')}`,
|
311
|
-
);
|
352
|
+
if (!joinResponse.serverVersion) {
|
353
|
+
throw new UnsupportedServer('unknown server version');
|
354
|
+
}
|
312
355
|
|
313
|
-
|
314
|
-
|
315
|
-
|
356
|
+
if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
|
357
|
+
log.debug('disabling dynacast due to server version');
|
358
|
+
// dynacast has a bug in 0.15.1, so we cannot use it then
|
359
|
+
roomOptions.dynacast = false;
|
360
|
+
}
|
316
361
|
|
317
|
-
|
318
|
-
|
319
|
-
// dynacast has a bug in 0.15.1, so we cannot use it then
|
320
|
-
this.options.dynacast = false;
|
321
|
-
}
|
362
|
+
return joinResponse;
|
363
|
+
};
|
322
364
|
|
323
|
-
|
365
|
+
private applyJoinResponse = (joinResponse: JoinResponse) => {
|
366
|
+
const pi = joinResponse.participant!;
|
324
367
|
|
325
|
-
|
326
|
-
|
368
|
+
this.localParticipant.sid = pi.sid;
|
369
|
+
this.localParticipant.identity = pi.identity;
|
327
370
|
|
328
|
-
|
329
|
-
|
330
|
-
|
371
|
+
this.localParticipant.updateInfo(pi);
|
372
|
+
// forward metadata changed for the local participant
|
373
|
+
this.setupLocalParticipantEvents();
|
331
374
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
});
|
344
|
-
}
|
375
|
+
// populate remote participants, these should not trigger new events
|
376
|
+
joinResponse.otherParticipants.forEach((info) => {
|
377
|
+
if (
|
378
|
+
info.sid !== this.localParticipant.sid &&
|
379
|
+
info.identity !== this.localParticipant.identity
|
380
|
+
) {
|
381
|
+
this.getOrCreateParticipant(info.sid, info);
|
382
|
+
} else {
|
383
|
+
log.warn('received info to create local participant as remote participant', {
|
384
|
+
info,
|
385
|
+
localParticipant: this.localParticipant,
|
345
386
|
});
|
346
|
-
|
347
|
-
this.name = joinResponse.room!.name;
|
348
|
-
this.sid = joinResponse.room!.sid;
|
349
|
-
this.metadata = joinResponse.room!.metadata;
|
350
|
-
if (this._isRecording !== joinResponse.room!.activeRecording) {
|
351
|
-
this._isRecording = joinResponse.room!.activeRecording;
|
352
|
-
this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
|
353
|
-
}
|
354
|
-
this.emit(RoomEvent.SignalConnected);
|
355
|
-
} catch (err) {
|
356
|
-
this.recreateEngine();
|
357
|
-
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
358
|
-
const resultingError = new ConnectionError(`could not establish signal connection`);
|
359
|
-
if (err instanceof Error) {
|
360
|
-
resultingError.message = `${resultingError.message}: ${err.message}`;
|
361
|
-
}
|
362
|
-
if (err instanceof ConnectionError) {
|
363
|
-
resultingError.reason = err.reason;
|
364
|
-
resultingError.status = err.status;
|
365
|
-
}
|
366
|
-
log.debug(`error trying to establish signal connection`, { error: err });
|
367
|
-
reject(resultingError);
|
368
|
-
return;
|
369
387
|
}
|
388
|
+
});
|
370
389
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
390
|
+
this.name = joinResponse.room!.name;
|
391
|
+
this.sid = joinResponse.room!.sid;
|
392
|
+
this.metadata = joinResponse.room!.metadata;
|
393
|
+
if (this._isRecording !== joinResponse.room!.activeRecording) {
|
394
|
+
this._isRecording = joinResponse.room!.activeRecording;
|
395
|
+
this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
|
396
|
+
}
|
397
|
+
};
|
398
|
+
|
399
|
+
private attemptConnection = async (
|
400
|
+
url: string,
|
401
|
+
token: string,
|
402
|
+
opts: RoomConnectOptions | undefined,
|
403
|
+
abortController: AbortController,
|
404
|
+
) => {
|
405
|
+
if (this.state === ConnectionState.Reconnecting) {
|
406
|
+
log.info('Reconnection attempt replaced by new connection attempt');
|
407
|
+
// make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
|
408
|
+
this.recreateEngine();
|
409
|
+
} else {
|
410
|
+
// create engine if previously disconnected
|
411
|
+
this.maybeCreateEngine();
|
412
|
+
}
|
413
|
+
|
414
|
+
this.acquireAudioContext();
|
415
|
+
|
416
|
+
this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
|
417
|
+
|
418
|
+
if (this.connOptions.rtcConfig) {
|
419
|
+
this.engine.rtcConfig = this.connOptions.rtcConfig;
|
420
|
+
}
|
421
|
+
if (this.connOptions.peerConnectionTimeout) {
|
422
|
+
this.engine.peerConnectionTimeout = this.connOptions.peerConnectionTimeout;
|
423
|
+
}
|
424
|
+
|
425
|
+
try {
|
426
|
+
const joinResponse = await this.connectSignal(
|
427
|
+
url,
|
428
|
+
token,
|
429
|
+
this.engine,
|
430
|
+
this.connOptions,
|
431
|
+
this.options,
|
432
|
+
abortController,
|
433
|
+
);
|
434
|
+
|
435
|
+
this.applyJoinResponse(joinResponse);
|
436
|
+
this.emit(RoomEvent.SignalConnected);
|
437
|
+
} catch (err) {
|
438
|
+
this.recreateEngine();
|
439
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
440
|
+
const resultingError = new ConnectionError(`could not establish signal connection`);
|
441
|
+
if (err instanceof Error) {
|
442
|
+
resultingError.message = `${resultingError.message}: ${err.message}`;
|
387
443
|
}
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
window.addEventListener('beforeunload', this.onBeforeUnload);
|
396
|
-
navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
|
397
|
-
}
|
398
|
-
this.setAndEmitConnectionState(ConnectionState.Connected);
|
399
|
-
this.emit(RoomEvent.Connected);
|
400
|
-
resolve();
|
401
|
-
});
|
402
|
-
};
|
403
|
-
this.connectFuture = new Future(connectFn, () => {
|
404
|
-
this.clearConnectionFutures();
|
405
|
-
});
|
444
|
+
if (err instanceof ConnectionError) {
|
445
|
+
resultingError.reason = err.reason;
|
446
|
+
resultingError.status = err.status;
|
447
|
+
}
|
448
|
+
log.debug(`error trying to establish signal connection`, { error: err });
|
449
|
+
throw resultingError;
|
450
|
+
}
|
406
451
|
|
407
|
-
|
452
|
+
if (abortController.signal.aborted) {
|
453
|
+
this.recreateEngine();
|
454
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
455
|
+
throw new ConnectionError(`Connection attempt aborted`);
|
456
|
+
}
|
457
|
+
|
458
|
+
try {
|
459
|
+
await this.engine.waitForPCInitialConnection(
|
460
|
+
this.connOptions.peerConnectionTimeout,
|
461
|
+
abortController,
|
462
|
+
);
|
463
|
+
} catch (e) {
|
464
|
+
this.recreateEngine();
|
465
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
466
|
+
throw e;
|
467
|
+
}
|
468
|
+
|
469
|
+
// also hook unload event
|
470
|
+
if (isWeb() && this.options.disconnectOnPageLeave) {
|
471
|
+
// capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
|
472
|
+
window.addEventListener('pagehide', this.onPageLeave);
|
473
|
+
window.addEventListener('beforeunload', this.onPageLeave);
|
474
|
+
navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
|
475
|
+
}
|
476
|
+
this.setAndEmitConnectionState(ConnectionState.Connected);
|
477
|
+
this.emit(RoomEvent.Connected);
|
408
478
|
};
|
409
479
|
|
410
480
|
/**
|
@@ -549,7 +619,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
549
619
|
}
|
550
620
|
}
|
551
621
|
|
552
|
-
private
|
622
|
+
private onPageLeave = async () => {
|
553
623
|
await this.disconnect();
|
554
624
|
};
|
555
625
|
|
@@ -853,7 +923,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
853
923
|
this.audioContext = undefined;
|
854
924
|
}
|
855
925
|
if (isWeb()) {
|
856
|
-
window.removeEventListener('beforeunload', this.
|
926
|
+
window.removeEventListener('beforeunload', this.onPageLeave);
|
927
|
+
window.removeEventListener('pagehide', this.onPageLeave);
|
857
928
|
navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
|
858
929
|
}
|
859
930
|
this.setAndEmitConnectionState(ConnectionState.Disconnected);
|
@@ -1275,8 +1346,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1275
1346
|
this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
|
1276
1347
|
};
|
1277
1348
|
|
1278
|
-
private onLocalTrackPublished = (pub: LocalTrackPublication) => {
|
1349
|
+
private onLocalTrackPublished = async (pub: LocalTrackPublication) => {
|
1279
1350
|
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
1351
|
+
if (pub.track instanceof LocalAudioTrack) {
|
1352
|
+
const trackIsSilent = await pub.track.checkForSilence();
|
1353
|
+
if (trackIsSilent) {
|
1354
|
+
this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
|
1355
|
+
}
|
1356
|
+
}
|
1280
1357
|
};
|
1281
1358
|
|
1282
1359
|
private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
|
@@ -1455,6 +1532,7 @@ export type RoomEventCallbacks = {
|
|
1455
1532
|
publication: LocalTrackPublication,
|
1456
1533
|
participant: LocalParticipant,
|
1457
1534
|
) => void;
|
1535
|
+
localAudioSilenceDetected: (publication: LocalTrackPublication) => void;
|
1458
1536
|
participantMetadataChanged: (
|
1459
1537
|
metadata: string | undefined,
|
1460
1538
|
participant: RemoteParticipant | LocalParticipant,
|
@@ -1491,4 +1569,5 @@ export type RoomEventCallbacks = {
|
|
1491
1569
|
audioPlaybackChanged: (playing: boolean) => void;
|
1492
1570
|
signalConnected: () => void;
|
1493
1571
|
recordingStatusChanged: (recording: boolean) => void;
|
1572
|
+
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
1494
1573
|
};
|
package/src/room/defaults.ts
CHANGED
package/src/room/errors.ts
CHANGED
package/src/room/events.ts
CHANGED
@@ -139,6 +139,14 @@ export enum RoomEvent {
|
|
139
139
|
*/
|
140
140
|
LocalTrackUnpublished = 'localTrackUnpublished',
|
141
141
|
|
142
|
+
/**
|
143
|
+
* When a local audio track is published the SDK checks whether there is complete silence
|
144
|
+
* on that track and emits the LocalAudioSilenceDetected event in that case.
|
145
|
+
* This allows for applications to show UI informing users that they might have to
|
146
|
+
* reset their audio hardware or check for proper device connectivity.
|
147
|
+
*/
|
148
|
+
LocalAudioSilenceDetected = 'localAudioSilenceDetected',
|
149
|
+
|
142
150
|
/**
|
143
151
|
* Active speakers changed. List of speakers are ordered by their audio level.
|
144
152
|
* loudest speakers first. This will include the LocalParticipant too.
|
@@ -256,6 +264,12 @@ export enum RoomEvent {
|
|
256
264
|
* args: (isRecording: boolean)
|
257
265
|
*/
|
258
266
|
RecordingStatusChanged = 'recordingStatusChanged',
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Emits whenever the current buffer status of a data channel changes
|
270
|
+
* args: (isLow: boolean, kind: [[DataPacket_Kind]])
|
271
|
+
*/
|
272
|
+
DCBufferStatusChanged = 'dcBufferStatusChanged',
|
259
273
|
}
|
260
274
|
|
261
275
|
export enum ParticipantEvent {
|
@@ -423,6 +437,7 @@ export enum EngineEvent {
|
|
423
437
|
MediaTrackAdded = 'mediaTrackAdded',
|
424
438
|
ActiveSpeakersUpdate = 'activeSpeakersUpdate',
|
425
439
|
DataPacketReceived = 'dataPacketReceived',
|
440
|
+
DCBufferStatusChanged = 'dcBufferStatusChanged',
|
426
441
|
}
|
427
442
|
|
428
443
|
export enum TrackEvent {
|
@@ -39,7 +39,8 @@ export default class LocalAudioTrack extends LocalTrack {
|
|
39
39
|
}
|
40
40
|
|
41
41
|
async mute(): Promise<LocalAudioTrack> {
|
42
|
-
await this.
|
42
|
+
const unlock = await this.muteLock.lock();
|
43
|
+
try {
|
43
44
|
// disabled special handling as it will cause BT headsets to switch communication modes
|
44
45
|
if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
|
45
46
|
log.debug('stopping mic track');
|
@@ -47,12 +48,15 @@ export default class LocalAudioTrack extends LocalTrack {
|
|
47
48
|
this._mediaStreamTrack.stop();
|
48
49
|
}
|
49
50
|
await super.mute();
|
50
|
-
|
51
|
-
|
51
|
+
return this;
|
52
|
+
} finally {
|
53
|
+
unlock();
|
54
|
+
}
|
52
55
|
}
|
53
56
|
|
54
57
|
async unmute(): Promise<LocalAudioTrack> {
|
55
|
-
await this.
|
58
|
+
const unlock = await this.muteLock.lock();
|
59
|
+
try {
|
56
60
|
if (
|
57
61
|
this.source === Track.Source.Microphone &&
|
58
62
|
(this.stopOnMute || this._mediaStreamTrack.readyState === 'ended') &&
|
@@ -62,8 +66,11 @@ export default class LocalAudioTrack extends LocalTrack {
|
|
62
66
|
await this.restartTrack();
|
63
67
|
}
|
64
68
|
await super.unmute();
|
65
|
-
|
66
|
-
|
69
|
+
|
70
|
+
return this;
|
71
|
+
} finally {
|
72
|
+
unlock();
|
73
|
+
}
|
67
74
|
}
|
68
75
|
|
69
76
|
async restartTrack(options?: AudioCaptureOptions) {
|
@@ -150,5 +157,6 @@ export default class LocalAudioTrack extends LocalTrack {
|
|
150
157
|
}
|
151
158
|
this.emit(TrackEvent.AudioSilenceDetected);
|
152
159
|
}
|
160
|
+
return trackIsSilent;
|
153
161
|
}
|
154
162
|
}
|
@@ -1,9 +1,14 @@
|
|
1
|
-
import Queue from 'async-await-queue';
|
2
1
|
import log from '../../logger';
|
3
2
|
import DeviceManager from '../DeviceManager';
|
4
3
|
import { TrackInvalidError } from '../errors';
|
5
4
|
import { TrackEvent } from '../events';
|
6
|
-
import {
|
5
|
+
import {
|
6
|
+
getEmptyAudioStreamTrack,
|
7
|
+
getEmptyVideoStreamTrack,
|
8
|
+
isMobile,
|
9
|
+
Mutex,
|
10
|
+
sleep,
|
11
|
+
} from '../utils';
|
7
12
|
import type { VideoCodec } from './options';
|
8
13
|
import { attachToElement, detachTrack, Track } from './Track';
|
9
14
|
|
@@ -22,7 +27,9 @@ export default abstract class LocalTrack extends Track {
|
|
22
27
|
|
23
28
|
protected providedByUser: boolean;
|
24
29
|
|
25
|
-
protected
|
30
|
+
protected muteLock: Mutex;
|
31
|
+
|
32
|
+
protected pauseUpstreamLock: Mutex;
|
26
33
|
|
27
34
|
/**
|
28
35
|
*
|
@@ -42,7 +49,8 @@ export default abstract class LocalTrack extends Track {
|
|
42
49
|
this.constraints = constraints ?? mediaTrack.getConstraints();
|
43
50
|
this.reacquireTrack = false;
|
44
51
|
this.providedByUser = userProvidedTrack;
|
45
|
-
this.
|
52
|
+
this.muteLock = new Mutex();
|
53
|
+
this.pauseUpstreamLock = new Mutex();
|
46
54
|
}
|
47
55
|
|
48
56
|
get id(): string {
|
@@ -246,7 +254,8 @@ export default abstract class LocalTrack extends Track {
|
|
246
254
|
};
|
247
255
|
|
248
256
|
async pauseUpstream() {
|
249
|
-
this.
|
257
|
+
const unlock = await this.pauseUpstreamLock.lock();
|
258
|
+
try {
|
250
259
|
if (this._isUpstreamPaused === true) {
|
251
260
|
return;
|
252
261
|
}
|
@@ -260,11 +269,14 @@ export default abstract class LocalTrack extends Track {
|
|
260
269
|
const emptyTrack =
|
261
270
|
this.kind === Track.Kind.Audio ? getEmptyAudioStreamTrack() : getEmptyVideoStreamTrack();
|
262
271
|
await this.sender.replaceTrack(emptyTrack);
|
263
|
-
}
|
272
|
+
} finally {
|
273
|
+
unlock();
|
274
|
+
}
|
264
275
|
}
|
265
276
|
|
266
277
|
async resumeUpstream() {
|
267
|
-
this.
|
278
|
+
const unlock = await this.pauseUpstreamLock.lock();
|
279
|
+
try {
|
268
280
|
if (this._isUpstreamPaused === false) {
|
269
281
|
return;
|
270
282
|
}
|
@@ -276,7 +288,9 @@ export default abstract class LocalTrack extends Track {
|
|
276
288
|
this.emit(TrackEvent.UpstreamResumed, this);
|
277
289
|
|
278
290
|
await this.sender.replaceTrack(this._mediaStreamTrack);
|
279
|
-
}
|
291
|
+
} finally {
|
292
|
+
unlock();
|
293
|
+
}
|
280
294
|
}
|
281
295
|
|
282
296
|
protected abstract monitorSender(): void;
|
@@ -97,26 +97,32 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
97
97
|
}
|
98
98
|
|
99
99
|
async mute(): Promise<LocalVideoTrack> {
|
100
|
-
await this.
|
100
|
+
const unlock = await this.muteLock.lock();
|
101
|
+
try {
|
101
102
|
if (this.source === Track.Source.Camera && !this.isUserProvided) {
|
102
103
|
log.debug('stopping camera track');
|
103
104
|
// also stop the track, so that camera indicator is turned off
|
104
105
|
this._mediaStreamTrack.stop();
|
105
106
|
}
|
106
107
|
await super.mute();
|
107
|
-
|
108
|
-
|
108
|
+
return this;
|
109
|
+
} finally {
|
110
|
+
unlock();
|
111
|
+
}
|
109
112
|
}
|
110
113
|
|
111
114
|
async unmute(): Promise<LocalVideoTrack> {
|
112
|
-
await this.
|
115
|
+
const unlock = await this.muteLock.lock();
|
116
|
+
try {
|
113
117
|
if (this.source === Track.Source.Camera && !this.isUserProvided) {
|
114
118
|
log.debug('reacquiring camera track');
|
115
119
|
await this.restartTrack();
|
116
120
|
}
|
117
121
|
await super.unmute();
|
118
|
-
|
119
|
-
|
122
|
+
return this;
|
123
|
+
} finally {
|
124
|
+
unlock();
|
125
|
+
}
|
120
126
|
}
|
121
127
|
|
122
128
|
async getSenderStats(): Promise<VideoSenderStats[]> {
|