livekit-client 2.0.2 → 2.0.4

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 +53 -17
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +158 -65
  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.map +1 -1
  10. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  11. package/dist/src/e2ee/KeyProvider.d.ts +1 -1
  12. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  13. package/dist/src/e2ee/types.d.ts +2 -0
  14. package/dist/src/e2ee/types.d.ts.map +1 -1
  15. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
  16. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  17. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
  18. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  19. package/dist/src/index.d.ts +2 -2
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/logger.d.ts +2 -0
  22. package/dist/src/logger.d.ts.map +1 -1
  23. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  24. package/dist/src/room/RTCEngine.d.ts +1 -0
  25. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  26. package/dist/src/room/Room.d.ts +1 -0
  27. package/dist/src/room/Room.d.ts.map +1 -1
  28. package/dist/src/room/events.d.ts +7 -2
  29. package/dist/src/room/events.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 +4 -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/Track.d.ts +2 -0
  35. package/dist/src/room/track/Track.d.ts.map +1 -1
  36. package/dist/src/room/track/options.d.ts +10 -0
  37. package/dist/src/room/track/options.d.ts.map +1 -1
  38. package/dist/src/room/track/types.d.ts +4 -0
  39. package/dist/src/room/track/types.d.ts.map +1 -1
  40. package/dist/ts4.2/src/e2ee/KeyProvider.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/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
  44. package/dist/ts4.2/src/index.d.ts +2 -2
  45. package/dist/ts4.2/src/logger.d.ts +2 -0
  46. package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
  47. package/dist/ts4.2/src/room/Room.d.ts +1 -0
  48. package/dist/ts4.2/src/room/events.d.ts +7 -2
  49. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +4 -1
  50. package/dist/ts4.2/src/room/track/Track.d.ts +2 -0
  51. package/dist/ts4.2/src/room/track/options.d.ts +10 -0
  52. package/dist/ts4.2/src/room/track/types.d.ts +4 -0
  53. package/package.json +1 -1
  54. package/src/api/SignalClient.ts +1 -0
  55. package/src/e2ee/E2eeManager.ts +2 -1
  56. package/src/e2ee/KeyProvider.ts +6 -1
  57. package/src/e2ee/types.ts +2 -0
  58. package/src/e2ee/worker/FrameCryptor.ts +26 -0
  59. package/src/e2ee/worker/ParticipantKeyHandler.ts +9 -5
  60. package/src/e2ee/worker/e2ee.worker.ts +17 -17
  61. package/src/index.ts +2 -1
  62. package/src/logger.ts +2 -0
  63. package/src/room/DeviceManager.ts +10 -1
  64. package/src/room/RTCEngine.ts +14 -0
  65. package/src/room/Room.ts +29 -4
  66. package/src/room/events.ts +5 -0
  67. package/src/room/participant/LocalParticipant.ts +4 -4
  68. package/src/room/track/LocalAudioTrack.ts +11 -0
  69. package/src/room/track/LocalTrack.ts +62 -36
  70. package/src/room/track/LocalVideoTrack.ts +10 -0
  71. package/src/room/track/Track.ts +2 -0
  72. package/src/room/track/options.ts +41 -8
  73. package/src/room/track/types.ts +5 -0
@@ -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;
@@ -18,6 +19,7 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
18
19
  protected processor?: TrackProcessor<TrackKind, any>;
19
20
  protected processorLock: Mutex;
20
21
  protected audioContext?: AudioContext;
