livekit-client 2.0.10 → 2.1.1

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 (67) 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 +12826 -15828
  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/PCTransport.d.ts.map +1 -1
  17. package/dist/src/room/RTCEngine.d.ts +3 -3
  18. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  19. package/dist/src/room/events.d.ts +5 -1
  20. package/dist/src/room/events.d.ts.map +1 -1
  21. package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
  22. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  23. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  24. package/dist/src/room/track/LocalAudioTrack.d.ts +7 -0
  25. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  26. package/dist/src/room/track/LocalTrackPublication.d.ts +3 -2
  27. package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  29. package/dist/src/room/track/Track.d.ts +2 -1
  30. package/dist/src/room/track/Track.d.ts.map +1 -1
  31. package/dist/src/room/track/options.d.ts +1 -1
  32. package/dist/src/room/track/options.d.ts.map +1 -1
  33. package/dist/src/room/utils.d.ts +4 -1
  34. package/dist/src/room/utils.d.ts.map +1 -1
  35. package/dist/src/utils/browserParser.d.ts +1 -0
  36. package/dist/src/utils/browserParser.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 +3 -3
  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/track/LocalAudioTrack.d.ts +7 -0
  44. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +3 -2
  45. package/dist/ts4.2/src/room/track/Track.d.ts +2 -1
  46. package/dist/ts4.2/src/room/track/options.d.ts +1 -1
  47. package/dist/ts4.2/src/room/utils.d.ts +4 -1
  48. package/dist/ts4.2/src/utils/browserParser.d.ts +1 -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/PCTransport.ts +0 -2
  56. package/src/room/RTCEngine.ts +18 -62
  57. package/src/room/events.ts +5 -0
  58. package/src/room/participant/LocalParticipant.ts +17 -5
  59. package/src/room/participant/publishUtils.ts +2 -1
  60. package/src/room/track/LocalAudioTrack.ts +40 -0
  61. package/src/room/track/LocalTrackPublication.ts +28 -2
  62. package/src/room/track/LocalVideoTrack.ts +7 -3
  63. package/src/room/track/Track.ts +13 -0
  64. package/src/room/track/options.ts +22 -1
  65. package/src/room/utils.ts +32 -27
  66. package/src/utils/browserParser.test.ts +4 -0
  67. package/src/utils/browserParser.ts +11 -1
@@ -1,6 +1,6 @@
1
- import type { TrackInfo } from '@livekit/protocol';
1
+ import { AudioTrackFeature, TrackInfo } from '@livekit/protocol';
2
2
  import type { LoggerOptions } from '../types';
3
- import type LocalAudioTrack from './LocalAudioTrack';
3
+ import LocalAudioTrack from './LocalAudioTrack';
4
4
  import type LocalTrack from './LocalTrack';
5
5
  import type LocalVideoTrack from './LocalVideoTrack';
6
6
  import type { Track } from './Track';
@@ -34,6 +34,7 @@ export default class LocalTrackPublication extends TrackPublication {
34
34
  * and signals "unmuted" event to other participants (unless the track is explicitly muted)
35
35
  */
36
36
  resumeUpstream(): Promise<void>;
37
+ getTrackFeatures(): AudioTrackFeature[];
37
38
  handleTrackEnded: () => void;
38
39
  }
39
40
  //# sourceMappingURL=LocalTrackPublication.d.ts.map
@@ -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;
@@ -15,13 +15,13 @@ export declare function supportsAV1(): boolean;
15
15
  export declare function supportsVP9(): boolean;
16
16
  export declare function isSVCCodec(codec?: string): boolean;
17
17
  export declare function supportsSetSinkId(elm?: HTMLMediaElement): boolean;
18
- export declare function supportsSetCodecPreferences(transceiver: RTCRtpTransceiver): boolean;
19
18
  export declare function isBrowserSupported(): boolean;
20
19
  export declare function isFireFox(): boolean;
21
20
  export declare function isChromiumBased(): boolean;
22
21
  export declare function isSafari(): boolean;
23
22
  export declare function isSafari17(): boolean;
24
23
  export declare function isMobile(): boolean;
24
+ export declare function isE2EESimulcastSupported(): boolean | undefined;
25
25
  export declare function isWeb(): boolean;
26
26
  export declare function isReactNative(): boolean;
27
27
  export declare function isCloud(serverUrl: URL): boolean;
@@ -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;
@@ -4,6 +4,7 @@ export type BrowserDetails = {
4
4
  name: DetectableBrowser;
5
5
  version: string;
6
6
  os?: DetectableOS;
7
+ osVersion?: string;
7
8
  };
