livekit-client 2.0.10 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
  };