livekit-client 2.5.0 → 2.5.2

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 (61) hide show
  1. package/README.md +4 -0
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +4 -2
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +517 -269
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  11. package/dist/src/room/PCTransport.d.ts.map +1 -1
  12. package/dist/src/room/PCTransportManager.d.ts +1 -0
  13. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  14. package/dist/src/room/Room.d.ts +8 -3
  15. package/dist/src/room/Room.d.ts.map +1 -1
  16. package/dist/src/room/events.d.ts +10 -2
  17. package/dist/src/room/events.d.ts.map +1 -1
  18. package/dist/src/room/participant/LocalParticipant.d.ts +4 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  20. package/dist/src/room/participant/Participant.d.ts +1 -0
  21. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  22. package/dist/src/room/timers.d.ts +4 -4
  23. package/dist/src/room/timers.d.ts.map +1 -1
  24. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  25. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  26. package/dist/src/room/track/create.d.ts +7 -0
  27. package/dist/src/room/track/create.d.ts.map +1 -1
  28. package/dist/src/room/track/options.d.ts +1 -1
  29. package/dist/src/room/types.d.ts +2 -0
  30. package/dist/src/room/types.d.ts.map +1 -1
  31. package/dist/src/room/utils.d.ts +1 -1
  32. package/dist/src/room/utils.d.ts.map +1 -1
  33. package/dist/ts4.2/src/room/PCTransportManager.d.ts +1 -0
  34. package/dist/ts4.2/src/room/Room.d.ts +8 -3
  35. package/dist/ts4.2/src/room/events.d.ts +10 -2
  36. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +4 -1
  37. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  38. package/dist/ts4.2/src/room/timers.d.ts +4 -4
  39. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -1
  40. package/dist/ts4.2/src/room/track/create.d.ts +7 -0
  41. package/dist/ts4.2/src/room/track/options.d.ts +1 -1
  42. package/dist/ts4.2/src/room/types.d.ts +2 -0
  43. package/dist/ts4.2/src/room/utils.d.ts +1 -1
  44. package/package.json +9 -9
  45. package/src/connectionHelper/checks/Checker.ts +1 -1
  46. package/src/e2ee/worker/FrameCryptor.ts +3 -1
  47. package/src/room/PCTransport.ts +3 -1
  48. package/src/room/PCTransportManager.ts +12 -4
  49. package/src/room/RTCEngine.ts +1 -1
  50. package/src/room/Room.ts +69 -7
  51. package/src/room/events.ts +10 -0
  52. package/src/room/participant/LocalParticipant.ts +126 -84
  53. package/src/room/participant/Participant.ts +1 -0
  54. package/src/room/timers.ts +15 -6
  55. package/src/room/track/LocalTrack.ts +4 -2
  56. package/src/room/track/LocalVideoTrack.test.ts +60 -0
  57. package/src/room/track/LocalVideoTrack.ts +1 -1
  58. package/src/room/track/create.ts +27 -8
  59. package/src/room/track/options.ts +1 -1
  60. package/src/room/types.ts +2 -0
  61. package/src/room/utils.ts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
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,23 +36,23 @@
36
36
  "author": "David Zhao <david@davidzhao.com>",
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
- "@livekit/protocol": "1.20.0",
39
+ "@livekit/protocol": "1.20.1",
40
40
  "events": "^3.3.0",
41
41
  "loglevel": "^1.8.0",
42
42
  "sdp-transform": "^2.14.1",
43
43
  "ts-debounce": "^4.0.0",
44
- "tslib": "2.6.3",
44
+ "tslib": "2.7.0",
45
45
  "typed-emitter": "^2.1.0",
46
46
  "webrtc-adapter": "^9.0.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@babel/core": "7.25.2",
50
- "@babel/preset-env": "7.25.3",
50
+ "@babel/preset-env": "7.25.4",
51
51
  "@bufbuild/protoc-gen-es": "^1.3.0",
