livekit-client 2.0.3 → 2.0.5

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.
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