livekit-client 2.5.1 → 2.5.3

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