livekit-client 2.0.9 → 2.1.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 (66) 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 +203 -140
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +3792 -6746
  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 +2 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/ConnectionCheck.d.ts +3 -2
  12. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  13. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  14. package/dist/src/index.d.ts +3 -2
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts +1 -0
  17. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  18. package/dist/src/room/events.d.ts +5 -1
  19. package/dist/src/room/events.d.ts.map +1 -1
  20. package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
  21. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  22. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  23. package/dist/src/room/stats.d.ts +6 -3
  24. package/dist/src/room/stats.d.ts.map +1 -1
  25. package/dist/src/room/track/LocalAudioTrack.d.ts +7 -0
  26. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  27. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalTrackPublication.d.ts +3 -2
  29. package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  31. package/dist/src/room/track/Track.d.ts +2 -1
  32. package/dist/src/room/track/Track.d.ts.map +1 -1
  33. package/dist/src/room/track/options.d.ts +1 -1
  34. package/dist/src/room/track/options.d.ts.map +1 -1
  35. package/dist/src/room/utils.d.ts +3 -0
  36. package/dist/src/room/utils.d.ts.map +1 -1
  37. package/dist/ts4.2/src/api/SignalClient.d.ts +2 -1
  38. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +3 -2
  39. package/dist/ts4.2/src/index.d.ts +3 -2
  40. package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
  41. package/dist/ts4.2/src/room/events.d.ts +5 -1
  42. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +1 -0
  43. package/dist/ts4.2/src/room/stats.d.ts +6 -3
  44. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +7 -0
  45. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +3 -2
  46. package/dist/ts4.2/src/room/track/Track.d.ts +2 -1
  47. package/dist/ts4.2/src/room/track/options.d.ts +1 -1
  48. package/dist/ts4.2/src/room/utils.d.ts +3 -0
  49. package/package.json +10 -10
  50. package/src/api/SignalClient.ts +9 -0
  51. package/src/connectionHelper/ConnectionCheck.ts +6 -3
  52. package/src/e2ee/worker/FrameCryptor.ts +0 -1
  53. package/src/e2ee/worker/e2ee.worker.ts +3 -1
  54. package/src/index.ts +3 -0
  55. package/src/room/RTCEngine.ts +11 -1
  56. package/src/room/events.ts +5 -0
  57. package/src/room/participant/LocalParticipant.ts +14 -0
  58. package/src/room/participant/publishUtils.ts +2 -1
  59. package/src/room/stats.ts +9 -3
  60. package/src/room/track/LocalAudioTrack.ts +40 -0
  61. package/src/room/track/LocalTrack.ts +4 -2
  62. package/src/room/track/LocalTrackPublication.ts +28 -2
  63. package/src/room/track/LocalVideoTrack.ts +16 -7
  64. package/src/room/track/Track.ts +13 -0
  65. package/src/room/track/options.ts +22 -1
  66. package/src/room/utils.ts +3 -0
@@ -1,4 +1,4 @@
1
- import { StreamState as ProtoStreamState, TrackSource, TrackType } from '@livekit/protocol';
1
+ import { AudioTrackFeature, StreamState as ProtoStreamState, TrackSource, TrackType } from '@livekit/protocol';
2
2
  import type TypedEventEmitter from 'typed-emitter';
3
3
  import type { SignalClient } from '../../api/SignalClient';
4
4
  import { StructuredLogger } from '../../logger';
@@ -136,6 +136,7 @@ export type TrackEventCallbacks = {
136
136
  upstreamPaused: (track: any) => void;
137
137
  upstreamResumed: (track: any) => void;
138
138
  trackProcessorUpdate: (processor?: TrackProcessor<Track.Kind, any>) => void;
139
+ audioTrackFeatureUpdate: (track: any, feature: AudioTrackFeature, enabled: boolean) => void;
139
140
  };
140
141
  export {};
141
142
  //# sourceMappingURL=Track.d.ts.map
@@ -263,7 +263,7 @@ export declare function isBackupCodec(codec: string): codec is BackupVideoCodec;
263
263
  /**
264
264
  * scalability modes for svc.
265
265
  */