8
9
  /**
9
10
  * @internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.0.10",
3
+ "version": "2.1.1",
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,
@@ -474,8 +474,6 @@ export default class PCTransport extends EventEmitter {
474
474
  await this.pc.setLocalDescription(sd);
475
475
  }
476
476
  } catch (e) {
477
- // this error cannot always be caught.
478
- // If the local description has a setCodecPreferences error, this error will be uncaught
479
477
  let msg = 'unknown error';
480
478
  if (e instanceof Error) {
481
479
  msg = e.message;
@@ -53,22 +53,14 @@ import { EngineEvent } from './events';
53
53
  import CriticalTimers from './timers';
54
54
  import type LocalTrack from './track/LocalTrack';
55
55
  import type LocalTrackPublication from './track/LocalTrackPublication';
56
- import type LocalVideoTrack from './track/LocalVideoTrack';
56
+ import LocalVideoTrack from './track/LocalVideoTrack';
57
57
  import type { SimulcastTrackInfo } from './track/LocalVideoTrack';
58
58
  import type RemoteTrackPublication from './track/RemoteTrackPublication';
59
- import { Track } from './track/Track';
59
+ import type { Track } from './track/Track';
60
60
  import type { TrackPublishOptions, VideoCodec } from './track/options';
61
61
  import { getTrackPublicationInfo } from './track/utils';
62
62
  import type { LoggerOptions } from './types';
63
- import {
64
- Mutex,
65
- isVideoCodec,
66
- isWeb,
67
- sleep,
68
- supportsAddTrack,
69
- supportsSetCodecPreferences,
70
- supportsTransceiver,
71
- } from './utils';
63
+ import { Mutex, isVideoCodec, isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';
72
64
 
73
65
  const lossyDataChannel = '_lossy';
74
66
  const reliableDataChannel = '_reliable';
@@ -169,6 +161,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
169
161
 
170
162
  private loggerOptions: LoggerOptions;
171
163
 
164
+ private publisherConnectionPromise: Promise<void> | undefined;
165
+
172
166
  constructor(private options: InternalRoomOptions) {
173
167
  super();
174
168
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine);
@@ -398,6 +392,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
398
392
  this.pcManager.onDataChannel = this.handleDataChannel;
399
393
  this.pcManager.onStateChange = async (connectionState, publisherState, subscriberState) => {
400
394
  this.log.debug(`primary PC state changed ${connectionState}`, this.logContext);
395
+
396
+ if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
397
+ // reset publisher connection promise
398
+ this.publisherConnectionPromise = undefined;
399
+ }
401
400
  if (connectionState === PCTransportState.CONNECTED) {
402
401
  const shouldEmit = this.pcState === PCState.New;
403
402
  this.pcState = PCState.Connected;
@@ -664,51 +663,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
664
663
  this.updateAndEmitDCBufferStatus(channelKind);
665
664
  };
666
665
 
667
- private setPreferredCodec(
668
- transceiver: RTCRtpTransceiver,
669
- kind: Track.Kind,
670
- videoCodec: VideoCodec,
671
- ) {
672
- if (!('getCapabilities' in RTCRtpReceiver)) {
673
- return;
674
- }
675
- // when setting codec preferences, the capabilites need to be read from the RTCRtpReceiver
676
- const cap = RTCRtpReceiver.getCapabilities(kind);
677
- if (!cap) return;
678
- this.log.debug('get receiver capabilities', { ...this.logContext, cap });
679
- const matched: RTCRtpCodecCapability[] = [];
680
- const partialMatched: RTCRtpCodecCapability[] = [];
681
- const unmatched: RTCRtpCodecCapability[] = [];
682
- cap.codecs.forEach((c) => {
683
- const codec = c.mimeType.toLowerCase();
684
- if (codec === 'audio/opus') {
685
- matched.push(c);
686
- return;
687
- }
688
- const matchesVideoCodec = codec === `video/${videoCodec}`;
689
- if (!matchesVideoCodec) {
690
- unmatched.push(c);
691
- return;
692
- }
693
- // for h264 codecs that have sdpFmtpLine available, use only if the
694
- // profile-level-id is 42e01f for cross-browser compatibility
695
- if (videoCodec === 'h264') {
696
- if (c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
697
- matched.push(c);
698
- } else {
699
- partialMatched.push(c);
700
- }
701
- return;
702
- }
703
-
704
- matched.push(c);
705
- });
706
-
707
- if (supportsSetCodecPreferences(transceiver)) {
708
- transceiver.setCodecPreferences(matched.concat(partialMatched, unmatched));
709
- }
710
- }
711
-
712
666
  async createSender(
713
667
  track: LocalTrack,
714
668
  opts: TrackPublishOptions,
@@ -759,6 +713,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
759
713
  streams.push(track.mediaStream);
760
714
  }
761
715
 
716
+ if (track instanceof LocalVideoTrack) {
717
+ track.codec = opts.videoCodec;
718
+ }
719
+
762
720
  const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly', streams };
763
721
  if (encodings) {
764
722
  transceiverInit.sendEncodings = encodings;
@@ -769,10 +727,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
769
727
  transceiverInit,
770
728
  );
771
729
 
772
- if (track.kind === Track.Kind.Video && opts.videoCodec) {
773
- this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
774
- track.codec = opts.videoCodec;
775
- }
776
730
  return transceiver.sender;
777
731
  }
778
732
 
@@ -797,7 +751,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
797
751
  if (!opts.videoCodec) {
798
752
  return;
799
753
  }
800
- this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
801
754
  track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
802
755
  return transceiver.sender;
803
756
  }
@@ -1179,7 +1132,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1179
1132
  }
1180
1133
 
1181
1134
  private async ensurePublisherConnected(kind: DataPacket_Kind) {
1182
- await this.ensureDataTransportConnected(kind, false);
1135
+ if (!this.publisherConnectionPromise) {
1136
+ this.publisherConnectionPromise = this.ensureDataTransportConnected(kind, false);
1137
+ }
1138
+ await this.publisherConnectionPromise;
1183
1139
  }
1184
1140
 
1185
1141
  /* @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
  }
@@ -40,9 +40,9 @@ import {
40
40
  import type { DataPublishOptions } from '../types';
41
41
  import {
42
42
  Future,
43
+ isE2EESimulcastSupported,
43
44
  isFireFox,
44
45
  isSVCCodec,
45
- isSafari,
46
46
  isSafari17,
47
47
  isWeb,
48
48
  supportsAV1,
@@ -621,10 +621,9 @@ export default class LocalParticipant extends Participant {
621
621
  ...options,
622
622
  };
623
623
 
624
- // disable simulcast if e2ee is set on safari
625
- if (isSafari() && this.roomOptions.e2ee) {
624
+ if (!isE2EESimulcastSupported() && this.roomOptions.e2ee) {
626
625
  this.log.info(
627
- `End-to-end encryption is set up, simulcast publishing will be disabled on Safari`,
626
+ `End-to-end encryption is set up, simulcast publishing will be disabled on Safari versions and iOS browsers running iOS < v17.2`,
628
627
  {
629
628
  ...this.logContext,
630
629
  },
@@ -685,6 +684,7 @@ export default class LocalParticipant extends Participant {
685
684
  track.on(TrackEvent.Ended, this.handleTrackEnded);
686
685
  track.on(TrackEvent.UpstreamPaused, this.onTrackUpstreamPaused);
687
686
  track.on(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
687
+ track.on(TrackEvent.AudioTrackFeatureUpdate, this.onTrackFeatureUpdate);
688
688
 
689
689
  // create track publication from track
690
690
  const req = new AddTrackRequest({
@@ -826,7 +826,6 @@ export default class LocalParticipant extends Participant {
826
826
  ...getLogContextFromTrack(track),
827
827
  codec: updatedCodec,
828
828
  });
829
- /* @ts-ignore */
830
829
  opts.videoCodec = updatedCodec;
