livekit-client 2.5.0 → 2.5.2

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