livekit-client 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/livekit-client.esm.mjs +457 -478
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/connectionHelper/ConnectionCheck.d.ts +2 -3
  6. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  7. package/dist/src/connectionHelper/checks/Checker.d.ts +2 -3
  8. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  9. package/dist/src/room/PCTransport.d.ts +1 -1
  10. package/dist/src/room/PCTransport.d.ts.map +1 -1
  11. package/dist/src/room/RTCEngine.d.ts +2 -4
  12. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  13. package/dist/src/room/Room.d.ts +3 -4
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  16. package/dist/src/room/participant/Participant.d.ts +2 -4
  17. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  18. package/dist/src/room/participant/RemoteParticipant.d.ts +2 -1
  19. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  20. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalTrack.d.ts +1 -0
  22. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  23. package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
  24. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -0
  26. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  27. package/dist/src/room/track/Track.d.ts +2 -4
  28. package/dist/src/room/track/Track.d.ts.map +1 -1
  29. package/dist/src/room/track/TrackPublication.d.ts +2 -4
  30. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  31. package/dist/src/room/track/options.d.ts +13 -4
  32. package/dist/src/room/track/options.d.ts.map +1 -1
  33. package/dist/src/room/track/types.d.ts +2 -1
  34. package/dist/src/room/track/types.d.ts.map +1 -1
  35. package/dist/src/room/utils.d.ts.map +1 -1
  36. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +2 -3
  37. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +2 -3
  38. package/dist/ts4.2/src/room/PCTransport.d.ts +1 -1
  39. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -4
  40. package/dist/ts4.2/src/room/Room.d.ts +3 -4
  41. package/dist/ts4.2/src/room/participant/Participant.d.ts +2 -4
  42. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +2 -1
  43. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
  44. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +1 -1
  45. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -0
  46. package/dist/ts4.2/src/room/track/Track.d.ts +2 -4
  47. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +2 -4
  48. package/dist/ts4.2/src/room/track/options.d.ts +13 -4
  49. package/dist/ts4.2/src/room/track/types.d.ts +2 -1
  50. package/package.json +2 -3
  51. package/src/connectionHelper/ConnectionCheck.ts +2 -3
  52. package/src/connectionHelper/checks/Checker.ts +2 -3
  53. package/src/logger.ts +4 -4
  54. package/src/room/PCTransport.ts +1 -1
  55. package/src/room/RTCEngine.ts +4 -4
  56. package/src/room/Room.ts +41 -11
  57. package/src/room/participant/LocalParticipant.ts +11 -3
  58. package/src/room/participant/Participant.ts +2 -4
  59. package/src/room/participant/RemoteParticipant.ts +4 -3
  60. package/src/room/participant/publishUtils.ts +16 -18
  61. package/src/room/track/LocalTrack.ts +61 -42
  62. package/src/room/track/LocalVideoTrack.ts +9 -7
  63. package/src/room/track/RemoteVideoTrack.ts +23 -6
  64. package/src/room/track/Track.ts +2 -4
  65. package/src/room/track/TrackPublication.ts +2 -4
  66. package/src/room/track/options.ts +13 -4
  67. package/src/room/track/types.ts +2 -1
  68. package/src/room/utils.ts +6 -3
@@ -1,4 +1,4 @@
1
- import type TypedEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
2
2
  import { DataPacket_Kind, ParticipantInfo, ParticipantPermission, ConnectionQuality as ProtoQuality, SubscriptionError } from '../../proto/livekit_models';
3
3
  import type LocalTrackPublication from '../track/LocalTrackPublication';
4
4
  import type RemoteTrack from '../track/RemoteTrack';
@@ -11,8 +11,7 @@ export declare enum ConnectionQuality {
11
11
  Poor = "poor",
12
12
  Unknown = "unknown"
13
13
  }