22
+ private restartLock;
21
23
  /**
22
24
  *
23
25
  * @param mediaTrack
@@ -40,7 +42,8 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
40
42
  getDeviceId(): Promise<string | undefined>;
41
43
  mute(): Promise<this>;
42
44
  unmute(): Promise<this>;
43
- 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>;
44
47
  protected restart(constraints?: MediaTrackConstraints): Promise<this>;
45
48
  protected setTrackMuted(muted: boolean): void;
46
49
  protected get needsReAcquisition(): boolean;
@@ -4,6 +4,7 @@ import { StructuredLogger } from '../../logger';
4
4
  import { TrackSource, TrackType } from '../../proto/livekit_models_pb';
5
5
  import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc_pb';
6
6
  import type { LoggerOptions } from '../types';
7
+ import type { TrackProcessor } from './processor/types';
7
8
  export declare enum VideoQuality {
8
9
  LOW = 0,
9
10
  MEDIUM = 1,
@@ -135,6 +136,7 @@ export type TrackEventCallbacks = {
135
136
  elementDetached: (element: HTMLMediaElement) => void;
136
137
  upstreamPaused: (track: any) => void;
137
138
  upstreamResumed: (track: any) => void;
139
+ trackProcessorUpdate: (processor?: TrackProcessor<Track.Kind, any>) => void;
138
140
  };
139
141
  export {};
140
142
  //# sourceMappingURL=Track.d.ts.map
@@ -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.2",
3
+ "version": "2.0.4",
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",
@@ -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}`);
@@ -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) {
@@ -12,7 +12,7 @@ import { createKeyMaterialFromBuffer, createKeyMaterialFromString } from './util
12
12
  export class BaseKeyProvider extends (EventEmitter as new () => TypedEventEmitter<KeyProviderCallbacks>) {
13
13
  private keyInfoMap: Map<string, KeyInfo>;
14
14
 
15
- private options: KeyProviderOptions;
15
+ private readonly options: KeyProviderOptions;
16
16
 
17
17
  constructor(options: Partial<KeyProviderOptions> = {}) {
18
18
  super();
@@ -29,6 +29,11 @@ export class BaseKeyProvider extends (EventEmitter as new () => TypedEventEmitte
29
29
  */
30
30
  protected onSetEncryptionKey(key: CryptoKey, participantIdentity?: string, keyIndex?: number) {
31
31
  const keyInfo: KeyInfo = { key, participantIdentity, keyIndex };
32
+ if (!this.options.sharedKey && !participantIdentity) {
33
+ throw new Error(
34
+ 'participant identity needs to be passed for encryption key if sharedKey option is false',
35
+ );
36
+ }
32
37
  this.keyInfoMap.set(`${participantIdentity ?? 'shared'}-${keyIndex ?? 0}`, keyInfo);
33
38
  this.emit(KeyProviderEvent.SetKey, keyInfo);
34
39
  }
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
 
@@ -83,6 +83,14 @@ export class FrameCryptor extends BaseFrameCryptor {
83
83
  this.sifGuard = new SifGuard();
84
84
  }
85
85
 
