livekit-client 2.5.0 → 2.5.2
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +4 -0
- 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 +517 -269
- 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/PCTransport.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 +8 -3
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +10 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +4 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/timers.d.ts +4 -4
- package/dist/src/room/timers.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/track/options.d.ts +1 -1
- package/dist/src/room/types.d.ts +2 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -1
- 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 +8 -3
- package/dist/ts4.2/src/room/events.d.ts +10 -2
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +4 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/src/room/timers.d.ts +4 -4
- 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/track/options.d.ts +1 -1
- package/dist/ts4.2/src/room/types.d.ts +2 -0
- package/dist/ts4.2/src/room/utils.d.ts +1 -1
- package/package.json +9 -9
- package/src/connectionHelper/checks/Checker.ts +1 -1
- package/src/e2ee/worker/FrameCryptor.ts +3 -1
- package/src/room/PCTransport.ts +3 -1
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/RTCEngine.ts +1 -1
- package/src/room/Room.ts +69 -7
- package/src/room/events.ts +10 -0
- package/src/room/participant/LocalParticipant.ts +126 -84
- package/src/room/participant/Participant.ts +1 -0
- package/src/room/timers.ts +15 -6
- package/src/room/track/LocalTrack.ts +4 -2
- package/src/room/track/LocalVideoTrack.test.ts +60 -0
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/create.ts +27 -8
- package/src/room/track/options.ts +1 -1
- package/src/room/types.ts +2 -0
- package/src/room/utils.ts +10 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "livekit-client",
|
3
|
-
"version": "2.5.
|
3
|
+
"version": "2.5.2",
|
4
4
|
"description": "JavaScript/TypeScript client SDK for LiveKit",
|
5
5
|
"main": "./dist/livekit-client.umd.js",
|
6
6
|
"unpkg": "./dist/livekit-client.umd.js",
|
@@ -36,23 +36,23 @@
|
|
36
36
|
"author": "David Zhao <david@davidzhao.com>",
|
37
37
|
"license": "Apache-2.0",
|
38
38
|
"dependencies": {
|
39
|
-
"@livekit/protocol": "1.20.
|
39
|
+
"@livekit/protocol": "1.20.1",
|
40
40
|
"events": "^3.3.0",
|
41
41
|
"loglevel": "^1.8.0",
|
42
42
|
"sdp-transform": "^2.14.1",
|
43
43
|
"ts-debounce": "^4.0.0",
|
44
|
-
"tslib": "2.
|
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.21.2",
|
77
77
|
"rollup-plugin-delete": "^2.0.0",
|
78
78
|
"rollup-plugin-re": "1.0.7",
|
79
79
|
"rollup-plugin-typescript2": "0.36.0",
|
80
80
|
"size-limit": "^8.2.4",
|
81
|
-
"typedoc": "0.26.
|
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.2",
|
85
85
|
"vitest": "^1.0.0"
|
86
86
|
},
|
87
87
|
"scripts": {
|
@@ -105,7 +105,7 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
|
|
105
105
|
if (this.room.state === ConnectionState.Connected) {
|
106
106
|
return this.room;
|
107
107
|
}
|
108
|
-
await this.room.connect(this.url, this.token);
|
108
|
+
await this.room.connect(this.url, this.token, this.connectOptions);
|
109
109
|
return this.room;
|
110
110
|
}
|
111
111
|
|
@@ -360,6 +360,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
360
360
|
}
|
361
361
|
} catch (error) {
|
362
362
|
if (error instanceof CryptorError && error.reason === CryptorErrorReason.InvalidKey) {
|
363
|
+
// emit an error if the key handler thinks we have a valid key
|
363
364
|
if (this.keys.hasValidKey) {
|
364
365
|
this.emit(CryptorEvent.Error, error);
|
365
366
|
this.keys.decryptionFailure();
|
@@ -369,7 +370,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
369
370
|
}
|
370
371
|
}
|
371
372
|
} else if (!this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
|
372
|
-
// emit an error
|
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
|
|
package/src/room/PCTransport.ts
CHANGED
@@ -23,6 +23,8 @@ eliminate this issue.
|
|
23
23
|
*/
|
24
24
|
const startBitrateForSVC = 0.7;
|
25
25
|
|
26
|
+
const debounceInterval = 20;
|
27
|
+
|
26
28
|
export const PCEvents = {
|
27
29
|
NegotiationStarted: 'negotiationStarted',
|
28
30
|
NegotiationComplete: 'negotiationComplete',
|
@@ -228,7 +230,7 @@ export default class PCTransport extends EventEmitter {
|
|
228
230
|
throw e;
|
229
231
|
}
|
230
232
|
}
|
231
|
-
},
|
233
|
+
}, debounceInterval);
|
232
234
|
|
233
235
|
async createAndSendOffer(options?: RTCOfferOptions) {
|
234
236
|
if (this.onOffer === undefined) {
|
@@ -57,6 +57,8 @@ export class PCTransportManager {
|
|
57
57
|
|
58
58
|
private connectionLock: Mutex;
|
59
59
|
|
60
|
+
private remoteOfferLock: Mutex;
|
61
|
+
|
60
62
|
private log = log;
|
61
63
|
|
62
64
|
private loggerOptions: LoggerOptions;
|
@@ -100,6 +102,7 @@ export class PCTransportManager {
|
|
100
102
|
this.state = PCTransportState.NEW;
|
101
103
|
|
102
104
|
this.connectionLock = new Mutex();
|
105
|
+
this.remoteOfferLock = new Mutex();
|
103
106
|
}
|
104
107
|
|
105
108
|
private get logContext() {
|
@@ -171,11 +174,16 @@ export class PCTransportManager {
|
|
171
174
|
sdp: sd.sdp,
|
172
175
|
signalingState: this.subscriber.getSignallingState().toString(),
|
173
176
|
});
|
174
|
-
await this.
|
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/RTCEngine.ts
CHANGED
package/src/room/Room.ts
CHANGED
@@ -57,6 +57,7 @@ import type { ConnectionQuality } from './participant/Participant';
|
|
57
57
|
import RemoteParticipant from './participant/RemoteParticipant';
|
58
58
|
import CriticalTimers from './timers';
|
59
59
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
60
|
+
import type LocalTrack from './track/LocalTrack';
|
60
61
|
import LocalTrackPublication from './track/LocalTrackPublication';
|
61
62
|
import LocalVideoTrack from './track/LocalVideoTrack';
|
62
63
|
import type RemoteTrack from './track/RemoteTrack';
|
@@ -162,6 +163,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
162
163
|
|
163
164
|
private isResuming: boolean = false;
|
164
165
|
|
166
|
+
/**
|
167
|
+
* map to store first point in time when a particular transcription segment was received
|
168
|
+
*/
|
169
|
+
private transcriptionReceivedTimes: Map<string, number>;
|
170
|
+
|
165
171
|
/**
|
166
172
|
* Creates a new Room, the primary construct for a LiveKit session.
|
167
173
|
* @param options
|
@@ -174,6 +180,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
174
180
|
this.options = { ...roomOptionDefaults, ...options };
|
175
181
|
|
176
182
|
this.log = getLogger(this.options.loggerName ?? LoggerNames.Room);
|
183
|
+
this.transcriptionReceivedTimes = new Map();
|
177
184
|
|
178
185
|
this.options.audioCaptureDefaults = {
|
179
186
|
...audioDefaults,
|
@@ -370,6 +377,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
370
377
|
})
|
371
378
|
.on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
|
372
379
|
this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
|
380
|
+
})
|
381
|
+
.on(EngineEvent.LocalTrackSubscribed, (subscribedSid) => {
|
382
|
+
const trackPublication = this.localParticipant
|
383
|
+
.getTrackPublications()
|
384
|
+
.find(({ trackSid }) => trackSid === subscribedSid) as LocalTrackPublication | undefined;
|
385
|
+
if (!trackPublication) {
|
386
|
+
this.log.warn(
|
387
|
+
'could not find local track subscription for subscribed event',
|
388
|
+
this.logContext,
|
389
|
+
);
|
390
|
+
return;
|
391
|
+
}
|
392
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackSubscribed, trackPublication);
|
393
|
+
this.emitWhenConnected(
|
394
|
+
RoomEvent.LocalTrackSubscribed,
|
395
|
+
trackPublication,
|
396
|
+
this.localParticipant,
|
397
|
+
);
|
373
398
|
});
|
374
399
|
|
375
400
|
if (this.localParticipant) {
|
@@ -382,9 +407,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
382
407
|
|
383
408
|
/**
|
384
409
|
* getLocalDevices abstracts navigator.mediaDevices.enumerateDevices.
|
385
|
-
* In particular, it
|
386
|
-
*
|
387
|
-
* The actual default device will be placed at top.
|
410
|
+
* In particular, it requests device permissions by default if needed
|
411
|
+
* and makes sure the returned device does not consist of dummy devices
|
388
412
|
* @param kind
|
389
413
|
* @returns a list of available local devices
|
390
414
|
*/
|
@@ -608,6 +632,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
608
632
|
|
609
633
|
this.localParticipant.sid = pi.sid;
|
610
634
|
this.localParticipant.identity = pi.identity;
|
635
|
+
this.localParticipant.setEnabledPublishCodecs(joinResponse.enabledPublishCodecs);
|
611
636
|
|
612
637
|
if (this.options.e2ee && this.e2eeManager) {
|
613
638
|
try {
|
@@ -1048,7 +1073,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1048
1073
|
let success = true;
|
1049
1074
|
const deviceConstraint = exact ? { exact: deviceId } : deviceId;
|
1050
1075
|
if (kind === 'audioinput') {
|
1051
|
-
const prevDeviceId =
|
1076
|
+
const prevDeviceId =
|
1077
|
+
this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
|
1052
1078
|
this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
|
1053
1079
|
deviceHasChanged = prevDeviceId !== deviceConstraint;
|
1054
1080
|
const tracks = Array.from(this.localParticipant.audioTrackPublications.values()).filter(
|
@@ -1063,7 +1089,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1063
1089
|
throw e;
|
1064
1090
|
}
|
1065
1091
|
} else if (kind === 'videoinput') {
|
1066
|
-
const prevDeviceId =
|
1092
|
+
const prevDeviceId =
|
1093
|
+
this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
|
1067
1094
|
this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
|
1068
1095
|
deviceHasChanged = prevDeviceId !== deviceConstraint;
|
1069
1096
|
const tracks = Array.from(this.localParticipant.videoTrackPublications.values()).filter(
|
@@ -1090,7 +1117,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1090
1117
|
(await DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) ?? '';
|
1091
1118
|
}
|
1092
1119
|
this.options.audioOutput ??= {};
|
1093
|
-
const prevDeviceId = this.options.audioOutput.deviceId;
|
1120
|
+
const prevDeviceId = this.getActiveDevice(kind) ?? this.options.audioOutput.deviceId;
|
1094
1121
|
this.options.audioOutput.deviceId = deviceId;
|
1095
1122
|
deviceHasChanged = prevDeviceId !== deviceConstraint;
|
1096
1123
|
|
@@ -1274,6 +1301,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1274
1301
|
this.clearConnectionReconcile();
|
1275
1302
|
this.isResuming = false;
|
1276
1303
|
this.bufferedEvents = [];
|
1304
|
+
this.transcriptionReceivedTimes.clear();
|
1277
1305
|
if (this.state === ConnectionState.Disconnected) {
|
1278
1306
|
return;
|
1279
1307
|
}
|
@@ -1535,7 +1563,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1535
1563
|
: this.getParticipantByIdentity(transcription.transcribedParticipantIdentity);
|
1536
1564
|
const publication = participant?.trackPublications.get(transcription.trackId);
|
1537
1565
|
|
1538
|
-
const segments = extractTranscriptionSegments(transcription);
|
1566
|
+
const segments = extractTranscriptionSegments(transcription, this.transcriptionReceivedTimes);
|
1539
1567
|
|
1540
1568
|
publication?.emit(TrackEvent.TranscriptionReceived, segments);
|
1541
1569
|
participant?.emit(ParticipantEvent.TranscriptionReceived, segments, publication);
|
@@ -1574,6 +1602,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1574
1602
|
};
|
1575
1603
|
|
1576
1604
|
private handleDeviceChange = async () => {
|
1605
|
+
const availableDevices = await DeviceManager.getInstance().getDevices();
|
1606
|
+
// inputs are automatically handled via TrackEvent.Ended causing a TrackEvent.Restarted. Here we only need to worry about audiooutputs changing
|
1607
|
+
const kinds: MediaDeviceKind[] = ['audiooutput'];
|
1608
|
+
for (let kind of kinds) {
|
1609
|
+
// switch to first available device if previously active device is not available any more
|
1610
|
+
const devicesOfKind = availableDevices.filter((d) => d.kind === kind);
|
1611
|
+
if (
|
1612
|
+
devicesOfKind.length > 0 &&
|
1613
|
+
!devicesOfKind.find((deviceInfo) => deviceInfo.deviceId === this.getActiveDevice(kind))
|
1614
|
+
) {
|
1615
|
+
await this.switchActiveDevice(kind, devicesOfKind[0].deviceId);
|
1616
|
+
}
|
1617
|
+
}
|
1618
|
+
|
1577
1619
|
this.emit(RoomEvent.MediaDevicesChanged);
|
1578
1620
|
};
|
1579
1621
|
|
@@ -1897,6 +1939,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1897
1939
|
|
1898
1940
|
private onLocalTrackPublished = async (pub: LocalTrackPublication) => {
|
1899
1941
|
pub.track?.on(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
|
1942
|
+
pub.track?.on(TrackEvent.Restarted, this.onLocalTrackRestarted);
|
1900
1943
|
pub.track?.getProcessor()?.onPublish?.(this);
|
1901
1944
|
|
1902
1945
|
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
@@ -1921,9 +1964,27 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1921
1964
|
|
1922
1965
|
private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
|
1923
1966
|
pub.track?.off(TrackEvent.TrackProcessorUpdate, this.onTrackProcessorUpdate);
|
1967
|
+
pub.track?.off(TrackEvent.Restarted, this.onLocalTrackRestarted);
|
1924
1968
|
this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
|
1925
1969
|
};
|
1926
1970
|
|
1971
|
+
private onLocalTrackRestarted = async (track: LocalTrack) => {
|
1972
|
+
const deviceId = await track.getDeviceId(false);
|
1973
|
+
const deviceKind = sourceToKind(track.source);
|
1974
|
+
if (
|
1975
|
+
deviceKind &&
|
1976
|
+
deviceId &&
|
1977
|
+
deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
|
1978
|
+
) {
|
1979
|
+
this.log.debug(
|
1980
|
+
`local track restarted, setting ${deviceKind} ${deviceId} active`,
|
1981
|
+
this.logContext,
|
1982
|
+
);
|
1983
|
+
this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
|
1984
|
+
this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
|
1985
|
+
}
|
1986
|
+
};
|
1987
|
+
|
1927
1988
|
private onLocalConnectionQualityChanged = (quality: ConnectionQuality) => {
|
1928
1989
|
this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
|
1929
1990
|
};
|
@@ -2202,4 +2263,5 @@ export type RoomEventCallbacks = {
|
|
2202
2263
|
encryptionError: (error: Error) => void;
|
2203
2264
|
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
2204
2265
|
activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
|
2266
|
+
localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
|
2205
2267
|
};
|
package/src/room/events.ts
CHANGED
@@ -323,6 +323,11 @@ export enum RoomEvent {
|
|
323
323
|
* args: (kind: MediaDeviceKind, deviceId: string)
|
324
324
|
*/
|
325
325
|
ActiveDeviceChanged = 'activeDeviceChanged',
|
326
|
+
|
327
|
+
/**
|
328
|
+
* fired when the first remote participant has subscribed to the localParticipant's track
|
329
|
+
*/
|
330
|
+
LocalTrackSubscribed = 'localTrackSubscribed',
|
326
331
|
}
|
327
332
|
|
328
333
|
export enum ParticipantEvent {
|
@@ -509,6 +514,11 @@ export enum ParticipantEvent {
|
|
509
514
|
* When a participant's attributes changed, this event will be emitted with the changed attributes
|
510
515
|
*/
|
511
516
|
AttributesChanged = 'attributesChanged',
|
517
|
+
|
518
|
+
/**
|
519
|
+
* fired on local participant only, when the first remote participant has subscribed to the track specified in the payload
|
520
|
+
*/
|
521
|
+
LocalTrackSubscribed = 'localTrackSubscribed',
|
512
522
|
}
|
513
523
|
|
514
524
|
/** @internal */
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import {
|
2
2
|
AddTrackRequest,
|
3
|
+
Codec,
|
3
4
|
DataPacket,
|
4
5
|
DataPacket_Kind,
|
5
6
|
Encryption_Type,
|
@@ -9,6 +10,7 @@ import {
|
|
9
10
|
RequestResponse_Reason,
|
10
11
|
SimulcastCodec,
|
11
12
|
SubscribedQualityUpdate,
|
13
|
+
TrackInfo,
|
12
14
|
TrackUnpublishedResponse,
|
13
15
|
UserPacket,
|
14
16
|
} from '@livekit/protocol';
|
@@ -29,6 +31,7 @@ import LocalTrack from '../track/LocalTrack';
|
|
29
31
|
import LocalTrackPublication from '../track/LocalTrackPublication';
|
30
32
|
import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
|
31
33
|
import { Track } from '../track/Track';
|
34
|
+
import { extractProcessorsFromOptions } from '../track/create';
|
32
35
|
import type {
|
33
36
|
AudioCaptureOptions,
|
34
37
|
BackupVideoCodec,
|
@@ -38,7 +41,6 @@ import type {
|
|
38
41
|
VideoCaptureOptions,
|
39
42
|
} from '../track/options';
|
40
43
|
import { ScreenSharePresets, VideoPresets, isBackupCodec } from '../track/options';
|
41
|
-
import type { TrackProcessor } from '../track/processor/types';
|
42
44
|
import {
|
43
45
|
constraintsForOptions,
|
44
46
|
getLogContextFromTrack,
|
@@ -110,6 +112,8 @@ export default class LocalParticipant extends Participant {
|
|
110
112
|
}
|
111
113
|
>;
|
112
114
|
|
115
|
+
private enabledPublishVideoCodecs: Codec[] = [];
|
116
|
+
|
113
117
|
/** @internal */
|
114
118
|
constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
|
115
119
|
super(sid, identity, undefined, undefined, {
|
@@ -482,6 +486,9 @@ export default class LocalParticipant extends Participant {
|
|
482
486
|
* @returns
|
483
487
|
*/
|
484
488
|
async createTracks(options?: CreateLocalTracksOptions): Promise<LocalTrack[]> {
|
489
|
+
options ??= {};
|
490
|
+
const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(options);
|
491
|
+
|
485
492
|
const mergedOptions = mergeDefaultOptions(
|
486
493
|
options,
|
487
494
|
this.roomOptions?.audioCaptureDefaults,
|
@@ -536,12 +543,10 @@ export default class LocalParticipant extends Participant {
|
|
536
543
|
track.setAudioContext(this.audioContext);
|
537
544
|
}
|
538
545
|
track.mediaStream = stream;
|
539
|
-
if (
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
await track.setProcessor(trackOptions.processor as TrackProcessor<Track.Kind.Video>);
|
544
|
-
}
|
546
|
+
if (track instanceof LocalAudioTrack && audioProcessor) {
|
547
|
+
await track.setProcessor(audioProcessor);
|
548
|
+
} else if (track instanceof LocalVideoTrack && videoProcessor) {
|
549
|
+
await track.setProcessor(videoProcessor);
|
545
550
|
}
|
546
551
|
return track;
|
547
552
|
}),
|
@@ -775,6 +780,17 @@ export default class LocalParticipant extends Participant {
|
|
775
780
|
if (opts.videoCodec === undefined) {
|
776
781
|
opts.videoCodec = defaultVideoCodec;
|
777
782
|
}
|
783
|
+
if (this.enabledPublishVideoCodecs.length > 0) {
|
784
|
+
// fallback to a supported codec if it is not supported
|
785
|
+
if (
|
786
|
+
!this.enabledPublishVideoCodecs.some(
|
787
|
+
(c) => opts.videoCodec === mimeTypeToVideoCodecString(c.mime),
|
788
|
+
)
|
789
|
+
) {
|
790
|
+
opts.videoCodec = mimeTypeToVideoCodecString(this.enabledPublishVideoCodecs[0].mime);
|
791
|
+
}
|
792
|
+
}
|
793
|
+
|
778
794
|
const videoCodec = opts.videoCodec;
|
779
795
|
|
780
796
|
// handle track actions
|
@@ -908,33 +924,87 @@ export default class LocalParticipant extends Participant {
|
|
908
924
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
909
925
|
}
|
910
926
|
|
911
|
-
const
|
912
|
-
|
913
|
-
|
914
|
-
let primaryCodecMime: string | undefined;
|
915
|
-
ti.codecs.forEach((codec) => {
|
916
|
-
if (primaryCodecMime === undefined) {
|
917
|
-
primaryCodecMime = codec.mimeType;
|
927
|
+
const negotiate = async () => {
|
928
|
+
if (!this.engine.pcManager) {
|
929
|
+
throw new UnexpectedConnectionState('pcManager is not ready');
|
918
930
|
}
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
if (
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
931
|
+
|
932
|
+
track.sender = await this.engine.createSender(track, opts, encodings);
|
933
|
+
|
934
|
+
if (track instanceof LocalVideoTrack) {
|
935
|
+
opts.degradationPreference ??= getDefaultDegradationPreference(track);
|
936
|
+
track.setDegradationPreference(opts.degradationPreference);
|
937
|
+
}
|
938
|
+
|
939
|
+
if (encodings) {
|
940
|
+
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
941
|
+
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
942
|
+
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
|
943
|
+
publish high quality audio track. But firefox always uses this value as the actual
|
944
|
+
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
945
|
+
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
946
|
+
fix the issue.
|
947
|
+
*/
|
948
|
+
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
949
|
+
for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
|
950
|
+
if (transceiver.sender === track.sender) {
|
951
|
+
trackTransceiver = transceiver;
|
952
|
+
break;
|
953
|
+
}
|
954
|
+
}
|
955
|
+
if (trackTransceiver) {
|
956
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
957
|
+
transceiver: trackTransceiver,
|
958
|
+
codec: 'opus',
|
959
|
+
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
|
960
|
+
});
|
961
|
+
}
|
962
|
+
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
|
963
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
964
|
+
cid: req.cid,
|
965
|
+
codec: track.codec,
|
966
|
+
maxbr: encodings[0].maxBitrate / 1000,
|
967
|
+
});
|
968
|
+
}
|
969
|
+
}
|
970
|
+
|
971
|
+
await this.engine.negotiate();
|
972
|
+
};
|
973
|
+
|
974
|
+
let ti: TrackInfo;
|
975
|
+
if (this.enabledPublishVideoCodecs.length > 0) {
|
976
|
+
const rets = await Promise.all([this.engine.addTrack(req), negotiate()]);
|
977
|
+
ti = rets[0];
|
978
|
+
} else {
|
979
|
+
ti = await this.engine.addTrack(req);
|
980
|
+
// server might not support the codec the client has requested, in that case, fallback
|
981
|
+
// to a supported codec
|
982
|
+
let primaryCodecMime: string | undefined;
|
983
|
+
ti.codecs.forEach((codec) => {
|
984
|
+
if (primaryCodecMime === undefined) {
|
985
|
+
primaryCodecMime = codec.mimeType;
|
986
|
+
}
|
987
|
+
});
|
988
|
+
if (primaryCodecMime && track.kind === Track.Kind.Video) {
|
989
|
+
const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
|
990
|
+
if (updatedCodec !== videoCodec) {
|
991
|
+
this.log.debug('falling back to server selected codec', {
|
992
|
+
...this.logContext,
|
993
|
+
...getLogContextFromTrack(track),
|
994
|
+
codec: updatedCodec,
|
995
|
+
});
|
996
|
+
opts.videoCodec = updatedCodec;
|
997
|
+
|
998
|
+
// recompute encodings since bitrates/etc could have changed
|
999
|
+
encodings = computeVideoEncodings(
|
1000
|
+
track.source === Track.Source.ScreenShare,
|
1001
|
+
req.width,
|
1002
|
+
req.height,
|
1003
|
+
opts,
|
1004
|
+
);
|
1005
|
+
}
|
937
1006
|
}
|
1007
|
+
await negotiate();
|
938
1008
|
}
|
939
1009
|
|
940
1010
|
const publication = new LocalTrackPublication(track.kind, ti, track, {
|
@@ -945,56 +1015,12 @@ export default class LocalParticipant extends Participant {
|
|
945
1015
|
publication.options = opts;
|
946
1016
|
track.sid = ti.sid;
|
947
1017
|
|
948
|
-
if (!this.engine.pcManager) {
|
949
|
-
throw new UnexpectedConnectionState('pcManager is not ready');
|
950
|
-
}
|
951
1018
|
this.log.debug(`publishing ${track.kind} with encodings`, {
|
952
1019
|
...this.logContext,
|
953
1020
|
encodings,
|
954
1021
|
trackInfo: ti,
|
955
1022
|
});
|
956
1023
|
|
957
|
-
track.sender = await this.engine.createSender(track, opts, encodings);
|
958
|
-
|
959
|
-
if (track instanceof LocalVideoTrack) {
|
960
|
-
opts.degradationPreference ??= getDefaultDegradationPreference(track);
|
961
|
-
track.setDegradationPreference(opts.degradationPreference);
|
962
|
-
}
|
963
|
-
|
964
|
-
if (encodings) {
|
965
|
-
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
966
|
-
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
967
|
-
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
|
968
|
-
publish high quality audio track. But firefox always uses this value as the actual
|
969
|
-
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
970
|
-
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
971
|
-
fix the issue.
|
972
|
-
*/
|
973
|
-
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
974
|
-
for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
|
975
|
-
if (transceiver.sender === track.sender) {
|
976
|
-
trackTransceiver = transceiver;
|
977
|
-
break;
|
978
|
-
}
|
979
|
-
}
|
980
|
-
if (trackTransceiver) {
|
981
|
-
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
982
|
-
transceiver: trackTransceiver,
|
983
|
-
codec: 'opus',
|
984
|
-
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
|
985
|
-
});
|
986
|
-
}
|
987
|
-
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
|
988
|
-
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
989
|
-
cid: req.cid,
|
990
|
-
codec: track.codec,
|
991
|
-
maxbr: encodings[0].maxBitrate / 1000,
|
992
|
-
});
|
993
|
-
}
|
994
|
-
}
|
995
|
-
|
996
|
-
await this.engine.negotiate();
|
997
|
-
|
998
1024
|
if (track instanceof LocalVideoTrack) {
|
999
1025
|
track.startMonitor(this.engine.client);
|
1000
1026
|
} else if (track instanceof LocalAudioTrack) {
|
@@ -1081,15 +1107,19 @@ export default class LocalParticipant extends Participant {
|
|
1081
1107
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
1082
1108
|
}
|
1083
1109
|
|
1084
|
-
const
|
1110
|
+
const negotiate = async () => {
|
1111
|
+
const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
|
1112
|
+
if (encodings) {
|
1113
|
+
transceiverInit.sendEncodings = encodings;
|
1114
|
+
}
|
1115
|
+
await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
|
1085
1116
|
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1117
|
+
await this.engine.negotiate();
|
1118
|
+
};
|
1119
|
+
|
1120
|
+
const rets = await Promise.all([this.engine.addTrack(req), negotiate()]);
|
1121
|
+
const ti = rets[0];
|
1091
1122
|
|
1092
|
-
await this.engine.negotiate();
|
1093
1123
|
this.log.debug(`published ${videoCodec} for track ${track.sid}`, {
|
1094
1124
|
...this.logContext,
|
1095
1125
|
encodings,
|
@@ -1309,6 +1339,13 @@ export default class LocalParticipant extends Participant {
|
|
1309
1339
|
}
|
1310
1340
|
}
|
1311
1341
|
|
1342
|
+
/** @internal */
|
1343
|
+
setEnabledPublishCodecs(codecs: Codec[]) {
|
1344
|
+
this.enabledPublishVideoCodecs = codecs.filter(
|
1345
|
+
(c) => c.mime.split('/')[0].toLowerCase() === 'video',
|
1346
|
+
);
|
1347
|
+
}
|
1348
|
+
|
1312
1349
|
/** @internal */
|
1313
1350
|
updateInfo(info: ParticipantInfo): boolean {
|
1314
1351
|
if (info.sid !== this.sid) {
|
@@ -1494,7 +1531,12 @@ export default class LocalParticipant extends Participant {
|
|
1494
1531
|
...this.logContext,
|
1495
1532
|
...getLogContextFromTrack(track),
|
1496
1533
|
});
|
1497
|
-
|
1534
|
+
if (track instanceof LocalAudioTrack) {
|
1535
|
+
// fall back to default device if available
|
1536
|
+
await track.restartTrack({ deviceId: 'default' });
|
1537
|
+
} else {
|
1538
|
+
await track.restartTrack();
|
1539
|
+
}
|
1498
1540
|
}
|
1499
1541
|
} catch (e) {
|
1500
1542
|
this.log.warn(`could not restart track, muting instead`, {
|
@@ -386,4 +386,5 @@ export type ParticipantEventCallbacks = {
|
|
386
386
|
status: TrackPublication.SubscriptionStatus,
|
387
387
|
) => void;
|
388
388
|
attributesChanged: (changedAttributes: Record<string, string>) => void;
|
389
|
+
localTrackSubscribed: (trackPublication: LocalTrackPublication) => void;
|
389
390
|
};
|