52
52
  "@changesets/cli": "2.27.7",
53
53
  "@livekit/changesets-changelog-github": "^0.0.4",
54
54
  "@rollup/plugin-babel": "6.0.4",
55
- "@rollup/plugin-commonjs": "25.0.8",
55
+ "@rollup/plugin-commonjs": "26.0.1",
56
56
  "@rollup/plugin-json": "6.1.0",
57
57
  "@rollup/plugin-node-resolve": "15.2.3",
58
58
  "@rollup/plugin-terser": "^0.4.0",
@@ -69,19 +69,19 @@
69
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
- "eslint-plugin-import": "2.29.1",
72
+ "eslint-plugin-import": "2.30.0",
73
73
  "gh-pages": "6.1.1",
74
74
  "jsdom": "^24.0.0",
75
75
  "prettier": "^3.0.0",
76
- "rollup": "4.19.2",
76
+ "rollup": "4.21.2",
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.26.5",
81
+ "typedoc": "0.26.6",
82
82
  "typedoc-plugin-no-inherit": "1.4.0",
83
83
  "typescript": "5.5.4",
84
- "vite": "5.3.5",
84
+ "vite": "5.4.2",
85
85
  "vitest": "^1.0.0"
86
86
  },
87
87
  "scripts": {
@@ -105,7 +105,7 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
105
105
  if (this.room.state === ConnectionState.Connected) {
106
106
  return this.room;
107
107
  }
108
- await this.room.connect(this.url, this.token);
108
+ await this.room.connect(this.url, this.token, this.connectOptions);
109
109
  return this.room;
110
110
  }
111
111
 
@@ -360,6 +360,7 @@ export class FrameCryptor extends BaseFrameCryptor {
360
360
  }
361
361
  } catch (error) {
362
362
  if (error instanceof CryptorError && error.reason === CryptorErrorReason.InvalidKey) {
363
+ // emit an error if the key handler thinks we have a valid key
363
364
  if (this.keys.hasValidKey) {
364
365
  this.emit(CryptorEvent.Error, error);
365
366
  this.keys.decryptionFailure();
@@ -369,7 +370,7 @@ export class FrameCryptor extends BaseFrameCryptor {
369
370
  }
370
371
  }
371
372
  } else if (!this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
372
- // emit an error in case the key index is out of bounds but the key handler thinks we still have a valid key
373
+ // emit an error if the key index is out of bounds but the key handler thinks we still have a valid key
373
374
  workerLogger.warn(`skipping decryption due to missing key at index ${keyIndex}`);
374
375
  this.emit(
375
376
  CryptorEvent.Error,
@@ -379,6 +380,7 @@ export class FrameCryptor extends BaseFrameCryptor {
379
380
  this.participantIdentity,
380
381
  ),
381
382
  );
383
+ this.keys.decryptionFailure();
382
384
  }
383
385
  }
384
386
 
@@ -23,6 +23,8 @@ eliminate this issue.
23
23
  */
24
24
  const startBitrateForSVC = 0.7;
25
25
 
26
+ const debounceInterval = 20;
27
+
26
28
  export const PCEvents = {
27
29
  NegotiationStarted: 'negotiationStarted',
28
30
  NegotiationComplete: 'negotiationComplete',
@@ -228,7 +230,7 @@ export default class PCTransport extends EventEmitter {
228
230
  throw e;
229
231
  }
230
232
  }
231
- }, 100);
233
+ }, debounceInterval);
232
234
 