86
+ private get logContext() {
87
+ return {
88
+ identity: this.participantIdentity,
89
+ trackId: this.trackId,
90
+ fallbackCodec: this.videoCodec,
91
+ };
92
+ }
93
+
86
94
  /**
87
95
  * Assign a different participant to the cryptor.
88
96
  * useful for transceiver re-use
@@ -96,6 +104,7 @@ export class FrameCryptor extends BaseFrameCryptor {
96
104
  }
97
105
 
98
106
  unsetParticipant() {
107
+ workerLogger.debug('unsetting participant', this.logContext);
99
108
  this.participantIdentity = undefined;
100
109
  }
101
110
 
@@ -143,6 +152,13 @@ export class FrameCryptor extends BaseFrameCryptor {
143
152
  this.videoCodec = codec;
144
153
  }
145
154
 
155
+ workerLogger.debug('Setting up frame cryptor transform', {
156
+ operation,
157
+ passedTrackId: trackId,
158
+ codec,
159
+ ...this.logContext,
160
+ });
161
+
146
162
  const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
147
163
  const transformStream = new TransformStream({
148
164
  transform: transformFn.bind(this),
@@ -159,6 +175,7 @@ export class FrameCryptor extends BaseFrameCryptor {
159
175
  }
160
176
 
161
177
  setSifTrailer(trailer: Uint8Array) {
178
+ workerLogger.debug('setting SIF trailer', { ...this.logContext, trailer });
162
179
  this.sifTrailer = trailer;
163
180
  }
164
181
 
@@ -212,6 +229,8 @@ export class FrameCryptor extends BaseFrameCryptor {
212
229
  encodedFrame.timestamp,
213
230
  );
214
231
  let frameInfo = this.getUnencryptedBytes(encodedFrame);
232
+ workerLogger.debug('frameInfo for encoded frame', { ...frameInfo, ...this.logContext });
233
+
215
234
  // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
216
235
  const frameHeader = new Uint8Array(encodedFrame.data, 0, frameInfo.unencryptedBytes);
217
236
 
@@ -262,6 +281,7 @@ export class FrameCryptor extends BaseFrameCryptor {
262
281
  workerLogger.error(e);
263
282
  }
264
283
  } else {
284
+ workerLogger.debug('failed to decrypt, emitting error', this.logContext);
265
285
  this.emit(
266
286
  CryptorEvent.Error,
267
287
  new CryptorError(`encryption key missing for encoding`, CryptorErrorReason.MissingKey),
@@ -284,11 +304,13 @@ export class FrameCryptor extends BaseFrameCryptor {
284
304
  // skip for decryption for empty dtx frames
285
305
  encodedFrame.data.byteLength === 0
286
306
  ) {
307
+ workerLogger.debug('skipping empty frame', this.logContext);
287
308
  this.sifGuard.recordUserFrame();
288
309
  return controller.enqueue(encodedFrame);
289
310
  }
290
311
 
291
312
  if (isFrameServerInjected(encodedFrame.data, this.sifTrailer)) {
313
+ workerLogger.debug('enqueue SIF', this.logContext);
292
314
  this.sifGuard.recordSif();
293
315
 
294
316
  if (this.sifGuard.isSifAllowed()) {
@@ -312,6 +334,7 @@ export class FrameCryptor extends BaseFrameCryptor {
312
334
  const decodedFrame = await this.decryptFrame(encodedFrame, keyIndex);
313
335
  this.keys.decryptionSuccess();
314
336
  if (decodedFrame) {
337
+ workerLogger.debug('enqueue decrypted frame', this.logContext);
315
338
  return controller.enqueue(decodedFrame);
316
339
  }
317
340
  } catch (error) {
@@ -352,6 +375,8 @@ export class FrameCryptor extends BaseFrameCryptor {
352
375
  throw new TypeError(`no encryption key found for decryption of ${this.participantIdentity}`);
353
376
  }
354
377
  let frameInfo = this.getUnencryptedBytes(encodedFrame);
378
+ workerLogger.debug('frameInfo for decoded frame', { ...frameInfo, ...this.logContext });
379
+
355
380
  // Construct frame trailer. Similar to the frame header described in
356
381
  // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
357
382
  // but we put it at the end.
@@ -566,6 +591,7 @@ export class FrameCryptor extends BaseFrameCryptor {
566
591
  // @ts-expect-error payloadType is not yet part of the typescript definition and currently not supported in Safari
567
592
  const payloadType = frame.getMetadata().payloadType;
568
593
  const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
594
+ workerLogger.debug('reading codec from frame', { codec, ...this.logContext });
569
595
  return codec;
570
596
  }
571
597
  }
@@ -133,15 +133,19 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
133
133
 
134
134
  /**
135
135
  * takes in a key material with `deriveBits` and `deriveKey` set as key usages
136
- * and derives encryption keys from the material and sets it on the key ring buffer
136
+ * and derives encryption keys from the material and sets it on the key ring buffers
137
137
  * together with the material
138
138
  * also updates the currentKeyIndex
139
139
  */
