livekit-client 2.0.2 → 2.0.4

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 +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
  }