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.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +4 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +867 -425
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +7 -4
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +4 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +11 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +2 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts +7 -0
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +6 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +3 -2
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/src/room/Room.d.ts +7 -4
- package/dist/ts4.2/src/room/events.d.ts +4 -1
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +11 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +2 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/create.d.ts +7 -0
- package/dist/ts4.2/src/room/types.d.ts +6 -0
- package/dist/ts4.2/src/room/utils.d.ts +3 -2
- package/package.json +9 -9
- package/src/connectionHelper/checks/Checker.ts +1 -1
- package/src/e2ee/worker/FrameCryptor.ts +3 -1
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/Room.ts +67 -7
- package/src/room/events.ts +4 -0
- package/src/room/participant/LocalParticipant.ts +146 -52
- package/src/room/participant/Participant.ts +2 -1
- package/src/room/track/LocalTrack.ts +4 -2
- package/src/room/track/create.ts +27 -8
- package/src/room/types.ts +7 -0
- 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.
|
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.
|
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.
|
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.
|
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": "
|
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.
|
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.
|
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.
|
81
|
+
"typedoc": "0.26.6",
|
82
82
|
"typedoc-plugin-no-inherit": "1.4.0",
|
83
83
|
"typescript": "5.5.4",
|
84
|
-
"vite": "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
|
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.
|
177
|
+
const unlock = await this.remoteOfferLock.lock();
|
178
|
+
try {
|
179
|
+
await this.subscriber.setRemoteDescription(sd);
|
175
180
|
|
176
|
-
|
177
|
-
|
178
|
-
|
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 {
|
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
|
410
|
-
*
|
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 =
|
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 =
|
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
|
};
|
package/src/room/events.ts
CHANGED
@@ -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
|
-
|
391
|
-
|
392
|
-
|
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
|
441
|
-
|
442
|
-
|
443
|
-
track = await this.
|
444
|
-
|
445
|
-
|
446
|
-
|
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 (
|
544
|
-
|
545
|
-
|
546
|
-
|
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
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
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
|
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
|
-
|
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
|
};
|