140
- async setKeyFromMaterial(material: CryptoKey, keyIndex = 0, emitRatchetEvent = false) {
141
- const newIndex = keyIndex >= 0 ? keyIndex % this.cryptoKeyRing.length : -1;
142
- workerLogger.debug(`setting new key with index ${newIndex}`);
140
+ async setKeyFromMaterial(material: CryptoKey, keyIndex: number, emitRatchetEvent = false) {
143
141
  const keySet = await deriveKeys(material, this.keyProviderOptions.ratchetSalt);
144
- this.setKeySet(keySet, newIndex >= 0 ? newIndex : this.currentKeyIndex, emitRatchetEvent);
142
+ const newIndex = keyIndex >= 0 ? keyIndex % this.cryptoKeyRing.length : this.currentKeyIndex;
143
+ workerLogger.debug(`setting new key with index ${keyIndex}`, {
144
+ usage: material.usages,
145
+ algorithm: material.algorithm,
146
+ ratchetSalt: this.keyProviderOptions.ratchetSalt,
147
+ });
148
+ this.setKeySet(keySet, newIndex, emitRatchetEvent);
145
149
  if (newIndex >= 0) this.currentKeyIndex = newIndex;
146
150
  }
147
151
 
@@ -21,8 +21,6 @@ let isEncryptionEnabled: boolean = false;
21
21
 
22
22
  let useSharedKey: boolean = false;
23
23
 
24
- let sharedKey: CryptoKey | undefined;
25
-
26
24
  let sifTrailer: Uint8Array | undefined;
27
25
 
28
26
  let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
@@ -34,6 +32,7 @@ onmessage = (ev) => {
34
32
 
35
33
  switch (kind) {
36
34
  case 'init':
35
+ workerLogger.setLevel(data.loglevel);
37
36
  workerLogger.info('worker initialized');
38
37
  keyProviderOptions = data.keyProviderOptions;
39
38
  useSharedKey = !!data.keyProviderOptions.sharedKey;
@@ -72,10 +71,9 @@ onmessage = (ev) => {
72
71
  break;
73
72
  case 'setKey':
74
73
  if (useSharedKey) {
75
- workerLogger.warn('set shared key');
76
74
  setSharedKey(data.key, data.keyIndex);
77
75
  } else if (data.participantIdentity) {
78
- workerLogger.warn(
76
+ workerLogger.info(
79
77
  `set participant sender key ${data.participantIdentity} index ${data.keyIndex}`,
80
78
  );
81
79
  getParticipantKeyHandler(data.participantIdentity).setKey(data.key, data.keyIndex);
@@ -125,9 +123,7 @@ async function handleRatchetRequest(data: RatchetRequestMessage['data']) {
125
123
  }
126
124
 
127
125
  function getTrackCryptor(participantIdentity: string, trackId: string) {
128
- let cryptor = participantCryptors.find(
129
- (c) => c.getParticipantIdentity() === participantIdentity && c.getTrackId() === trackId,
130
- );
126
+ let cryptor = participantCryptors.find((c) => c.getTrackId() === trackId);
131
127
  if (!cryptor) {
132
128
  workerLogger.info('creating new cryptor for', { participantIdentity });
133
129
  if (!keyProviderOptions) {
@@ -146,8 +142,7 @@ function getTrackCryptor(participantIdentity: string, trackId: string) {
146
142
  // assign new participant id to track cryptor and pass in correct key handler
147
143
  cryptor.setParticipant(participantIdentity, getParticipantKeyHandler(participantIdentity));
148
144
  }
149
- if (sharedKey) {
150
- }
145
+
151
146
  return cryptor;
152
147
  }
153
148
 
@@ -158,9 +153,6 @@ function getParticipantKeyHandler(participantIdentity: string) {
158
153
  let keys = participantKeys.get(participantIdentity);
159
154
  if (!keys) {
160
155
  keys = new ParticipantKeyHandler(participantIdentity, keyProviderOptions);
161
- if (sharedKey) {
162
- keys.setKey(sharedKey);
163
- }
164
156
  keys.on(KeyHandlerEvent.KeyRatcheted, emitRatchetedKeys);
165
157
  participantKeys.set(participantIdentity, keys);
166
158
  }
@@ -169,24 +161,32 @@ function getParticipantKeyHandler(participantIdentity: string) {
169
161
 
170
162
  function getSharedKeyHandler() {
171
163
  if (!sharedKeyHandler) {
164
+ workerLogger.debug('creating new shared key handler');
172
165
  sharedKeyHandler = new ParticipantKeyHandler('shared-key', keyProviderOptions);
173
166
  }
174
167
  return sharedKeyHandler;
175
168
  }
176
169
 
177
170
  function unsetCryptorParticipant(trackId: string, participantIdentity: string) {
178
- participantCryptors
179
- .find((c) => c.getParticipantIdentity() === participantIdentity && c.getTrackId() === trackId)
180
- ?.unsetParticipant();
171
+ const cryptor = participantCryptors.find(
172
+ (c) => c.getParticipantIdentity() === participantIdentity && c.getTrackId() === trackId,
173
+ );
174
+ if (!cryptor) {
175
+ workerLogger.warn('Could not unset participant on cryptor', { trackId, participantIdentity });
176
+ } else {
177
+ cryptor.unsetParticipant();
178
+ }
181
179
  }
182
180
 
183
181
  function setEncryptionEnabled(enable: boolean, participantIdentity: string) {
182
+ workerLogger.debug(`setting encryption enabled for all tracks of ${participantIdentity}`, {
183
+ enable,
184
+ });
184
185
  encryptionEnabledMap.set(participantIdentity, enable);
185
186
  }
186
187
 
187
188
  function setSharedKey(key: CryptoKey, index?: number) {
188
- workerLogger.debug('setting shared key');
189
- sharedKey = key;
189
+ workerLogger.info('set shared key', { index });
190
190
  getSharedKeyHandler().setKey(key, index);
191
191
  }
192
192
 
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
@@ -31,6 +31,8 @@ export type StructuredLogger = {
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');
@@ -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
  }
@@ -418,6 +418,19 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
418
418
  );
419
419
  }
420
420
  }
421
+
422
+ // detect cases where both signal client and peer connection are severed and assume that user has lost network connection
423
+ const isSignalSevered =
424
+ this.client.isDisconnected ||
425
+ this.client.currentState === SignalConnectionState.RECONNECTING;
426
+ const isPCSevered = [
427
+ PCTransportState.FAILED,
428
+ PCTransportState.CLOSING,
429
+ PCTransportState.CLOSED,
430
+ ].includes(connectionState);
431
+ if (isSignalSevered && isPCSevered && !this._isClosed) {
432
+ this.emit(EngineEvent.Offline);
433
+ }
421
434
  };
422
435
  this.pcManager.onTrack = (ev: RTCTrackEvent) => {
423
436
  this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
@@ -1400,4 +1413,5 @@ export type EngineEventCallbacks = {
1400
1413
  subscribedQualityUpdate: (update: SubscribedQualityUpdate) => void;
1401
1414
  localTrackUnpublished: (unpublishedResponse: TrackUnpublishedResponse) => void;
1402
1415
  remoteMute: (trackSid: string, muted: boolean) => void;
1416
+ offline: () => void;
1403
1417
  };
package/src/room/Room.ts CHANGED
@@ -60,6 +60,7 @@ import type RemoteTrack from './track/RemoteTrack';
60
60
  import RemoteTrackPublication from './track/RemoteTrackPublication';
61
61
  import { Track } from './track/Track';
62
62
  import type { TrackPublication } from './track/TrackPublication';
63
+ import type { TrackProcessor } from './track/processor/types';
63
64
  import type { AdaptiveStreamSettings } from './track/types';
64
65
  import { getNewAudioContext, sourceToKind } from './track/utils';
65
66
  import type { SimulationOptions, SimulationScenario } from './types';
@@ -348,6 +349,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
348
349
  })
349
350
  .on(EngineEvent.Restarting, this.handleRestarting)
350
351
  .on(EngineEvent.SignalRestarted, this.handleSignalRestarted)
352
+ .on(EngineEvent.Offline, () => {
353
+ if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
354
+ this.emit(RoomEvent.Reconnecting);
355
+ }
356
+ })
351
357
  .on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
352
358
  this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
353
359
  });
@@ -573,16 +579,23 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
573
579
  this.localParticipant.sid = pi.sid;
574
580
  this.localParticipant.identity = pi.identity;
575
581
 
582
+ if (this.options.e2ee && this.e2eeManager) {
583
+ try {
584
+ this.e2eeManager.setSifTrailer(joinResponse.sifTrailer);
585
+ } catch (e: any) {
586
+ this.log.error(e instanceof Error ? e.message : 'Could not set SifTrailer', {
587
+ ...this.logContext,
588
+ error: e,
589
+ });
590
+ }
591
+ }
592
+
576
593
  // populate remote participants, these should not trigger new events
577
594
  this.handleParticipantUpdates([pi, ...joinResponse.otherParticipants]);
578
595
 
579
596
  if (joinResponse.room) {
580
597
  this.handleRoomUpdate(joinResponse.room);
581
598
  }
582
-
583
- if (this.options.e2ee && this.e2eeManager) {
584
- this.e2eeManager.setSifTrailer(joinResponse.sifTrailer);
585
- }
586
599
  };
587
600
 
588
601
  private attemptConnection = async (
@@ -857,6 +870,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
857
870
  }
858
871
 
859
872
  private onPageLeave = async () => {
873
+ this.log.info('Page leave detected, disconnecting', this.logContext);
860
874
  await this.disconnect();
861
875
  };
862
876
 
@@ -1034,6 +1048,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1034
1048
  ) {
1035
1049
  throw new Error('cannot switch audio output, setSinkId not supported');
1036
1050
  }
