livekit-client 2.5.0 → 2.5.2
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/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
|
};
|