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.
- 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
|
};
|