1051
+ if (this.options.webAudioMix) {
1052
+ // setting `default` for web audio output doesn't work, so we need to normalize the id before
1053
+ deviceId =
1054
+ (await DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) ?? '';
1055
+ }
1037
1056
  this.options.audioOutput ??= {};
1038
1057
  const prevDeviceId = this.options.audioOutput.deviceId;
1039
1058
  this.options.audioOutput.deviceId = deviceId;
@@ -1775,7 +1794,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1775
1794
  this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
1776
1795
  };
1777
1796
 
1797
+ private onTrackProcessorUpdate = (processor?: TrackProcessor<Track.Kind, any>) => {
1798
+ processor?.onPublish?.(this);
1799
+ };
1800
+
1778
1801
  private onLocalTrackPublished = async (pub: LocalTrackPublication) => {
1802
+ pub.track?.on(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
1779
1803
  pub.track?.getProcessor()?.onPublish?.(this);
1780
1804
 
1781
1805
  this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
@@ -1799,6 +1823,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1799
1823
  };
1800
1824
 
1801
1825
  private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
1826
+ pub.track?.off(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
1802
1827
  this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
1803
1828
  };
1804
1829
 
@@ -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 {
@@ -552,4 +553,8 @@ export enum TrackEvent {
552
553
  * Fires on RemoteTrackPublication
553
554
  */
554
555
  SubscriptionFailed = 'subscriptionFailed',
556
+ /**
557
+ * @internal
558
+ */
559
+ TrackProcessorUpdate = 'trackProcessorUpdate',
555
560
  }
@@ -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) => {
@@ -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 !==
@@ -163,6 +173,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
163
173
  if (this.processor.processedTrack) {
164
174
  await this.sender?.replaceTrack(this.processor.processedTrack);
165
175
  }
176
+ this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
166
177
  } finally {
167
178
  unlock();
168
179
  }