livekit-client 2.5.1 → 2.5.3

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 (48) 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 +4 -2
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +867 -425
  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/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  10. package/dist/src/room/PCTransportManager.d.ts +1 -0
  11. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  12. package/dist/src/room/Room.d.ts +7 -4
  13. package/dist/src/room/Room.d.ts.map +1 -1
  14. package/dist/src/room/events.d.ts +4 -1
  15. package/dist/src/room/events.d.ts.map +1 -1
  16. package/dist/src/room/participant/LocalParticipant.d.ts +11 -1
  17. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  18. package/dist/src/room/participant/Participant.d.ts +2 -1
  19. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  20. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  21. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  22. package/dist/src/room/track/create.d.ts +7 -0
  23. package/dist/src/room/track/create.d.ts.map +1 -1
  24. package/dist/src/room/types.d.ts +6 -0
  25. package/dist/src/room/types.d.ts.map +1 -1
  26. package/dist/src/room/utils.d.ts +3 -2
  27. package/dist/src/room/utils.d.ts.map +1 -1
  28. package/dist/ts4.2/src/room/PCTransportManager.d.ts +1 -0
  29. package/dist/ts4.2/src/room/Room.d.ts +7 -4
  30. package/dist/ts4.2/src/room/events.d.ts +4 -1
  31. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +11 -1
  32. package/dist/ts4.2/src/room/participant/Participant.d.ts +2 -1
  33. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -1
  34. package/dist/ts4.2/src/room/track/create.d.ts +7 -0
  35. package/dist/ts4.2/src/room/types.d.ts +6 -0
  36. package/dist/ts4.2/src/room/utils.d.ts +3 -2
  37. package/package.json +9 -9
  38. package/src/connectionHelper/checks/Checker.ts +1 -1
  39. package/src/e2ee/worker/FrameCryptor.ts +3 -1
  40. package/src/room/PCTransportManager.ts +12 -4
  41. package/src/room/Room.ts +67 -7
  42. package/src/room/events.ts +4 -0
  43. package/src/room/participant/LocalParticipant.ts +146 -52
  44. package/src/room/participant/Participant.ts +2 -1
  45. package/src/room/track/LocalTrack.ts +4 -2
  46. package/src/room/track/create.ts +27 -8
  47. package/src/room/types.ts +7 -0
  48. package/src/room/utils.ts +17 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
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.1",
39
+ "@livekit/protocol": "1.22.0",
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.22.4",
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.6",
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
 