831
830
 
832
831
  // recompute encodings since bitrates/etc could have changed
@@ -1037,6 +1036,7 @@ export default class LocalParticipant extends Participant {
1037
1036
  track.off(TrackEvent.Ended, this.handleTrackEnded);
1038
1037
  track.off(TrackEvent.UpstreamPaused, this.onTrackUpstreamPaused);
1039
1038
  track.off(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
1039
+ track.off(TrackEvent.AudioTrackFeatureUpdate, this.onTrackFeatureUpdate);
1040
1040
 
1041
1041
  if (stopOnUnpublish === undefined) {
1042
1042
  stopOnUnpublish = this.roomOptions?.stopLocalTrackOnUnpublish ?? true;
@@ -1293,6 +1293,18 @@ export default class LocalParticipant extends Participant {
1293
1293
  this.onTrackMuted(track, track.isMuted);
1294
1294
  };
1295
1295
 
1296
+ private onTrackFeatureUpdate = (track: LocalAudioTrack) => {
1297
+ const pub = this.audioTrackPublications.get(track.sid!);
1298
+ if (!pub) {
1299
+ this.log.warn(
1300
+ `Could not update local audio track settings, missing publication for track ${track.sid}`,
1301
+ this.logContext,
1302
+ );
1303
+ return;
1304
+ }
1305
+ this.engine.client.sendUpdateLocalAudioTrack(pub.trackSid, pub.getTrackFeatures());
1306
+ };
1307
+
1296
1308
  private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
1297
1309
  if (!this.roomOptions?.dynacast) {
1298
1310
  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
  }
@@ -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 {
@@ -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
  };
@@ -572,13 +572,17 @@ export function videoLayersFromEncodings(
572
572
  const encodingSM = encodings[0].scalabilityMode as string;
573
573
  const sm = new ScalabilityMode(encodingSM);
574
574
  const layers = [];
575
+ const resRatio = sm.suffix == 'h' ? 1.5 : 2;
576
+ const bitratesRatio = sm.suffix == 'h' ? 2 : 3;
575
577
  for (let i = 0; i < sm.spatial; i += 1) {
576
578
  layers.push(
577
579
  new VideoLayer({
578
580
  quality: VideoQuality.HIGH - i,
579
- width: Math.ceil(width / 2 ** i),
580
- height: Math.ceil(height / 2 ** i),
581
- 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,
582
586
  ssrc: 0,
583
587
  }),
584
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
  };