livekit-client 2.20.1 → 2.20.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/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 +33 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +589 -163
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +33 -7
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -0
- 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/RTCEngine.d.ts +45 -4
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +2 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +45 -4
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
- package/package.json +7 -3
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +34 -2
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.ts +66 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +275 -2
- package/src/room/RTCEngine.ts +276 -71
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.ts +7 -1
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/errors.ts +4 -4
- package/src/room/participant/LocalParticipant.ts +16 -6
- package/src/room/participant/Participant.ts +5 -0
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +10 -6
- package/src/room/track/options.ts +11 -1
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +46 -1
|
@@ -8226,6 +8226,8 @@ var LoggerNames;
|
|
|
8226
8226
|
LoggerNames["PCTransport"] = "livekit-pc-transport";
|
|
8227
8227
|
LoggerNames["E2EE"] = "lk-e2ee";
|
|
8228
8228
|
LoggerNames["DataTracks"] = "livekit-data-tracks";
|
|
8229
|
+
LoggerNames["Region"] = "livekit-region";
|
|
8230
|
+
LoggerNames["ICE"] = "livekit-ice";
|
|
8229
8231
|
})(LoggerNames || (LoggerNames = {}));
|
|
8230
8232
|
let livekitLogger = loglevelExports.getLogger(LoggerNames.Default);
|
|
8231
8233
|
const livekitLoggers = Object.values(LoggerNames).map(name => loglevelExports.getLogger(name));
|
|
@@ -8810,7 +8812,7 @@ function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
|
|
|
8810
8812
|
}
|
|
8811
8813
|
const addEventListener = Object.getOwnPropertyDescriptor(EventTarget.prototype, 'addEventListener');
|
|
8812
8814
|
if (!addEventListener.writable) {
|
|
8813
|
-
log$
|
|
8815
|
+
log$5('Unable to polyfill events');
|
|
8814
8816
|
return;
|
|
8815
8817
|
}
|
|
8816
8818
|
const proto = window.RTCPeerConnection.prototype;
|
|
@@ -8890,7 +8892,7 @@ function disableWarnings(bool) {
|
|
|
8890
8892
|
deprecationWarnings_ = !bool;
|
|
8891
8893
|
return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
|
|
8892
8894
|
}
|
|
8893
|
-
function log$
|
|
8895
|
+
function log$5() {
|
|
8894
8896
|
if (typeof window === 'object') {
|
|
8895
8897
|
if (logDisabled_) {
|
|
8896
8898
|
return;
|
|
@@ -9052,7 +9054,7 @@ function filterStats(result, track, outbound) {
|
|
|
9052
9054
|
* tree.
|
|
9053
9055
|
*/
|
|
9054
9056
|
/* eslint-env node */
|
|
9055
|
-
const logging = log$
|
|
9057
|
+
const logging = log$5;
|
|
9056
9058
|
function shimGetUserMedia$2(window, browserDetails) {
|
|
9057
9059
|
if (browserDetails.version >= 64) {
|
|
9058
9060
|
return;
|
|
@@ -11679,7 +11681,7 @@ function adapterFactory() {
|
|
|
11679
11681
|
shimSafari: true
|
|
11680
11682
|
};
|
|
11681
11683
|
// Utils.
|
|
11682
|
-
const logging = log$
|
|
11684
|
+
const logging = log$5;
|
|
11683
11685
|
const browserDetails = detectBrowser(window);
|
|
11684
11686
|
const adapter = {
|
|
11685
11687
|
browserDetails,
|
|
@@ -11881,7 +11883,7 @@ function getMatch(exp, ua) {
|
|
|
11881
11883
|
}
|
|
11882
11884
|
function getOSVersion(ua) {
|
|
11883
11885
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
|
11884
|
-
}var version$1 = "2.20.
|
|
11886
|
+
}var version$1 = "2.20.2";const version = version$1;
|
|
11885
11887
|
const protocolVersion = 17;
|
|
11886
11888
|
/** Initial client protocol. */
|
|
11887
11889
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
@@ -11972,25 +11974,25 @@ class TrackInvalidError extends LivekitError {
|
|
|
11972
11974
|
}
|
|
11973
11975
|
class UnsupportedServer extends LivekitError {
|
|
11974
11976
|
constructor(message) {
|
|
11975
|
-
super(10, message
|
|
11977
|
+
super(10, message || 'unsupported server');
|
|
11976
11978
|
this.name = 'UnsupportedServer';
|
|
11977
11979
|
}
|
|
11978
11980
|
}
|
|
11979
11981
|
class UnexpectedConnectionState extends LivekitError {
|
|
11980
11982
|
constructor(message) {
|
|
11981
|
-
super(12, message
|
|
11983
|
+
super(12, message || 'unexpected connection state');
|
|
11982
11984
|
this.name = 'UnexpectedConnectionState';
|
|
11983
11985
|
}
|
|
11984
11986
|
}
|
|
11985
11987
|
class NegotiationError extends LivekitError {
|
|
11986
11988
|
constructor(message) {
|
|
11987
|
-
super(13, message
|
|
11989
|
+
super(13, message || 'unable to negotiate');
|
|
11988
11990
|
this.name = 'NegotiationError';
|
|
11989
11991
|
}
|
|
11990
11992
|
}
|
|
11991
11993
|
class PublishDataError extends LivekitError {
|
|
11992
11994
|
constructor(message) {
|
|
11993
|
-
super(14, message
|
|
11995
|
+
super(14, message || 'unable to publish data');
|
|
11994
11996
|
this.name = 'PublishDataError';
|
|
11995
11997
|
}
|
|
11996
11998
|
}
|
|
@@ -13195,6 +13197,9 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13195
13197
|
}
|
|
13196
13198
|
/** @internal */
|
|
13197
13199
|
setStreamState(value) {
|
|
13200
|
+
if (this._streamState !== value) {
|
|
13201
|
+
this.log.debug("stream state changed: ".concat(this._streamState, " -> ").concat(value));
|
|
13202
|
+
}
|
|
13198
13203
|
this._streamState = value;
|
|
13199
13204
|
}
|
|
13200
13205
|
constructor(mediaTrack, kind) {
|
|
@@ -13219,8 +13224,8 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13219
13224
|
this.handleAppVisibilityChanged();
|
|
13220
13225
|
}
|
|
13221
13226
|
};
|
|
13222
|
-
this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track);
|
|
13223
13227
|
this.loggerContextCb = loggerOptions.loggerContextCb;
|
|
13228
|
+
this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track, () => this.logContext);
|
|
13224
13229
|
this.setMaxListeners(100);
|
|
13225
13230
|
this.kind = kind;
|
|
13226
13231
|
this._mediaStreamTrack = mediaTrack;
|
|
@@ -13288,9 +13293,11 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13288
13293
|
this.emit(hasAudio ? TrackEvent.AudioPlaybackFailed : TrackEvent.VideoPlaybackFailed, e);
|
|
13289
13294
|
} else if (e.name === 'AbortError') {
|
|
13290
13295
|
// commonly triggered by another `play` request, only log for debugging purposes
|
|
13291
|
-
|
|
13296
|
+
this.log.debug("".concat(hasAudio ? 'audio' : 'video', " playback aborted, likely due to new play request"));
|
|
13292
13297
|
} else {
|
|
13293
|
-
|
|
13298
|
+
this.log.warn("could not playback ".concat(hasAudio ? 'audio' : 'video'), {
|
|
13299
|
+
error: e
|
|
13300
|
+
});
|
|
13294
13301
|
}
|
|
13295
13302
|
// If audio playback isn't allowed make sure we still play back the video
|
|
13296
13303
|
if (hasAudio && element && allMediaStreamTracks.some(tr => tr.kind === 'video') && e.name === 'NotAllowedError') {
|
|
@@ -13333,6 +13340,7 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13333
13340
|
}
|
|
13334
13341
|
}
|
|
13335
13342
|
stop() {
|
|
13343
|
+
this.log.debug('stopping track');
|
|
13336
13344
|
this.stopMonitor();
|
|
13337
13345
|
this._mediaStreamTrack.stop();
|
|
13338
13346
|
}
|
|
@@ -13353,12 +13361,12 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13353
13361
|
}
|
|
13354
13362
|
/** @internal */
|
|
13355
13363
|
updateLoggerOptions(loggerOptions) {
|
|
13356
|
-
if (loggerOptions.loggerName) {
|
|
13357
|
-
this.log = getLogger(loggerOptions.loggerName);
|
|
13358
|
-
}
|
|
13359
13364
|
if (loggerOptions.loggerContextCb) {
|
|
13360
13365
|
this.loggerContextCb = loggerOptions.loggerContextCb;
|
|
13361
13366
|
}
|
|
13367
|
+
if (loggerOptions.loggerName) {
|
|
13368
|
+
this.log = getLogger(loggerOptions.loggerName, () => this.logContext);
|
|
13369
|
+
}
|
|
13362
13370
|
}
|
|
13363
13371
|
recycleElement(element) {
|
|
13364
13372
|
if (element instanceof HTMLAudioElement) {
|
|
@@ -16317,6 +16325,10 @@ var SignalConnectionState;
|
|
|
16317
16325
|
})(SignalConnectionState || (SignalConnectionState = {}));
|
|
16318
16326
|
/** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
|
|
16319
16327
|
const MAX_WS_CLOSE_TIME = 250;
|
|
16328
|
+
/**
|
|
16329
|
+
* How long (in ms) to wait for the first message after the WebSocket upgrade.
|
|
16330
|
+
*/
|
|
16331
|
+
const JOIN_RESPONSE_TIMEOUT = 5000;
|
|
16320
16332
|
/** @internal */
|
|
16321
16333
|
class SignalClient {
|
|
16322
16334
|
get currentState() {
|
|
@@ -16465,7 +16477,6 @@ class SignalClient {
|
|
|
16465
16477
|
this.ws = new WebSocketStream(rtcUrl);
|
|
16466
16478
|
try {
|
|
16467
16479
|
this.ws.closed.then(closeInfo => {
|
|
16468
|
-
var _a;
|
|
16469
16480
|
if (this.isEstablishingConnection) {
|
|
16470
16481
|
reject(ConnectionError.internal("Websocket got closed during a (re)connection attempt: ".concat(closeInfo.reason)));
|
|
16471
16482
|
}
|
|
@@ -16477,7 +16488,7 @@ class SignalClient {
|
|
|
16477
16488
|
state: this.state
|
|
16478
16489
|
});
|
|
16479
16490
|
if (this.state === SignalConnectionState.CONNECTED) {
|
|
16480
|
-
this.handleOnClose(
|
|
16491
|
+
this.handleOnClose(closeInfo.reason || 'Unexpected WS error');
|
|
16481
16492
|
}
|
|
16482
16493
|
}
|
|
16483
16494
|
return;
|
|
@@ -16505,7 +16516,26 @@ class SignalClient {
|
|
|
16505
16516
|
}
|
|
16506
16517
|
const signalReader = connection.readable.getReader();
|
|
16507
16518
|
this.streamWriter = connection.writable.getWriter();
|
|
16508
|
-
|
|
16519
|
+
// wsTimeout only guarded the upgrade; guard the first-message read with
|
|
16520
|
+
// its own timeout so a silent server can't hang join() forever.
|
|
16521
|
+
let firstMessage;
|
|
16522
|
+
let firstMessageTimeout;
|
|
16523
|
+
try {
|
|
16524
|
+
firstMessage = yield Promise.race([signalReader.read(), new Promise((_, rejectRead) => {
|
|
16525
|
+
firstMessageTimeout = setTimeout(() => {
|
|
16526
|
+
rejectRead(ConnectionError.timeout('signal connection timed out while waiting for the first message'));
|
|
16527
|
+
}, JOIN_RESPONSE_TIMEOUT);
|
|
16528
|
+
})]);
|
|
16529
|
+
} catch (e) {
|
|
16530
|
+
// No first message in time: release the reader and tear down the ws
|
|
16531
|
+
// so we surface the timeout instead of leaking an open connection.
|
|
16532
|
+
signalReader.releaseLock();
|
|
16533
|
+
reject(e);
|
|
16534
|
+
this.close();
|
|
16535
|
+
return;
|
|
16536
|
+
} finally {
|
|
16537
|
+
clearTimeout(firstMessageTimeout);
|
|
16538
|
+
}
|
|
16509
16539
|
signalReader.releaseLock();
|
|
16510
16540
|
if (!firstMessage.value) {
|
|
16511
16541
|
throw ConnectionError.internal('no message received as first message');
|
|
@@ -17243,21 +17273,50 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
|
|
|
17243
17273
|
constructor() {
|
|
17244
17274
|
this.buffer = [];
|
|
17245
17275
|
this._totalSize = 0;
|
|
17276
|
+
this._sentSize = 0;
|
|
17246
17277
|
}
|
|
17247
17278
|
push(item) {
|
|
17248
17279
|
this.buffer.push(item);
|
|
17249
17280
|
this._totalSize += item.data.byteLength;
|
|
17281
|
+
if (item.sent) {
|
|
17282
|
+
this._sentSize += item.data.byteLength;
|
|
17283
|
+
}
|
|
17250
17284
|
}
|
|
17251
17285
|
pop() {
|
|
17252
17286
|
const item = this.buffer.shift();
|
|
17253
17287
|
if (item) {
|
|
17254
17288
|
this._totalSize -= item.data.byteLength;
|
|
17289
|
+
if (item.sent) {
|
|
17290
|
+
this._sentSize -= item.data.byteLength;
|
|
17291
|
+
}
|
|
17255
17292
|
}
|
|
17256
17293
|
return item;
|
|
17257
17294
|
}
|
|
17258
17295
|
getAll() {
|
|
17259
17296
|
return this.buffer.slice();
|
|
17260
17297
|
}
|
|
17298
|
+
/** Every queued packet not yet handed to the channel, in sequence order. */
|
|
17299
|
+
getUnsent() {
|
|
17300
|
+
return this.buffer.filter(item => !item.sent);
|
|
17301
|
+
}
|
|
17302
|
+
/** Marks a single queued packet as handed to the channel. */
|
|
17303
|
+
markSent(item) {
|
|
17304
|
+
if (!item.sent) {
|
|
17305
|
+
item.sent = true;
|
|
17306
|
+
this._sentSize += item.data.byteLength;
|
|
17307
|
+
}
|
|
17308
|
+
}
|
|
17309
|
+
/**
|
|
17310
|
+
* Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
|
|
17311
|
+
* still buffered was sent on the previous channel (or deferred) and must be re-handed to the
|
|
17312
|
+
* current one, so none of it counts as sent until the replay actually transmits it.
|
|
17313
|
+
*/
|
|
17314
|
+
markAllUnsent() {
|
|
17315
|
+
for (const item of this.buffer) {
|
|
17316
|
+
item.sent = false;
|
|
17317
|
+
}
|
|
17318
|
+
this._sentSize = 0;
|
|
17319
|
+
}
|
|
17261
17320
|
popToSequence(sequence) {
|
|
17262
17321
|
while (this.buffer.length > 0) {
|
|
17263
17322
|
const first = this.buffer[0];
|
|
@@ -17271,7 +17330,12 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
|
|
|
17271
17330
|
alignBufferedAmount(bufferedAmount) {
|
|
17272
17331
|
while (this.buffer.length > 0) {
|
|
17273
17332
|
const first = this.buffer[0];
|
|
17274
|
-
|
|
17333
|
+
// Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
|
|
17334
|
+
// resume replay — trimming them would silently lose them.
|
|
17335
|
+
if (!first.sent) {
|
|
17336
|
+
break;
|
|
17337
|
+
}
|
|
17338
|
+
if (this._sentSize - first.data.byteLength <= bufferedAmount) {
|
|
17275
17339
|
break;
|
|
17276
17340
|
}
|
|
17277
17341
|
this.pop();
|
|
@@ -18056,13 +18120,20 @@ function requireLib() {
|
|
|
18056
18120
|
lib.parseImageAttributes = parser.parseImageAttributes;
|
|
18057
18121
|
lib.parseSimulcastStreamList = parser.parseSimulcastStreamList;
|
|
18058
18122
|
return lib;
|
|
18059
|
-
}var libExports = requireLib();/*
|
|
18060
|
-
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
18065
|
-
|
|
18123
|
+
}var libExports = requireLib();/*
|
|
18124
|
+
* Video codecs use a very low bitrate at the beginning and increase slowly by
|
|
18125
|
+
* the bandwidth estimator until they reach the target bitrate. The process commonly
|
|
18126
|
+
* costs more than 10 seconds causing subscribers to get blurry video at the first
|
|
18127
|
+
* few seconds. We use x-google-start-bitrate to hint the BWE to start higher.
|
|
18128
|
+
*
|
|
18129
|
+
* Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target.
|
|
18130
|
+
* Why same for all codecs: Target bitrate already accounts for codec efficiency
|
|
18131
|
+
* (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
|
|
18132
|
+
* Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
|
|
18133
|
+
*/
|
|
18134
|
+
const startBitrateMultiplier = 0.9;
|
|
18135
|
+
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
18136
|
+
const maxStartBitrateKbps = 1000;
|
|
18066
18137
|
const debounceInterval = 20;
|
|
18067
18138
|
const PCEvents = {
|
|
18068
18139
|
NegotiationStarted: 'negotiationStarted',
|
|
@@ -18087,6 +18158,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18087
18158
|
var _a;
|
|
18088
18159
|
super();
|
|
18089
18160
|
this.log = livekitLogger;
|
|
18161
|
+
this.iceLog = livekitLogger;
|
|
18090
18162
|
this.ddExtID = 0;
|
|
18091
18163
|
this.latestOfferId = 0;
|
|
18092
18164
|
this.latestAcknowledgedOfferId = 0;
|
|
@@ -18113,6 +18185,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18113
18185
|
if (!this._pc) {
|
|
18114
18186
|
return;
|
|
18115
18187
|
}
|
|
18188
|
+
this.log.debug('closing peer connection');
|
|
18116
18189
|
this.pendingInitialOffer = undefined;
|
|
18117
18190
|
this._pc.close();
|
|
18118
18191
|
this._pc.onconnectionstatechange = null;
|
|
@@ -18128,8 +18201,9 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18128
18201
|
this._pc.oniceconnectionstatechange = null;
|
|
18129
18202
|
this._pc = null;
|
|
18130
18203
|
};
|
|
18131
|
-
this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport);
|
|
18132
18204
|
this.loggerOptions = loggerOptions;
|
|
18205
|
+
this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport, () => this.logContext);
|
|
18206
|
+
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
|
18133
18207
|
this.config = config;
|
|
18134
18208
|
this._pc = this.createPC();
|
|
18135
18209
|
this.offerLock = new _();
|
|
@@ -18139,26 +18213,39 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18139
18213
|
pc.onicecandidate = ev => {
|
|
18140
18214
|
var _a;
|
|
18141
18215
|
if (!ev.candidate) return;
|
|
18216
|
+
this.iceLog.debug('local ICE candidate gathered', {
|
|
18217
|
+
candidate: ev.candidate.candidate
|
|
18218
|
+
});
|
|
18142
18219
|
(_a = this.onIceCandidate) === null || _a === void 0 ? void 0 : _a.call(this, ev.candidate);
|
|
18143
18220
|
};
|
|
18144
18221
|
pc.onicecandidateerror = ev => {
|
|
18145
18222
|
var _a;
|
|
18223
|
+
this.iceLog.debug('ICE candidate error', {
|
|
18224
|
+
event: ev
|
|
18225
|
+
});
|
|
18146
18226
|
(_a = this.onIceCandidateError) === null || _a === void 0 ? void 0 : _a.call(this, ev);
|
|
18147
18227
|
};
|
|
18148
18228
|
pc.oniceconnectionstatechange = () => {
|
|
18149
18229
|
var _a;
|
|
18230
|
+
this.iceLog.debug("ICE connection state: ".concat(pc.iceConnectionState));
|
|
18150
18231
|
(_a = this.onIceConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.iceConnectionState);
|
|
18151
18232
|
};
|
|
18152
18233
|
pc.onsignalingstatechange = () => {
|
|
18153
18234
|
var _a;
|
|
18235
|
+
this.log.debug("signaling state: ".concat(pc.signalingState));
|
|
18154
18236
|
(_a = this.onSignalingStatechange) === null || _a === void 0 ? void 0 : _a.call(this, pc.signalingState);
|
|
18155
18237
|
};
|
|
18156
18238
|
pc.onconnectionstatechange = () => {
|
|
18157
18239
|
var _a;
|
|
18240
|
+
this.log.debug("connection state: ".concat(pc.connectionState));
|
|
18158
18241
|
(_a = this.onConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.connectionState);
|
|
18159
18242
|
};
|
|
18160
18243
|
pc.ondatachannel = ev => {
|
|
18161
18244
|
var _a;
|
|
18245
|
+
this.log.debug('data channel opened by peer', {
|
|
18246
|
+
label: ev.channel.label,
|
|
18247
|
+
id: ev.channel.id
|
|
18248
|
+
});
|
|
18162
18249
|
(_a = this.onDataChannel) === null || _a === void 0 ? void 0 : _a.call(this, ev);
|
|
18163
18250
|
};
|
|
18164
18251
|
pc.ontrack = ev => {
|
|
@@ -18179,6 +18266,9 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18179
18266
|
if (this.pc.remoteDescription && !this.restartingIce) {
|
|
18180
18267
|
return this.pc.addIceCandidate(candidate);
|
|
18181
18268
|
}
|
|
18269
|
+
this.iceLog.debug('queuing remote ICE candidate until remote description applied', {
|
|
18270
|
+
pendingCount: this.pendingCandidates.length + 1
|
|
18271
|
+
});
|
|
18182
18272
|
this.pendingCandidates.push(candidate);
|
|
18183
18273
|
});
|
|
18184
18274
|
}
|
|
@@ -18186,10 +18276,10 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18186
18276
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18187
18277
|
var _a, _b;
|
|
18188
18278
|
if (sd.type === 'answer' && this.latestOfferId > 0 && offerId > 0 && offerId !== this.latestOfferId) {
|
|
18189
|
-
this.log.warn('ignoring answer for old offer',
|
|
18279
|
+
this.log.warn('ignoring answer for old offer', {
|
|
18190
18280
|
offerId,
|
|
18191
18281
|
latestOfferId: this.latestOfferId
|
|
18192
|
-
})
|
|
18282
|
+
});
|
|
18193
18283
|
return false;
|
|
18194
18284
|
}
|
|
18195
18285
|
let mungedSDP = undefined;
|
|
@@ -18207,7 +18297,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18207
18297
|
sdpParsed.media.forEach(media => {
|
|
18208
18298
|
ensureIPAddrMatchVersion(media);
|
|
18209
18299
|
});
|
|
18210
|
-
this.log.debug('setting pending initial offer before processing answer'
|
|
18300
|
+
this.log.debug('setting pending initial offer before processing answer');
|
|
18211
18301
|
yield this.setMungedSDP(initialOffer, libExports.write(sdpParsed));
|
|
18212
18302
|
}
|
|
18213
18303
|
const sdpParsed = libExports.parse((_b = sd.sdp) !== null && _b !== void 0 ? _b : '');
|
|
@@ -18265,6 +18355,11 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18265
18355
|
mungedSDP = libExports.write(sdpParsed);
|
|
18266
18356
|
}
|
|
18267
18357
|
yield this.setMungedSDP(sd, mungedSDP, true);
|
|
18358
|
+
if (this.pendingCandidates.length > 0) {
|
|
18359
|
+
this.iceLog.debug('flushing queued ICE candidates', {
|
|
18360
|
+
count: this.pendingCandidates.length
|
|
18361
|
+
});
|
|
18362
|
+
}
|
|
18268
18363
|
this.pendingCandidates.forEach(candidate => {
|
|
18269
18364
|
this.pc.addIceCandidate(candidate);
|
|
18270
18365
|
});
|
|
@@ -18300,7 +18395,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18300
18395
|
const unlock = yield this.offerLock.lock();
|
|
18301
18396
|
try {
|
|
18302
18397
|
if (this.pc.signalingState !== 'stable') {
|
|
18303
|
-
this.log.warn('signaling state is not stable, cannot create initial offer'
|
|
18398
|
+
this.log.warn('signaling state is not stable, cannot create initial offer');
|
|
18304
18399
|
return;
|
|
18305
18400
|
}
|
|
18306
18401
|
const offerId = this.latestOfferId + 1;
|
|
@@ -18333,7 +18428,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18333
18428
|
return;
|
|
18334
18429
|
}
|
|
18335
18430
|
if (options === null || options === void 0 ? void 0 : options.iceRestart) {
|
|
18336
|
-
this.
|
|
18431
|
+
this.iceLog.debug('restarting ICE');
|
|
18337
18432
|
this.restartingIce = true;
|
|
18338
18433
|
}
|
|
18339
18434
|
if (this._pc && (this._pc.signalingState === 'have-local-offer' || this.pendingInitialOffer)) {
|
|
@@ -18346,22 +18441,22 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18346
18441
|
yield this._pc.setRemoteDescription(currentSD);
|
|
18347
18442
|
} else {
|
|
18348
18443
|
this.renegotiate = true;
|
|
18349
|
-
this.log.debug('requesting renegotiation'
|
|
18444
|
+
this.log.debug('requesting renegotiation');
|
|
18350
18445
|
return;
|
|
18351
18446
|
}
|
|
18352
18447
|
} else if (!this._pc || this._pc.signalingState === 'closed') {
|
|
18353
|
-
this.log.warn('could not createOffer with closed peer connection'
|
|
18448
|
+
this.log.warn('could not createOffer with closed peer connection');
|
|
18354
18449
|
return;
|
|
18355
18450
|
}
|
|
18356
18451
|
// actually negotiate
|
|
18357
|
-
this.log.debug('starting to negotiate'
|
|
18452
|
+
this.log.debug('starting to negotiate');
|
|
18358
18453
|
// increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
|
|
18359
18454
|
const offerId = this.latestOfferId + 1;
|
|
18360
18455
|
this.latestOfferId = offerId;
|
|
18361
18456
|
const offer = yield this.pc.createOffer(options);
|
|
18362
|
-
this.log.debug('original offer',
|
|
18457
|
+
this.log.debug('original offer', {
|
|
18363
18458
|
sdp: offer.sdp
|
|
18364
|
-
}
|
|
18459
|
+
});
|
|
18365
18460
|
const sdpParsed = libExports.parse((_a = offer.sdp) !== null && _a !== void 0 ? _a : '');
|
|
18366
18461
|
sdpParsed.media.forEach(media => {
|
|
18367
18462
|
ensureIPAddrMatchVersion(media);
|
|
@@ -18387,12 +18482,15 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18387
18482
|
this.ensureVideoDDExtensionForSVC(media, sdpParsed);
|
|
18388
18483
|
}
|
|
18389
18484
|
// mung sdp for bitrate setting that can't apply by sendEncoding
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
const
|
|
18485
|
+
// Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
|
|
18486
|
+
// Screen share is not capped since text/UI clarity requires high bitrate from the start
|
|
18487
|
+
// TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
|
|
18488
|
+
const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
18489
|
+
const startBitrate = trackbr.isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
18490
|
+
let fmtpFound = false;
|
|
18394
18491
|
for (const fmtp of media.fmtp) {
|
|
18395
18492
|
if (fmtp.payload === codecPayload) {
|
|
18493
|
+
fmtpFound = true;
|
|
18396
18494
|
// if another track's fmtp already is set, we cannot override the bitrate
|
|
18397
18495
|
// this has the unfortunate consequence of being forced to use the
|
|
18398
18496
|
// initial track's bitrate for all tracks
|
|
@@ -18402,6 +18500,13 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18402
18500
|
break;
|
|
18403
18501
|
}
|
|
18404
18502
|
}
|
|
18503
|
+
// VP8 and some codecs may not have an existing fmtp line - create one
|
|
18504
|
+
if (!fmtpFound) {
|
|
18505
|
+
media.fmtp.push({
|
|
18506
|
+
payload: codecPayload,
|
|
18507
|
+
config: "x-google-start-bitrate=".concat(startBitrate)
|
|
18508
|
+
});
|
|
18509
|
+
}
|
|
18405
18510
|
return true;
|
|
18406
18511
|
});
|
|
18407
18512
|
}
|
|
@@ -18416,10 +18521,10 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18416
18521
|
conformBundledCodecFmtp(sdpParsed.media, media => placeholderMids.has(getMidString(media.mid)));
|
|
18417
18522
|
}
|
|
18418
18523
|
if (this.latestOfferId > offerId) {
|
|
18419
|
-
this.log.warn('latestOfferId mismatch',
|
|
18524
|
+
this.log.warn('latestOfferId mismatch', {
|
|
18420
18525
|
latestOfferId: this.latestOfferId,
|
|
18421
18526
|
offerId
|
|
18422
|
-
})
|
|
18527
|
+
});
|
|
18423
18528
|
return;
|
|
18424
18529
|
}
|
|
18425
18530
|
yield this.setMungedSDP(offer, libExports.write(sdpParsed));
|
|
@@ -18567,7 +18672,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18567
18672
|
if (munged) {
|
|
18568
18673
|
sd.sdp = munged;
|
|
18569
18674
|
try {
|
|
18570
|
-
this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description")
|
|
18675
|
+
this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"));
|
|
18571
18676
|
if (remote) {
|
|
18572
18677
|
yield this.pc.setRemoteDescription(sd);
|
|
18573
18678
|
} else {
|
|
@@ -18575,11 +18680,11 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18575
18680
|
}
|
|
18576
18681
|
return;
|
|
18577
18682
|
} catch (e) {
|
|
18578
|
-
this.log.warn("not able to set ".concat(sd.type, ", falling back to unmodified sdp"),
|
|
18683
|
+
this.log.warn("not able to set ".concat(sd.type, ", falling back to unmodified sdp"), {
|
|
18579
18684
|
error: e,
|
|
18580
18685
|
mungedSdp: munged,
|
|
18581
18686
|
originalSdp
|
|
18582
|
-
})
|
|
18687
|
+
});
|
|
18583
18688
|
sd.sdp = originalSdp;
|
|
18584
18689
|
}
|
|
18585
18690
|
}
|
|
@@ -18606,9 +18711,9 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18606
18711
|
if (!remote && this.pc.remoteDescription) {
|
|
18607
18712
|
fields.remoteSdp = this.pc.remoteDescription;
|
|
18608
18713
|
}
|
|
18609
|
-
this.log.error("unable to set ".concat(sd.type),
|
|
18714
|
+
this.log.error("unable to set ".concat(sd.type), {
|
|
18610
18715
|
fields
|
|
18611
|
-
})
|
|
18716
|
+
});
|
|
18612
18717
|
throw new NegotiationError(msg);
|
|
18613
18718
|
}
|
|
18614
18719
|
});
|
|
@@ -18869,6 +18974,7 @@ class PCTransportManager {
|
|
|
18869
18974
|
var _a;
|
|
18870
18975
|
this.peerConnectionTimeout = roomConnectOptionDefaults.peerConnectionTimeout;
|
|
18871
18976
|
this.log = livekitLogger;
|
|
18977
|
+
this.iceLog = livekitLogger;
|
|
18872
18978
|
this.updateState = () => {
|
|
18873
18979
|
var _a, _b;
|
|
18874
18980
|
const previousState = this.state;
|
|
@@ -18887,12 +18993,13 @@ class PCTransportManager {
|
|
|
18887
18993
|
this.state = PCTransportState.NEW;
|
|
18888
18994
|
}
|
|
18889
18995
|
if (previousState !== this.state) {
|
|
18890
|
-
this.log.debug("pc state change: from ".concat(PCTransportState[previousState], " to ").concat(PCTransportState[this.state])
|
|
18996
|
+
this.log.debug("pc state change: from ".concat(PCTransportState[previousState], " to ").concat(PCTransportState[this.state]));
|
|
18891
18997
|
(_a = this.onStateChange) === null || _a === void 0 ? void 0 : _a.call(this, this.state, this.publisher.getConnectionState(), (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.getConnectionState());
|
|
18892
18998
|
}
|
|
18893
18999
|
};
|
|
18894
|
-
this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager);
|
|
18895
19000
|
this.loggerOptions = loggerOptions;
|
|
19001
|
+
this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager, () => this.logContext);
|
|
19002
|
+
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
|
18896
19003
|
this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
|
|
18897
19004
|
this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
|
|
18898
19005
|
this.publisher = new PCTransport(rtcConfig, loggerOptions);
|
|
@@ -18965,9 +19072,9 @@ class PCTransportManager {
|
|
|
18965
19072
|
publisher.removeTrack(sender);
|
|
18966
19073
|
}
|
|
18967
19074
|
} catch (e) {
|
|
18968
|
-
this.log.warn('could not removeTrack',
|
|
19075
|
+
this.log.warn('could not removeTrack', {
|
|
18969
19076
|
error: e
|
|
18970
|
-
})
|
|
19077
|
+
});
|
|
18971
19078
|
}
|
|
18972
19079
|
}
|
|
18973
19080
|
}
|
|
@@ -18977,6 +19084,7 @@ class PCTransportManager {
|
|
|
18977
19084
|
}
|
|
18978
19085
|
triggerIceRestart() {
|
|
18979
19086
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19087
|
+
this.iceLog.warn('triggering ICE restart');
|
|
18980
19088
|
if (this.subscriber) {
|
|
18981
19089
|
this.subscriber.restartingIce = true;
|
|
18982
19090
|
}
|
|
@@ -18991,6 +19099,10 @@ class PCTransportManager {
|
|
|
18991
19099
|
addIceCandidate(candidate, target) {
|
|
18992
19100
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18993
19101
|
var _a;
|
|
19102
|
+
this.iceLog.debug('adding remote ICE candidate', {
|
|
19103
|
+
target,
|
|
19104
|
+
candidate
|
|
19105
|
+
});
|
|
18994
19106
|
if (target === SignalTarget.PUBLISHER) {
|
|
18995
19107
|
yield this.publisher.addIceCandidate(candidate);
|
|
18996
19108
|
} else {
|
|
@@ -19001,11 +19113,11 @@ class PCTransportManager {
|
|
|
19001
19113
|
createSubscriberAnswerFromOffer(sd, offerId) {
|
|
19002
19114
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19003
19115
|
var _a, _b, _c;
|
|
19004
|
-
this.log.debug('received server offer',
|
|
19116
|
+
this.log.debug('received server offer', {
|
|
19005
19117
|
RTCSdpType: sd.type,
|
|
19006
19118
|
sdp: sd.sdp,
|
|
19007
19119
|
signalingState: (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.getSignallingState().toString()
|
|
19008
|
-
})
|
|
19120
|
+
});
|
|
19009
19121
|
const unlock = yield this.remoteOfferLock.lock();
|
|
19010
19122
|
try {
|
|
19011
19123
|
const success = yield (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.setRemoteDescription(sd, offerId);
|
|
@@ -19022,6 +19134,9 @@ class PCTransportManager {
|
|
|
19022
19134
|
}
|
|
19023
19135
|
updateConfiguration(config, iceRestart) {
|
|
19024
19136
|
var _a;
|
|
19137
|
+
this.log.debug('updating rtc configuration', {
|
|
19138
|
+
iceRestart
|
|
19139
|
+
});
|
|
19025
19140
|
this.publisher.setConfiguration(config);
|
|
19026
19141
|
(_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.setConfiguration(config);
|
|
19027
19142
|
if (iceRestart) {
|
|
@@ -19034,7 +19149,7 @@ class PCTransportManager {
|
|
|
19034
19149
|
const unlock = yield this.connectionLock.lock();
|
|
19035
19150
|
try {
|
|
19036
19151
|
if (this.isPublisherConnectionRequired && this.publisher.getConnectionState() !== 'connected' && this.publisher.getConnectionState() !== 'connecting') {
|
|
19037
|
-
this.log.debug('negotiation required, start negotiating'
|
|
19152
|
+
this.log.debug('negotiation required, start negotiating');
|
|
19038
19153
|
this.publisher.negotiate();
|
|
19039
19154
|
}
|
|
19040
19155
|
yield Promise.all((_a = this.requiredTransports) === null || _a === void 0 ? void 0 : _a.map(transport => this.ensureTransportConnected(transport, abortController, timeout)));
|
|
@@ -19148,7 +19263,7 @@ class PCTransportManager {
|
|
|
19148
19263
|
}
|
|
19149
19264
|
return new Promise((resolve, reject) => __awaiter(_this, void 0, void 0, function* () {
|
|
19150
19265
|
const abortHandler = () => {
|
|
19151
|
-
this.log.warn('abort transport connection'
|
|
19266
|
+
this.log.warn('abort transport connection');
|
|
19152
19267
|
CriticalTimers.clearTimeout(connectTimeout);
|
|
19153
19268
|
reject(ConnectionError.cancelled('room connection has been cancelled'));
|
|
19154
19269
|
};
|
|
@@ -19984,6 +20099,18 @@ class LocalTrack extends Track {
|
|
|
19984
20099
|
yield this.restart(constraints);
|
|
19985
20100
|
});
|
|
19986
20101
|
}
|
|
20102
|
+
applyConstraints(constraints) {
|
|
20103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20104
|
+
const unlock = yield this.trackChangeLock.lock();
|
|
20105
|
+
try {
|
|
20106
|
+
const res = yield this._mediaStreamTrack.applyConstraints(constraints);
|
|
20107
|
+
this._constraints = Object.assign(Object.assign({}, this._constraints), constraints);
|
|
20108
|
+
return res;
|
|
20109
|
+
} finally {
|
|
20110
|
+
unlock();
|
|
20111
|
+
}
|
|
20112
|
+
});
|
|
20113
|
+
}
|
|
19987
20114
|
restart(constraints, isUnmuting) {
|
|
19988
20115
|
const _super = Object.create(null, {
|
|
19989
20116
|
restart: {
|
|
@@ -20420,14 +20547,21 @@ class ScalabilityMode {
|
|
|
20420
20547
|
return "L".concat(this.spatial, "T").concat(this.temporal).concat((_a = this.suffix) !== null && _a !== void 0 ? _a : '');
|
|
20421
20548
|
}
|
|
20422
20549
|
}
|
|
20550
|
+
/**
|
|
20551
|
+
* Returns the appropriate degradation preference for a video track based on its source.
|
|
20552
|
+
*
|
|
20553
|
+
* - Camera: 'maintain-framerate' (smoother video for real-time communication)
|
|
20554
|
+
* - Screen share: 'maintain-resolution' (clarity is critical for reading text/UI)
|
|
20555
|
+
* - Other/unknown: 'balanced'
|
|
20556
|
+
*/
|
|
20423
20557
|
function getDefaultDegradationPreference(track) {
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20558
|
+
switch (track.source) {
|
|
20559
|
+
case Track.Source.Camera:
|
|
20560
|
+
return 'maintain-framerate';
|
|
20561
|
+
case Track.Source.ScreenShare:
|
|
20562
|
+
return 'maintain-resolution';
|
|
20563
|
+
default:
|
|
20564
|
+
return 'balanced';
|
|
20431
20565
|
}
|
|
20432
20566
|
}const refreshSubscribedCodecAfterNewCodec = 5000;
|
|
20433
20567
|
class LocalVideoTrack extends LocalTrack {
|
|
@@ -21195,8 +21329,16 @@ const dataTrackDataChannel = '_data_track';
|
|
|
21195
21329
|
const minReconnectWait = 2 * 1000;
|
|
21196
21330
|
const leaveReconnect = 'leave-reconnect';
|
|
21197
21331
|
const reliabeReceiveStateTTL = 30000;
|
|
21198
|
-
|
|
21199
|
-
|
|
21332
|
+
// Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
|
|
21333
|
+
// freely up to the high-water mark; once it's exceeded they block until the browser's
|
|
21334
|
+
// `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
|
|
21335
|
+
// The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
|
|
21336
|
+
// isn't starved, while the high-water mark bounds the buffer well below the level that would abort
|
|
21337
|
+
// the channel (see livekit/client-sdk-js#1995).
|
|
21338
|
+
const reliableDataChannelWaterMarkLow = 64 * 1024;
|
|
21339
|
+
const reliableDataChannelWaterMarkHigh = 1024 * 1024;
|
|
21340
|
+
const lossyDataChannelWaterMarkLow = 8 * 1024;
|
|
21341
|
+
const lossyDataChannelWaterMarkHigh = 256 * 1024;
|
|
21200
21342
|
const initialMediaSectionsAudio = 3;
|
|
21201
21343
|
const initialMediaSectionsVideo = 3;
|
|
21202
21344
|
var PCState;
|
|
@@ -21263,7 +21405,6 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21263
21405
|
this.midToTrackId = {};
|
|
21264
21406
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
21265
21407
|
this.isWaitingForNetworkReconnect = false;
|
|
21266
|
-
this.bufferStatusLowClosingFuture = new Future();
|
|
21267
21408
|
this.handleDataChannel = _a => __awaiter(this, [_a], void 0, function (_ref) {
|
|
21268
21409
|
var _this = this;
|
|
21269
21410
|
let channel = _ref.channel;
|
|
@@ -21457,18 +21598,53 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21457
21598
|
this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
|
21458
21599
|
}
|
|
21459
21600
|
}
|
|
21460
|
-
|
|
21461
|
-
|
|
21462
|
-
this.dcBufferStatus.
|
|
21463
|
-
|
|
21601
|
+
try {
|
|
21602
|
+
const status = this.isBelowLowWaterMark(kind);
|
|
21603
|
+
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
|
21604
|
+
this.dcBufferStatus.set(kind, status);
|
|
21605
|
+
this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
|
|
21606
|
+
}
|
|
21607
|
+
} catch (e) {
|
|
21608
|
+
this.log.warn('could not update buffer status', {
|
|
21609
|
+
error: e
|
|
21610
|
+
});
|
|
21464
21611
|
}
|
|
21465
21612
|
};
|
|
21466
|
-
|
|
21613
|
+
/**
|
|
21614
|
+
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
21615
|
+
* this is true and block once it goes false.
|
|
21616
|
+
*/
|
|
21617
|
+
this.isBelowHighWaterMark = kind => {
|
|
21467
21618
|
const dc = this.dataChannelForKind(kind);
|
|
21468
|
-
if (dc) {
|
|
21469
|
-
|
|
21619
|
+
if (!dc) {
|
|
21620
|
+
throw new TypeError("Could not get data channel for kind ".concat(kind));
|
|
21621
|
+
}
|
|
21622
|
+
// because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
|
|
21623
|
+
const highMark = kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
|
|
21624
|
+
return dc.bufferedAmount <= highMark;
|
|
21625
|
+
};
|
|
21626
|
+
/**
|
|
21627
|
+
* Whether the send buffer has drained to its low-water mark. Drives the public
|
|
21628
|
+
* {@link EngineEvent.DCBufferStatusChanged} event.
|
|
21629
|
+
*/
|
|
21630
|
+
this.isBelowLowWaterMark = kind => {
|
|
21631
|
+
const dc = this.dataChannelForKind(kind);
|
|
21632
|
+
if (!dc) {
|
|
21633
|
+
throw new TypeError("Could not get data channel for kind ".concat(kind));
|
|
21470
21634
|
}
|
|
21635
|
+
// because RTCDatachannel has the threshold built in we can read it dynamically to account for changing thresholds over time
|
|
21636
|
+
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
|
21471
21637
|
};
|
|
21638
|
+
/** Per-kind lock serializing senders that have to wait for the buffer to drain. */
|
|
21639
|
+
this.waitForBufferHeadroomLocks = new Map();
|
|
21640
|
+
/**
|
|
21641
|
+
* Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
|
|
21642
|
+
* stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
|
|
21643
|
+
* waiter is parked on the channel object it captured at wait entry; if that object is abandoned
|
|
21644
|
+
* its events may never fire again, so the abort is what rejects the waiter and releases the
|
|
21645
|
+
* headroom lock instead of stranding every future sender behind it.
|
|
21646
|
+
*/
|
|
21647
|
+
this.waiterAbortControllers = new Map();
|
|
21472
21648
|
this.onRtpMapAvailable = rtpTypes => {
|
|
21473
21649
|
const rtpMap = new Map();
|
|
21474
21650
|
rtpTypes.forEach(rtp => {
|
|
@@ -21539,13 +21715,6 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21539
21715
|
this.client.onRequestResponse = response => this.emit(EngineEvent.SignalRequestResponse, response);
|
|
21540
21716
|
this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
21541
21717
|
this.client.onJoined = joinResponse => this.emit(EngineEvent.Joined, joinResponse);
|
|
21542
|
-
this.on(EngineEvent.Closing, () => {
|
|
21543
|
-
var _a, _b;
|
|
21544
|
-
(_b = (_a = this.bufferStatusLowClosingFuture).reject) === null || _b === void 0 ? void 0 : _b.call(_a, new UnexpectedConnectionState('engine closed'));
|
|
21545
|
-
});
|
|
21546
|
-
// Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
|
|
21547
|
-
// when no waitForBufferStatusLow callers are attached.
|
|
21548
|
-
this.bufferStatusLowClosingFuture.promise.catch(() => {});
|
|
21549
21718
|
}
|
|
21550
21719
|
/** @internal */
|
|
21551
21720
|
get logContext() {
|
|
@@ -21691,6 +21860,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21691
21860
|
cleanupPeerConnections() {
|
|
21692
21861
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21693
21862
|
var _a;
|
|
21863
|
+
// Reject parked headroom waiters up front: closing the peer connection is allowed (per spec)
|
|
21864
|
+
// to transition data channels to 'closed' without firing events, which would otherwise strand
|
|
21865
|
+
// a waiter holding the headroom lock.
|
|
21866
|
+
this.invalidateDataChannelWaiters('peer connections cleaned up');
|
|
21694
21867
|
const dcCleanup = dc => {
|
|
21695
21868
|
if (!dc) {
|
|
21696
21869
|
return;
|
|
@@ -22062,6 +22235,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22062
22235
|
if (!this.pcManager) {
|
|
22063
22236
|
return;
|
|
22064
22237
|
}
|
|
22238
|
+
// Waiters parked on the old channel objects would never see another event from them once they
|
|
22239
|
+
// are replaced below — reject them so the headroom lock is released and queued senders
|
|
22240
|
+
// re-check against the new channels.
|
|
22241
|
+
this.invalidateDataChannelWaiters('data channels recreated');
|
|
22065
22242
|
// clear old data channel callbacks if recreate
|
|
22066
22243
|
if (this.lossyDC) {
|
|
22067
22244
|
this.lossyDC.onmessage = null;
|
|
@@ -22102,10 +22279,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22102
22279
|
this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
|
|
22103
22280
|
this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
|
|
22104
22281
|
this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
|
|
22105
|
-
// set up dc buffer threshold
|
|
22106
|
-
this.lossyDC.bufferedAmountLowThreshold =
|
|
22107
|
-
this.reliableDC.bufferedAmountLowThreshold =
|
|
22108
|
-
this.dataTrackDC.bufferedAmountLowThreshold =
|
|
22282
|
+
// set up dc buffer threshold - if this is not set, it will default to 0
|
|
22283
|
+
this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
22284
|
+
this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
|
|
22285
|
+
this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
22109
22286
|
// handle buffer amount low events
|
|
22110
22287
|
this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
|
|
22111
22288
|
this.reliableDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
|
|
@@ -22118,7 +22295,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22118
22295
|
if (dc) {
|
|
22119
22296
|
// control buffered latency to ~100ms
|
|
22120
22297
|
const threshold = this.lossyDataStatByterate / 10;
|
|
22121
|
-
dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold,
|
|
22298
|
+
dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelWaterMarkLow), lossyDataChannelWaterMarkHigh);
|
|
22122
22299
|
}
|
|
22123
22300
|
}, 1000);
|
|
22124
22301
|
}
|
|
@@ -22430,7 +22607,11 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22430
22607
|
this.createDataChannels();
|
|
22431
22608
|
}
|
|
22432
22609
|
if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
|
|
22433
|
-
this.resendReliableMessagesForResume(res.lastMessageSeq)
|
|
22610
|
+
this.resendReliableMessagesForResume(res.lastMessageSeq).catch(error => {
|
|
22611
|
+
this.log.warn('failed to resend reliable messages after resume', Object.assign(Object.assign({}, this.logContext), {
|
|
22612
|
+
error
|
|
22613
|
+
}));
|
|
22614
|
+
});
|
|
22434
22615
|
}
|
|
22435
22616
|
// resume success
|
|
22436
22617
|
this.emit(EngineEvent.Resumed);
|
|
@@ -22515,20 +22696,55 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22515
22696
|
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
22516
22697
|
return this.sendLossyBytes(msg, kind);
|
|
22517
22698
|
case DataChannelKind.RELIABLE:
|
|
22518
|
-
|
|
22519
|
-
if (dc) {
|
|
22520
|
-
yield this.waitForBufferStatusLow(kind);
|
|
22521
|
-
this.reliableMessageBuffer.push({
|
|
22522
|
-
data: msg,
|
|
22523
|
-
sequence: packet.sequence
|
|
22524
|
-
});
|
|
22699
|
+
{
|
|
22525
22700
|
if (this.attemptingReconnect) {
|
|
22701
|
+
// A reconnect is already underway — queue for the resume replay instead of parking on a
|
|
22702
|
+
// channel that is being torn down. The send resolves; delivery is deferred to the replay.
|
|
22703
|
+
this.reliableMessageBuffer.push({
|
|
22704
|
+
data: msg,
|
|
22705
|
+
sequence: packet.sequence,
|
|
22706
|
+
sent: false
|
|
22707
|
+
});
|
|
22526
22708
|
return;
|
|
22527
22709
|
}
|
|
22528
|
-
dc.
|
|
22710
|
+
const dc = this.dataChannelForKind(kind);
|
|
22711
|
+
if (dc) {
|
|
22712
|
+
try {
|
|
22713
|
+
yield this.waitForBufferHeadroomWithLock(kind);
|
|
22714
|
+
} catch (error) {
|
|
22715
|
+
if (this.isClosed) {
|
|
22716
|
+
// No replay is coming after an engine close — surface the failure.
|
|
22717
|
+
throw error;
|
|
22718
|
+
}
|
|
22719
|
+
// Transient teardown (the channel closed or was replaced while we waited): the
|
|
22720
|
+
// reliable channel promises delivery across resume, so queue the packet for the
|
|
22721
|
+
// replay instead of rejecting a send the app can't meaningfully retry.
|
|
22722
|
+
this.reliableMessageBuffer.push({
|
|
22723
|
+
data: msg,
|
|
22724
|
+
sequence: packet.sequence,
|
|
22725
|
+
sent: false
|
|
22726
|
+
});
|
|
22727
|
+
return;
|
|
22728
|
+
}
|
|
22729
|
+
if (this.attemptingReconnect) {
|
|
22730
|
+
// A reconnect began while we waited for headroom — same deal as above.
|
|
22731
|
+
this.reliableMessageBuffer.push({
|
|
22732
|
+
data: msg,
|
|
22733
|
+
sequence: packet.sequence,
|
|
22734
|
+
sent: false
|
|
22735
|
+
});
|
|
22736
|
+
return;
|
|
22737
|
+
}
|
|
22738
|
+
this.reliableMessageBuffer.push({
|
|
22739
|
+
data: msg,
|
|
22740
|
+
sequence: packet.sequence,
|
|
22741
|
+
sent: true
|
|
22742
|
+
});
|
|
22743
|
+
dc.send(msg);
|
|
22744
|
+
}
|
|
22745
|
+
this.updateAndEmitDCBufferStatus(kind);
|
|
22746
|
+
break;
|
|
22529
22747
|
}
|
|
22530
|
-
this.updateAndEmitDCBufferStatus(kind);
|
|
22531
|
-
break;
|
|
22532
22748
|
}
|
|
22533
22749
|
});
|
|
22534
22750
|
}
|
|
@@ -22536,36 +22752,56 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22536
22752
|
sendLossyBytes(bytes_1, kind_1) {
|
|
22537
22753
|
return __awaiter(this, arguments, void 0, function (bytes, kind) {
|
|
22538
22754
|
var _this3 = this;
|
|
22539
|
-
let
|
|
22755
|
+
let bufferStatusFullBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
|
|
22540
22756
|
return function* () {
|
|
22541
|
-
//
|
|
22757
|
+
// Make sure we do have a data connection. This matters most for the direct data-track path
|
|
22758
|
+
// (Room's packetAvailable handler), which doesn't go through sendDataPacket: it both waits for
|
|
22759
|
+
// the channel to be open and — on lazily negotiated publisher connections — is what kicks the
|
|
22760
|
+
// negotiation off in the first place. The call is memoized, so the steady-state cost is one
|
|
22761
|
+
// await on an already-resolved promise.
|
|
22542
22762
|
yield _this3.ensurePublisherConnected(kind);
|
|
22543
22763
|
const dc = _this3.dataChannelForKind(kind);
|
|
22544
|
-
|
|
22545
|
-
if (
|
|
22764
|
+
try {
|
|
22765
|
+
if (dc) {
|
|
22546
22766
|
// Depending on the exact circumstance that data is being sent, either drop or wait for the
|
|
22547
|
-
// buffer
|
|
22548
|
-
switch (
|
|
22767
|
+
// buffer to drain below the high-water mark before continuing.
|
|
22768
|
+
switch (bufferStatusFullBehavior) {
|
|
22549
22769
|
case 'wait':
|
|
22550
|
-
|
|
22770
|
+
if (!_this3.isBelowHighWaterMark(kind)) {
|
|
22771
|
+
yield _this3.waitForBufferHeadroomWithLock(kind);
|
|
22772
|
+
}
|
|
22551
22773
|
break;
|
|
22552
22774
|
case 'drop':
|
|
22553
|
-
//
|
|
22554
|
-
|
|
22555
|
-
|
|
22556
|
-
|
|
22557
|
-
_this3.
|
|
22775
|
+
// we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
|
|
22776
|
+
if (!_this3.isBelowLowWaterMark(kind)) {
|
|
22777
|
+
// Drop messages to reduce latency
|
|
22778
|
+
_this3.lossyDataDropCount += 1;
|
|
22779
|
+
if (_this3.lossyDataDropCount % 100 === 0) {
|
|
22780
|
+
_this3.log.warn("dropping lossy data channel messages, total dropped: ".concat(_this3.lossyDataDropCount));
|
|
22781
|
+
}
|
|
22782
|
+
return;
|
|
22558
22783
|
}
|
|
22559
|
-
return;
|
|
22560
22784
|
}
|
|
22785
|
+
if (kind === DataChannelKind.LOSSY) {
|
|
22786
|
+
// The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
|
|
22787
|
+
// bytes here would inflate it and let the lossy channel buffer far more latency than the
|
|
22788
|
+
// ~100ms the tuning targets.
|
|
22789
|
+
_this3.lossyDataStatCurrentBytes += bytes.byteLength;
|
|
22790
|
+
}
|
|
22791
|
+
if (_this3.attemptingReconnect) {
|
|
22792
|
+
return;
|
|
22793
|
+
}
|
|
22794
|
+
dc.send(bytes);
|
|
22561
22795
|
}
|
|
22562
|
-
_this3.
|
|
22563
|
-
|
|
22564
|
-
|
|
22796
|
+
_this3.updateAndEmitDCBufferStatus(kind);
|
|
22797
|
+
} catch (error) {
|
|
22798
|
+
// ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
|
|
22799
|
+
if (error instanceof TypeError) {
|
|
22800
|
+
_this3.log.error(error);
|
|
22801
|
+
} else {
|
|
22802
|
+
throw error;
|
|
22565
22803
|
}
|
|
22566
|
-
dc.send(bytes);
|
|
22567
22804
|
}
|
|
22568
|
-
_this3.updateAndEmitDCBufferStatus(kind);
|
|
22569
22805
|
}();
|
|
22570
22806
|
});
|
|
22571
22807
|
}
|
|
@@ -22575,33 +22811,129 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22575
22811
|
const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
|
|
22576
22812
|
if (dc) {
|
|
22577
22813
|
this.reliableMessageBuffer.popToSequence(lastMessageSeq);
|
|
22578
|
-
|
|
22579
|
-
|
|
22580
|
-
|
|
22814
|
+
// Hold the headroom lock across the whole replay. Releasing it between messages would let a
|
|
22815
|
+
// concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
|
|
22816
|
+
// queued on the lock — hit the wire mid-replay, and receivers would then discard the
|
|
22817
|
+
// remaining lower-sequence resent messages as duplicates.
|
|
22818
|
+
const unlock = yield this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
|
|
22819
|
+
try {
|
|
22820
|
+
// Everything left after the ack cutoff must be re-handed to the current channel.
|
|
22821
|
+
this.reliableMessageBuffer.markAllUnsent();
|
|
22822
|
+
// Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
|
|
22823
|
+
// sent:false) during our own awaits appends after this pass started, so we pick it up on
|
|
22824
|
+
// the next one. We mark each packet only once we've actually handed it to the channel — a
|
|
22825
|
+
// blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
|
|
22826
|
+
// and a later alignBufferedAmount would then drop it for good. If the loop throws
|
|
22827
|
+
// mid-drain, unsent entries keep their flag and the next replay picks them up.
|
|
22828
|
+
for (let batch = this.reliableMessageBuffer.getUnsent(); batch.length > 0; batch = this.reliableMessageBuffer.getUnsent()) {
|
|
22829
|
+
for (const item of batch) {
|
|
22830
|
+
// Respect flow control on resume too, so a large resend doesn't overflow the buffer.
|
|
22831
|
+
// We already hold the lock across the whole replay, so use the lock-free wait.
|
|
22832
|
+
yield this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
|
|
22833
|
+
dc.send(item.data);
|
|
22834
|
+
this.reliableMessageBuffer.markSent(item);
|
|
22835
|
+
}
|
|
22836
|
+
}
|
|
22837
|
+
} finally {
|
|
22838
|
+
unlock();
|
|
22839
|
+
}
|
|
22581
22840
|
}
|
|
22582
22841
|
this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
|
|
22583
22842
|
});
|
|
22584
22843
|
}
|
|
22585
|
-
|
|
22844
|
+
waiterAbortSignal(kind) {
|
|
22845
|
+
let controller = this.waiterAbortControllers.get(kind);
|
|
22846
|
+
if (!controller) {
|
|
22847
|
+
controller = new AbortController();
|
|
22848
|
+
this.waiterAbortControllers.set(kind, controller);
|
|
22849
|
+
}
|
|
22850
|
+
return controller.signal;
|
|
22851
|
+
}
|
|
22852
|
+
/** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
|
|
22853
|
+
invalidateDataChannelWaiters(reason) {
|
|
22854
|
+
for (const controller of this.waiterAbortControllers.values()) {
|
|
22855
|
+
controller.abort(reason);
|
|
22856
|
+
}
|
|
22857
|
+
this.waiterAbortControllers.clear();
|
|
22858
|
+
}
|
|
22859
|
+
/** Acquires the per-kind headroom lock, resolving with the unlock function. */
|
|
22860
|
+
getBufferHeadroomLock(kind) {
|
|
22861
|
+
let lock = this.waitForBufferHeadroomLocks.get(kind);
|
|
22862
|
+
if (!lock) {
|
|
22863
|
+
lock = new _();
|
|
22864
|
+
this.waitForBufferHeadroomLocks.set(kind, lock);
|
|
22865
|
+
}
|
|
22866
|
+
return lock;
|
|
22867
|
+
}
|
|
22868
|
+
/**
|
|
22869
|
+
* Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
|
|
22870
|
+
* common single-send entry point. Callers that need to hold the lock across several sends (the
|
|
22871
|
+
* resume replay) acquire it via {@link getBufferHeadroomLock} and call
|
|
22872
|
+
* {@link waitForBufferHeadroomWithoutLock} per message instead.
|
|
22873
|
+
*/
|
|
22874
|
+
waitForBufferHeadroomWithLock(kind) {
|
|
22875
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
22876
|
+
const unlock = yield this.getBufferHeadroomLock(kind).lock();
|
|
22877
|
+
try {
|
|
22878
|
+
yield this.waitForBufferHeadroomWithoutLock(kind);
|
|
22879
|
+
} finally {
|
|
22880
|
+
unlock();
|
|
22881
|
+
}
|
|
22882
|
+
});
|
|
22883
|
+
}
|
|
22884
|
+
/**
|
|
22885
|
+
* Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
|
|
22886
|
+
* needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
|
|
22887
|
+
* already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
|
|
22888
|
+
* holds the lock across its whole batch and calls this per message so no other sender can
|
|
22889
|
+
* interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
|
|
22890
|
+
*
|
|
22891
|
+
* Serializing through the per-kind lock means that, once the buffer drains, queued callers
|
|
22892
|
+
* refill it one at a time (up to the high-water mark) rather than all at once and overflowing
|
|
22893
|
+
* the SCTP send buffer (see livekit/client-sdk-js#1995).
|
|
22894
|
+
*/
|
|
22895
|
+
waitForBufferHeadroomWithoutLock(kind) {
|
|
22586
22896
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22587
|
-
|
|
22588
|
-
|
|
22589
|
-
|
|
22590
|
-
|
|
22591
|
-
|
|
22897
|
+
if (this.isClosed) {
|
|
22898
|
+
throw new UnexpectedConnectionState('engine closed');
|
|
22899
|
+
}
|
|
22900
|
+
if (this.isBelowHighWaterMark(kind)) {
|
|
22901
|
+
return;
|
|
22902
|
+
}
|
|
22903
|
+
const dc = this.dataChannelForKind(kind);
|
|
22904
|
+
if (!dc) {
|
|
22905
|
+
throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind));
|
|
22906
|
+
}
|
|
22907
|
+
const abortSignal = this.waiterAbortSignal(kind);
|
|
22908
|
+
yield new TypedPromise((resolve, reject) => {
|
|
22909
|
+
const onBufferedAmountLow = () => {
|
|
22910
|
+
cleanup();
|
|
22592
22911
|
resolve();
|
|
22593
|
-
}
|
|
22594
|
-
|
|
22595
|
-
|
|
22596
|
-
|
|
22597
|
-
|
|
22598
|
-
|
|
22599
|
-
|
|
22600
|
-
|
|
22601
|
-
|
|
22602
|
-
|
|
22912
|
+
};
|
|
22913
|
+
const onDCClose = () => {
|
|
22914
|
+
cleanup();
|
|
22915
|
+
reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " closed while draining the buffer")));
|
|
22916
|
+
};
|
|
22917
|
+
const onAbort = () => {
|
|
22918
|
+
cleanup();
|
|
22919
|
+
reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " was replaced or torn down while waiting for headroom")));
|
|
22920
|
+
};
|
|
22921
|
+
const cleanup = () => {
|
|
22922
|
+
dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
22923
|
+
dc.removeEventListener('close', onDCClose);
|
|
22924
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
22925
|
+
};
|
|
22926
|
+
if (abortSignal.aborted) {
|
|
22927
|
+
onAbort();
|
|
22928
|
+
return;
|
|
22603
22929
|
}
|
|
22604
|
-
|
|
22930
|
+
dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
22931
|
+
// Proxy along any error caused by the data channel closing while we wait.
|
|
22932
|
+
dc.addEventListener('close', onDCClose);
|
|
22933
|
+
// The channel object we're parked on can be abandoned without ever firing another event
|
|
22934
|
+
// (e.g. createDataChannels replacing it); the abort is our way out.
|
|
22935
|
+
abortSignal.addEventListener('abort', onAbort);
|
|
22936
|
+
});
|
|
22605
22937
|
});
|
|
22606
22938
|
}
|
|
22607
22939
|
/**
|
|
@@ -22883,7 +23215,8 @@ function applyUserDataCompat(newObj, oldObj) {
|
|
|
22883
23215
|
const destinationIdentities = newObj.destinationIdentities.length !== 0 ? newObj.destinationIdentities : oldObj.destinationIdentities;
|
|
22884
23216
|
newObj.destinationIdentities = destinationIdentities;
|
|
22885
23217
|
oldObj.destinationIdentities = destinationIdentities;
|
|
22886
|
-
}const
|
|
23218
|
+
}const log$4 = getLogger(LoggerNames.Region);
|
|
23219
|
+
const DEFAULT_MAX_AGE_MS = 5000;
|
|
22887
23220
|
const STOP_REFETCH_DELAY_MS = 30000;
|
|
22888
23221
|
class RegionUrlProvider {
|
|
22889
23222
|
static fetchRegionSettings(serverUrl, token, signal) {
|
|
@@ -22937,10 +23270,10 @@ class RegionUrlProvider {
|
|
|
22937
23270
|
RegionUrlProvider.updateCachedRegionSettings(url, token, newSettings);
|
|
22938
23271
|
} catch (error) {
|
|
22939
23272
|
if (error instanceof ConnectionError && error.reason === ConnectionErrorReason.NotAllowed) {
|
|
22940
|
-
|
|
23273
|
+
log$4.debug('token is not valid, cancelling auto region refresh');
|
|
22941
23274
|
return;
|
|
22942
23275
|
}
|
|
22943
|
-
|
|
23276
|
+
log$4.debug('auto refetching of region settings failed', {
|
|
22944
23277
|
error
|
|
22945
23278
|
});
|
|
22946
23279
|
// continue retrying with the same max age
|
|
@@ -22973,7 +23306,7 @@ class RegionUrlProvider {
|
|
|
22973
23306
|
tracker.cleanupTimeout = setTimeout(() => {
|
|
22974
23307
|
const currentTracker = RegionUrlProvider.connectionTrackers.get(hostname);
|
|
22975
23308
|
if (currentTracker && currentTracker.connectionCount === 0) {
|
|
22976
|
-
|
|
23309
|
+
log$4.debug('stopping region refetch after disconnect delay', {
|
|
22977
23310
|
hostname
|
|
22978
23311
|
});
|
|
22979
23312
|
RegionUrlProvider.stopRefetch(hostname);
|
|
@@ -23053,7 +23386,9 @@ class RegionUrlProvider {
|
|
|
23053
23386
|
if (regionsLeft.length > 0) {
|
|
23054
23387
|
const nextRegion = regionsLeft[0];
|
|
23055
23388
|
this.attemptedRegions.push(nextRegion);
|
|
23056
|
-
|
|
23389
|
+
log$4.info("switching to region: ".concat(nextRegion.region), {
|
|
23390
|
+
region: nextRegion.region
|
|
23391
|
+
});
|
|
23057
23392
|
return nextRegion.url;
|
|
23058
23393
|
} else {
|
|
23059
23394
|
return null;
|
|
@@ -24829,6 +25164,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
24829
25164
|
let bufferSize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : READABLE_STREAM_DEFAULT_BUFFER_SIZE;
|
|
24830
25165
|
let streamController = null;
|
|
24831
25166
|
const sfuSubscriptionComplete = new Future();
|
|
25167
|
+
// Hold the descriptor by reference: SID reassignment re-keys the map, so lookups
|
|
25168
|
+
// with the SID captured at subscribe time would fail.
|
|
25169
|
+
const descriptor = this.descriptors.get(sid);
|
|
24832
25170
|
const detachSignal = () => {
|
|
24833
25171
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
24834
25172
|
};
|
|
@@ -24838,8 +25176,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
24838
25176
|
log$1.warn("ReadableStream subscribed to ".concat(sid, " was not started."));
|
|
24839
25177
|
return;
|
|
24840
25178
|
}
|
|
24841
|
-
|
|
24842
|
-
if (!descriptor) {
|
|
25179
|
+
if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
|
|
24843
25180
|
log$1.warn("Unknown track ".concat(sid, ", skipping cancel..."));
|
|
24844
25181
|
return;
|
|
24845
25182
|
}
|
|
@@ -24858,9 +25195,8 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
24858
25195
|
if (!streamController) {
|
|
24859
25196
|
return;
|
|
24860
25197
|
}
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
currentDescriptor.subscription.streamControllers.delete(streamController);
|
|
25198
|
+
if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.subscription.type) === 'active') {
|
|
25199
|
+
descriptor.subscription.streamControllers.delete(streamController);
|
|
24864
25200
|
}
|
|
24865
25201
|
streamController.error(DataTrackSubscribeError.cancelled());
|
|
24866
25202
|
(_a = sfuSubscriptionComplete.reject) === null || _a === void 0 ? void 0 : _a.call(sfuSubscriptionComplete, DataTrackSubscribeError.cancelled());
|
|
@@ -24871,8 +25207,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
24871
25207
|
streamController = controller;
|
|
24872
25208
|
this.subscribeRequest(sid, signal).then(() => __awaiter(this, void 0, void 0, function* () {
|
|
24873
25209
|
var _a, _b, _c;
|
|
24874
|
-
|
|
24875
|
-
if (!descriptor) {
|
|
25210
|
+
if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
|
|
24876
25211
|
log$1.error("Unknown track ".concat(sid));
|
|
24877
25212
|
const err = DataTrackSubscribeError.disconnected();
|
|
24878
25213
|
controller.error(err);
|
|
@@ -24987,7 +25322,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
24987
25322
|
};
|
|
24988
25323
|
// Let the SFU know that the subscribe has been cancelled
|
|
24989
25324
|
this.emit('sfuUpdateSubscription', {
|
|
24990
|
-
sid,
|
|
25325
|
+
sid: descriptor.info.sid,
|
|
24991
25326
|
subscribe: false
|
|
24992
25327
|
});
|
|
24993
25328
|
if (previousDescriptorSubscription.type === 'pending') {
|
|
@@ -25101,6 +25436,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
25101
25436
|
if (this.descriptors.has(info.sid)) {
|
|
25102
25437
|
continue;
|
|
25103
25438
|
}
|
|
25439
|
+
if (this.handleSidReassigned(publisherIdentity, info)) {
|
|
25440
|
+
continue;
|
|
25441
|
+
}
|
|
25104
25442
|
yield this.handleTrackPublished(publisherIdentity, info);
|
|
25105
25443
|
}
|
|
25106
25444
|
publisherParticipantToSidsInUpdate.set(publisherIdentity, sidsInUpdate);
|
|
@@ -25158,6 +25496,62 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
25158
25496
|
});
|
|
25159
25497
|
});
|
|
25160
25498
|
}
|
|
25499
|
+
/**
|
|
25500
|
+
* Detects and handles SID reassignment, which occurs when the publisher
|
|
25501
|
+
* republishes its tracks after a full reconnect.
|
|
25502
|
+
*
|
|
25503
|
+
* Returns `true` if an SID reassignment occurred, `false` otherwise.
|
|
25504
|
+
*/
|
|
25505
|
+
handleSidReassigned(publisherIdentity, info) {
|
|
25506
|
+
// Publisher identity and pub handle are stable across republications.
|
|
25507
|
+
const existingEntry = Array.from(this.descriptors.entries()).find(_ref1 => {
|
|
25508
|
+
let _ref10 = _slicedToArray(_ref1, 2);
|
|
25509
|
+
_ref10[0];
|
|
25510
|
+
let descriptor = _ref10[1];
|
|
25511
|
+
return descriptor.publisherIdentity === publisherIdentity && descriptor.info.pubHandle === info.pubHandle;
|
|
25512
|
+
});
|
|
25513
|
+
if (!existingEntry) {
|
|
25514
|
+
return false;
|
|
25515
|
+
}
|
|
25516
|
+
const _existingEntry = _slicedToArray(existingEntry, 2),
|
|
25517
|
+
oldSid = _existingEntry[0],
|
|
25518
|
+
descriptor = _existingEntry[1];
|
|
25519
|
+
// Invariant: other than SID, info should not have changed.
|
|
25520
|
+
// TODO: consider refactoring to move SID out of info to allow for direct comparison.
|
|
25521
|
+
const _descriptor$info = descriptor.info,
|
|
25522
|
+
name = _descriptor$info.name,
|
|
25523
|
+
usesE2ee = _descriptor$info.usesE2ee;
|
|
25524
|
+
if (name !== info.name || usesE2ee !== info.usesE2ee) {
|
|
25525
|
+
log$1.warn("Info mismatch for ".concat(oldSid, ", treating as new publication"));
|
|
25526
|
+
return false;
|
|
25527
|
+
}
|
|
25528
|
+
const newSid = info.sid;
|
|
25529
|
+
log$1.debug("SID reassigned: ".concat(oldSid, " -> ").concat(newSid));
|
|
25530
|
+
if (!this.descriptors.delete(oldSid)) {
|
|
25531
|
+
return false;
|
|
25532
|
+
}
|
|
25533
|
+
descriptor.info.sid = newSid;
|
|
25534
|
+
switch (descriptor.subscription.type) {
|
|
25535
|
+
case 'none':
|
|
25536
|
+
break;
|
|
25537
|
+
case 'pending':
|
|
25538
|
+
case 'active':
|
|
25539
|
+
// The SFU does not carry subscriptions across a publisher's full
|
|
25540
|
+
// reconnect; re-request the subscription under the new SID.
|
|
25541
|
+
this.emit('sfuUpdateSubscription', {
|
|
25542
|
+
sid: newSid,
|
|
25543
|
+
subscribe: true
|
|
25544
|
+
});
|
|
25545
|
+
break;
|
|
25546
|
+
}
|
|
25547
|
+
if (descriptor.subscription.type === 'active') {
|
|
25548
|
+
// Keep the routing index consistent until the SFU assigns a new handle
|
|
25549
|
+
// (see `registerSubscriberHandle`).
|
|
25550
|
+
this.subscriptionHandles.set(descriptor.subscription.subcriptionHandle, newSid);
|
|
25551
|
+
}
|
|
25552
|
+
this.descriptors.set(newSid, descriptor);
|
|
25553
|
+
return true;
|
|
25554
|
+
}
|
|
25161
25555
|
handleTrackUnpublished(sid) {
|
|
25162
25556
|
const descriptor = this.descriptors.get(sid);
|
|
25163
25557
|
if (!descriptor) {
|
|
@@ -25179,10 +25573,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
25179
25573
|
/** Mapping between track handles attached to incoming packets to the
|
|
25180
25574
|
* track SIDs they belong to. */
|
|
25181
25575
|
mapping) {
|
|
25182
|
-
for (const
|
|
25183
|
-
var
|
|
25184
|
-
const handle =
|
|
25185
|
-
const sid =
|
|
25576
|
+
for (const _ref11 of mapping.entries()) {
|
|
25577
|
+
var _ref12 = _slicedToArray(_ref11, 2);
|
|
25578
|
+
const handle = _ref12[0];
|
|
25579
|
+
const sid = _ref12[1];
|
|
25186
25580
|
this.registerSubscriberHandle(handle, sid);
|
|
25187
25581
|
}
|
|
25188
25582
|
}
|
|
@@ -25203,12 +25597,17 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
25203
25597
|
case 'active':
|
|
25204
25598
|
{
|
|
25205
25599
|
// Update handle for an active subscription. This can occur following a full reconnect.
|
|
25600
|
+
this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle);
|
|
25206
25601
|
descriptor.subscription.subcriptionHandle = assignedHandle;
|
|
25207
25602
|
this.subscriptionHandles.set(assignedHandle, sid);
|
|
25208
25603
|
return;
|
|
25209
25604
|
}
|
|
25210
25605
|
case 'pending':
|
|
25211
25606
|
{
|
|
25607
|
+
log$1.debug("data track subscription activated", {
|
|
25608
|
+
sid,
|
|
25609
|
+
handle: assignedHandle
|
|
25610
|
+
});
|
|
25212
25611
|
const pipeline = new IncomingDataTrackPipeline({
|
|
25213
25612
|
info: descriptor.info,
|
|
25214
25613
|
publisherIdentity: descriptor.publisherIdentity,
|
|
@@ -25275,10 +25674,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
25275
25674
|
* tracks are subscribed to locally.
|
|
25276
25675
|
*/
|
|
25277
25676
|
resendSubscriptionUpdates() {
|
|
25278
|
-
for (const
|
|
25279
|
-
var
|
|
25280
|
-
const sid =
|
|
25281
|
-
const descriptor =
|
|
25677
|
+
for (const _ref13 of this.descriptors) {
|
|
25678
|
+
var _ref14 = _slicedToArray(_ref13, 2);
|
|
25679
|
+
const sid = _ref14[0];
|
|
25680
|
+
const descriptor = _ref14[1];
|
|
25282
25681
|
if (descriptor.subscription.type === 'none') {
|
|
25283
25682
|
continue;
|
|
25284
25683
|
}
|
|
@@ -25944,10 +26343,16 @@ class OutgoingDataTrackManager extends eventsExports.EventEmitter {
|
|
|
25944
26343
|
{
|
|
25945
26344
|
if (result.type === 'ok') {
|
|
25946
26345
|
const info = result.data;
|
|
26346
|
+
log.debug("SFU accepted publish request for handle ".concat(handle), {
|
|
26347
|
+
sid: info.sid
|
|
26348
|
+
});
|
|
25947
26349
|
const e2eeManager = info.usesE2ee ? this.e2eeManager : null;
|
|
25948
26350
|
this.descriptors.set(info.pubHandle, Descriptor.active(info, e2eeManager));
|
|
25949
26351
|
(_b = (_a = descriptor.completionFuture).resolve) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
25950
26352
|
} else {
|
|
26353
|
+
log.debug("SFU rejected publish request for handle ".concat(handle), {
|
|
26354
|
+
error: result.error
|
|
26355
|
+
});
|
|
25951
26356
|
(_d = (_c = descriptor.completionFuture).reject) === null || _d === void 0 ? void 0 : _d.call(_c, result.error);
|
|
25952
26357
|
}
|
|
25953
26358
|
return;
|
|
@@ -27403,6 +27808,11 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
27403
27808
|
this.audioTrackPublications.forEach(track => isAudioTrack(track.track) && track.track.setAudioContext(ctx));
|
|
27404
27809
|
}
|
|
27405
27810
|
addTrackPublication(publication) {
|
|
27811
|
+
this.log.debug("adding track publication", {
|
|
27812
|
+
trackSid: publication.trackSid,
|
|
27813
|
+
source: publication.source,
|
|
27814
|
+
kind: publication.kind
|
|
27815
|
+
});
|
|
27406
27816
|
// forward publication driven events
|
|
27407
27817
|
publication.on(TrackEvent.Muted, () => {
|
|
27408
27818
|
this.emit(ParticipantEvent.TrackMuted, publication);
|
|
@@ -28447,7 +28857,7 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
28447
28857
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
|
28448
28858
|
}
|
|
28449
28859
|
const negotiate = () => __awaiter(this, void 0, void 0, function* () {
|
|
28450
|
-
var _a, _b, _c;
|
|
28860
|
+
var _a, _b, _c, _d;
|
|
28451
28861
|
if (!this.engine.pcManager) {
|
|
28452
28862
|
throw new UnexpectedConnectionState('pcManager is not ready');
|
|
28453
28863
|
}
|
|
@@ -28483,12 +28893,22 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
28483
28893
|
maxbr: ((_b = encodings[0]) === null || _b === void 0 ? void 0 : _b.maxBitrate) ? encodings[0].maxBitrate / 1000 : 0
|
|
28484
28894
|
});
|
|
28485
28895
|
}
|
|
28486
|
-
} else if (track.codec &&
|
|
28487
|
-
|
|
28488
|
-
|
|
28489
|
-
|
|
28490
|
-
|
|
28491
|
-
|
|
28896
|
+
} else if (track.codec && isVideoCodec(track.codec)) {
|
|
28897
|
+
// Apply start bitrate for all video codecs to prevent initial blurriness.
|
|
28898
|
+
// - SVC codecs: use first encoding's bitrate (single stream with built-in layers)
|
|
28899
|
+
// - Simulcast: sum all encoding bitrates (independent streams, BWE needs total)
|
|
28900
|
+
const targetBitrate = isSVCCodec(track.codec) ? (_d = (_c = encodings[0]) === null || _c === void 0 ? void 0 : _c.maxBitrate) !== null && _d !== void 0 ? _d : 0 : encodings.reduce((sum, enc) => {
|
|
28901
|
+
var _a;
|
|
28902
|
+
return sum + ((_a = enc.maxBitrate) !== null && _a !== void 0 ? _a : 0);
|
|
28903
|
+
}, 0);
|
|
28904
|
+
if (targetBitrate > 0) {
|
|
28905
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
|
28906
|
+
cid: req.cid,
|
|
28907
|
+
codec: track.codec,
|
|
28908
|
+
maxbr: targetBitrate / 1000,
|
|
28909
|
+
isScreenShare: track.source === Track.Source.ScreenShare
|
|
28910
|
+
});
|
|
28911
|
+
}
|
|
28492
28912
|
}
|
|
28493
28913
|
}
|
|
28494
28914
|
yield this.engine.negotiate();
|
|
@@ -30206,7 +30626,13 @@ class Room extends eventsExports.EventEmitter {
|
|
|
30206
30626
|
});
|
|
30207
30627
|
try {
|
|
30208
30628
|
yield Promise.all([this.acquireAudioContext(), ...elements.map(e => {
|
|
30209
|
-
|
|
30629
|
+
// when webAudioMix is enabled, attached elements are deliberately kept muted by
|
|
30630
|
+
// RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
|
|
30631
|
+
// unmuting them here would cause double playback on platforms where element.volume
|
|
30632
|
+
// has no effect (e.g. iOS Safari)
|
|
30633
|
+
if (!this.options.webAudioMix) {
|
|
30634
|
+
e.muted = false;
|
|
30635
|
+
}
|
|
30210
30636
|
return e.play();
|
|
30211
30637
|
})]);
|
|
30212
30638
|
this.handleAudioPlaybackStarted();
|