@@ -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) {
package/src/room/Room.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ChatMessage as ChatMessageModel,
2
3
  ConnectionQualityUpdate,
3
4
  type DataPacket,
4
5
  DataPacket_Kind,
@@ -57,6 +58,7 @@ import type { ConnectionQuality } from './participant/Participant';
57
58
  import RemoteParticipant from './participant/RemoteParticipant';
58
59
  import CriticalTimers from './timers';
59
60
  import LocalAudioTrack from './track/LocalAudioTrack';
61
+ import type LocalTrack from './track/LocalTrack';
60
62
  import LocalTrackPublication from './track/LocalTrackPublication';
61
63
  import LocalVideoTrack from './track/LocalVideoTrack';
62
64
  import type RemoteTrack from './track/RemoteTrack';
@@ -66,11 +68,17 @@ import type { TrackPublication } from './track/TrackPublication';
66
68
  import type { TrackProcessor } from './track/processor/types';
67
69
  import type { AdaptiveStreamSettings } from './track/types';
68
70
  import { getNewAudioContext, sourceToKind } from './track/utils';
69
- import type { SimulationOptions, SimulationScenario, TranscriptionSegment } from './types';
71
+ import type {
72
+ ChatMessage,
73
+ SimulationOptions,
74
+ SimulationScenario,
75
+ TranscriptionSegment,
76
+ } from './types';
70
77
  import {
71
78
  Future,
72
79
  Mutex,
73
80
  createDummyVideoStreamTrack,
81
+ extractChatMessage,
74
82
  extractTranscriptionSegments,
75
83
  getEmptyAudioStreamTrack,
76
84
  isBrowserSupported,
@@ -406,9 +414,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
406
414
 
407
415
  /**
408
416
  * getLocalDevices abstracts navigator.mediaDevices.enumerateDevices.
409
- * In particular, it handles Chrome's unique behavior of creating `default`
410
- * devices. When encountered, it'll be removed from the list of devices.
411
- * The actual default device will be placed at top.
417
+ * In particular, it requests device permissions by default if needed
418
+ * and makes sure the returned device does not consist of dummy devices
412
419
  * @param kind
413
420
  * @returns a list of available local devices
414
421
  */
@@ -1073,7 +1080,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1073
1080
  let success = true;
1074
1081
  const deviceConstraint = exact ? { exact: deviceId } : deviceId;
1075
1082
  if (kind === 'audioinput') {
1076
- const prevDeviceId = this.options.audioCaptureDefaults!.deviceId;
1083
+ const prevDeviceId =
1084
+ this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
1077
1085
  this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
1078
1086
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1079
1087
  const tracks = Array.from(this.localParticipant.audioTrackPublications.values()).filter(
@@ -1088,7 +1096,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1088
1096
  throw e;
1089
1097
  }
1090
1098
  } else if (kind === 'videoinput') {
1091
- const prevDeviceId = this.options.videoCaptureDefaults!.deviceId;
1099
+ const prevDeviceId =
1100
+ this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
1092
1101
  this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
1093
1102
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1094
1103
  const tracks = Array.from(this.localParticipant.videoTrackPublications.values()).filter(
@@ -1115,7 +1124,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1115
1124
  (await DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) ?? '';
1116
1125
  }
1117
1126
  this.options.audioOutput ??= {};
1118
- const prevDeviceId = this.options.audioOutput.deviceId;
1127
+ const prevDeviceId = this.getActiveDevice(kind) ?? this.options.audioOutput.deviceId;
1119
1128
  this.options.audioOutput.deviceId = deviceId;
1120
1129
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1121
1130
 
@@ -1154,6 +1163,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1154
1163
  .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
1155
1164
  .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
1156
1165
  .on(ParticipantEvent.AudioStreamAcquired, this.startAudio)
1166
+ .on(ParticipantEvent.ChatMessage, this.onLocalChatMessageSent)
1157
1167
  .on(
1158
1168
  ParticipantEvent.ParticipantPermissionsChanged,
1159
1169
  this.onLocalParticipantPermissionsChanged,
@@ -1334,6 +1344,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1334
1344
  .off(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
1335
1345
  .off(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
1336
1346
  .off(ParticipantEvent.AudioStreamAcquired, this.startAudio)
1347
+ .off(ParticipantEvent.ChatMessage, this.onLocalChatMessageSent)
1337
1348
  .off(
1338
1349
  ParticipantEvent.ParticipantPermissionsChanged,
1339
1350
  this.onLocalParticipantPermissionsChanged,
@@ -1527,6 +1538,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1527
1538
  this.handleTranscription(participant, packet.value.value);
1528
1539
  } else if (packet.value.case === 'sipDtmf') {
1529
1540
  this.handleSipDtmf(participant, packet.value.value);
1541
+ } else if (packet.value.case === 'chatMessage') {
1542
+ this.handleChatMessage(participant, packet.value.value);
1530
1543
  }
1531
1544
  };
1532
1545
 
@@ -1568,6 +1581,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1568
1581
  this.emit(RoomEvent.TranscriptionReceived, segments, participant, publication);
1569
1582
  };
1570
1583
 
1584
+ private handleChatMessage = (
1585
+ participant: RemoteParticipant | undefined,
1586
+ chatMessage: ChatMessageModel,
1587
+ ) => {
1588
+ const msg = extractChatMessage(chatMessage);
1589
+ this.emit(RoomEvent.ChatMessage, msg, participant);
1590
+ };
1591
+
1571
1592
  private handleAudioPlaybackStarted = () => {
1572
1593
  if (this.canPlaybackAudio) {
1573
1594
  return;
@@ -1600,6 +1621,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1600
1621
  };
1601
1622
 
1602
1623
  private handleDeviceChange = async () => {
1624
+ // check for available devices, but don't request permissions in order to avoid prompts for kinds that haven't been used before
1625
+ const availableDevices = await DeviceManager.getInstance().getDevices(undefined, false);
1626
+ // inputs are automatically handled via TrackEvent.Ended causing a TrackEvent.Restarted. Here we only need to worry about audiooutputs changing
1627
+ const kinds: MediaDeviceKind[] = ['audiooutput'];
1628
+ for (let kind of kinds) {
1629
+ // switch to first available device if previously active device is not available any more
1630
+ const devicesOfKind = availableDevices.filter((d) => d.kind === kind);
1631
+ if (
1632
+ devicesOfKind.length > 0 &&
1633
+ !devicesOfKind.find((deviceInfo) => deviceInfo.deviceId === this.getActiveDevice(kind))
1634
+ ) {
1635
+ await this.switchActiveDevice(kind, devicesOfKind[0].deviceId);
1636
+ }
1637
+ }
1638
+
1603
1639
  this.emit(RoomEvent.MediaDevicesChanged);
1604
1640
  };
1605
1641
 
@@ -1923,6 +1959,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1923
1959
 
1924
1960
  private onLocalTrackPublished = async (pub: LocalTrackPublication) => {
1925
1961
  pub.track?.on(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
1962
+ pub.track?.on(TrackEvent.Restarted, this.onLocalTrackRestarted);
1926
1963
  pub.track?.getProcessor()?.onPublish?.(this);
1927
1964
 
1928
1965
  this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
@@ -1947,9 +1984,27 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1947
1984
 
1948
1985
  private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
1949
1986
  pub.track?.off(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
1987
+ pub.track?.off(TrackEvent.Restarted, this.onLocalTrackRestarted);
1950
1988
  this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
1951
1989
  };
1952
1990
 
1991
+ private onLocalTrackRestarted = async (track: LocalTrack) => {
1992
+ const deviceId = await track.getDeviceId(false);
1993
+ const deviceKind = sourceToKind(track.source);
1994
+ if (
1995
+ deviceKind &&
1996
+ deviceId &&
1997
+ deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
1998
+ ) {
1999
+ this.log.debug(
2000
+ `local track restarted, setting ${deviceKind} ${deviceId} active`,
2001
+ this.logContext,
2002
+ );
2003
+ this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
2004
+ this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
2005
+ }
2006
+ };
2007
+
1953
2008
  private onLocalConnectionQualityChanged = (quality: ConnectionQuality) => {
1954
2009
  this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
1955
2010
  };
@@ -1962,6 +2017,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1962
2017
  this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
1963
2018
  };
1964
2019
 
2020
+ private onLocalChatMessageSent = (msg: ChatMessage) => {
2021
+ this.emit(RoomEvent.ChatMessage, msg, this.localParticipant);
2022
+ };
2023
+
1965
2024
  /**
1966
2025
  * Allows to populate a room with simulated participants.
1967
2026
  * No actual connection to a server will be established, all state is
@@ -2228,5 +2287,6 @@ export type RoomEventCallbacks = {
2228
2287
  encryptionError: (error: Error) => void;
2229
2288
  dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
2230
2289
  activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
2290
+ chatMessage: (message: ChatMessage, participant?: RemoteParticipant | LocalParticipant) => void;
2231
2291
  localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
2232
2292
  };
@@ -324,6 +324,7 @@ export enum RoomEvent {
324
324
  */
325
325
  ActiveDeviceChanged = 'activeDeviceChanged',
326
326
 
327
+ ChatMessage = 'chatMessage',
327
328
  /**
328
329
  * fired when the first remote participant has subscribed to the localParticipant's track
329
330
  */
@@ -519,6 +520,9 @@ export enum ParticipantEvent {
519
520
  * fired on local participant only, when the first remote participant has subscribed to the track specified in the payload
520
521
  */
521
522
  LocalTrackSubscribed = 'localTrackSubscribed',
523
+
524
+ /** only emitted on local participant */
525
+ ChatMessage = 'chatMessage',
522
526
  }
523
527
 
524
528
  /** @internal */
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AddTrackRequest,
3
+ ChatMessage as ChatMessageModel,
3
4
  Codec,
4
5
  DataPacket,
5
6
  DataPacket_Kind,
@@ -13,6 +14,7 @@ import {
13
14
  TrackInfo,
14
15
  TrackUnpublishedResponse,
15
16
  UserPacket,
17
+ protoInt64,
16
18
  } from '@livekit/protocol';
17
19
  import type { InternalRoomOptions } from '../../options';
18
20
  import { PCTransportState } from '../PCTransportManager';
@@ -31,6 +33,7 @@ import LocalTrack from '../track/LocalTrack';
31
33
  import LocalTrackPublication from '../track/LocalTrackPublication';
32
34
  import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
33
35
  import { Track } from '../track/Track';
36
+ import { extractProcessorsFromOptions } from '../track/create';
34
37
  import type {
35
38
  AudioCaptureOptions,
36
39
  BackupVideoCodec,
@@ -40,7 +43,6 @@ import type {
40
43
  VideoCaptureOptions,
41
44
  } from '../track/options';
42
45
  import { ScreenSharePresets, VideoPresets, isBackupCodec } from '../track/options';
43
- import type { TrackProcessor } from '../track/processor/types';
44
46
  import {
45
47
  constraintsForOptions,
46
48
  getLogContextFromTrack,
@@ -48,7 +50,7 @@ import {
48
50
  mimeTypeToVideoCodecString,
49
51
  screenCaptureToDisplayMediaStreamOptions,
50
52
  } from '../track/utils';
51
- import type { DataPublishOptions } from '../types';
53
+ import type { ChatMessage, DataPublishOptions } from '../types';
52
54
  import {
53
55
  Future,
54
56
  isE2EESimulcastSupported,
@@ -88,6 +90,8 @@ export default class LocalParticipant extends Participant {
88
90
 
89
91
  private pendingPublishPromises = new Map<LocalTrack, Promise<LocalTrackPublication>>();
90
92
 
93
+ private republishPromise: Promise<void> | undefined;
94
+
91
95
  private cameraError: Error | undefined;
92
96
 
93
97
  private microphoneError: Error | undefined;
@@ -380,6 +384,9 @@ export default class LocalParticipant extends Participant {
380
384
  publishOptions?: TrackPublishOptions,
381
385
  ) {
382
386
  this.log.debug('setTrackEnabled', { ...this.logContext, source, enabled });
387
+ if (this.republishPromise) {
388
+ await this.republishPromise;
389
+ }
383
390
  let track = this.getTrackPublication(source);
384
391
  if (enabled) {
385
392
  if (track) {
@@ -387,9 +394,12 @@ export default class LocalParticipant extends Participant {
387
394
  } else {
388
395
  let localTracks: Array<LocalTrack> | undefined;
389
396
  if (this.pendingPublishing.has(source)) {
390
- this.log.info('skipping duplicate published source', { ...this.logContext, source });
391
- // no-op it's already been requested
392
- return;
397
+ const pendingTrack = await this.waitForPendingPublicationOfSource(source);
398
+ if (!pendingTrack) {
399
+ this.log.info('skipping duplicate published source', { ...this.logContext, source });
400
+ }
401
+ await pendingTrack?.unmute();
402
+ return pendingTrack;
393
403
  }
394
404
  this.pendingPublishing.add(source);
395
405
  try {
@@ -437,16 +447,22 @@ export default class LocalParticipant extends Participant {
437
447
  this.pendingPublishing.delete(source);
438
448
  }
439
449
  }
440
- } else if (track && track.track) {
441
- // screenshare cannot be muted, unpublish instead
442
- if (source === Track.Source.ScreenShare) {
443
- track = await this.unpublishTrack(track.track);
444
- const screenAudioTrack = this.getTrackPublication(Track.Source.ScreenShareAudio);
445
- if (screenAudioTrack && screenAudioTrack.track) {
446
- this.unpublishTrack(screenAudioTrack.track);
450
+ } else {
451
+ if (!track?.track) {
452
+ // if there's no track available yet first wait for pending publishing promises of that source to see if it becomes available
453
+ track = await this.waitForPendingPublicationOfSource(source);
454
+ }
455
+ if (track && track.track) {
456
+ // screenshare cannot be muted, unpublish instead
457
+ if (source === Track.Source.ScreenShare) {
458
+ track = await this.unpublishTrack(track.track);
459
+ const screenAudioTrack = this.getTrackPublication(Track.Source.ScreenShareAudio);
460
+ if (screenAudioTrack && screenAudioTrack.track) {
461
+ this.unpublishTrack(screenAudioTrack.track);
462
+ }
463
+ } else {
464
+ await track.mute();
447
465
  }
448
- } else {
449
- await track.mute();
450
466
  }
451
467
  }
452
468
  return track;
@@ -486,6 +502,9 @@ export default class LocalParticipant extends Participant {
486
502
  * @returns
487
503
  */
488
504
  async createTracks(options?: CreateLocalTracksOptions): Promise<LocalTrack[]> {
505
+ options ??= {};
506
+ const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(options);
507
+
489
508
  const mergedOptions = mergeDefaultOptions(
490
509
  options,
491
510
  this.roomOptions?.audioCaptureDefaults,
@@ -540,12 +559,10 @@ export default class LocalParticipant extends Participant {
540
559
  track.setAudioContext(this.audioContext);
541
560
  }
542
561
  track.mediaStream = stream;
543
- if (trackOptions.processor) {
544
- if (track instanceof LocalAudioTrack) {
545
- await track.setProcessor(trackOptions.processor as TrackProcessor<Track.Kind.Audio>);
546
- } else {
547
- await track.setProcessor(trackOptions.processor as TrackProcessor<Track.Kind.Video>);
548
- }
562
+ if (track instanceof LocalAudioTrack && audioProcessor) {
563
+ await track.setProcessor(audioProcessor);
564
+ } else if (track instanceof LocalVideoTrack && videoProcessor) {
565
+ await track.setProcessor(videoProcessor);
549
566
  }
550
567
  return track;
551
568
  }),
@@ -610,15 +627,23 @@ export default class LocalParticipant extends Participant {
610
627
  * @param track
611
628
  * @param options
612
629
  */
613
- async publishTrack(
630
+ async publishTrack(track: LocalTrack | MediaStreamTrack, options?: TrackPublishOptions) {
631
+ return this.publishOrRepublishTrack(track, options);
632
+ }
633
+
634
+ private async publishOrRepublishTrack(
614
635
  track: LocalTrack | MediaStreamTrack,
615
636
  options?: TrackPublishOptions,
637
+ isRepublish = false,
616
638
  ): Promise<LocalTrackPublication> {
617
639
  if (track instanceof LocalAudioTrack) {
618
640
  track.setAudioContext(this.audioContext);
619
641
  }
620
642
 
621
643
  await this.reconnectFuture?.promise;
644
+ if (this.republishPromise && !isRepublish) {
645
+ await this.republishPromise;
646
+ }
622
647
  if (track instanceof LocalTrack && this.pendingPublishPromises.has(track)) {
623
648
  await this.pendingPublishPromises.get(track);
624
649
  }
@@ -1247,39 +1272,53 @@ export default class LocalParticipant extends Participant {
1247
1272
  }
1248
1273
 
1249
1274
  async republishAllTracks(options?: TrackPublishOptions, restartTracks: boolean = true) {
1250
- const localPubs: LocalTrackPublication[] = [];
1251
- this.trackPublications.forEach((pub) => {
1252
- if (pub.track) {
1253
- if (options) {
1254
- pub.options = { ...pub.options, ...options };
1255
- }
1256
- localPubs.push(pub);
1275
+ if (this.republishPromise) {
1276
+ await this.republishPromise;
1277
+ }
1278
+ this.republishPromise = new Promise(async (resolve, reject) => {
1279
+ try {
1280
+ const localPubs: LocalTrackPublication[] = [];
1281
+ this.trackPublications.forEach((pub) => {
1282
+ if (pub.track) {
1283
+ if (options) {
1284
+ pub.options = { ...pub.options, ...options };
1285
+ }
1286
+ localPubs.push(pub);
1287
+ }
1288
+ });
1289
+
1290
+ await Promise.all(
1291
+ localPubs.map(async (pub) => {
1292
+ const track = pub.track!;
1293
+ await this.unpublishTrack(track, false);
1294
+ if (
1295
+ restartTracks &&
1296
+ !track.isMuted &&
1297
+ track.source !== Track.Source.ScreenShare &&
1298
+ track.source !== Track.Source.ScreenShareAudio &&
1299
+ (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
1300
+ !track.isUserProvided
1301
+ ) {
1302
+ // generally we need to restart the track before publishing, often a full reconnect
1303
+ // is necessary because computer had gone to sleep.
1304
+ this.log.debug('restarting existing track', {
1305
+ ...this.logContext,
1306
+ track: pub.trackSid,
1307
+ });
1308
+ await track.restartTrack();
1309
+ }
1310
+ await this.publishOrRepublishTrack(track, pub.options, true);
1311
+ }),
1312
+ );
1313
+ resolve();
1314
+ } catch (error: any) {
1315
+ reject(error);
1316
+ } finally {
1317
+ this.republishPromise = undefined;
1257
1318
  }
1258
1319
  });
1259
1320
 
1260
- await Promise.all(
1261
- localPubs.map(async (pub) => {
1262
- const track = pub.track!;
1263
- await this.unpublishTrack(track, false);
1264
- if (
1265
- restartTracks &&
1266
- !track.isMuted &&
1267
- track.source !== Track.Source.ScreenShare &&
1268
- track.source !== Track.Source.ScreenShareAudio &&
1269
- (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
1270
- !track.isUserProvided
1271
- ) {
1272
- // generally we need to restart the track before publishing, often a full reconnect
1273
- // is necessary because computer had gone to sleep.
1274
- this.log.debug('restarting existing track', {
1275
- ...this.logContext,
1276
- track: pub.trackSid,
1277
- });
1278
- await track.restartTrack();
1279
- }
1280
- await this.publishTrack(track, pub.options);
1281
- }),
1282
- );
1321
+ await this.republishPromise;
1283
1322
  }
1284
1323
 
1285
1324
  /**
@@ -1310,6 +1349,47 @@ export default class LocalParticipant extends Participant {
1310
1349
  await this.engine.sendDataPacket(packet, kind);
1311
1350
  }
1312
1351
 
1352
+ async sendChatMessage(text: string): Promise<ChatMessage> {
1353
+ const msg = {
1354
+ id: crypto.randomUUID(),
1355
+ message: text,
1356
+ timestamp: Date.now(),
1357
+ } as const satisfies ChatMessage;
1358
+ const packet = new DataPacket({
1359
+ value: {
1360
+ case: 'chatMessage',
1361
+ value: new ChatMessageModel({
1362
+ ...msg,
1363
+ timestamp: protoInt64.parse(msg.timestamp),
1364
+ }),
1365
+ },
1366
+ });
1367
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1368
+ this.emit(ParticipantEvent.ChatMessage, msg);
1369
+ return msg;
1370
+ }
1371
+
1372
+ async editChatMessage(editText: string, originalMessage: ChatMessage) {
1373
+ const msg = {
1374
+ ...originalMessage,
1375
+ message: editText,
1376
+ editTimestamp: Date.now(),
1377
+ } as const satisfies ChatMessage;
1378
+ const packet = new DataPacket({
1379
+ value: {
1380
+ case: 'chatMessage',
1381
+ value: new ChatMessageModel({
1382
+ ...msg,
1383
+ timestamp: protoInt64.parse(msg.timestamp),
1384
+ editTimestamp: protoInt64.parse(msg.editTimestamp),
1385
+ }),
1386
+ },
1387
+ });
1388
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1389
+ this.emit(ParticipantEvent.ChatMessage, msg);
1390
+ return msg;
1391
+ }
1392
+
1313
1393
  /**
1314
1394
  * Control who can subscribe to LocalParticipant's published tracks.
1315
1395
  *
@@ -1530,7 +1610,12 @@ export default class LocalParticipant extends Participant {
1530
1610
  ...this.logContext,
1531
1611
  ...getLogContextFromTrack(track),
1532
1612
  });
1533
- await track.restartTrack();
1613
+ if (track instanceof LocalAudioTrack) {
1614
+ // fall back to default device if available
1615
+ await track.restartTrack({ deviceId: 'default' });
1616
+ } else {
1617
+ await track.restartTrack();
1618
+ }
1534
1619
  }
1535
1620
  } catch (e) {
1536
1621
  this.log.warn(`could not restart track, muting instead`, {
@@ -1565,4 +1650,13 @@ export default class LocalParticipant extends Participant {
1565
1650
  });
1566
1651
  return publication;
1567
1652
  }
1653
+
1654
+ private async waitForPendingPublicationOfSource(source: Track.Source) {
1655
+ const publishPromiseEntry = Array.from(this.pendingPublishPromises.entries()).find(
1656
+ ([pendingTrack]) => pendingTrack.source === source,
1657
+ );
1658
+ if (publishPromiseEntry) {
1659
+ return publishPromiseEntry[1];
1660
+ }
1661
+ }
1568
1662
  }
@@ -19,7 +19,7 @@ import type RemoteTrackPublication from '../track/RemoteTrackPublication';
19
19
  import { Track } from '../track/Track';
20
20
  import type { TrackPublication } from '../track/TrackPublication';
21
21
  import { diffAttributes } from '../track/utils';
22
- import type { LoggerOptions, TranscriptionSegment } from '../types';
22
+ import type { ChatMessage, LoggerOptions, TranscriptionSegment } from '../types';
23
23
 
24
24
  export enum ConnectionQuality {
25
25
  Excellent = 'excellent',
@@ -387,4 +387,5 @@ export type ParticipantEventCallbacks = {
387
387
  ) => void;
388
388
  attributesChanged: (changedAttributes: Record<string, string>) => void;
389
389
  localTrackSubscribed: (trackPublication: LocalTrackPublication) => void;
390
+ chatMessage: (msg: ChatMessage) => void;
390
391
  };