livekit-client 1.7.1 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +20 -1
- package/dist/livekit-client.esm.mjs +2178 -1060
- 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/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 +8 -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 +4 -0
- package/dist/src/room/Room.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 +8 -2
- package/dist/src/room/events.d.ts.map +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 +1 -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/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/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 +8 -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 +4 -0
- package/dist/ts4.2/src/room/errors.d.ts +2 -1
- package/dist/ts4.2/src/room/events.d.ts +8 -2
- 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 +1 -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/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 +143 -40
- package/src/room/ReconnectPolicy.ts +2 -0
- package/src/room/RegionUrlProvider.ts +73 -0
- package/src/room/Room.ts +201 -132
- package/src/room/errors.ts +1 -0
- package/src/room/events.ts +7 -0
- package/src/room/track/LocalAudioTrack.ts +13 -6
- package/src/room/track/LocalTrack.ts +22 -8
- package/src/room/track/LocalVideoTrack.ts +12 -6
- package/src/room/track/RemoteTrackPublication.ts +4 -3
- package/src/room/track/RemoteVideoTrack.ts +5 -4
- package/src/room/track/Track.ts +46 -31
- 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,155 +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
|
-
// capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
|
396
|
-
window.addEventListener('pagehide', this.onPageLeave);
|
397
|
-
window.addEventListener('beforeunload', this.onPageLeave);
|
398
|
-
navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
|
399
|
-
}
|
400
|
-
this.setAndEmitConnectionState(ConnectionState.Connected);
|
401
|
-
this.emit(RoomEvent.Connected);
|
402
|
-
resolve();
|
403
|
-
});
|
404
|
-
};
|
405
|
-
this.connectFuture = new Future(connectFn, () => {
|
406
|
-
this.clearConnectionFutures();
|
407
|
-
});
|
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
|
+
}
|
408
451
|
|
409
|
-
|
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);
|
410
478
|
};
|
411
479
|
|
412
480
|
/**
|
@@ -1501,4 +1569,5 @@ export type RoomEventCallbacks = {
|
|
1501
1569
|
audioPlaybackChanged: (playing: boolean) => void;
|
1502
1570
|
signalConnected: () => void;
|
1503
1571
|
recordingStatusChanged: (recording: boolean) => void;
|
1572
|
+
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
1504
1573
|
};
|
package/src/room/errors.ts
CHANGED
package/src/room/events.ts
CHANGED
@@ -264,6 +264,12 @@ export enum RoomEvent {
|
|
264
264
|
* args: (isRecording: boolean)
|
265
265
|
*/
|
266
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',
|
267
273
|
}
|
268
274
|
|
269
275
|
export enum ParticipantEvent {
|
@@ -431,6 +437,7 @@ export enum EngineEvent {
|
|
431
437
|
MediaTrackAdded = 'mediaTrackAdded',
|
432
438
|
ActiveSpeakersUpdate = 'activeSpeakersUpdate',
|
433
439
|
DataPacketReceived = 'dataPacketReceived',
|
440
|
+
DCBufferStatusChanged = 'dcBufferStatusChanged',
|
434
441
|
}
|
435
442
|
|
436
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) {
|
@@ -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[]> {
|
@@ -4,7 +4,7 @@ import { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc
|
|
4
4
|
import { TrackEvent } from '../events';
|
5
5
|
import type RemoteTrack from './RemoteTrack';
|
6
6
|
import RemoteVideoTrack from './RemoteVideoTrack';
|
7
|
-
import
|
7
|
+
import { Track } from './Track';
|
8
8
|
import { TrackPublication } from './TrackPublication';
|
9
9
|
|
10
10
|
export default class RemoteTrackPublication extends TrackPublication {
|
@@ -181,6 +181,7 @@ export default class RemoteTrackPublication extends TrackPublication {
|
|
181
181
|
prevTrack.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
|
182
182
|
prevTrack.off(TrackEvent.Ended, this.handleEnded);
|
183
183
|
prevTrack.detach();
|
184
|
+
prevTrack.stopMonitor();
|
184
185
|
this.emit(TrackEvent.Unsubscribed, prevTrack);
|
185
186
|
}
|
186
187
|
super.setTrack(track);
|
@@ -238,8 +239,8 @@ export default class RemoteTrackPublication extends TrackPublication {
|
|
238
239
|
}
|
239
240
|
|
240
241
|
private isManualOperationAllowed(): boolean {
|
241
|
-
if (this.isAdaptiveStream) {
|
242
|
-
log.warn('adaptive stream is enabled, cannot change track settings', {
|
242
|
+
if (this.kind === Track.Kind.Video && this.isAdaptiveStream) {
|
243
|
+
log.warn('adaptive stream is enabled, cannot change video track settings', {
|
243
244
|
trackSid: this.trackSid,
|
244
245
|
});
|
245
246
|
return false;
|
@@ -4,6 +4,7 @@ import { TrackEvent } from '../events';
|
|
4
4
|
import { computeBitrate, VideoReceiverStats } from '../stats';
|
5
5
|
import CriticalTimers from '../timers';
|
6
6
|
import {
|
7
|
+
getDevicePixelRatio,
|
7
8
|
getIntersectionObserver,
|
8
9
|
getResizeObserver,
|
9
10
|
isWeb,
|
@@ -26,7 +27,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
26
27
|
|
27
28
|
private lastDimensions?: Track.Dimensions;
|
28
29
|
|
29
|
-
private
|
30
|
+
private isObserved: boolean = false;
|
30
31
|
|
31
32
|
constructor(
|
32
33
|
mediaTrack: MediaStreamTrack,
|
@@ -43,7 +44,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
43
44
|
}
|
44
45
|
|
45
46
|
get mediaStreamTrack() {
|
46
|
-
if (this.isAdaptiveStream && !this.
|
47
|
+
if (this.isAdaptiveStream && !this.isObserved) {
|
47
48
|
log.warn(
|
48
49
|
'When using adaptiveStream, you need to use remoteVideoTrack.attach() to add the track to a HTMLVideoElement, otherwise your video tracks might never start',
|
49
50
|
);
|
@@ -83,7 +84,6 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
83
84
|
const elementInfo = new HTMLElementInfo(element);
|
84
85
|
this.observeElementInfo(elementInfo);
|
85
86
|
}
|
86
|
-
this.hasUsedAttach = true;
|
87
87
|
return element;
|
88
88
|
}
|
89
89
|
|
@@ -110,6 +110,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
110
110
|
// the tab comes into focus for the first time.
|
111
111
|
this.debouncedHandleResize();
|
112
112
|
this.updateVisibility();
|
113
|
+
this.isObserved = true;
|
113
114
|
} else {
|
114
115
|
log.warn('visibility resize observer not triggered');
|
115
116
|
}
|
@@ -253,7 +254,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
253
254
|
let maxHeight = 0;
|
254
255
|
for (const info of this.elementInfos) {
|
255
256
|
const pixelDensity = this.adaptiveStreamSettings?.pixelDensity ?? 1;
|
256
|
-
const pixelDensityValue = pixelDensity === 'screen' ?
|
257
|
+
const pixelDensityValue = pixelDensity === 'screen' ? getDevicePixelRatio() : pixelDensity;
|
257
258
|
const currentElementWidth = info.width() * pixelDensityValue;
|
258
259
|
const currentElementHeight = info.height() * pixelDensityValue;
|
259
260
|
if (currentElementWidth + currentElementHeight > maxWidth + maxHeight) {
|