livekit-client 2.12.0 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +1 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +145 -56
- 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/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +2 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +2 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +11 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +14 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +3 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/room/Room.d.ts +2 -1
- package/dist/ts4.2/src/room/errors.d.ts +2 -1
- package/dist/ts4.2/src/room/events.d.ts +11 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +14 -1
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/utils.d.ts +3 -0
- package/package.json +11 -11
- package/src/room/RTCEngine.ts +6 -4
- package/src/room/Room.ts +14 -6
- package/src/room/errors.ts +1 -0
- package/src/room/events.ts +12 -0
- package/src/room/participant/LocalParticipant.ts +37 -18
- package/src/room/participant/Participant.ts +48 -3
- package/src/room/participant/publishUtils.ts +4 -0
- package/src/room/track/LocalVideoTrack.ts +14 -5
- package/src/room/track/create.ts +3 -5
- package/src/room/utils.ts +14 -1
package/src/room/Room.ts
CHANGED
@@ -103,7 +103,7 @@ import {
|
|
103
103
|
isLocalParticipant,
|
104
104
|
isReactNative,
|
105
105
|
isRemotePub,
|
106
|
-
|
106
|
+
isSafariBased,
|
107
107
|
isWeb,
|
108
108
|
numberToBigInt,
|
109
109
|
sleep,
|
@@ -1356,6 +1356,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1356
1356
|
// @ts-expect-error setSinkId is not yet in the typescript type of AudioContext
|
1357
1357
|
this.audioContext?.setSinkId(deviceId);
|
1358
1358
|
}
|
1359
|
+
|
1359
1360
|
// also set audio output on all audio elements, even if webAudioMix is enabled in order to workaround echo cancellation not working on chrome with non-default output devices
|
1360
1361
|
// see https://issues.chromium.org/issues/40252911#comment7
|
1361
1362
|
await Promise.all(
|
@@ -1644,6 +1645,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1644
1645
|
participant.unpublishTrack(publication.trackSid, true);
|
1645
1646
|
});
|
1646
1647
|
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
1648
|
+
participant.setDisconnected();
|
1647
1649
|
this.localParticipant?.handleParticipantDisconnected(participant.identity);
|
1648
1650
|
}
|
1649
1651
|
|
@@ -2039,14 +2041,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
2039
2041
|
}
|
2040
2042
|
}
|
2041
2043
|
|
2042
|
-
if ((kind === 'audioinput' && !
|
2044
|
+
if ((kind === 'audioinput' && !isSafariBased()) || kind === 'videoinput') {
|
2043
2045
|
// airpods on Safari need special handling for audioinput as the track doesn't end as soon as you take them out
|
2044
2046
|
continue;
|
2045
2047
|
}
|
2046
2048
|
// switch to first available device if previously active device is not available any more
|
2047
2049
|
if (
|
2048
2050
|
devicesOfKind.length > 0 &&
|
2049
|
-
!devicesOfKind.find((deviceInfo) => deviceInfo.deviceId === this.getActiveDevice(kind))
|
2051
|
+
!devicesOfKind.find((deviceInfo) => deviceInfo.deviceId === this.getActiveDevice(kind)) &&
|
2052
|
+
// avoid switching audio output on safari without explicit user action as it leads to slowed down audio playback
|
2053
|
+
(kind !== 'audiooutput' || !isSafariBased())
|
2050
2054
|
) {
|
2051
2055
|
await this.switchActiveDevice(kind, devicesOfKind[0].deviceId);
|
2052
2056
|
}
|
@@ -2240,6 +2244,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
2240
2244
|
status,
|
2241
2245
|
participant,
|
2242
2246
|
);
|
2247
|
+
})
|
2248
|
+
.on(ParticipantEvent.Active, () => {
|
2249
|
+
this.emitWhenConnected(RoomEvent.ParticipantActive, participant);
|
2243
2250
|
});
|
2244
2251
|
|
2245
2252
|
// update info at the end after callbacks have been set up
|
@@ -2432,8 +2439,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
2432
2439
|
this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
|
2433
2440
|
};
|
2434
2441
|
|
2435
|
-
private onMediaDevicesError = (e: Error) => {
|
2436
|
-
this.emit(RoomEvent.MediaDevicesError, e);
|
2442
|
+
private onMediaDevicesError = (e: Error, kind?: MediaDeviceKind) => {
|
2443
|
+
this.emit(RoomEvent.MediaDevicesError, e, kind);
|
2437
2444
|
};
|
2438
2445
|
|
2439
2446
|
private onLocalParticipantPermissionsChanged = (prevPermissions?: ParticipantPermission) => {
|
@@ -2687,7 +2694,7 @@ export type RoomEventCallbacks = {
|
|
2687
2694
|
publication?: TrackPublication,
|
2688
2695
|
) => void;
|
2689
2696
|
connectionQualityChanged: (quality: ConnectionQuality, participant: Participant) => void;
|
2690
|
-
mediaDevicesError: (error: Error) => void;
|
2697
|
+
mediaDevicesError: (error: Error, kind?: MediaDeviceKind) => void;
|
2691
2698
|
trackStreamStateChanged: (
|
2692
2699
|
publication: RemoteTrackPublication,
|
2693
2700
|
streamState: Track.StreamState,
|
@@ -2714,4 +2721,5 @@ export type RoomEventCallbacks = {
|
|
2714
2721
|
chatMessage: (message: ChatMessage, participant?: RemoteParticipant | LocalParticipant) => void;
|
2715
2722
|
localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
|
2716
2723
|
metricsReceived: (metrics: MetricsBatch, participant?: Participant) => void;
|
2724
|
+
participantActive: (participant: Participant) => void;
|
2717
2725
|
};
|
package/src/room/errors.ts
CHANGED
package/src/room/events.ts
CHANGED
@@ -203,6 +203,13 @@ export enum RoomEvent {
|
|
203
203
|
*/
|
204
204
|
ParticipantAttributesChanged = 'participantAttributesChanged',
|
205
205
|
|
206
|
+
/**
|
207
|
+
* Emitted when the participant's state changes to ACTIVE and is ready to send/receive data messages
|
208
|
+
*
|
209
|
+
* args: (participant: [[Participant]])
|
210
|
+
*/
|
211
|
+
ParticipantActive = 'participantActive',
|
212
|
+
|
206
213
|
/**
|
207
214
|
* Room metadata is a simple way for app-specific state to be pushed to
|
208
215
|
* all users.
|
@@ -540,6 +547,11 @@ export enum ParticipantEvent {
|
|
540
547
|
|
541
548
|
/** only emitted on local participant */
|
542
549
|
ChatMessage = 'chatMessage',
|
550
|
+
|
551
|
+
/**
|
552
|
+
* Emitted when the participant's state changes to ACTIVE and is ready to send/receive data messages
|
553
|
+
*/
|
554
|
+
Active = 'active',
|
543
555
|
}
|
544
556
|
|
545
557
|
/** @internal */
|
@@ -71,6 +71,7 @@ import {
|
|
71
71
|
mergeDefaultOptions,
|
72
72
|
mimeTypeToVideoCodecString,
|
73
73
|
screenCaptureToDisplayMediaStreamOptions,
|
74
|
+
sourceToKind,
|
74
75
|
} from '../track/utils';
|
75
76
|
import {
|
76
77
|
type ByteStreamInfo,
|
@@ -517,7 +518,7 @@ export default class LocalParticipant extends Participant {
|
|
517
518
|
tr.stop();
|
518
519
|
});
|
519
520
|
if (e instanceof Error) {
|
520
|
-
this.emit(ParticipantEvent.MediaDevicesError, e);
|
521
|
+
this.emit(ParticipantEvent.MediaDevicesError, e, sourceToKind(source));
|
521
522
|
}
|
522
523
|
this.pendingPublishing.delete(source);
|
523
524
|
throw e;
|
@@ -1142,11 +1143,32 @@ export default class LocalParticipant extends Participant {
|
|
1142
1143
|
};
|
1143
1144
|
|
1144
1145
|
let ti: TrackInfo;
|
1146
|
+
const addTrackPromise = new Promise<TrackInfo>(async (resolve, reject) => {
|
1147
|
+
try {
|
1148
|
+
ti = await this.engine.addTrack(req);
|
1149
|
+
resolve(ti);
|
1150
|
+
} catch (err) {
|
1151
|
+
if (track.sender && this.engine.pcManager?.publisher) {
|
1152
|
+
this.engine.pcManager.publisher.removeTrack(track.sender);
|
1153
|
+
await this.engine.negotiate().catch((negotiateErr) => {
|
1154
|
+
this.log.error(
|
1155
|
+
'failed to negotiate after removing track due to failed add track request',
|
1156
|
+
{
|
1157
|
+
...this.logContext,
|
1158
|
+
...getLogContextFromTrack(track),
|
1159
|
+
error: negotiateErr,
|
1160
|
+
},
|
1161
|
+
);
|
1162
|
+
});
|
1163
|
+
}
|
1164
|
+
reject(err);
|
1165
|
+
}
|
1166
|
+
});
|
1145
1167
|
if (this.enabledPublishVideoCodecs.length > 0) {
|
1146
|
-
const rets = await Promise.all([
|
1168
|
+
const rets = await Promise.all([addTrackPromise, negotiate()]);
|
1147
1169
|
ti = rets[0];
|
1148
1170
|
} else {
|
1149
|
-
ti = await
|
1171
|
+
ti = await addTrackPromise;
|
1150
1172
|
// server might not support the codec the client has requested, in that case, fallback
|
1151
1173
|
// to a supported codec
|
1152
1174
|
let primaryCodecMime: string | undefined;
|
@@ -1780,6 +1802,7 @@ export default class LocalParticipant extends Participant {
|
|
1780
1802
|
streamId,
|
1781
1803
|
topic: info.topic,
|
1782
1804
|
timestamp: numberToBigInt(Date.now()),
|
1805
|
+
attributes: info.attributes,
|
1783
1806
|
contentHeader: {
|
1784
1807
|
case: 'byteHeader',
|
1785
1808
|
value: new DataStream_ByteHeader({
|
@@ -2153,22 +2176,18 @@ export default class LocalParticipant extends Participant {
|
|
2153
2176
|
});
|
2154
2177
|
return;
|
2155
2178
|
}
|
2156
|
-
if (
|
2157
|
-
|
2158
|
-
|
2159
|
-
|
2160
|
-
|
2161
|
-
|
2162
|
-
|
2163
|
-
this.
|
2164
|
-
|
2165
|
-
|
2166
|
-
|
2167
|
-
await this.publishAdditionalCodecForTrack(pub.videoTrack, codec, pub.options);
|
2168
|
-
}
|
2179
|
+
if (!pub.videoTrack) {
|
2180
|
+
return;
|
2181
|
+
}
|
2182
|
+
const newCodecs = await pub.videoTrack.setPublishingCodecs(update.subscribedCodecs);
|
2183
|
+
for await (const codec of newCodecs) {
|
2184
|
+
if (isBackupCodec(codec)) {
|
2185
|
+
this.log.debug(`publish ${codec} for ${pub.videoTrack.sid}`, {
|
2186
|
+
...this.logContext,
|
2187
|
+
...getLogContextFromTrack(pub),
|
2188
|
+
});
|
2189
|
+
await this.publishAdditionalCodecForTrack(pub.videoTrack, codec, pub.options);
|
2169
2190
|
}
|
2170
|
-
} else if (update.subscribedQualities.length > 0) {
|
2171
|
-
await pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
|
2172
2191
|
}
|
2173
2192
|
};
|
2174
2193
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import {
|
2
2
|
DataPacket_Kind,
|
3
3
|
ParticipantInfo,
|
4
|
+
ParticipantInfo_State,
|
4
5
|
ParticipantInfo_Kind as ParticipantKind,
|
5
6
|
ParticipantPermission,
|
6
7
|
ConnectionQuality as ProtoQuality,
|
@@ -18,7 +19,7 @@ import { Track } from '../track/Track';
|
|
18
19
|
import type { TrackPublication } from '../track/TrackPublication';
|
19
20
|
import { diffAttributes } from '../track/utils';
|
20
21
|
import type { ChatMessage, LoggerOptions, TranscriptionSegment } from '../types';
|
21
|
-
import { isAudioTrack } from '../utils';
|
22
|
+
import { Future, isAudioTrack } from '../utils';
|
22
23
|
|
23
24
|
export enum ConnectionQuality {
|
24
25
|
Excellent = 'excellent',
|
@@ -93,6 +94,8 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
93
94
|
|
94
95
|
protected loggerOptions?: LoggerOptions;
|
95
96
|
|
97
|
+
protected activeFuture?: Future<void>;
|
98
|
+
|
96
99
|
protected get logContext() {
|
97
100
|
return {
|
98
101
|
...this.loggerOptions?.loggerContextCb?.(),
|
@@ -110,6 +113,10 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
110
113
|
return this.permissions?.agent || this.kind === ParticipantKind.AGENT;
|
111
114
|
}
|
112
115
|
|
116
|
+
get isActive() {
|
117
|
+
return this.participantInfo?.state === ParticipantInfo_State.ACTIVE;
|
118
|
+
}
|
119
|
+
|
113
120
|
get kind() {
|
114
121
|
return this._kind;
|
115
122
|
}
|
@@ -173,6 +180,28 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
173
180
|
}
|
174
181
|
}
|
175
182
|
|
183
|
+
/**
|
184
|
+
* Waits until the participant is active and ready to receive data messages
|
185
|
+
* @returns a promise that resolves when the participant is active
|
186
|
+
*/
|
187
|
+
waitUntilActive(): Promise<void> {
|
188
|
+
if (this.isActive) {
|
189
|
+
return Promise.resolve();
|
190
|
+
}
|
191
|
+
|
192
|
+
if (this.activeFuture) {
|
193
|
+
return this.activeFuture.promise;
|
194
|
+
}
|
195
|
+
|
196
|
+
this.activeFuture = new Future<void>();
|
197
|
+
|
198
|
+
this.once(ParticipantEvent.Active, () => {
|
199
|
+
this.activeFuture?.resolve?.();
|
200
|
+
this.activeFuture = undefined;
|
201
|
+
});
|
202
|
+
return this.activeFuture.promise;
|
203
|
+
}
|
204
|
+
|
176
205
|
get connectionQuality(): ConnectionQuality {
|
177
206
|
return this._connectionQuality;
|
178
207
|
}
|
@@ -224,12 +253,17 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
224
253
|
this._setName(info.name);
|
225
254
|
this._setMetadata(info.metadata);
|
226
255
|
this._setAttributes(info.attributes);
|
256
|
+
if (
|
257
|
+
info.state === ParticipantInfo_State.ACTIVE &&
|
258
|
+
this.participantInfo?.state !== ParticipantInfo_State.ACTIVE
|
259
|
+
) {
|
260
|
+
this.emit(ParticipantEvent.Active);
|
261
|
+
}
|
227
262
|
if (info.permission) {
|
228
263
|
this.setPermissions(info.permission);
|
229
264
|
}
|
230
265
|
// set this last so setMetadata can detect changes
|
231
266
|
this.participantInfo = info;
|
232
|
-
this.log.trace('update participant info', { ...this.logContext, info });
|
233
267
|
return true;
|
234
268
|
}
|
235
269
|
|
@@ -310,6 +344,16 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
310
344
|
}
|
311
345
|
}
|
312
346
|
|
347
|
+
/**
|
348
|
+
* @internal
|
349
|
+
*/
|
350
|
+
setDisconnected() {
|
351
|
+
if (this.activeFuture) {
|
352
|
+
this.activeFuture.reject?.(new Error('Participant disconnected'));
|
353
|
+
this.activeFuture = undefined;
|
354
|
+
}
|
355
|
+
}
|
356
|
+
|
313
357
|
/**
|
314
358
|
* @internal
|
315
359
|
*/
|
@@ -377,7 +421,7 @@ export type ParticipantEventCallbacks = {
|
|
377
421
|
publication: RemoteTrackPublication,
|
378
422
|
status: TrackPublication.PermissionStatus,
|
379
423
|
) => void;
|
380
|
-
mediaDevicesError: (error: Error) => void;
|
424
|
+
mediaDevicesError: (error: Error, kind?: MediaDeviceKind) => void;
|
381
425
|
audioStreamAcquired: () => void;
|
382
426
|
participantPermissionsChanged: (prevPermissions?: ParticipantPermission) => void;
|
383
427
|
trackSubscriptionStatusChanged: (
|
@@ -387,4 +431,5 @@ export type ParticipantEventCallbacks = {
|
|
387
431
|
attributesChanged: (changedAttributes: Record<string, string>) => void;
|
388
432
|
localTrackSubscribed: (trackPublication: LocalTrackPublication) => void;
|
389
433
|
chatMessage: (msg: ChatMessage) => void;
|
434
|
+
active: () => void;
|
390
435
|
};
|
@@ -19,6 +19,7 @@ import {
|
|
19
19
|
isReactNative,
|
20
20
|
isSVCCodec,
|
21
21
|
isSafari,
|
22
|
+
isSafariSvcApi,
|
22
23
|
unwrapConstraint,
|
23
24
|
} from '../utils';
|
24
25
|
|
@@ -158,12 +159,15 @@ export function computeVideoEncodings(
|
|
158
159
|
(browser?.name === 'Chrome' && compareVersions(browser?.version, '113') < 0)
|
159
160
|
) {
|
160
161
|
const bitratesRatio = sm.suffix == 'h' ? 2 : 3;
|
162
|
+
// safari 18.4 uses a different svc API that requires scaleResolutionDownBy to be set.
|
163
|
+
const requireScale = isSafariSvcApi(browser);
|
161
164
|
for (let i = 0; i < sm.spatial; i += 1) {
|
162
165
|
// in legacy SVC, scaleResolutionDownBy cannot be set
|
163
166
|
encodings.push({
|
164
167
|
rid: videoRids[2 - i],
|
165
168
|
maxBitrate: videoEncoding.maxBitrate / bitratesRatio ** i,
|
166
169
|
maxFramerate: original.encoding.maxFramerate,
|
170
|
+
scaleResolutionDownBy: requireScale ? 2 ** i : undefined,
|
167
171
|
});
|
168
172
|
}
|
169
173
|
// legacy SVC, scalabilityMode is set only on the first encoding
|
@@ -12,7 +12,7 @@ import { ScalabilityMode } from '../participant/publishUtils';
|
|
12
12
|
import type { VideoSenderStats } from '../stats';
|
13
13
|
import { computeBitrate, monitorFrequency } from '../stats';
|
14
14
|
import type { LoggerOptions } from '../types';
|
15
|
-
import { compareVersions, isFireFox, isMobile, isWeb } from '../utils';
|
15
|
+
import { compareVersions, isFireFox, isMobile, isSVCCodec, isWeb } from '../utils';
|
16
16
|
import LocalTrack from './LocalTrack';
|
17
17
|
import { Track, VideoQuality } from './Track';
|
18
18
|
import type { VideoCaptureOptions, VideoCodec } from './options';
|
@@ -239,7 +239,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
239
239
|
);
|
240
240
|
}
|
241
241
|
this.log.debug(`setting publishing quality. max quality ${maxQuality}`, this.logContext);
|
242
|
-
this.setPublishingLayers(qualities);
|
242
|
+
this.setPublishingLayers(isSVCCodec(this.codec), qualities);
|
243
243
|
}
|
244
244
|
|
245
245
|
async restartTrack(options?: VideoCaptureOptions) {
|
@@ -334,7 +334,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
334
334
|
});
|
335
335
|
// only enable simulcast codec for preference codec setted
|
336
336
|
if (!this.codec && codecs.length > 0) {
|
337
|
-
await this.setPublishingLayers(codecs[0].qualities);
|
337
|
+
await this.setPublishingLayers(isSVCCodec(codecs[0].codec), codecs[0].qualities);
|
338
338
|
return [];
|
339
339
|
}
|
340
340
|
|
@@ -343,7 +343,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
343
343
|
const newCodecs: VideoCodec[] = [];
|
344
344
|
for await (const codec of codecs) {
|
345
345
|
if (!this.codec || this.codec === codec.codec) {
|
346
|
-
await this.setPublishingLayers(codec.qualities);
|
346
|
+
await this.setPublishingLayers(isSVCCodec(codec.codec), codec.qualities);
|
347
347
|
} else {
|
348
348
|
const simulcastCodecInfo = this.simulcastCodecs.get(codec.codec as VideoCodec);
|
349
349
|
this.log.debug(`try setPublishingCodec for ${codec.codec}`, {
|
@@ -364,6 +364,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
364
364
|
simulcastCodecInfo.encodings!,
|
365
365
|
codec.qualities,
|
366
366
|
this.senderLock,
|
367
|
+
isSVCCodec(codec.codec),
|
367
368
|
this.log,
|
368
369
|
this.logContext,
|
369
370
|
);
|
@@ -377,7 +378,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
377
378
|
* @internal
|
378
379
|
* Sets layers that should be publishing
|
379
380
|
*/
|
380
|
-
async setPublishingLayers(qualities: SubscribedQuality[]) {
|
381
|
+
async setPublishingLayers(isSvc: boolean, qualities: SubscribedQuality[]) {
|
381
382
|
this.log.debug('setting publishing layers', { ...this.logContext, qualities });
|
382
383
|
if (!this.sender || !this.encodings) {
|
383
384
|
return;
|
@@ -388,6 +389,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
388
389
|
this.encodings,
|
389
390
|
qualities,
|
390
391
|
this.senderLock,
|
392
|
+
isSvc,
|
391
393
|
this.log,
|
392
394
|
this.logContext,
|
393
395
|
);
|
@@ -434,6 +436,7 @@ async function setPublishingLayersForSender(
|
|
434
436
|
senderEncodings: RTCRtpEncodingParameters[],
|
435
437
|
qualities: SubscribedQuality[],
|
436
438
|
senderLock: Mutex,
|
439
|
+
isSVC: boolean,
|
437
440
|
log: StructuredLogger,
|
438
441
|
logContext: Record<string, unknown>,
|
439
442
|
) {
|
@@ -498,6 +501,12 @@ async function setPublishingLayersForSender(
|
|
498
501
|
}
|
499
502
|
}
|
500
503
|
} else {
|
504
|
+
if (isSVC) {
|
505
|
+
const hasEnabledEncoding = qualities.some((q) => q.enabled);
|
506
|
+
if (hasEnabledEncoding) {
|
507
|
+
qualities.forEach((q) => (q.enabled = true));
|
508
|
+
}
|
509
|
+
}
|
501
510
|
// simulcast dynacast encodings
|
502
511
|
encodings.forEach((encoding, idx) => {
|
503
512
|
let rid = encoding.rid ?? '';
|
package/src/room/track/create.ts
CHANGED
@@ -78,18 +78,16 @@ export async function createLocalTracks(
|
|
78
78
|
deviceId: { ideal: deviceId },
|
79
79
|
};
|
80
80
|
}
|
81
|
-
// TODO if internal options don't have device Id specified, set it to 'default'
|
82
81
|
if (
|
83
82
|
internalOptions.audio === true ||
|
84
83
|
(typeof internalOptions.audio === 'object' && !internalOptions.audio.deviceId)
|
85
84
|
) {
|
86
85
|
internalOptions.audio = { deviceId: 'default' };
|
87
86
|
}
|
88
|
-
if (
|
89
|
-
internalOptions.video === true ||
|
90
|
-
(typeof internalOptions.video === 'object' && !internalOptions.video.deviceId)
|
91
|
-
) {
|
87
|
+
if (internalOptions.video === true) {
|
92
88
|
internalOptions.video = { deviceId: 'default' };
|
89
|
+
} else if (typeof internalOptions.video === 'object' && !internalOptions.video.deviceId) {
|
90
|
+
internalOptions.video.deviceId = 'default';
|
93
91
|
}
|
94
92
|
const opts = mergeDefaultOptions(internalOptions, audioDefaults, videoDefaults);
|
95
93
|
const constraints = constraintsForOptions(opts);
|
package/src/room/utils.ts
CHANGED
@@ -5,7 +5,7 @@ import {
|
|
5
5
|
DisconnectReason,
|
6
6
|
Transcription as TranscriptionModel,
|
7
7
|
} from '@livekit/protocol';
|
8
|
-
import { getBrowser } from '../utils/browserParser';
|
8
|
+
import { type BrowserDetails, getBrowser } from '../utils/browserParser';
|
9
9
|
import { protocolVersion, version } from '../version';
|
10
10
|
import { type ConnectionError, ConnectionErrorReason } from './errors';
|
11
11
|
import type LocalParticipant from './participant/LocalParticipant';
|
@@ -143,11 +143,24 @@ export function isSafari(): boolean {
|
|
143
143
|
return getBrowser()?.name === 'Safari';
|
144
144
|
}
|
145
145
|
|
146
|
+
export function isSafariBased(): boolean {
|
147
|
+
const b = getBrowser();
|
148
|
+
return b?.name === 'Safari' || b?.os === 'iOS';
|
149
|
+
}
|
150
|
+
|
146
151
|
export function isSafari17(): boolean {
|
147
152
|
const b = getBrowser();
|
148
153
|
return b?.name === 'Safari' && b.version.startsWith('17.');
|
149
154
|
}
|
150
155
|
|
156
|
+
export function isSafariSvcApi(browser?: BrowserDetails): boolean {
|
157
|
+
if (!browser) {
|
158
|
+
browser = getBrowser();
|
159
|
+
}
|
160
|
+
// Safari 18.4 requires legacy svc api and scaleResolutionDown to be set
|
161
|
+
return browser?.name === 'Safari' && compareVersions(browser.version, '18.3') > 0;
|
162
|
+
}
|
163
|
+
|
151
164
|
export function isMobile(): boolean {
|
152
165
|
if (!isWeb()) return false;
|
153
166
|
|