14
- declare const Participant_base: new () => TypedEmitter<ParticipantEventCallbacks>;
15
- export default class Participant extends Participant_base {
14
+ export default class Participant extends EventEmitter<ParticipantEventCallbacks> {
16
15
  protected participantInfo?: ParticipantInfo;
17
16
  audioTracks: Map<string, TrackPublication>;
18
17
  videoTracks: Map<string, TrackPublication>;
@@ -90,5 +89,4 @@ export type ParticipantEventCallbacks = {
90
89
  participantPermissionsChanged: (prevPermissions?: ParticipantPermission) => void;
91
90
  trackSubscriptionStatusChanged: (publication: RemoteTrackPublication, status: TrackPublication.SubscriptionStatus) => void;
92
91
  };
93
- export {};
94
92
  //# sourceMappingURL=Participant.d.ts.map
@@ -1,3 +1,4 @@
1
+ import type EventEmitter from 'eventemitter3';
1
2
  import type { SignalClient } from '../../api/SignalClient';
2
3
  import type { ParticipantInfo } from '../../proto/livekit_models';
3
4
  import RemoteTrackPublication from '../track/RemoteTrackPublication';
@@ -48,6 +49,6 @@ export default class RemoteParticipant extends Participant {
48
49
  */
49
50
  setAudioOutput(output: AudioOutputOptions): Promise<void>;
50
51
  /** @internal */
51
- emit<E extends keyof ParticipantEventCallbacks>(event: E, ...args: Parameters<ParticipantEventCallbacks[E]>): boolean;
52
+ emit<T extends EventEmitter.EventNames<ParticipantEventCallbacks>>(event: T, ...args: EventEmitter.EventArgs<ParticipantEventCallbacks, T>): boolean;
52
53
  }
53
54
  //# sourceMappingURL=RemoteParticipant.d.ts.map
@@ -29,6 +29,7 @@ export default abstract class LocalTrack extends Track {
29
29
  get isUpstreamPaused(): boolean;
30
30
  get isUserProvided(): boolean;
31
31
  get mediaStreamTrack(): MediaStreamTrack;
32
+ private setMediaStreamTrack;
32
33
  waitForDimensions(timeout?: number): Promise<Track.Dimensions>;
33
34
  /**
34
35
  * @returns DeviceID of the device that is currently being used for this track
@@ -50,5 +50,5 @@ export default class LocalVideoTrack extends LocalTrack {
50
50
  protected handleAppVisibilityChanged(): Promise<void>;
51
51
  }
52
52
  export declare function videoQualityForRid(rid: string): VideoQuality;
53
- export declare function videoLayersFromEncodings(width: number, height: number, encodings?: RTCRtpEncodingParameters[]): VideoLayer[];
53
+ export declare function videoLayersFromEncodings(width: number, height: number, encodings?: RTCRtpEncodingParameters[], svc?: boolean): VideoLayer[];
54
54
  //# sourceMappingURL=LocalVideoTrack.d.ts.map
@@ -37,6 +37,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
37
37
  private readonly debouncedHandleResize;
38
38
  private updateVisibility;
39
39
  private updateDimensions;
40
+ private getPixelDensity;
40
41
  }
41
42
  export interface ElementInfo {
42
43
  element: object;
@@ -1,9 +1,8 @@
1
- import type TypedEventEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
2
2
  import type { SignalClient } from '../../api/SignalClient';
3
3
  import { TrackSource, TrackType } from '../../proto/livekit_models';
4
4
  import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc';
5
- declare const Track_base: new () => TypedEventEmitter<TrackEventCallbacks>;
6
- export declare abstract class Track extends Track_base {
5
+ export declare abstract class Track extends EventEmitter<TrackEventCallbacks> {
7
6
  kind: Track.Kind;
8
7
  attachedElements: HTMLMediaElement[];
9
8
  isMuted: boolean;
@@ -121,5 +120,4 @@ export type TrackEventCallbacks = {
121
120
  upstreamPaused: (track: any) => void;
122
121
  upstreamResumed: (track: any) => void;
123
122
  };
124
- export {};
125
123
  //# sourceMappingURL=Track.d.ts.map
@@ -1,4 +1,4 @@
1
- import type TypedEventEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
2
2
  import type { SubscriptionError, TrackInfo } from '../../proto/livekit_models';
3
3
  import type { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc';
4
4
  import LocalAudioTrack from './LocalAudioTrack';
@@ -7,8 +7,7 @@ import RemoteAudioTrack from './RemoteAudioTrack';
7
7
  import type RemoteTrack from './RemoteTrack';
8
8
  import RemoteVideoTrack from './RemoteVideoTrack';
9
9
  import { Track } from './Track';
10
- declare const TrackPublication_base: new () => TypedEventEmitter<PublicationEventCallbacks>;
11
- export declare class TrackPublication extends TrackPublication_base {
10
+ export declare class TrackPublication extends EventEmitter<PublicationEventCallbacks> {
12
11
  kind: Track.Kind;
13
12
  trackName: string;
14
13
  trackSid: Track.SID;
@@ -65,5 +64,4 @@ export type PublicationEventCallbacks = {
65
64
  subscriptionStatusChanged: (status: TrackPublication.SubscriptionStatus, prevStatus: TrackPublication.SubscriptionStatus) => void;
66
65
  subscriptionFailed: (error: SubscriptionError) => void;
67
66
  };
68
- export {};
69
67
  //# sourceMappingURL=TrackPublication.d.ts.map
@@ -54,10 +54,19 @@ export interface TrackPublishDefaults {
54
54
  */
55
55
  scalabilityMode?: ScalabilityMode;
56
56
  /**
57
- * custom video simulcast layers for camera tracks, defaults to h180, h360
58
- * You can specify up to two custom layers that will be used instead of
59
- * the LiveKit default layers.
60
- * Note: the layers need to be ordered from lowest to highest quality
57
+ * Up to two additional simulcast layers to publish in addition to the original
58
+ * Track.
59
+ * When left blank, it defaults to h180, h360.
60
+ * If a SVC codec is used (VP9 or AV1), this field has no effect.
61
+ *
62
+ * To publish three total layers, you would specify:
63
+ * {
64
+ * videoEncoding: {...}, // encoding of the primary layer
65
+ * videoSimulcastLayers: [
66
+ * VideoPresets.h540,
67
+ * VideoPresets.h216,
68
+ * ],
69
+ * }
61
70
  */
62
71
  videoSimulcastLayers?: Array<VideoPreset>;
63
72
  /**
@@ -6,7 +6,8 @@ export type AudioTrack = RemoteAudioTrack | LocalAudioTrack;
6
6
  export type VideoTrack = RemoteVideoTrack | LocalVideoTrack;
7
7
  export type AdaptiveStreamSettings = {
8
8
  /**
9
- * Set a custom pixel density, defaults to 1
9
+ * Set a custom pixel density. Defaults to 2 for high density screens (3+) or
10
+ * 1 otherwise.
10
11
  * When streaming videos on a ultra high definition screen this setting
11
12
  * let's you account for the devicePixelRatio of those screens.
12
13
  * Set it to `screen` to use the actual pixel density of the screen
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
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",
@@ -42,12 +42,11 @@
42
42
  "size-limit": "size-limit"
43
43
  },
44
44
  "dependencies": {
45
- "events": "^3.3.0",
45
+ "eventemitter3": "^5.0.1",
46
46
  "loglevel": "^1.8.0",
47
47
  "protobufjs": "^7.0.0",
48
48
  "sdp-transform": "^2.14.1",
49
49
  "ts-debounce": "^4.0.0",
50
- "typed-emitter": "^2.1.0",
51
50
  "webrtc-adapter": "^8.1.1"
52
51
  },
53
52
  "devDependencies": {
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import { CheckStatus, Checker } from './checks/Checker';
4
3
  import type { CheckInfo, InstantiableCheck } from './checks/Checker';
5
4
  import { PublishAudioCheck } from './checks/publishAudio';
@@ -11,7 +10,7 @@ import { WebSocketCheck } from './checks/websocket';
11
10
 
12
11
  export type { CheckInfo, CheckStatus };
13
12
 
14
- export class ConnectionCheck extends (EventEmitter as new () => TypedEmitter<ConnectionCheckCallbacks>) {
13
+ export class ConnectionCheck extends EventEmitter<ConnectionCheckCallbacks> {
15
14
  token: string;
16
15
 
17
16
  url: string;
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import type { RoomConnectOptions, RoomOptions } from '../../options';
4
3
  import type RTCEngine from '../../room/RTCEngine';
5
4
  import Room, { ConnectionState } from '../../room/Room';
@@ -30,7 +29,7 @@ export interface CheckerOptions {
30
29
  connectOptions?: RoomConnectOptions;
31
30
  }
32
31
 
33
- export abstract class Checker extends (EventEmitter as new () => TypedEmitter<CheckerCallbacks>) {
32
+ export abstract class Checker extends EventEmitter<CheckerCallbacks> {
34
33
  protected url: string;
35
34
 
36
35
  protected token: string;
package/src/logger.ts CHANGED
@@ -21,7 +21,7 @@ type StructuredLogger = {
21
21
 
22
22
  const livekitLogger = log.getLogger('livekit');
23
23
 
24
- livekitLogger.setLevel(LogLevel.info);
24
+ livekitLogger.setDefaultLevel(LogLevel.info);
25
25
 
26
26
  export default livekitLogger as StructuredLogger;
27
27
 
@@ -38,10 +38,10 @@ export type LogExtension = (level: LogLevel, msg: string, context?: object) => v
38
38
  export function setLogExtension(extension: LogExtension) {
39
39
  const originalFactory = livekitLogger.methodFactory;
40
40
 
41
- livekitLogger.methodFactory = (methodName, logLevel, loggerName) => {
42
- const rawMethod = originalFactory(methodName, logLevel, loggerName);
41
+ livekitLogger.methodFactory = (methodName, configLevel, loggerName) => {
42
+ const rawMethod = originalFactory(methodName, configLevel, loggerName);
43
43
 
44
- const configLevel = livekitLogger.getLevel();
44
+ const logLevel = LogLevel[methodName as LogLevelString];
45
45
  const needLog = logLevel >= configLevel && logLevel < LogLevel.silent;
46
46
 
47
47
  return (msg, context?: [msg: string, context: object]) => {
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from 'events';
1
+ import EventEmitter from 'eventemitter3';
2
2
  import { parse, write } from 'sdp-transform';
3
3
  import type { MediaDescription } from 'sdp-transform';
4
4
  import { debounce } from 'ts-debounce';
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEventEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import { SignalClient } from '../api/SignalClient';
4
3
  import type { SignalOptions } from '../api/SignalClient';
5
4
  import log from '../logger';
@@ -65,7 +64,7 @@ enum PCState {
65
64
  }
66
65
 
67
66
  /** @internal */
68
- export default class RTCEngine extends (EventEmitter as new () => TypedEventEmitter<EngineEventCallbacks>) {
67
+ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
69
68
  publisher?: PCTransport;
70
69
 
71
70
  subscriber?: PCTransport;
@@ -1046,6 +1045,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1046
1045
  };
1047
1046
  this.once(EngineEvent.Restarted, onRestarted);
1048
1047
  this.once(EngineEvent.Disconnected, onDisconnected);
1048
+ this.once(EngineEvent.Closing, onDisconnected);
1049
1049
  });
1050
1050
  };