266
- export type ScalabilityMode = 'L1T3' | 'L2T3' | 'L2T3_KEY' | 'L3T3' | 'L3T3_KEY';
266
+ export type ScalabilityMode = 'L1T1' | 'L1T2' | 'L1T3' | 'L2T1' | 'L2T1h' | 'L2T1_KEY' | 'L2T2' | 'L2T2h' | 'L2T2_KEY' | 'L2T3' | 'L2T3h' | 'L2T3_KEY' | 'L3T1' | 'L3T1h' | 'L3T1_KEY' | 'L3T2' | 'L3T2h' | 'L3T2_KEY' | 'L3T3' | 'L3T3h' | 'L3T3_KEY';
267
267
  export declare namespace AudioPresets {
268
268
  const telephone: AudioPreset;
269
269
  const speech: AudioPreset;
@@ -79,6 +79,9 @@ export declare function createAudioAnalyser(track: LocalAudioTrack | RemoteAudio
79
79
  analyser: AnalyserNode;
80
80
  cleanup: () => Promise<void>;
81
81
  };
82
+ /**
83
+ * @internal
84
+ */
82
85
  export declare class Mutex {
83
86
  private _locking;
84
87
  private _locks;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.0.9",
3
+ "version": "2.1.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",
@@ -36,7 +36,7 @@
36
36
  "author": "David Zhao <david@davidzhao.com>",
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
- "@livekit/protocol": "1.10.4",
39
+ "@livekit/protocol": "1.13.0",
40
40
  "events": "^3.3.0",
41
41
  "loglevel": "^1.8.0",
42
42
  "sdp-transform": "^2.14.1",
@@ -46,8 +46,8 @@
46
46
  "webrtc-adapter": "^8.1.1"
47
47
  },
48
48
  "devDependencies": {
49
- "@babel/core": "7.23.9",
50
- "@babel/preset-env": "7.23.9",
49
+ "@babel/core": "7.24.4",
50
+ "@babel/preset-env": "7.24.4",
51
51
  "@bufbuild/protoc-gen-es": "^1.3.0",
52
52
  "@changesets/cli": "2.27.1",
53
53
  "@livekit/changesets-changelog-github": "^0.0.4",
@@ -65,23 +65,23 @@
65
65
  "@typescript-eslint/eslint-plugin": "5.62.0",
66
66
  "@typescript-eslint/parser": "5.62.0",
67
67
  "downlevel-dts": "^0.11.0",
68
- "eslint": "8.56.0",
69
- "eslint-config-airbnb-typescript": "17.1.0",
68
+ "eslint": "8.57.0",
69
+ "eslint-config-airbnb-typescript": "18.0.0",
70
70
  "eslint-config-prettier": "9.1.0",
71
71
  "eslint-plugin-ecmascript-compat": "^3.0.0",
72
72
  "eslint-plugin-import": "2.29.1",
73
73
  "gh-pages": "6.1.1",
74
74
  "jsdom": "^24.0.0",
75
75
  "prettier": "^3.0.0",
76
- "rollup": "4.9.6",
76
+ "rollup": "4.14.0",
77
77
  "rollup-plugin-delete": "^2.0.0",
78
78
  "rollup-plugin-re": "1.0.7",
79
79
  "rollup-plugin-typescript2": "0.36.0",
80
80
  "size-limit": "^8.2.4",
81
- "typedoc": "0.25.7",
81
+ "typedoc": "0.25.12",
82
82
  "typedoc-plugin-no-inherit": "1.4.0",
83
- "typescript": "5.3.3",
84
- "vite": "5.0.12",
83
+ "typescript": "5.4.3",
84
+ "vite": "5.0.13",
85
85
  "vitest": "^1.0.0"
86
86
  },
87
87
  "scripts": {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AddTrackRequest,
3
+ AudioTrackFeature,
3
4
  ClientInfo,
4
5
  ConnectionQualityUpdate,
5
6
  DisconnectReason,
@@ -27,6 +28,7 @@ import {
27
28
  TrackPublishedResponse,
28
29
  TrackUnpublishedResponse,
29
30
  TrickleRequest,
31
+ UpdateLocalAudioTrack,
30
32
  UpdateParticipantMetadata,
31
33
  UpdateSubscription,
32
34
  UpdateTrackSettings,
@@ -583,6 +585,13 @@ export class SignalClient {
583
585
  ]);
584
586
  }
585
587
 
588
+ sendUpdateLocalAudioTrack(trackSid: string, features: AudioTrackFeature[]) {
589
+ return this.sendRequest({
590
+ case: 'updateAudioTrack',
591
+ value: new UpdateLocalAudioTrack({ trackSid, features }),
592
+ });
593
+ }
594
+
586
595
  sendLeave() {
587
596
  return this.sendRequest({
588
597
  case: 'leave',
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import type TypedEmitter from 'typed-emitter';
3
+ import type { CheckInfo, CheckerOptions, InstantiableCheck } from './checks/Checker';
3
4
  import { CheckStatus, Checker } from './checks/Checker';
4
- import type { CheckInfo, InstantiableCheck } from './checks/Checker';
5
5
  import { PublishAudioCheck } from './checks/publishAudio';
6
6
  import { PublishVideoCheck } from './checks/publishVideo';
7
7
  import { ReconnectCheck } from './checks/reconnect';
@@ -16,12 +16,15 @@ export class ConnectionCheck extends (EventEmitter as new () => TypedEmitter<Con
16
16
 
17
17
  url: string;
18
18
 
19
+ options: CheckerOptions = {};
20
+
19
21
  private checkResults: Map<number, CheckInfo> = new Map();
20
22
 
21
- constructor(url: string, token: string) {
23
+ constructor(url: string, token: string, options: CheckerOptions = {}) {
22
24
  super();
23
25
  this.url = url;
24
26
  this.token = token;
27
+ this.options = options;
25
28
  }
26
29
 
27
30
  private getNextCheckId() {
@@ -50,7 +53,7 @@ export class ConnectionCheck extends (EventEmitter as new () => TypedEmitter<Con
50
53
 
51
54
  async createAndRunCheck<T extends Checker>(check: InstantiableCheck<T>) {
52
55
  const checkId = this.getNextCheckId();
53
- const test = new check(this.url, this.token);
56
+ const test = new check(this.url, this.token, this.options);
54
57
  const handleUpdate = (info: CheckInfo) => {
55
58
  this.updateCheck(checkId, info);
56
59
  };
@@ -607,7 +607,6 @@ export class FrameCryptor extends BaseFrameCryptor {
607
607
  if (this.rtpMap.size === 0) {
608
608
  return undefined;
609
609
  }
610
- // @ts-expect-error payloadType is not yet part of the typescript definition and currently not supported in Safari
611
610
  const payloadType = frame.getMetadata().payloadType;
612
611
  const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
613
612
  return codec;
@@ -245,9 +245,11 @@ function handleSifTrailer(trailer: Uint8Array) {
245
245
  if (self.RTCTransformEvent) {
246
246
  workerLogger.debug('setup transform event');
247
247
  // @ts-ignore
248
- self.onrtctransform = (event) => {
248
+ self.onrtctransform = (event: RTCTransformEvent) => {
249
+ // @ts-ignore .transformer property is part of RTCTransformEvent
249
250
  const transformer = event.transformer;
250
251
  workerLogger.debug('transformer', transformer);
252
+ // @ts-ignore monkey patching non standard flag
251
253
  transformer.handled = true;
252
254
  const { kind, participantIdentity, trackId, codec } = transformer.options;
253
255
  const cryptor = getTrackCryptor(participantIdentity, trackId);
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import { TrackPublication } from './room/track/TrackPublication';
20
20
  import type { LiveKitReactNativeInfo } from './room/types';
21
21
  import type { AudioAnalyserOptions } from './room/utils';
22
22
  import {
23
+ Mutex,
23
24
  createAudioAnalyser,
24
25
  getEmptyAudioStreamTrack,
25
26
  getEmptyVideoStreamTrack,
@@ -32,6 +33,7 @@ import {
32
33
  import { getBrowser } from './utils/browserParser';
33
34
 
34
35
  export * from './connectionHelper/ConnectionCheck';
36
+ export * from './connectionHelper/checks/Checker';
35
37
  export * from './e2ee';
36
38
  export * from './options';
37
39
  export * from './room/errors';
@@ -79,6 +81,7 @@ export {
79
81
  supportsAdaptiveStream,
80
82
  supportsDynacast,
81
83
  supportsVP9,
84
+ Mutex,
82
85
  };
83
86
  export type {
84
87
  AudioAnalyserOptions,
@@ -169,6 +169,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
169
169
 
170
170
  private loggerOptions: LoggerOptions;
171
171
 
172
+ private publisherConnectionPromise: Promise<void> | undefined;
173
+
172
174
  constructor(private options: InternalRoomOptions) {
173
175
  super();
174
176
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine);
@@ -398,6 +400,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
398
400
  this.pcManager.onDataChannel = this.handleDataChannel;
399
401
  this.pcManager.onStateChange = async (connectionState, publisherState, subscriberState) => {
400
402
  this.log.debug(`primary PC state changed ${connectionState}`, this.logContext);
403
+
404
+ if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
405
+ // reset publisher connection promise
406
+ this.publisherConnectionPromise = undefined;
407
+ }
401
408
  if (connectionState === PCTransportState.CONNECTED) {
402
409
  const shouldEmit = this.pcState === PCState.New;
403
410
  this.pcState = PCState.Connected;
@@ -1179,7 +1186,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1179
1186
  }
1180
1187
 
1181
1188
  private async ensurePublisherConnected(kind: DataPacket_Kind) {
1182
- await this.ensureDataTransportConnected(kind, false);
1189
+ if (!this.publisherConnectionPromise) {
1190
+ this.publisherConnectionPromise = this.ensureDataTransportConnected(kind, false);
1191
+ }
1192
+ await this.publisherConnectionPromise;
1183
1193
  }
1184
1194
 
1185
1195
  /* @internal */
@@ -557,4 +557,9 @@ export enum TrackEvent {
557
557
  * @internal
558
558
  */
559
559
  TrackProcessorUpdate = 'trackProcessorUpdate',
560
+
561
+ /**
562
+ * @internal
563
+ */
564
+ AudioTrackFeatureUpdate = 'audioTrackFeatureUpdate',
560
565
  }
@@ -685,6 +685,7 @@ export default class LocalParticipant extends Participant {
685
685
  track.on(TrackEvent.Ended, this.handleTrackEnded);
686
686
  track.on(TrackEvent.UpstreamPaused, this.onTrackUpstreamPaused);
687
687
  track.on(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
688
+ track.on(TrackEvent.AudioTrackFeatureUpdate, this.onTrackFeatureUpdate);
688
689
 
689
690
  // create track publication from track
690
691
  const req = new AddTrackRequest({
@@ -1037,6 +1038,7 @@ export default class LocalParticipant extends Participant {
1037
1038
  track.off(TrackEvent.Ended, this.handleTrackEnded);
1038
1039
  track.off(TrackEvent.UpstreamPaused, this.onTrackUpstreamPaused);
1039
1040
  track.off(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
1041
+ track.off(TrackEvent.AudioTrackFeatureUpdate, this.onTrackFeatureUpdate);
1040
1042
 
1041
1043
  if (stopOnUnpublish === undefined) {
1042
1044
  stopOnUnpublish = this.roomOptions?.stopLocalTrackOnUnpublish ?? true;
@@ -1293,6 +1295,18 @@ export default class LocalParticipant extends Participant {
1293
1295
  this.onTrackMuted(track, track.isMuted);
1294
1296
  };
1295
1297
 
1298
+ private onTrackFeatureUpdate = (track: LocalAudioTrack) => {
1299
+ const pub = this.audioTrackPublications.get(track.sid!);
1300
+ if (!pub) {
1301
+ this.log.warn(
1302
+ `Could not update local audio track settings, missing publication for track ${track.sid}`,
1303
+ this.logContext,
1304
+ );
1305
+ return;
1306
+ }
1307
+ this.engine.client.sendUpdateLocalAudioTrack(pub.trackSid, pub.getTrackFeatures());
1308
+ };
1309
+
1296
1310
  private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
1297
1311
  if (!this.roomOptions?.dynacast) {
1298
1312
  return;
@@ -150,11 +150,12 @@ export function computeVideoEncodings(
150
150
  isSafari() ||
151
151
  (browser?.name === 'Chrome' && compareVersions(browser?.version, '113') < 0)
152
152
  ) {
153
+ const bitratesRatio = sm.suffix == 'h' ? 2 : 3;
153
154
  for (let i = 0; i < sm.spatial; i += 1) {
154
155
  // in legacy SVC, scaleResolutionDownBy cannot be set
155
156
  encodings.push({
156
157
  rid: videoRids[2 - i],
157
- maxBitrate: videoEncoding.maxBitrate / 3 ** i,
158
+ maxBitrate: videoEncoding.maxBitrate / bitratesRatio ** i,
158
159
  maxFramerate: original.encoding.maxFramerate,
159
160
  });
160
161
  }
package/src/room/stats.ts CHANGED
@@ -42,14 +42,20 @@ export interface VideoSenderStats extends SenderStats {
42
42
 
43
43
  frameHeight: number;
44
44
 
45
+ framesPerSecond: number;
46
+
45
47
  framesSent: number;
46
48
 
47
49
  // bandwidth, cpu, other, none
48
- qualityLimitationReason: string;
50
+ qualityLimitationReason?: string;
51
+
52
+ qualityLimitationDurations?: Record<string, number>;
53
+
54
+ qualityLimitationResolutionChanges?: number;
49
55
 
50
- qualityLimitationResolutionChanges: number;
56
+ retransmittedPacketsSent?: number;
51
57
 
52
- retransmittedPacketsSent: number;
58
+ targetBitrate: number;
53
59
  }
54
60
 
55
61
  interface ReceiverStats {
@@ -1,3 +1,4 @@
1
+ import { AudioTrackFeature } from '@livekit/protocol';
1
2
  import { TrackEvent } from '../events';
2
3
  import { computeBitrate, monitorFrequency } from '../stats';
3
4
  import type { AudioSenderStats } from '../stats';
@@ -15,8 +16,17 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
15
16
 
16
17
  private prevStats?: AudioSenderStats;
17
18
 
19
+ private isKrispNoiseFilterEnabled = false;
20
+
18
21
  protected processor?: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions> | undefined;
19
22
 
23
+ /**
24
+ * boolean indicating whether enhanced noise cancellation is currently being used on this track
25
+ */
26
+ get enhancedNoiseCancellation() {
27
+ return this.isKrispNoiseFilterEnabled;
28
+ }
29
+
20
30
  /**
21
31
  *
22
32
  * @param mediaTrack
@@ -152,6 +162,28 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
152
162
  this.prevStats = stats;
153
163
  };
154
164
 
165
+ private handleKrispNoiseFilterEnable = () => {
166
+ this.isKrispNoiseFilterEnabled = true;
167
+ this.log.debug(`Krisp noise filter enabled`, this.logContext);
168
+ this.emit(
169
+ TrackEvent.AudioTrackFeatureUpdate,
170
+ this,
171
+ AudioTrackFeature.TF_ENHANCED_NOISE_CANCELLATION,
172
+ true,
173
+ );
174
+ };
175
+
176
+ private handleKrispNoiseFilterDisable = () => {
177
+ this.isKrispNoiseFilterEnabled = false;
178
+ this.log.debug(`Krisp noise filter disabled`, this.logContext);
179
+ this.emit(
180
+ TrackEvent.AudioTrackFeatureUpdate,
181
+ this,
182
+ AudioTrackFeature.TF_ENHANCED_NOISE_CANCELLATION,
183
+ false,
184
+ );
185
+ };
186
+
155
187
  async setProcessor(processor: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>) {
156
188
  const unlock = await this.processorLock.lock();
157
189
  try {
@@ -175,6 +207,14 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
175
207
  this.processor = processor;
176
208
  if (this.processor.processedTrack) {
177
209
  await this.sender?.replaceTrack(this.processor.processedTrack);
210
+ this.processor.processedTrack.addEventListener(
211
+ 'enable-lk-krisp-noise-filter',
212
+ this.handleKrispNoiseFilterEnable,
213
+ );
214
+ this.processor.processedTrack.addEventListener(
215
+ 'disable-lk-krisp-noise-filter',
216
+ this.handleKrispNoiseFilterDisable,
217
+ );
178
218
  }
179
219
  this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
180
220
  } finally {
@@ -448,6 +448,10 @@ export default abstract class LocalTrack<
448
448
  const unlock = await this.processorLock.lock();
449
449
  try {
450
450
  this.log.debug('setting up processor', this.logContext);
451
+
452
+ this.processorElement =
453
+ this.processorElement ?? (document.createElement(this.kind) as HTMLMediaElement);
454
+
451
455
  const processorOptions = {
452
456
  kind: this.kind,
453
457
  track: this._mediaStreamTrack,
@@ -461,8 +465,6 @@ export default abstract class LocalTrack<
461
465
  if (this.kind === 'unknown') {
462
466
  throw TypeError('cannot set processor on track of unknown kind');
463
467
  }
464
- this.processorElement =
465
- this.processorElement ?? (document.createElement(this.kind) as HTMLMediaElement);
466
468
 
467
469
  attachToElement(this._mediaStreamTrack, this.processorElement);
468
470
  this.processorElement.muted = true;
@@ -1,7 +1,7 @@
1
- import type { TrackInfo } from '@livekit/protocol';
1
+ import { AudioTrackFeature, TrackInfo } from '@livekit/protocol';
2
2
  import { TrackEvent } from '../events';
3
3
  import type { LoggerOptions } from '../types';
4
- import type LocalAudioTrack from './LocalAudioTrack';
4
+ import LocalAudioTrack from './LocalAudioTrack';
5
5
  import type LocalTrack from './LocalTrack';
6
6
  import type LocalVideoTrack from './LocalVideoTrack';
7
7
  import type { Track } from './Track';
@@ -82,6 +82,32 @@ export default class LocalTrackPublication extends TrackPublication {
82
82
  await this.track?.resumeUpstream();
83
83
  }
84
84
 
85
+ getTrackFeatures() {
86
+ if (this.track instanceof LocalAudioTrack) {
87
+ const settings = this.track!.mediaStreamTrack.getSettings();
88
+ const features: Set<AudioTrackFeature> = new Set();
89
+ if (settings.autoGainControl) {
90
+ features.add(AudioTrackFeature.TF_AUTO_GAIN_CONTROL);
91
+ }
92
+ if (settings.echoCancellation) {
93
+ features.add(AudioTrackFeature.TF_ECHO_CANCELLATION);
94
+ }
95
+ if (settings.noiseSuppression) {
96
+ features.add(AudioTrackFeature.TF_NOISE_SUPPRESSION);
97
+ }
98
+ if (settings.channelCount && settings.channelCount > 1) {
99
+ features.add(AudioTrackFeature.TF_STEREO);
100
+ }
101
+ if (!this.options?.dtx) {
102
+ features.add(AudioTrackFeature.TF_STEREO);
103
+ }
104
+ if (this.track.enhancedNoiseCancellation) {
105
+ features.add(AudioTrackFeature.TF_ENHANCED_NOISE_CANCELLATION);
106
+ }
107
+ return Array.from(features.values());
108
+ } else return [];
109
+ }
110
+
85
111
  handleTrackEnded = () => {
86
112
  this.emit(TrackEvent.Ended);
87
113
  };
@@ -180,17 +180,20 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
180
180
  streamId: v.id,
181
181
  frameHeight: v.frameHeight,
182
182
  frameWidth: v.frameWidth,
183
+ framesPerSecond: v.framesPerSecond,
184
+ framesSent: v.framesSent,
183
185
  firCount: v.firCount,
184
186
  pliCount: v.pliCount,
185
187
  nackCount: v.nackCount,
186
188
  packetsSent: v.packetsSent,
187
189
  bytesSent: v.bytesSent,
188
- framesSent: v.framesSent,
189
- timestamp: v.timestamp,
190
- rid: v.rid ?? v.id,
191
- retransmittedPacketsSent: v.retransmittedPacketsSent,
192
190
  qualityLimitationReason: v.qualityLimitationReason,
191
+ qualityLimitationDurations: v.qualityLimitationDurations,
193
192
  qualityLimitationResolutionChanges: v.qualityLimitationResolutionChanges,
193
+ rid: v.rid ?? v.id,
194
+ retransmittedPacketsSent: v.retransmittedPacketsSent,
195
+ targetBitrate: v.targetBitrate,
196
+ timestamp: v.timestamp,
194
197
  };
195
198
 
196
199
  // locate the appropriate remote-inbound-rtp item
@@ -205,6 +208,8 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
205
208
  }
206
209
  });
207
210
 
211
+ // make sure highest res layer is always first
212
+ items.sort((a, b) => (b.frameWidth ?? 0) - (a.frameWidth ?? 0));
208
213
  return items;
209
214
  }
210
215
 
@@ -567,13 +572,17 @@ export function videoLayersFromEncodings(
567
572
  const encodingSM = encodings[0].scalabilityMode as string;
568
573
  const sm = new ScalabilityMode(encodingSM);
569
574
  const layers = [];
575
+ const resRatio = sm.suffix == 'h' ? 1.5 : 2;
576
+ const bitratesRatio = sm.suffix == 'h' ? 2 : 3;
570
577
  for (let i = 0; i < sm.spatial; i += 1) {
571
578
  layers.push(
572
579
  new VideoLayer({
573
580
  quality: VideoQuality.HIGH - i,
574
- width: Math.ceil(width / 2 ** i),
575
- height: Math.ceil(height / 2 ** i),
576
- bitrate: encodings[0].maxBitrate ? Math.ceil(encodings[0].maxBitrate / 3 ** i) : 0,
581
+ width: Math.ceil(width / resRatio ** i),
582
+ height: Math.ceil(height / resRatio ** i),
583
+ bitrate: encodings[0].maxBitrate
584
+ ? Math.ceil(encodings[0].maxBitrate / bitratesRatio ** i)
585
+ : 0,
577
586
  ssrc: 0,
578
587
  }),
579
588
  );
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AudioTrackFeature,
2
3
  VideoQuality as ProtoQuality,
3
4
  StreamState as ProtoStreamState,
4
5
  TrackSource,
@@ -300,6 +301,17 @@ export abstract class Track<
300
301
 
301
302
  protected async handleAppVisibilityChanged() {
302
303
  this.isInBackground = document.visibilityState === 'hidden';
304
+ if (!this.isInBackground && this.kind === Track.Kind.Video) {
305
+ setTimeout(
306
+ () =>
307
+ this.attachedElements.forEach((el) =>
308
+ el.play().catch(() => {
309
+ /** catch clause necessary for Safari */
310
+ }),
311
+ ),
312
+ 0,
313
+ );
314
+ }
303
315
  }
304
316
 
305
317
  protected addAppVisibilityListener() {
@@ -504,4 +516,5 @@ export type TrackEventCallbacks = {
504
516
  upstreamPaused: (track: any) => void;
505
517
  upstreamResumed: (track: any) => void;
506
518
  trackProcessorUpdate: (processor?: TrackProcessor<Track.Kind, any>) => void;
519
+ audioTrackFeatureUpdate: (track: any, feature: AudioTrackFeature, enabled: boolean) => void;
507
520
  };
@@ -350,7 +350,28 @@ export function isBackupCodec(codec: string): codec is BackupVideoCodec {
350
350
  /**
351
351
  * scalability modes for svc.
352
352
  */
353
- export type ScalabilityMode = 'L1T3' | 'L2T3' | 'L2T3_KEY' | 'L3T3' | 'L3T3_KEY';
353
+ export type ScalabilityMode =
354
+ | 'L1T1'
355
+ | 'L1T2'
356
+ | 'L1T3'
357
+ | 'L2T1'
358
+ | 'L2T1h'
359
+ | 'L2T1_KEY'
360
+ | 'L2T2'
361
+ | 'L2T2h'
362
+ | 'L2T2_KEY'
363
+ | 'L2T3'
364
+ | 'L2T3h'
365
+ | 'L2T3_KEY'
366
+ | 'L3T1'
367
+ | 'L3T1h'
368
+ | 'L3T1_KEY'
369
+ | 'L3T2'
370
+ | 'L3T2h'
371
+ | 'L3T2_KEY'
372
+ | 'L3T3'
373
+ | 'L3T3h'
374
+ | 'L3T3_KEY';
354
375
 
355
376
  export namespace AudioPresets {
356
377
  export const telephone: AudioPreset = {
package/src/room/utils.ts CHANGED
@@ -447,6 +447,9 @@ export function createAudioAnalyser(
447
447
  return { calculateVolume, analyser, cleanup };
448
448
  }
449
449
 
450
+ /**
451
+ * @internal
452
+ */
450
453
  export class Mutex {
451
454
  private _locking: Promise<void>;
452
455