233
235
  async createAndSendOffer(options?: RTCOfferOptions) {
234
236
  if (this.onOffer === undefined) {
@@ -57,6 +57,8 @@ export class PCTransportManager {
57
57
 
58
58
  private connectionLock: Mutex;
59
59
 
60
+ private remoteOfferLock: Mutex;
61
+
60
62
  private log = log;
61
63
 
62
64
  private loggerOptions: LoggerOptions;
@@ -100,6 +102,7 @@ export class PCTransportManager {
100
102
  this.state = PCTransportState.NEW;
101
103
 
102
104
  this.connectionLock = new Mutex();
105
+ this.remoteOfferLock = new Mutex();
103
106
  }
104
107
 
105
108
  private get logContext() {
@@ -171,11 +174,16 @@ export class PCTransportManager {
171
174
  sdp: sd.sdp,
172
175
  signalingState: this.subscriber.getSignallingState().toString(),
173
176
  });
174
- await this.subscriber.setRemoteDescription(sd);
177
+ const unlock = await this.remoteOfferLock.lock();
178
+ try {
179
+ await this.subscriber.setRemoteDescription(sd);
175
180
 
176
- // answer the offer
177
- const answer = await this.subscriber.createAndSetAnswer();
178
- return answer;
181
+ // answer the offer
182
+ const answer = await this.subscriber.createAndSetAnswer();
183
+ return answer;
184
+ } finally {
185
+ unlock();
186
+ }
179
187
  }
180
188
 
181
189
  updateConfiguration(config: RTCConfiguration, iceRestart?: boolean) {
@@ -232,7 +232,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
232
232
  }
233
233
 
234
234
  // create offer
235
- if (!this.subscriberPrimary) {
235
+ if (!this.subscriberPrimary || joinResponse.fastPublish) {
236
236
  this.negotiate();
237
237
  }
238
238
 
package/src/room/Room.ts CHANGED
@@ -57,6 +57,7 @@ import type { ConnectionQuality } from './participant/Participant';
57
57
  import RemoteParticipant from './participant/RemoteParticipant';
58
58
  import CriticalTimers from './timers';
59
59
  import LocalAudioTrack from './track/LocalAudioTrack';
60
+ import type LocalTrack from './track/LocalTrack';
60
61
  import LocalTrackPublication from './track/LocalTrackPublication';
61
62
  import LocalVideoTrack from './track/LocalVideoTrack';
62
63
  import type RemoteTrack from './track/RemoteTrack';
@@ -162,6 +163,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
162
163
 
163
164
  private isResuming: boolean = false;
164
165
 
166
+ /**
167
+ * map to store first point in time when a particular transcription segment was received
168
+ */
169
+ private transcriptionReceivedTimes: Map<string, number>;
170
+
165
171
  /**
166
172
  * Creates a new Room, the primary construct for a LiveKit session.
167
173
  * @param options
@@ -174,6 +180,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
174
180
  this.options = { ...roomOptionDefaults, ...options };
175
181
 
176
182
  this.log = getLogger(this.options.loggerName ?? LoggerNames.Room);
183
+ this.transcriptionReceivedTimes = new Map();
177
184
 
178
185
  this.options.audioCaptureDefaults = {
179
186
  ...audioDefaults,
@@ -370,6 +377,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
370
377
  })
371
378
  .on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
372
379
  this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
380
+ })
381
+ .on(EngineEvent.LocalTrackSubscribed, (subscribedSid) => {
382
+ const trackPublication = this.localParticipant
383
+ .getTrackPublications()
384
+ .find(({ trackSid }) => trackSid === subscribedSid) as LocalTrackPublication | undefined;
385
+ if (!trackPublication) {
386
+ this.log.warn(
387
+ 'could not find local track subscription for subscribed event',
388
+ this.logContext,
389
+ );
390
+ return;
391
+ }
392
+ this.localParticipant.emit(ParticipantEvent.LocalTrackSubscribed, trackPublication);
393
+ this.emitWhenConnected(
394
+ RoomEvent.LocalTrackSubscribed,
395
+ trackPublication,
396
+ this.localParticipant,
397
+ );
373
398
  });
374
399
 
375
400
  if (this.localParticipant) {
@@ -382,9 +407,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
382
407
 
383
408
  /**
384
409
  * getLocalDevices abstracts navigator.mediaDevices.enumerateDevices.
385
- * In particular, it handles Chrome's unique behavior of creating `default`
386
- * devices. When encountered, it'll be removed from the list of devices.
387
- * The actual default device will be placed at top.
410
+ * In particular, it requests device permissions by default if needed
411
+ * and makes sure the returned device does not consist of dummy devices
388
412
  * @param kind
389
413
  * @returns a list of available local devices
390
414
  */
@@ -608,6 +632,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
608
632
 
609
633
  this.localParticipant.sid = pi.sid;
610
634
  this.localParticipant.identity = pi.identity;
635
+ this.localParticipant.setEnabledPublishCodecs(joinResponse.enabledPublishCodecs);
611
636
 
612
637
  if (this.options.e2ee && this.e2eeManager) {
613
638
  try {
@@ -1048,7 +1073,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1048
1073
  let success = true;
1049
1074
  const deviceConstraint = exact ? { exact: deviceId } : deviceId;
1050
1075
  if (kind === 'audioinput') {
1051
- const prevDeviceId = this.options.audioCaptureDefaults!.deviceId;
1076
+ const prevDeviceId =
1077
+ this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
1052
1078
  this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
1053
1079
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1054
1080
  const tracks = Array.from(this.localParticipant.audioTrackPublications.values()).filter(
@@ -1063,7 +1089,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1063
1089
  throw e;
1064
1090
  }
1065
1091
  } else if (kind === 'videoinput') {
1066
- const prevDeviceId = this.options.videoCaptureDefaults!.deviceId;
1092
+ const prevDeviceId =
1093
+ this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
1067
1094
  this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
1068
1095
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1069
1096
  const tracks = Array.from(this.localParticipant.videoTrackPublications.values()).filter(
@@ -1090,7 +1117,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1090
1117
  (await DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) ?? '';
1091
1118
  }
1092
1119
  this.options.audioOutput ??= {};
1093
- const prevDeviceId = this.options.audioOutput.deviceId;
1120
+ const prevDeviceId = this.getActiveDevice(kind) ?? this.options.audioOutput.deviceId;
1094
1121
  this.options.audioOutput.deviceId = deviceId;
1095
1122
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1096
1123
 
@@ -1274,6 +1301,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1274
1301
  this.clearConnectionReconcile();
1275
1302
  this.isResuming = false;
1276
1303
  this.bufferedEvents = [];
1304
+ this.transcriptionReceivedTimes.clear();
1277
1305
  if (this.state === ConnectionState.Disconnected) {
1278
1306
  return;
1279
1307
  }
@@ -1535,7 +1563,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1535
1563
  : this.getParticipantByIdentity(transcription.transcribedParticipantIdentity);
1536
1564
  const publication = participant?.trackPublications.get(transcription.trackId);
1537
1565
 
1538
- const segments = extractTranscriptionSegments(transcription);
1566
+ const segments = extractTranscriptionSegments(transcription, this.transcriptionReceivedTimes);
1539
1567
 
1540
1568
  publication?.emit(TrackEvent.TranscriptionReceived, segments);
1541
1569
  participant?.emit(ParticipantEvent.TranscriptionReceived, segments, publication);
@@ -1574,6 +1602,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1574
1602
  };
1575
1603
 
1576
1604
  private handleDeviceChange = async () => {
1605
+ const availableDevices = await DeviceManager.getInstance().getDevices();
1606
+ // inputs are automatically handled via TrackEvent.Ended causing a TrackEvent.Restarted. Here we only need to worry about audiooutputs changing
1607
+ const kinds: MediaDeviceKind[] = ['audiooutput'];
1608
+ for (let kind of kinds) {
1609
+ // switch to first available device if previously active device is not available any more
1610
+ const devicesOfKind = availableDevices.filter((d) => d.kind === kind);
1611
+ if (
1612
+ devicesOfKind.length > 0 &&
1613
+ !devicesOfKind.find((deviceInfo) => deviceInfo.deviceId === this.getActiveDevice(kind))
1614
+ ) {
1615
+ await this.switchActiveDevice(kind, devicesOfKind[0].deviceId);
1616
+ }
1617
+ }
1618
+
1577
1619
  this.emit(RoomEvent.MediaDevicesChanged);
1578
1620
  };
1579
1621
 
@@ -1897,6 +1939,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1897
1939
 
1898
1940
  private onLocalTrackPublished = async (pub: LocalTrackPublication) => {
1899
1941
  pub.track?.on(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
1942
+ pub.track?.on(TrackEvent.Restarted, this.onLocalTrackRestarted);
1900
1943
  pub.track?.getProcessor()?.onPublish?.(this);
1901
1944
 
1902
1945
  this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
@@ -1921,9 +1964,27 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1921
1964
 
1922
1965
  private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
1923
1966
  pub.track?.off(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
1967
+ pub.track?.off(TrackEvent.Restarted, this.onLocalTrackRestarted);
1924
1968
  this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
1925
1969
  };
1926
1970
 
1971
+ private onLocalTrackRestarted = async (track: LocalTrack) => {
1972
+ const deviceId = await track.getDeviceId(false);
1973
+ const deviceKind = sourceToKind(track.source);
1974
+ if (
1975
+ deviceKind &&
1976
+ deviceId &&
1977
+ deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
1978
+ ) {
1979
+ this.log.debug(
1980
+ `local track restarted, setting ${deviceKind} ${deviceId} active`,
1981
+ this.logContext,
1982
+ );
1983
+ this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
1984
+ this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
1985
+ }
1986
+ };
1987
+
1927
1988
  private onLocalConnectionQualityChanged = (quality: ConnectionQuality) => {
1928
1989
  this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
1929
1990
  };
@@ -2202,4 +2263,5 @@ export type RoomEventCallbacks = {
2202
2263
  encryptionError: (error: Error) => void;
2203
2264
  dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
2204
2265
  activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
2266
+ localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
2205
2267
  };
@@ -323,6 +323,11 @@ export enum RoomEvent {
323
323
  * args: (kind: MediaDeviceKind, deviceId: string)
324
324
  */
325
325
  ActiveDeviceChanged = 'activeDeviceChanged',
326
+
327
+ /**
328
+ * fired when the first remote participant has subscribed to the localParticipant's track
329
+ */
330
+ LocalTrackSubscribed = 'localTrackSubscribed',
326
331
  }
327
332
 
328
333
  export enum ParticipantEvent {
@@ -509,6 +514,11 @@ export enum ParticipantEvent {
509
514
  * When a participant's attributes changed, this event will be emitted with the changed attributes
510
515
  */
511
516
  AttributesChanged = 'attributesChanged',
517
+
518
+ /**
519
+ * fired on local participant only, when the first remote participant has subscribed to the track specified in the payload
520
+ */
521
+ LocalTrackSubscribed = 'localTrackSubscribed',
512
522
  }
513
523
 
514
524
  /** @internal */
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AddTrackRequest,
3
+ Codec,
3
4
  DataPacket,
4
5
  DataPacket_Kind,
5
6
  Encryption_Type,
@@ -9,6 +10,7 @@ import {
9
10
  RequestResponse_Reason,
10
11
  SimulcastCodec,
11
12
  SubscribedQualityUpdate,
13
+ TrackInfo,
12
14
  TrackUnpublishedResponse,
13
15
  UserPacket,
14
16
  } from '@livekit/protocol';
@@ -29,6 +31,7 @@ import LocalTrack from '../track/LocalTrack';
29
31
  import LocalTrackPublication from '../track/LocalTrackPublication';
30
32
  import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
31
33
  import { Track } from '../track/Track';
34
+ import { extractProcessorsFromOptions } from '../track/create';
32
35
  import type {
33
36
  AudioCaptureOptions,
34
37
  BackupVideoCodec,
@@ -38,7 +41,6 @@ import type {
38
41
  VideoCaptureOptions,
39
42
  } from '../track/options';
40
43
  import { ScreenSharePresets, VideoPresets, isBackupCodec } from '../track/options';
41
- import type { TrackProcessor } from '../track/processor/types';
42
44
  import {
43
45
  constraintsForOptions,
44
46
  getLogContextFromTrack,
@@ -110,6 +112,8 @@ export default class LocalParticipant extends Participant {
110
112
  }
111
113
  >;
112
114
 
115
+ private enabledPublishVideoCodecs: Codec[] = [];
116
+
113
117
  /** @internal */
114
118
  constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
115
119
  super(sid, identity, undefined, undefined, {
@@ -482,6 +486,9 @@ export default class LocalParticipant extends Participant {
482
486
  * @returns
483
487
  */
484
488
  async createTracks(options?: CreateLocalTracksOptions): Promise<LocalTrack[]> {
489
+ options ??= {};
490
+ const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(options);
491
+
485
492
  const mergedOptions = mergeDefaultOptions(
486
493
  options,
487
494
  this.roomOptions?.audioCaptureDefaults,
@@ -536,12 +543,10 @@ export default class LocalParticipant extends Participant {
536
543
  track.setAudioContext(this.audioContext);
537
544
  }
538
545
  track.mediaStream = stream;
539
- if (trackOptions.processor) {
540
- if (track instanceof LocalAudioTrack) {
541
- await track.setProcessor(trackOptions.processor as TrackProcessor<Track.Kind.Audio>);
542
- } else {
543
- await track.setProcessor(trackOptions.processor as TrackProcessor<Track.Kind.Video>);
544
- }
546
+ if (track instanceof LocalAudioTrack && audioProcessor) {
547
+ await track.setProcessor(audioProcessor);
548
+ } else if (track instanceof LocalVideoTrack && videoProcessor) {
549
+ await track.setProcessor(videoProcessor);
545
550
  }
546
551
  return track;
547
552
  }),
@@ -775,6 +780,17 @@ export default class LocalParticipant extends Participant {
775
780
  if (opts.videoCodec === undefined) {
776
781
  opts.videoCodec = defaultVideoCodec;
777
782
  }
783
+ if (this.enabledPublishVideoCodecs.length > 0) {
784
+ // fallback to a supported codec if it is not supported
785
+ if (
786
+ !this.enabledPublishVideoCodecs.some(
787
+ (c) => opts.videoCodec === mimeTypeToVideoCodecString(c.mime),
788
+ )
789
+ ) {
790
+ opts.videoCodec = mimeTypeToVideoCodecString(this.enabledPublishVideoCodecs[0].mime);
791
+ }
792
+ }
793
+
778
794
  const videoCodec = opts.videoCodec;
779
795
 
780
796
  // handle track actions
@@ -908,33 +924,87 @@ export default class LocalParticipant extends Participant {
908
924
  throw new UnexpectedConnectionState('cannot publish track when not connected');
909
925
  }
910
926
 
911
- const ti = await this.engine.addTrack(req);
912
- // server might not support the codec the client has requested, in that case, fallback
913
- // to a supported codec
914
- let primaryCodecMime: string | undefined;
915
- ti.codecs.forEach((codec) => {
916
- if (primaryCodecMime === undefined) {
917
- primaryCodecMime = codec.mimeType;
927
+ const negotiate = async () => {
928
+ if (!this.engine.pcManager) {
929
+ throw new UnexpectedConnectionState('pcManager is not ready');
918
930
  }
919
- });
920
- if (primaryCodecMime && track.kind === Track.Kind.Video) {
921
- const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
922
- if (updatedCodec !== videoCodec) {
923
- this.log.debug('falling back to server selected codec', {
924
- ...this.logContext,
925
- ...getLogContextFromTrack(track),
926
- codec: updatedCodec,
927
- });
928
- opts.videoCodec = updatedCodec;
929
-
930
- // recompute encodings since bitrates/etc could have changed
931
- encodings = computeVideoEncodings(
932
- track.source === Track.Source.ScreenShare,
933
- req.width,
934
- req.height,
935
- opts,
936
- );
931
+
932
+ track.sender = await this.engine.createSender(track, opts, encodings);
933
+
934
+ if (track instanceof LocalVideoTrack) {
935
+ opts.degradationPreference ??= getDefaultDegradationPreference(track);
936
+ track.setDegradationPreference(opts.degradationPreference);
937
+ }
938
+
939
+ if (encodings) {
940
+ if (isFireFox() && track.kind === Track.Kind.Audio) {
941
+ /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
942
+ livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
943
+ publish high quality audio track. But firefox always uses this value as the actual
944
+ bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
945
+ So the client need to modify maxaverragebitrates in answer sdp to user provided value to
946
+ fix the issue.
947
+ */
948
+ let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
949
+ for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
950
+ if (transceiver.sender === track.sender) {
951
+ trackTransceiver = transceiver;
952
+ break;
953
+ }
954
+ }
955
+ if (trackTransceiver) {
956
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
957
+ transceiver: trackTransceiver,
958
+ codec: 'opus',
959
+ maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
960
+ });
961
+ }
962
+ } else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
963
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
964
+ cid: req.cid,
965
+ codec: track.codec,
966
+ maxbr: encodings[0].maxBitrate / 1000,
967
+ });
968
+ }
969
+ }
970
+
971
+ await this.engine.negotiate();
972
+ };
973
+
974
+ let ti: TrackInfo;
975
+ if (this.enabledPublishVideoCodecs.length > 0) {
976
+ const rets = await Promise.all([this.engine.addTrack(req), negotiate()]);
977
+ ti = rets[0];
978
+ } else {
979
+ ti = await this.engine.addTrack(req);
980
+ // server might not support the codec the client has requested, in that case, fallback
981
+ // to a supported codec
982
+ let primaryCodecMime: string | undefined;
983
+ ti.codecs.forEach((codec) => {
984
+ if (primaryCodecMime === undefined) {
985
+ primaryCodecMime = codec.mimeType;
986
+ }
987
+ });
988
+ if (primaryCodecMime && track.kind === Track.Kind.Video) {
989
+ const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
990
+ if (updatedCodec !== videoCodec) {
991
+ this.log.debug('falling back to server selected codec', {
992
+ ...this.logContext,
993
+ ...getLogContextFromTrack(track),
994
+ codec: updatedCodec,
995
+ });
996
+ opts.videoCodec = updatedCodec;
997
+
998
+ // recompute encodings since bitrates/etc could have changed
999
+ encodings = computeVideoEncodings(
1000
+ track.source === Track.Source.ScreenShare,
1001
+ req.width,
1002
+ req.height,
1003
+ opts,
1004
+ );
1005
+ }
937
1006
  }
1007
+ await negotiate();
938
1008
  }
939
1009
 
940
1010
  const publication = new LocalTrackPublication(track.kind, ti, track, {
@@ -945,56 +1015,12 @@ export default class LocalParticipant extends Participant {
945
1015
  publication.options = opts;
946
1016
  track.sid = ti.sid;
947
1017
 
948
- if (!this.engine.pcManager) {
949
- throw new UnexpectedConnectionState('pcManager is not ready');
950
- }
951
1018
  this.log.debug(`publishing ${track.kind} with encodings`, {
952
1019
  ...this.logContext,
953
1020
  encodings,
954
1021
  trackInfo: ti,
955
1022
  });
956
1023
 
957
- track.sender = await this.engine.createSender(track, opts, encodings);
958
-
959
- if (track instanceof LocalVideoTrack) {
960
- opts.degradationPreference ??= getDefaultDegradationPreference(track);
961
- track.setDegradationPreference(opts.degradationPreference);
962
- }
963
-
964
- if (encodings) {
965
- if (isFireFox() && track.kind === Track.Kind.Audio) {
966
- /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
967
- livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
968
- publish high quality audio track. But firefox always uses this value as the actual
969
- bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
970
- So the client need to modify maxaverragebitrates in answer sdp to user provided value to
971
- fix the issue.
972
- */
973
- let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
974
- for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
975
- if (transceiver.sender === track.sender) {
976
- trackTransceiver = transceiver;
977
- break;
978
- }
979
- }
980
- if (trackTransceiver) {
981
- this.engine.pcManager.publisher.setTrackCodecBitrate({
982
- transceiver: trackTransceiver,
983
- codec: 'opus',
984
- maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
985
- });
986
- }
987
- } else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
988
- this.engine.pcManager.publisher.setTrackCodecBitrate({
989
- cid: req.cid,
990
- codec: track.codec,
991
- maxbr: encodings[0].maxBitrate / 1000,
992
- });
993
- }
994
- }
995
-
996
- await this.engine.negotiate();
997
-
998
1024
  if (track instanceof LocalVideoTrack) {
999
1025
  track.startMonitor(this.engine.client);
1000
1026
  } else if (track instanceof LocalAudioTrack) {
@@ -1081,15 +1107,19 @@ export default class LocalParticipant extends Participant {
1081
1107
  throw new UnexpectedConnectionState('cannot publish track when not connected');
1082
1108
  }
1083
1109
 
1084
- const ti = await this.engine.addTrack(req);
1110
+ const negotiate = async () => {
1111
+ const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
1112
+ if (encodings) {
1113
+ transceiverInit.sendEncodings = encodings;
1114
+ }
1115
+ await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
1085
1116
 
1086
- const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
1087
- if (encodings) {
1088
- transceiverInit.sendEncodings = encodings;
1089
- }
1090
- await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
1117
+ await this.engine.negotiate();
1118
+ };
1119
+
1120
+ const rets = await Promise.all([this.engine.addTrack(req), negotiate()]);
1121
+ const ti = rets[0];
1091
1122
 
1092
- await this.engine.negotiate();
1093
1123
  this.log.debug(`published ${videoCodec} for track ${track.sid}`, {
1094
1124
  ...this.logContext,
1095
1125
  encodings,
@@ -1309,6 +1339,13 @@ export default class LocalParticipant extends Participant {
1309
1339
  }
1310
1340
  }
1311
1341
 
1342
+ /** @internal */
1343
+ setEnabledPublishCodecs(codecs: Codec[]) {
1344
+ this.enabledPublishVideoCodecs = codecs.filter(
1345
+ (c) => c.mime.split('/')[0].toLowerCase() === 'video',
1346
+ );
1347
+ }
1348
+
1312
1349
  /** @internal */
1313
1350
  updateInfo(info: ParticipantInfo): boolean {
1314
1351
  if (info.sid !== this.sid) {
@@ -1494,7 +1531,12 @@ export default class LocalParticipant extends Participant {
1494
1531
  ...this.logContext,
1495
1532
  ...getLogContextFromTrack(track),
1496
1533
  });
1497
- await track.restartTrack();
1534
+ if (track instanceof LocalAudioTrack) {
1535
+ // fall back to default device if available
1536
+ await track.restartTrack({ deviceId: 'default' });
1537
+ } else {
1538
+ await track.restartTrack();
1539
+ }
1498
1540
  }
1499
1541
  } catch (e) {
1500
1542
  this.log.warn(`could not restart track, muting instead`, {
@@ -386,4 +386,5 @@ export type ParticipantEventCallbacks = {
386
386
  status: TrackPublication.SubscriptionStatus,
387
387
  ) => void;
388
388
  attributesChanged: (changedAttributes: Record<string, string>) => void;
389
+ localTrackSubscribed: (trackPublication: LocalTrackPublication) => void;
389
390
  };