livekit-client 1.7.1 → 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 +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) {
|