livekit-client 2.0.3 → 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +11 -8
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +166 -77
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +1 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  12. package/dist/src/e2ee/types.d.ts +2 -0
  13. package/dist/src/e2ee/types.d.ts.map +1 -1
  14. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
  15. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  16. package/dist/src/index.d.ts +2 -2
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/logger.d.ts +4 -2
  19. package/dist/src/logger.d.ts.map +1 -1
  20. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  21. package/dist/src/room/RTCEngine.d.ts +4 -2
  22. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  23. package/dist/src/room/Room.d.ts.map +1 -1
  24. package/dist/src/room/events.d.ts +2 -1
  25. package/dist/src/room/events.d.ts.map +1 -1
  26. package/dist/src/room/participant/Participant.d.ts +1 -2
  27. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  28. package/dist/src/room/participant/RemoteParticipant.d.ts +4 -0
  29. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  31. package/dist/src/room/track/LocalTrack.d.ts +3 -1
  32. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/options.d.ts +10 -0
  35. package/dist/src/room/track/options.d.ts.map +1 -1
  36. package/dist/src/room/track/types.d.ts +4 -0
  37. package/dist/src/room/track/types.d.ts.map +1 -1
  38. package/dist/src/room/track/utils.d.ts.map +1 -1
  39. package/dist/src/room/utils.d.ts.map +1 -1
  40. package/dist/ts4.2/src/api/SignalClient.d.ts +1 -1
  41. package/dist/ts4.2/src/e2ee/types.d.ts +2 -0
  42. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +1 -0
  43. package/dist/ts4.2/src/index.d.ts +2 -2
  44. package/dist/ts4.2/src/logger.d.ts +4 -2
  45. package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -2
  46. package/dist/ts4.2/src/room/events.d.ts +2 -1
  47. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -2
  48. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +4 -0
  49. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -1
  50. package/dist/ts4.2/src/room/track/options.d.ts +10 -0
  51. package/dist/ts4.2/src/room/track/types.d.ts +4 -0
  52. package/package.json +1 -1
  53. package/src/api/SignalClient.ts +10 -5
  54. package/src/e2ee/E2eeManager.ts +2 -1
  55. package/src/e2ee/types.ts +2 -0
  56. package/src/e2ee/worker/FrameCryptor.ts +12 -6
  57. package/src/e2ee/worker/e2ee.worker.ts +1 -0
  58. package/src/index.ts +2 -1
  59. package/src/logger.ts +23 -18
  60. package/src/room/DeviceManager.ts +10 -1
  61. package/src/room/RTCEngine.ts +26 -8
  62. package/src/room/Room.ts +26 -2
  63. package/src/room/events.ts +1 -0
  64. package/src/room/participant/LocalParticipant.ts +4 -4
  65. package/src/room/participant/Participant.ts +0 -2
  66. package/src/room/participant/RemoteParticipant.ts +8 -0
  67. package/src/room/track/LocalAudioTrack.ts +10 -0
  68. package/src/room/track/LocalTrack.ts +20 -3
  69. package/src/room/track/LocalVideoTrack.ts +15 -1
  70. package/src/room/track/options.ts +41 -8
  71. package/src/room/track/types.ts +5 -0
  72. package/src/room/track/utils.ts +18 -11
  73. package/src/room/utils.ts +3 -0