1051
1051
 
@@ -1172,7 +1172,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1172
1172
  this.hasPublished = true;
1173
1173
 
1174
1174
  const handleClosed = () => {
1175
- log.debug('engine disconnected while negotiation was ongoing');
1175
+ log.warn('engine disconnected while negotiation was ongoing');
1176
1176
  cleanup();
1177
1177
  resolve();
1178
1178
  return;
package/src/room/Room.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import 'webrtc-adapter';
4
3
  import { toProtoSessionDescription } from '../api/SignalClient';
5
4
  import log from '../logger';
@@ -65,6 +64,7 @@ import {
65
64
  createDummyVideoStreamTrack,
66
65
  getEmptyAudioStreamTrack,
67
66
  isCloud,
67
+ isSafari,
68
68
  isWeb,
69
69
  supportsSetSinkId,
70
70
  unpackStreamId,
@@ -90,7 +90,7 @@ export const RoomState = ConnectionState;
90
90
  *
91
91
  * @noInheritDoc
92
92
  */
93
- class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>) {
93
+ class Room extends EventEmitter<RoomEventCallbacks> {
94
94
  state: ConnectionState = ConnectionState.Disconnected;
95
95
 
96
96
  /** map of sid: [[RemoteParticipant]] */
@@ -140,7 +140,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
140
140
  */
141
141
  constructor(options?: RoomOptions) {
142
142
  super();
143
- this.setMaxListeners(100);
144
143
  this.participants = new Map();
145
144
  this.cachedParticipantSids = [];
146
145
  this.identityToSid = new Map();
@@ -495,6 +494,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
495
494
  // capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
496
495
  window.addEventListener('pagehide', this.onPageLeave);
497
496
  window.addEventListener('beforeunload', this.onPageLeave);
497
+ }
498
+ if (isWeb()) {
499
+ document.addEventListener('freeze', this.onPageLeave);
498
500
  navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
499
501
  }
500
502
  this.setAndEmitConnectionState(ConnectionState.Connected);
@@ -657,8 +659,31 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
657
659
  */
658
660
  async startAudio() {
659
661
  await this.acquireAudioContext();
660
-
661
662
  const elements: Array<HTMLMediaElement> = [];
663
+
664
+ if (isSafari()) {
665
+ /**
666
+ * iOS Safari blocks audio element playback if
667
+ * - user is not publishing audio themselves and
668
+ * - no other audio source is playing
669
+ *
670
+ * as a workaround, we create an audio element with an empty track, so that
671
+ * silent audio is always playing
672
+ */
673
+ const audioId = 'livekit-dummy-audio-el';
674
+ let dummyAudioEl = document.getElementById(audioId) as HTMLAudioElement | null;
675
+ if (!dummyAudioEl) {
676
+ dummyAudioEl = document.createElement('audio');
677
+ dummyAudioEl.autoplay = true;
678
+ dummyAudioEl.hidden = true;
679
+ const track = getEmptyAudioStreamTrack();
680
+ track.enabled = true;
681
+ dummyAudioEl.srcObject = new MediaStream([track]);
682
+ document.body.append(dummyAudioEl);
683
+ }
684
+ elements.push(dummyAudioEl);
685
+ }
686
+
662
687
  this.participants.forEach((p) => {
663
688
  p.audioTracks.forEach((t) => {
664
689
  if (t.track) {
@@ -965,6 +990,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
965
990
  if (isWeb()) {
966
991
  window.removeEventListener('beforeunload', this.onPageLeave);
967
992
  window.removeEventListener('pagehide', this.onPageLeave);
993
+ window.removeEventListener('freeze', this.onPageLeave);
968
994
  navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
969
995
  }
970
996
  } finally {
@@ -1420,9 +1446,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1420
1446
  return true;
1421
1447
  }
1422
1448
 
1423
- private emitWhenConnected<E extends keyof RoomEventCallbacks>(
1424
- event: E,
1425
- ...args: Parameters<RoomEventCallbacks[E]>
1449
+ private emitWhenConnected<T extends EventEmitter.EventNames<RoomEventCallbacks>>(
1450
+ event: T,
1451
+ ...args: EventEmitter.EventArgs<RoomEventCallbacks, T>
1426
1452
  ): boolean {
1427
1453
  if (this.state === ConnectionState.Connected) {
1428
1454
  return this.emit(event, ...args);
@@ -1599,10 +1625,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1599
1625
  }
1600
1626
 
1601
1627
  // /** @internal */
1602
- emit<E extends keyof RoomEventCallbacks>(
1603
- event: E,
1604
- ...args: Parameters<RoomEventCallbacks[E]>
1628
+ emit<T extends EventEmitter.EventNames<RoomEventCallbacks>>(
1629
+ event: T,
1630
+ ...args: EventEmitter.EventArgs<RoomEventCallbacks, T>
1605
1631
  ): boolean {
1632
+ // emit<E extends keyof RoomEventCallbacks>(
1633
+ // event: E,
1634
+ // ...args: Parameters<RoomEventCallbacks[E]>
1635
+ // ): boolean {
1606
1636
  // active speaker updates are too spammy
1607
1637
  if (event !== RoomEvent.ActiveSpeakersChanged) {
1608
1638
  log.debug(`room event ${event}`, { event, args });
@@ -143,8 +143,11 @@ export default class LocalParticipant extends Participant {
143
143
  };
144
144
 
145
145
  private handleDisconnected = () => {
146
- this.reconnectFuture?.reject?.('Got disconnected during publishing attempt');
147
- this.reconnectFuture = undefined;
146
+ if (this.reconnectFuture) {
147
+ this.reconnectFuture.promise.catch((e) => log.warn(e));
148
+ this.reconnectFuture?.reject?.('Got disconnected during reconnection attempt');
149
+ this.reconnectFuture = undefined;
150
+ }
148
151
  };
149
152
 
150
153
  /**
@@ -666,7 +669,12 @@ export default class LocalParticipant extends Participant {
666
669
  dims.height,
667
670
  opts,
668
671
  );
669
- req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
672
+ req.layers = videoLayersFromEncodings(
673
+ req.width,
674
+ req.height,
675
+ encodings,
676
+ isSVCCodec(opts.videoCodec),
677
+ );
670
678
  } else if (track.kind === Track.Kind.Audio) {
671
679
  encodings = [
672
680
  {
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import log from '../../logger';
4
3
  import {
5
4
  DataPacket_Kind,
@@ -35,7 +34,7 @@ function qualityFromProto(q: ProtoQuality): ConnectionQuality {
35
34
  }
36
35
  }
37
36
 
38
- export default class Participant extends (EventEmitter as new () => TypedEmitter<ParticipantEventCallbacks>) {
37
+ export default class Participant extends EventEmitter<ParticipantEventCallbacks> {
39
38
  protected participantInfo?: ParticipantInfo;
40
39
 
41
40
  audioTracks: Map<string, TrackPublication>;
@@ -72,7 +71,6 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
72
71
  /** @internal */
73
72
  constructor(sid: string, identity: string, name?: string, metadata?: string) {
74
73
  super();
75
- this.setMaxListeners(100);
76
74
  this.sid = sid;
77
75
  this.identity = identity;
78
76
  this.name = name;
@@ -1,3 +1,4 @@
1
+ import type EventEmitter from 'eventemitter3';
1
2
  import type { SignalClient } from '../../api/SignalClient';
2
3
  import log from '../../logger';
3
4
  import type { ParticipantInfo, SubscriptionError } from '../../proto/livekit_models';
@@ -345,9 +346,9 @@ export default class RemoteParticipant extends Participant {
345
346
  }
346
347
 
347
348
  /** @internal */
348
- emit<E extends keyof ParticipantEventCallbacks>(
349
- event: E,
350
- ...args: Parameters<ParticipantEventCallbacks[E]>
349
+ emit<T extends EventEmitter.EventNames<ParticipantEventCallbacks>>(
350
+ event: T,
351
+ ...args: EventEmitter.EventArgs<ParticipantEventCallbacks, T>
351
352
  ): boolean {
352
353
  log.trace('participant event', { participant: this.sid, event, args });
353
354
  return super.emit(event, ...args);
@@ -123,27 +123,25 @@ export function computeVideoEncodings(
123
123
  if (scalabilityMode && isSVCCodec(videoCodec)) {
124
124
  log.debug(`using svc with scalabilityMode ${scalabilityMode}`);
125
125
 
126
- const encodings: RTCRtpEncodingParameters[] = [];
126
+ const sm = new ScalabilityMode(scalabilityMode);
127
127
 
128
- // svc use first encoding as the original, so we sort encoding from high to low
129
- switch (scalabilityMode) {
130
- case 'L3T3':
131
- case 'L3T3_KEY':
132
- encodings.push({
133
- rid: videoRids[2],
134
- maxBitrate: videoEncoding.maxBitrate,
135
- /* @ts-ignore */
136
- maxFramerate: original.encoding.maxFramerate,
137
- /* @ts-ignore */
138
- scalabilityMode: scalabilityMode,
139
- });
140
- log.debug('encodings', encodings);
141
- return encodings;
128
+ const encodings: RTCRtpEncodingParameters[] = [];
142
129
 
143
- default:
144
- // TODO : support other scalability modes
145
- throw new Error(`unsupported scalabilityMode: ${scalabilityMode}`);
130
+ if (sm.spatial > 3) {
131
+ throw new Error(`unsupported scalabilityMode: ${scalabilityMode}`);
132
+ }
133
+ for (let i = 0; i < sm.spatial; i += 1) {
134
+ encodings.push({
135
+ rid: videoRids[2 - i],
136
+ maxBitrate: videoEncoding.maxBitrate / 3 ** i,
137
+ /* @ts-ignore */
138
+ maxFramerate: original.encoding.maxFramerate,
139
+ });
146
140
  }
141
+ /* @ts-ignore */
142
+ encodings[0].scalabilityMode = scalabilityMode;
143
+ log.debug('encodings', encodings);
144
+ return encodings;
147
145
  }
148
146
 
149
147
  if (!useSimulcast) {
@@ -47,12 +47,16 @@ export default abstract class LocalTrack extends Track {
47
47
  userProvidedTrack = false,
48
48
  ) {
49
49
  super(mediaTrack, kind);
50
- this._mediaStreamTrack.addEventListener('ended', this.handleEnded);
51
- this.constraints = constraints ?? mediaTrack.getConstraints();
52
50
  this.reacquireTrack = false;
53
51
  this.providedByUser = userProvidedTrack;
54
52
  this.muteLock = new Mutex();
55
53
  this.pauseUpstreamLock = new Mutex();
54
+ // added to satisfy TS compiler, constraints are synced with MediaStreamTrack
55
+ this.constraints = mediaTrack.getConstraints();
56
+ this.setMediaStreamTrack(mediaTrack);
57
+ if (constraints) {
58
+ this.constraints = constraints;
59
+ }
56
60
  }
57
61
 
58
62
  get id(): string {
@@ -88,6 +92,53 @@ export default abstract class LocalTrack extends Track {
88
92
  return this.processor?.processedTrack ?? this._mediaStreamTrack;
89
93
  }
90
94
 
95
+ private async setMediaStreamTrack(newTrack: MediaStreamTrack) {
96
+ if (newTrack === this._mediaStreamTrack) {
97
+ return;
98
+ }
99
+ if (this._mediaStreamTrack) {
100
+ // detach
101
+ this.attachedElements.forEach((el) => {
102
+ detachTrack(this._mediaStreamTrack, el);
103
+ });
104
+ this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
105
+ this._mediaStreamTrack.removeEventListener('mute', this.pauseUpstream);
106
+ this._mediaStreamTrack.removeEventListener('unmute', this.resumeUpstream);
107
+ if (!this.providedByUser) {
108
+ this._mediaStreamTrack.stop();
109
+ }
110
+ }
111
+
112
+ this.mediaStream = new MediaStream([newTrack]);
113
+ if (newTrack) {
114
+ newTrack.addEventListener('ended', this.handleEnded);
115
+ // when underlying track emits mute, it indicates that the device is unable
116
+ // to produce media. In this case we'll need to signal with remote that
117
+ // the track is "muted"
118
+ // note this is different from LocalTrack.mute because we do not want to
119
+ // touch MediaStreamTrack.enabled
120
+ newTrack.addEventListener('mute', () => {
121
+ log.info('pausing upstream due to device mute');
122
+ this.pauseUpstream();
123
+ });
124
+ newTrack.addEventListener('unmute', this.resumeUpstream);
125
+ this.constraints = newTrack.getConstraints();
126
+ }
127
+ if (this.sender) {
128
+ await this.sender.replaceTrack(newTrack);
129
+ }
130
+ this._mediaStreamTrack = newTrack;
131
+ if (newTrack) {
132
+ // sync muted state with the enabled state of the newly provided track
133
+ this._mediaStreamTrack.enabled = !this.isMuted;
134
+ // when a valid track is replace, we'd want to start producing
135
+ await this.resumeUpstream();
136
+ this.attachedElements.forEach((el) => {
137
+ attachToElement(newTrack, el);
138
+ });
139
+ }
140
+ }
141
+
91
142
  async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
92
143
  if (this.kind === Track.Kind.Audio) {
93
144
  throw new Error('cannot get dimensions for audio tracks');
@@ -133,37 +184,12 @@ export default abstract class LocalTrack extends Track {
133
184
  throw new TrackInvalidError('unable to replace an unpublished track');
134
185
  }
135
186
 
136
- // detach
137
- this.attachedElements.forEach((el) => {
138
- detachTrack(this._mediaStreamTrack, el);
139
- });
140
- this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
141
- // on Safari, the old audio track must be stopped before attempting to acquire
142
- // the new track, otherwise the new track will stop with
143
- // 'A MediaStreamTrack ended due to a capture failure`
144
- if (!this.providedByUser) {
145
- this._mediaStreamTrack.stop();
146
- }
147
-
148
- track.addEventListener('ended', this.handleEnded);
149
187
  log.debug('replace MediaStreamTrack');
150
-
151
- if (this.sender) {
152
- await this.sender.replaceTrack(track);
153
- }
154
- this._mediaStreamTrack = track;
155
-
156
- // sync muted state with the enabled state of the newly provided track
157
- this._mediaStreamTrack.enabled = !this.isMuted;
158
-
159
- await this.resumeUpstream();
160
-
161
- this.attachedElements.forEach((el) => {
162
- attachToElement(track, el);
163
- });
164
-
165
- this.mediaStream = new MediaStream([track]);
188
+ this.setMediaStreamTrack(track);
189
+ // this must be synced *after* setting mediaStreamTrack above, since it relies
190
+ // on the previous state in order to cleanup
166
191
  this.providedByUser = userProvidedTrack;
192
+
167
193
  if (this.processor) {
168
194
  await this.stopProcessor();
169
195
  }
@@ -187,7 +213,8 @@ export default abstract class LocalTrack extends Track {
187
213
  streamConstraints.audio = constraints;
188
214
  }
189
215
 
190
- // detach
216
+ // these steps are duplicated from setMediaStreamTrack because we must stop
217
+ // the previous tracks before new tracks can be acquired
191
218
  this.attachedElements.forEach((el) => {
192
219
  detachTrack(this.mediaStreamTrack, el);
193
220
  });
@@ -203,16 +230,7 @@ export default abstract class LocalTrack extends Track {
203
230
  newTrack.addEventListener('ended', this.handleEnded);
204
231
  log.debug('re-acquired MediaStreamTrack');
205
232
 
206
- if (this.sender) {
207
- // Track can be restarted after it's unpublished
208
- await this.sender.replaceTrack(newTrack);
209
- }
210
-
211
- this._mediaStreamTrack = newTrack;
212
-
213
- await this.resumeUpstream();
214
-
215
- this.mediaStream = mediaStream;
233
+ this.setMediaStreamTrack(newTrack);
216
234
  this.constraints = constraints;
217
235
  if (this.processor) {
218
236
  const processor = this.processor;
@@ -315,6 +333,7 @@ export default abstract class LocalTrack extends Track {
315
333
  this._isUpstreamPaused = false;
316
334
  this.emit(TrackEvent.UpstreamResumed, this);
317
335
 
336
+ // this operation is noop if mediastreamtrack is already being sent
318
337
  await this.sender.replaceTrack(this._mediaStreamTrack);
319
338
  } finally {
320
339
  unlock();