@@ -44,8 +44,7 @@ export default class Participant extends Participant_base {
44
44
  protected log: StructuredLogger;
45
45
  protected loggerOptions?: LoggerOptions;
46
46
  protected get logContext(): {
47
- participantSid: string;
48
- participantId: string;
47
+ [x: string]: unknown;
49
48
  };
50
49
  get isEncrypted(): boolean;
51
50
  get isAgent(): boolean;
@@ -16,6 +16,10 @@ export default class RemoteParticipant extends Participant {
16
16
  private audioOutput?;
17
17
  /** @internal */
18
18
  static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo): RemoteParticipant;
19
+ protected get logContext(): {
20
+ rpID: string;
21
+ remoteParticipant: string;
22
+ };
19
23
  /** @internal */
20
24
  constructor(signalClient: SignalClient, sid: string, identity?: string, name?: string, metadata?: string, loggerOptions?: LoggerOptions);
21
25
  protected addTrackPublication(publication: RemoteTrackPublication): void;
@@ -3,6 +3,7 @@ import { Mutex } from '../utils';
3
3
  import { Track } from './Track';
4
4
  import type { VideoCodec } from './options';
5
5
  import type { TrackProcessor } from './processor/types';
6
+ import type { ReplaceTrackOptions } from './types';
6
7
  export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Kind> extends Track<TrackKind> {
7
8
  /** @internal */
8
9
  sender?: RTCRtpSender;
@@ -41,7 +42,8 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
41
42
  getDeviceId(): Promise<string | undefined>;
42
43
  mute(): Promise<this>;
43
44
  unmute(): Promise<this>;
44
- replaceTrack(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<this>;
45
+ replaceTrack(track: MediaStreamTrack, options?: ReplaceTrackOptions): Promise<typeof this>;
46
+ replaceTrack(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<typeof this>;
45
47
  protected restart(constraints?: MediaTrackConstraints): Promise<this>;
46
48
  protected setTrackMuted(muted: boolean): void;
47
49
  protected get needsReAcquisition(): boolean;
@@ -220,10 +220,20 @@ export interface VideoEncoding {
220
220
  maxFramerate?: number;
221
221
  priority?: RTCPriorityType;
222
222
  }
223
+ export interface VideoPresetOptions {
224
+ width: number;
225
+ height: number;
226
+ aspectRatio?: number;
227
+ maxBitrate: number;
228
+ maxFramerate?: number;
229
+ priority?: RTCPriorityType;
230
+ }
223
231
  export declare class VideoPreset {
224
232
  encoding: VideoEncoding;
225
233
  width: number;
226
234
  height: number;
235
+ aspectRatio?: number;
236
+ constructor(videoPresetOptions: VideoPresetOptions);
227
237
  constructor(width: number, height: number, maxBitrate: number, maxFramerate?: number, priority?: RTCPriorityType);
228
238
  get resolution(): VideoResolution;
229
239
  }
@@ -21,4 +21,8 @@ export type AdaptiveStreamSettings = {
21
21
  */
22
22
  pauseVideoInBackground?: boolean;
23
23
  };
24
+ export interface ReplaceTrackOptions {
25
+ userProvidedTrack?: boolean;
26
+ stopProcessor?: boolean;
27
+ }
24
28
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -216,7 +216,7 @@ export class SignalClient {
216
216
  token: string,
217
217
  sid?: string,
218
218
  reason?: ReconnectReason,
219
- ): Promise<ReconnectResponse | void> {
219
+ ): Promise<ReconnectResponse | undefined> {
220
220
  if (!this.options) {
221
221
  this.log.warn(
222
222
  'attempted to reconnect without signal options being set, ignoring',
@@ -242,7 +242,7 @@ export class SignalClient {
242
242
  token: string,
243
243
  opts: ConnectOpts,
244
244
  abortSignal?: AbortSignal,
245
- ): Promise<JoinResponse | ReconnectResponse | void> {
245
+ ): Promise<JoinResponse | ReconnectResponse | undefined> {
246
246
  this.connectOptions = opts;
247
247
  url = toWebsocketUrl(url);
248
248
  // strip trailing slash
@@ -252,7 +252,7 @@ export class SignalClient {
252
252
  const clientInfo = getClientInfo();
253
253
  const params = createConnectionParams(token, clientInfo, opts);
254
254
 
255
- return new Promise<JoinResponse | ReconnectResponse | void>(async (resolve, reject) => {
255
+ return new Promise<JoinResponse | ReconnectResponse | undefined>(async (resolve, reject) => {
256
256
  const unlock = await this.connectionLock.lock();
257
257
  try {
258
258
  const abortHandler = async () => {
@@ -283,6 +283,7 @@ export class SignalClient {
283
283
 
284
284
  this.ws.onerror = async (ev: Event) => {
285
285
  if (this.state !== SignalConnectionState.CONNECTED) {
286
+ this.state = SignalConnectionState.DISCONNECTED;
286
287
  clearTimeout(wsTimeout);
287
288
  try {
288
289
  const resp = await fetch(`http${url.substring(2)}/validate${params}`);
@@ -355,9 +356,13 @@ export class SignalClient {
355
356
  abortSignal?.removeEventListener('abort', abortHandler);
356
357
  this.startPingInterval();
357
358
  if (resp.message?.case === 'reconnect') {
358
- resolve(resp.message?.value);
359
+ resolve(resp.message.value);
359
360
  } else {
360
- resolve();
361
+ this.log.debug(
362
+ 'declaring signal reconnected without reconnect response received',
363
+ this.logContext,
364
+ );
365
+ resolve(undefined);
361
366
  shouldProcessMessage = true;
362
367
  }
363
368
  } else if (this.isEstablishingConnection && resp.message.case === 'leave') {
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import type TypedEventEmitter from 'typed-emitter';
3
- import log from '../logger';
3
+ import log, { LogLevel, workerLogger } from '../logger';
4
4
  import { Encryption_Type, TrackInfo } from '../proto/livekit_models_pb';
5
5
  import type RTCEngine from '../room/RTCEngine';
6
6
  import type Room from '../room/Room';
@@ -68,6 +68,7 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
68
68
  kind: 'init',
69
69
  data: {
70
70
  keyProviderOptions: this.keyProvider.getOptions(),
71
+ loglevel: workerLogger.getLevel() as LogLevel,
71
72
  },
72
73
  };
73
74
  if (this.worker) {
package/src/e2ee/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { LogLevel } from '../logger';
1
2
  import type { VideoCodec } from '../room/track/options';
2
3
  import type { BaseKeyProvider } from './KeyProvider';
3
4
 
@@ -10,6 +11,7 @@ export interface InitMessage extends BaseMessage {
10
11
  kind: 'init';
11
12
  data: {
12
13
  keyProviderOptions: KeyProviderOptions;
14
+ loglevel: LogLevel;
13
15
  };
14
16
  }
15
17
 
@@ -67,6 +67,8 @@ export class FrameCryptor extends BaseFrameCryptor {
67
67
 
68
68
  private sifGuard: SifGuard;
69
69
 
70
+ private detectedCodec?: VideoCodec;
71
+
70
72
  constructor(opts: {
71
73
  keys: ParticipantKeyHandler;
72
74
  participantIdentity: string;
@@ -85,8 +87,8 @@ export class FrameCryptor extends BaseFrameCryptor {
85
87
 
86
88
  private get logContext() {
87
89
  return {
88
- identity: this.participantIdentity,
89
- trackId: this.trackId,
90
+ participant: this.participantIdentity,
91
+ mediaTrackId: this.trackId,
90
92
  fallbackCodec: this.videoCodec,
91
93
  };
92
94
  }
@@ -229,7 +231,6 @@ export class FrameCryptor extends BaseFrameCryptor {
229
231
  encodedFrame.timestamp,
230
232
  );
231
233
  let frameInfo = this.getUnencryptedBytes(encodedFrame);
232
- workerLogger.debug('frameInfo for encoded frame', { ...frameInfo, ...this.logContext });
233
234
 
234
235
  // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
235
236
  const frameHeader = new Uint8Array(encodedFrame.data, 0, frameInfo.unencryptedBytes);
@@ -334,7 +335,6 @@ export class FrameCryptor extends BaseFrameCryptor {
334
335
  const decodedFrame = await this.decryptFrame(encodedFrame, keyIndex);
335
336
  this.keys.decryptionSuccess();
336
337
  if (decodedFrame) {
337
- workerLogger.debug('enqueue decrypted frame', this.logContext);
338
338
  return controller.enqueue(decodedFrame);
339
339
  }
340
340
  } catch (error) {
@@ -375,7 +375,6 @@ export class FrameCryptor extends BaseFrameCryptor {
375
375
  throw new TypeError(`no encryption key found for decryption of ${this.participantIdentity}`);
376
376
  }
377
377
  let frameInfo = this.getUnencryptedBytes(encodedFrame);
378
- workerLogger.debug('frameInfo for decoded frame', { ...frameInfo, ...this.logContext });
379
378
 
380
379
  // Construct frame trailer. Similar to the frame header described in
381
380
  // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
@@ -534,6 +533,14 @@ export class FrameCryptor extends BaseFrameCryptor {
534
533
  var frameInfo = { unencryptedBytes: 0, isH264: false };
535
534
  if (isVideoFrame(frame)) {
536
535
  let detectedCodec = this.getVideoCodec(frame) ?? this.videoCodec;
536
+ if (detectedCodec !== this.detectedCodec) {
537
+ workerLogger.debug('detected different codec', {
538
+ detectedCodec,
539
+ oldCodec: this.detectedCodec,
540
+ ...this.logContext,
541
+ });
542
+ this.detectedCodec = detectedCodec;
543
+ }
537
544
 
538
545
  if (detectedCodec === 'av1' || detectedCodec === 'vp9') {
539
546
  throw new Error(`${detectedCodec} is not yet supported for end to end encryption`);
@@ -591,7 +598,6 @@ export class FrameCryptor extends BaseFrameCryptor {
591
598
  // @ts-expect-error payloadType is not yet part of the typescript definition and currently not supported in Safari
592
599
  const payloadType = frame.getMetadata().payloadType;
593
600
  const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
594
- workerLogger.debug('reading codec from frame', { codec, ...this.logContext });
595
601
  return codec;
596
602
  }
597
603
  }
@@ -32,6 +32,7 @@ onmessage = (ev) => {
32
32
 
33
33
  switch (kind) {
34
34
  case 'init':
35
+ workerLogger.setLevel(data.loglevel);
35
36
  workerLogger.info('worker initialized');
36
37
  keyProviderOptions = data.keyProviderOptions;
37
38
  useSharedKey = !!data.keyProviderOptions.sharedKey;
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LogLevel, getLogger, setLogExtension, setLogLevel } from './logger';
1
+ import { LogLevel, LoggerNames, getLogger, setLogExtension, setLogLevel } from './logger';
2
2
  import { DataPacket_Kind, DisconnectReason } from './proto/livekit_models_pb';
3
3
  import DefaultReconnectPolicy from './room/DefaultReconnectPolicy';
4
4
  import Room, { ConnectionState } from './room/Room';
@@ -55,6 +55,7 @@ export {
55
55
  supportsVP9,
56
56
  createAudioAnalyser,
57
57
  LogLevel,
58
+ LoggerNames,
58
59
  getLogger,
59
60
  Room,
60
61
  ConnectionState,
package/src/logger.ts CHANGED
@@ -24,16 +24,19 @@ export enum LoggerNames {
24
24
 
25
25
  type LogLevelString = keyof typeof LogLevel;
26
26
 
27
- export type StructuredLogger = {
27
+ export type StructuredLogger = log.Logger & {
28
28
  trace: (msg: string, context?: object) => void;
29
29
  debug: (msg: string, context?: object) => void;
30
30
  info: (msg: string, context?: object) => void;
31
31
  warn: (msg: string, context?: object) => void;
32
32
  error: (msg: string, context?: object) => void;
33
33
  setDefaultLevel: (level: log.LogLevelDesc) => void;
34
+ setLevel: (level: log.LogLevelDesc) => void;
35
+ getLevel: () => number;
34
36
  };
35
37
 
36
38
  let livekitLogger = log.getLogger('livekit');
39
+ const livekitLoggers = Object.values(LoggerNames).map((name) => log.getLogger(name));
37
40
 
38
41
  livekitLogger.setDefaultLevel(LogLevel.info);
39
42
 
@@ -52,9 +55,7 @@ export function setLogLevel(level: LogLevel | LogLevelString, loggerName?: Logge
52
55
  if (loggerName) {
53
56
  log.getLogger(loggerName).setLevel(level);
54
57
  }
55
- for (const logger of Object.entries(log.getLoggers())
56
- .filter(([logrName]) => logrName.startsWith('livekit'))
57
- .map(([, logr]) => logr)) {
58
+ for (const logger of livekitLoggers) {
58
59
  logger.setLevel(level);
59
60
  }
60
61
  }
@@ -65,24 +66,28 @@ export type LogExtension = (level: LogLevel, msg: string, context?: object) => v
65
66
  * use this to hook into the logging function to allow sending internal livekit logs to third party services
66
67
  * if set, the browser logs will lose their stacktrace information (see https://github.com/pimterry/loglevel#writing-plugins)
67
68
  */
68
- export function setLogExtension(extension: LogExtension, logger = livekitLogger) {
69
- const originalFactory = logger.methodFactory;
69
+ export function setLogExtension(extension: LogExtension, logger?: StructuredLogger) {
70
+ const loggers = logger ? [logger] : livekitLoggers;
70
71
 
71
- logger.methodFactory = (methodName, configLevel, loggerName) => {
72
- const rawMethod = originalFactory(methodName, configLevel, loggerName);
72
+ loggers.forEach((logR) => {
73
+ const originalFactory = logR.methodFactory;
73
74
 
74
- const logLevel = LogLevel[methodName as LogLevelString];
75
- const needLog = logLevel >= configLevel && logLevel < LogLevel.silent;
75
+ logR.methodFactory = (methodName, configLevel, loggerName) => {
76
+ const rawMethod = originalFactory(methodName, configLevel, loggerName);
76
77
 
77
- return (msg, context?: [msg: string, context: object]) => {
78
- if (context) rawMethod(msg, context);
79
- else rawMethod(msg);
80
- if (needLog) {
81
- extension(logLevel, msg, context);
82
- }
78
+ const logLevel = LogLevel[methodName as LogLevelString];
79
+ const needLog = logLevel >= configLevel && logLevel < LogLevel.silent;
80
+
81
+ return (msg, context?: [msg: string, context: object]) => {
82
+ if (context) rawMethod(msg, context);
83
+ else rawMethod(msg);
84
+ if (needLog) {
85
+ extension(logLevel, msg, context);
86
+ }
87
+ };
83
88
  };
84
- };
85
- logger.setLevel(logger.getLevel()); // Be sure to call setLevel method in order to apply plugin
89
+ logR.setLevel(logR.getLevel());
90
+ });
86
91
  }
87
92
 
88
93
  export const workerLogger = log.getLogger('lk-e2ee') as StructuredLogger;
@@ -80,7 +80,16 @@ export default class DeviceManager {
80
80
  // device has been chosen
81
81
  const devices = await this.getDevices(kind);
82
82
 
83
- const device = devices.find((d) => d.groupId === groupId && d.deviceId !== defaultId);
83
+ // `default` devices will have the same groupId as the entry with the actual device id so we store the counts for each group id
84
+ const groupIdCounts = new Map(devices.map((d) => [d.groupId, 0]));
85
+
86
+ devices.forEach((d) => groupIdCounts.set(d.groupId, (groupIdCounts.get(d.groupId) ?? 0) + 1));
87
+
88
+ const device = devices.find(
89
+ (d) =>
90
+ (groupId === d.groupId || (groupIdCounts.get(d.groupId) ?? 0) > 1) &&
91
+ d.deviceId !== defaultId,
92
+ );
84
93
 
85
94
  return device?.deviceId;
86
95
  }
@@ -205,8 +205,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
205
205
  get logContext() {
206
206
  return {
207
207
  room: this.latestJoinResponse?.room?.name,
208
- roomSid: this.latestJoinResponse?.room?.sid,
209
- identity: this.latestJoinResponse?.participant?.identity,
208
+ roomID: this.latestJoinResponse?.room?.sid,
209
+ participant: this.latestJoinResponse?.participant?.identity,
210
+ pID: this.latestJoinResponse?.participant?.sid,
210
211
  };
211
212
  }
212
213
 
@@ -418,6 +419,19 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
418
419
  );
419
420
  }
420
421
  }
422
+
423
+ // detect cases where both signal client and peer connection are severed and assume that user has lost network connection
424
+ const isSignalSevered =
425
+ this.client.isDisconnected ||
426
+ this.client.currentState === SignalConnectionState.RECONNECTING;
427
+ const isPCSevered = [
428
+ PCTransportState.FAILED,
429
+ PCTransportState.CLOSING,
430
+ PCTransportState.CLOSED,
431
+ ].includes(connectionState);
432
+ if (isSignalSevered && isPCSevered && !this._isClosed) {
433
+ this.emit(EngineEvent.Offline);
434
+ }
421
435
  };
422
436
  this.pcManager.onTrack = (ev: RTCTrackEvent) => {
423
437
  this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
@@ -992,14 +1006,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
992
1006
 
993
1007
  this.log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`, this.logContext);
994
1008
  this.emit(EngineEvent.Resuming);
995
-
1009
+ let res: ReconnectResponse | undefined;
996
1010
  try {
997
1011
  this.setupSignalClientCallbacks();
998
- const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
999
- if (res) {
1000
- const rtcConfig = this.makeRTCConfiguration(res);
1001
- this.pcManager.updateConfiguration(rtcConfig);
1002
- }
1012
+ res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
1003
1013
  } catch (error) {
1004
1014
  let message = '';
1005
1015
  if (error instanceof Error) {
@@ -1016,6 +1026,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1016
1026
  }
1017
1027
  this.emit(EngineEvent.SignalResumed);
1018
1028
 
1029
+ if (res) {
1030
+ const rtcConfig = this.makeRTCConfiguration(res);
1031
+ this.pcManager.updateConfiguration(rtcConfig);
1032
+ } else {
1033
+ this.log.warn('Did not receive reconnect response', this.logContext);
1034
+ }
1035
+
1019
1036
  if (this.shouldFailNext) {
1020
1037
  this.shouldFailNext = false;
1021
1038
  throw new Error('simulated failure');
@@ -1400,4 +1417,5 @@ export type EngineEventCallbacks = {
1400
1417
  subscribedQualityUpdate: (update: SubscribedQualityUpdate) => void;
1401
1418
  localTrackUnpublished: (unpublishedResponse: TrackUnpublishedResponse) => void;
1402
1419
  remoteMute: (trackSid: string, muted: boolean) => void;
1420
+ offline: () => void;
1403
1421
  };
package/src/room/Room.ts CHANGED
@@ -69,7 +69,9 @@ import {
69
69
  Mutex,
70
70
  createDummyVideoStreamTrack,
71
71
  getEmptyAudioStreamTrack,
72
+ isBrowserSupported,
72
73
  isCloud,
74
+ isReactNative,
73
75
  isWeb,
74
76
  supportsSetSinkId,
75
77
  toHttpUrl,
@@ -247,8 +249,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
247
249
  private get logContext() {
248
250
  return {
249
251
  room: this.name,
250
- roomSid: this.roomInfo?.sid,
251
- identity: this.localParticipant.identity,
252
+ roomID: this.roomInfo?.sid,
253
+ participant: this.localParticipant.identity,
254
+ pID: this.localParticipant.sid,
252
255
  };
253
256
  }
254
257
 
@@ -349,6 +352,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
349
352
  })
350
353
  .on(EngineEvent.Restarting, this.handleRestarting)
351
354
  .on(EngineEvent.SignalRestarted, this.handleSignalRestarted)
355
+ .on(EngineEvent.Offline, () => {
356
+ if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
357
+ this.emit(RoomEvent.Reconnecting);
358
+ }
359
+ })
352
360
  .on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
353
361
  this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
354
362
  });
@@ -410,6 +418,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
410
418
  }
411
419
 
412
420
  connect = async (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
421
+ if (!isBrowserSupported()) {
422
+ if (isReactNative()) {
423
+ throw Error("WebRTC isn't detected, have you called registerGlobals?");
424
+ } else {
425
+ throw Error(
426
+ "LiveKit doesn't seem to be supported on this browser. Try to update your browser and make sure no browser extensions are disabling webRTC.",
427
+ );
428
+ }
429
+ }
430
+
413
431
  // In case a disconnect called happened right before the connect call, make sure the disconnect is completed first by awaiting its lock
414
432
  const unlockDisconnect = await this.disconnectLock.lock();
415
433
 
@@ -865,6 +883,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
865
883
  }
866
884
 
867
885
  private onPageLeave = async () => {
886
+ this.log.info('Page leave detected, disconnecting', this.logContext);
868
887
  await this.disconnect();
869
888
  };
870
889
 
@@ -1042,6 +1061,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1042
1061
  ) {
1043
1062
  throw new Error('cannot switch audio output, setSinkId not supported');
1044
1063
  }
1064
+ if (this.options.webAudioMix) {
1065
+ // setting `default` for web audio output doesn't work, so we need to normalize the id before
1066
+ deviceId =
1067
+ (await DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) ?? '';
1068
+ }
1045
1069
  this.options.audioOutput ??= {};
1046
1070
  const prevDeviceId = this.options.audioOutput.deviceId;
1047
1071
  this.options.audioOutput.deviceId = deviceId;
@@ -491,6 +491,7 @@ export enum EngineEvent {
491
491
  RemoteMute = 'remoteMute',
492
492
  SubscribedQualityUpdate = 'subscribedQualityUpdate',
493
493
  LocalTrackUnpublished = 'localTrackUnpublished',
494
+ Offline = 'offline',
494
495
  }
495
496
 
496
497
  export enum TrackEvent {
@@ -513,6 +513,10 @@ export default class LocalParticipant extends Participant {
513
513
  track: LocalTrack | MediaStreamTrack,
514
514
  options?: TrackPublishOptions,
515
515
  ): Promise<LocalTrackPublication> {
516
+ if (track instanceof LocalAudioTrack) {
517
+ track.setAudioContext(this.audioContext);
518
+ }
519
+
516
520
  await this.reconnectFuture?.promise;
517
521
  if (track instanceof LocalTrack && this.pendingPublishPromises.has(track)) {
518
522
  await this.pendingPublishPromises.get(track);
@@ -566,10 +570,6 @@ export default class LocalParticipant extends Participant {
566
570
  });
567
571
  }
568
572
 
569
- if (track instanceof LocalAudioTrack) {
570
- track.setAudioContext(this.audioContext);
571
- }
572
-
573
573
  // is it already published? if so skip
574
574
  let existingPublication: LocalTrackPublication | undefined;
575
575
  this.trackPublications.forEach((publication) => {
@@ -88,8 +88,6 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
88
88
  protected get logContext() {
89
89
  return {
90
90
  ...this.loggerOptions?.loggerContextCb?.(),
91
- participantSid: this.sid,
92
- participantId: this.identity,
93
91
  };
94
92
  }
95
93
 
@@ -33,6 +33,14 @@ export default class RemoteParticipant extends Participant {
33
33
  return new RemoteParticipant(signalClient, pi.sid, pi.identity, pi.name, pi.metadata);
34
34
  }
35
35
 
36
+ protected get logContext() {
37
+ return {
38
+ ...super.logContext,
39
+ rpID: this.sid,
40
+ remoteParticipant: this.identity,
41
+ };
42
+ }
43
+
36
44
  /** @internal */
37
45
  constructor(
38
46
  signalClient: SignalClient,
@@ -51,6 +51,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
51
51
  async mute(): Promise<typeof this> {
52
52
  const unlock = await this.muteLock.lock();
53
53
  try {
54
+ if (this.isMuted) {
55
+ this.log.debug('Track already muted', this.logContext);
56
+ return this;
57
+ }
58
+
54
59
  // disabled special handling as it will cause BT headsets to switch communication modes
55
60
  if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
56
61
  this.log.debug('stopping mic track', this.logContext);
@@ -67,6 +72,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
67
72
  async unmute(): Promise<typeof this> {
68
73
  const unlock = await this.muteLock.lock();
69
74
  try {
75
+ if (!this.isMuted) {
76
+ this.log.debug('Track already unmuted', this.logContext);
77
+ return this;
78
+ }
79
+
70
80
  const deviceHasChanged =
71
81
  this._constraints.deviceId &&
72
82
  this._mediaStreamTrack.getSettings().deviceId !==
@@ -8,6 +8,7 @@ import { Mutex, compareVersions, isMobile, sleep } from '../utils';
8
8
  import { Track, attachToElement, detachTrack } from './Track';
9
9
  import type { VideoCodec } from './options';
10
10
  import type { TrackProcessor } from './processor/types';
11
+ import type { ReplaceTrackOptions } from './types';
11
12
 
12
13
  const defaultDimensionsTimeout = 1000;
13
14
 
@@ -224,18 +225,34 @@ export default abstract class LocalTrack<
224
225
  return this;
225
226
  }
226
227
 
227
- async replaceTrack(track: MediaStreamTrack, userProvidedTrack = true) {
228
+ async replaceTrack(track: MediaStreamTrack, options?: ReplaceTrackOptions): Promise<typeof this>;
229
+ async replaceTrack(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<typeof this>;
230
+ async replaceTrack(
231
+ track: MediaStreamTrack,
232
+ userProvidedOrOptions: boolean | ReplaceTrackOptions | undefined,
233
+ ) {
228
234
  if (!this.sender) {
229
235
  throw new TrackInvalidError('unable to replace an unpublished track');
230
236
  }
231
237
 
238
+ let userProvidedTrack: boolean | undefined;
239
+ let stopProcessor: boolean | undefined;
240
+
241
+ if (typeof userProvidedOrOptions === 'boolean') {
242
+ userProvidedTrack = userProvidedOrOptions;
243
+ } else if (userProvidedOrOptions !== undefined) {
244
+ userProvidedTrack = userProvidedOrOptions.userProvidedTrack;
245
+ stopProcessor = userProvidedOrOptions.stopProcessor;
246
+ }
247
+
248
+ this.providedByUser = userProvidedTrack ?? true;
249
+
232
250
  this.log.debug('replace MediaStreamTrack', this.logContext);
233
251
  await this.setMediaStreamTrack(track);
234
252
  // this must be synced *after* setting mediaStreamTrack above, since it relies
235
253
  // on the previous state in order to cleanup
236
- this.providedByUser = userProvidedTrack;
237
254
 
238
- if (this.processor) {
255
+ if (stopProcessor && this.processor) {
239
256
  await this.stopProcessor();
240
257
  }
241
258
  return this;
@@ -118,6 +118,11 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
118
118
  async mute(): Promise<typeof this> {
119
119
  const unlock = await this.muteLock.lock();
120
120
  try {
121
+ if (this.isMuted) {
122
+ this.log.debug('Track already muted', this.logContext);
123
+ return this;
124
+ }
125
+
121
126
  if (this.source === Track.Source.Camera && !this.isUserProvided) {
122
127
  this.log.debug('stopping camera track', this.logContext);
123
128
  // also stop the track, so that camera indicator is turned off
@@ -133,6 +138,11 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
133
138
  async unmute(): Promise<typeof this> {
134
139
  const unlock = await this.muteLock.lock();
135
140
  try {
141
+ if (!this.isMuted) {
142
+ this.log.debug('Track already unmuted', this.logContext);
143
+ return this;
144
+ }
145
+
136
146
  if (this.source === Track.Source.Camera && !this.isUserProvided) {
137
147
  this.log.debug('reacquiring camera track', this.logContext);
138
148
  await this.restartTrack();
@@ -414,7 +424,11 @@ async function setPublishingLayersForSender(
414
424
  }
415
425
 
416
426
  if (encodings.length !== senderEncodings.length) {
417
- log.warn('cannot set publishing layers, encodings mismatch');
427
+ log.warn('cannot set publishing layers, encodings mismatch', {
428
+ ...logContext,
429
+ encodings,
430
+ senderEncodings,
431
+ });
418
432
  return;
419
433
  }
420
434