livekit-client 2.21.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- 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 +2 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +480 -150
- 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 +2 -1
- 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/room/PCTransport.d.ts +49 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +20 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +2 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +15 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/ts4.2/room/PCTransport.d.ts +49 -2
- package/dist/ts4.2/room/RTCEngine.d.ts +20 -1
- package/dist/ts4.2/room/Room.d.ts +2 -0
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/utils.d.ts +15 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/package.json +13 -12
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/e2ee/utils.ts +1 -1
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +181 -80
- package/src/room/RTCEngine.test.ts +339 -2
- package/src/room/RTCEngine.ts +152 -11
- package/src/room/Room.test.ts +134 -3
- package/src/room/Room.ts +52 -17
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
- package/src/room/data-stream/incoming/StreamReader.ts +1 -1
- package/src/room/errors.ts +1 -2
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +2 -1
- package/src/room/utils.test.ts +71 -2
- package/src/room/utils.ts +52 -0
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
|
@@ -12160,7 +12160,7 @@ function getMatch(exp, ua) {
|
|
|
12160
12160
|
}
|
|
12161
12161
|
function getOSVersion(ua) {
|
|
12162
12162
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
|
12163
|
-
}var version$1 = "2.
|
|
12163
|
+
}var version$1 = "2.22.0";const version = version$1;
|
|
12164
12164
|
const protocolVersion = 17;
|
|
12165
12165
|
/** Initial client protocol. */
|
|
12166
12166
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
@@ -13640,8 +13640,9 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13640
13640
|
if (this.monitorInterval) {
|
|
13641
13641
|
clearInterval(this.monitorInterval);
|
|
13642
13642
|
}
|
|
13643
|
-
if (this.timeSyncHandle) {
|
|
13643
|
+
if (this.timeSyncHandle !== undefined) {
|
|
13644
13644
|
cancelAnimationFrame(this.timeSyncHandle);
|
|
13645
|
+
this.timeSyncHandle = undefined;
|
|
13645
13646
|
}
|
|
13646
13647
|
}
|
|
13647
13648
|
/** @internal */
|
|
@@ -13929,6 +13930,45 @@ function supportsVP9() {
|
|
|
13929
13930
|
function isSVCCodec(codec) {
|
|
13930
13931
|
return codec === 'av1' || codec === 'vp9';
|
|
13931
13932
|
}
|
|
13933
|
+
/**
|
|
13934
|
+
* Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
|
|
13935
|
+
*
|
|
13936
|
+
* Chrome only offers the extension on transceivers that can send, so one we create to receive on
|
|
13937
|
+
* never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
|
|
13938
|
+
* assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
|
|
13939
|
+
* transceiver rather than munging the extension into the SDP leaves the browser owning the
|
|
13940
|
+
* extension id, which is what keeps that id consistent across the bundle and across
|
|
13941
|
+
* renegotiations.
|
|
13942
|
+
*
|
|
13943
|
+
* A no-op where the browser offers no such control, or does not know the extension at all.
|
|
13944
|
+
* @internal
|
|
13945
|
+
*/
|
|
13946
|
+
function negotiateDependencyDescriptor(transceiver) {
|
|
13947
|
+
var _a;
|
|
13948
|
+
const extensions = (_a = transceiver.getHeaderExtensionsToNegotiate) === null || _a === void 0 ? void 0 : _a.call(transceiver);
|
|
13949
|
+
if (!extensions || !transceiver.setHeaderExtensionsToNegotiate) {
|
|
13950
|
+
return false;
|
|
13951
|
+
}
|
|
13952
|
+
const dd = extensions.find(ext => ext.uri === ddExtensionURI);
|
|
13953
|
+
if (!dd) {
|
|
13954
|
+
return false;
|
|
13955
|
+
}
|
|
13956
|
+
if (dd.direction !== 'stopped') {
|
|
13957
|
+
return true;
|
|
13958
|
+
}
|
|
13959
|
+
// sendrecv rather than recvonly, so that the extension is written as a plain `a=extmap` line —
|
|
13960
|
+
// the form the server already emits where it is the one offering — rather than one carrying a
|
|
13961
|
+
// `/recvonly` suffix that its parser may not expect
|
|
13962
|
+
dd.direction = 'sendrecv';
|
|
13963
|
+
try {
|
|
13964
|
+
transceiver.setHeaderExtensionsToNegotiate(extensions);
|
|
13965
|
+
return true;
|
|
13966
|
+
} catch (e) {
|
|
13967
|
+
// a rejected direction throws. Negotiating without the extension is what happened before this
|
|
13968
|
+
// existed, so it is not worth failing the connection over
|
|
13969
|
+
return false;
|
|
13970
|
+
}
|
|
13971
|
+
}
|
|
13932
13972
|
function supportsSetSinkId(elm) {
|
|
13933
13973
|
if (!document || isSafariBased()) {
|
|
13934
13974
|
return false;
|
|
@@ -14470,6 +14510,17 @@ function isCompressionStreamSupported() {
|
|
|
14470
14510
|
function isPublisherOfferWithJoinSupported() {
|
|
14471
14511
|
// we have connectivity issue about publisher offer with join on firefox #1919
|
|
14472
14512
|
return isCompressionStreamSupported() && !isFireFox();
|
|
14513
|
+
}
|
|
14514
|
+
function extractTrackSid(mediaTrack, stream) {
|
|
14515
|
+
const _unpackStreamId = unpackStreamId(stream.id),
|
|
14516
|
+
_unpackStreamId2 = _slicedToArray(_unpackStreamId, 2),
|
|
14517
|
+
streamId = _unpackStreamId2[1];
|
|
14518
|
+
if (streamId === null || streamId === void 0 ? void 0 : streamId.startsWith('TR')) {
|
|
14519
|
+
return streamId;
|
|
14520
|
+
}
|
|
14521
|
+
if (mediaTrack.id.startsWith('TR')) {
|
|
14522
|
+
return mediaTrack.id;
|
|
14523
|
+
}
|
|
14473
14524
|
}function createRtcUrl(url, searchParams) {
|
|
14474
14525
|
let useV0Path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
14475
14526
|
const v0Url = createV0RtcUrl(url, searchParams);
|
|
@@ -14968,6 +15019,34 @@ function computeBitrate(currentStats, prevStats) {
|
|
|
14968
15019
|
}class RemoteTrack extends Track {
|
|
14969
15020
|
constructor(mediaTrack, sid, kind, receiver, loggerOptions) {
|
|
14970
15021
|
super(mediaTrack, kind, loggerOptions);
|
|
15022
|
+
this.timeSyncLoop = () => {
|
|
15023
|
+
var _a;
|
|
15024
|
+
if (this.listenerCount(TrackEvent.TimeSyncUpdate) === 0) {
|
|
15025
|
+
// nobody is listening anymore, pause the loop until a new listener subscribes
|
|
15026
|
+
this.timeSyncHandle = undefined;
|
|
15027
|
+
return;
|
|
15028
|
+
}
|
|
15029
|
+
this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
|
|
15030
|
+
const sources = (_a = this.receiver) === null || _a === void 0 ? void 0 : _a.getSynchronizationSources()[0];
|
|
15031
|
+
if (sources) {
|
|
15032
|
+
const timestamp = sources.timestamp,
|
|
15033
|
+
rtpTimestamp = sources.rtpTimestamp;
|
|
15034
|
+
if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
|
|
15035
|
+
this.emit(TrackEvent.TimeSyncUpdate, {
|
|
15036
|
+
timestamp,
|
|
15037
|
+
rtpTimestamp
|
|
15038
|
+
});
|
|
15039
|
+
this.rtpTimestamp = rtpTimestamp;
|
|
15040
|
+
}
|
|
15041
|
+
}
|
|
15042
|
+
};
|
|
15043
|
+
this.onTimeSyncListenerAdded = event => {
|
|
15044
|
+
if (event === TrackEvent.TimeSyncUpdate && this.timeSyncHandle === undefined) {
|
|
15045
|
+
// `newListener` fires before the listener is registered, so schedule the
|
|
15046
|
+
// next frame instead of entering the loop (which would see a count of 0)
|
|
15047
|
+
this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
|
|
15048
|
+
}
|
|
15049
|
+
};
|
|
14971
15050
|
this.sid = sid;
|
|
14972
15051
|
this.receiver = receiver;
|
|
14973
15052
|
}
|
|
@@ -15065,24 +15144,19 @@ function computeBitrate(currentStats, prevStats) {
|
|
|
15065
15144
|
this.registerTimeSyncUpdate();
|
|
15066
15145
|
}
|
|
15067
15146
|
}
|
|
15147
|
+
/* @internal */
|
|
15148
|
+
stopMonitor() {
|
|
15149
|
+
super.stopMonitor();
|
|
15150
|
+
this.off('newListener', this.onTimeSyncListenerAdded);
|
|
15151
|
+
}
|
|
15068
15152
|
registerTimeSyncUpdate() {
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
|
|
15077
|
-
this.emit(TrackEvent.TimeSyncUpdate, {
|
|
15078
|
-
timestamp,
|
|
15079
|
-
rtpTimestamp
|
|
15080
|
-
});
|
|
15081
|
-
this.rtpTimestamp = rtpTimestamp;
|
|
15082
|
-
}
|
|
15083
|
-
}
|
|
15084
|
-
};
|
|
15085
|
-
loop();
|
|
15153
|
+
// `newListener` isn't part of the typed event map, hence the cast
|
|
15154
|
+
const emitter = this;
|
|
15155
|
+
emitter.off('newListener', this.onTimeSyncListenerAdded);
|
|
15156
|
+
emitter.on('newListener', this.onTimeSyncListenerAdded);
|
|
15157
|
+
if (this.timeSyncHandle === undefined) {
|
|
15158
|
+
this.timeSyncLoop();
|
|
15159
|
+
}
|
|
15086
15160
|
}
|
|
15087
15161
|
}const REACTION_DELAY = 100;
|
|
15088
15162
|
class RemoteVideoTrack extends RemoteTrack {
|
|
@@ -18392,6 +18466,51 @@ const startBitrateMultiplier = 0.9;
|
|
|
18392
18466
|
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
18393
18467
|
const maxStartBitrateKbps = 1000;
|
|
18394
18468
|
const debounceInterval = 20;
|
|
18469
|
+
/**
|
|
18470
|
+
* Applies the configured start bitrate when this media section belongs to `cid`.
|
|
18471
|
+
* This SDP munging is used for a bitrate setting that cannot be applied through
|
|
18472
|
+
* `RTCRtpEncodingParameters`.
|
|
18473
|
+
*
|
|
18474
|
+
* Returns `undefined` when the section does not belong to the track, `0` when
|
|
18475
|
+
* it does but does not offer the requested codec, and the codec payload when the
|
|
18476
|
+
* requested codec is present (whether the bitrate was added or already set).
|
|
18477
|
+
*
|
|
18478
|
+
* @internal
|
|
18479
|
+
*/
|
|
18480
|
+
function applyVideoStartBitrate(media, cid, codec, maxbr) {
|
|
18481
|
+
let isScreenShare = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
|
|
18482
|
+
var _a, _b, _c;
|
|
18483
|
+
if (!((_a = media.msid) === null || _a === void 0 ? void 0 : _a.includes(cid))) {
|
|
18484
|
+
return undefined;
|
|
18485
|
+
}
|
|
18486
|
+
const codecPayload = (_c = (_b = media.rtp.find(rtp => rtp.codec.toUpperCase() === codec.toUpperCase())) === null || _b === void 0 ? void 0 : _b.payload) !== null && _c !== void 0 ? _c : 0;
|
|
18487
|
+
if (codecPayload === 0) {
|
|
18488
|
+
return 0;
|
|
18489
|
+
}
|
|
18490
|
+
// Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
|
|
18491
|
+
// from starting too aggressively. Screen share is not capped since text/UI
|
|
18492
|
+
// clarity requires high bitrate from the start.
|
|
18493
|
+
// TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
|
|
18494
|
+
const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
|
|
18495
|
+
const startBitrate = isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
18496
|
+
const fmtp = media.fmtp.find(entry => entry.payload === codecPayload);
|
|
18497
|
+
if (fmtp) {
|
|
18498
|
+
// If another track's fmtp already has a start bitrate, it cannot be
|
|
18499
|
+
// overridden here because the payload type is shared across the bundle.
|
|
18500
|
+
// This forces every track sharing that payload to use the initial track's
|
|
18501
|
+
// start bitrate.
|
|
18502
|
+
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
|
18503
|
+
fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
|
|
18504
|
+
}
|
|
18505
|
+
} else {
|
|
18506
|
+
// VP8 and some codecs may not have an existing fmtp line.
|
|
18507
|
+
media.fmtp.push({
|
|
18508
|
+
payload: codecPayload,
|
|
18509
|
+
config: "x-google-start-bitrate=".concat(startBitrate)
|
|
18510
|
+
});
|
|
18511
|
+
}
|
|
18512
|
+
return codecPayload;
|
|
18513
|
+
}
|
|
18395
18514
|
const PCEvents = {
|
|
18396
18515
|
NegotiationStarted: 'negotiationStarted',
|
|
18397
18516
|
NegotiationComplete: 'negotiationComplete',
|
|
@@ -18693,9 +18812,13 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18693
18812
|
// the only exception to this is when ICE restart is needed
|
|
18694
18813
|
const currentSD = this._pc.remoteDescription;
|
|
18695
18814
|
if ((options === null || options === void 0 ? void 0 : options.iceRestart) && currentSD) {
|
|
18696
|
-
//
|
|
18697
|
-
//
|
|
18815
|
+
// roll the remote description back in so createOffer produces a valid
|
|
18816
|
+
// ICE-restart offer on top of the already-negotiated state
|
|
18698
18817
|
yield this._pc.setRemoteDescription(currentSD);
|
|
18818
|
+
} else if (options === null || options === void 0 ? void 0 : options.iceRestart) {
|
|
18819
|
+
// ICE restart with no remote description to restart on: `renegotiate` would stall
|
|
18820
|
+
// (the pending offer is never answered), so throw for the caller to recreate the PC.
|
|
18821
|
+
throw new NegotiationError('ICE restart requested without a remote description, peer connection must be recreated');
|
|
18699
18822
|
} else {
|
|
18700
18823
|
this.renegotiate = true;
|
|
18701
18824
|
this.log.debug('requesting renegotiation');
|
|
@@ -18721,48 +18844,15 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18721
18844
|
ensureAudioNackAndStereo(media, ['all'], []);
|
|
18722
18845
|
} else if (media.type === 'video') {
|
|
18723
18846
|
this.trackBitrates.some(trackbr => {
|
|
18724
|
-
if (!
|
|
18847
|
+
if (!trackbr.cid) {
|
|
18725
18848
|
return false;
|
|
18726
18849
|
}
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
|
|
18730
|
-
codecPayload = rtp.payload;
|
|
18731
|
-
return true;
|
|
18732
|
-
}
|
|
18850
|
+
const codecPayload = applyVideoStartBitrate(media, trackbr.cid, trackbr.codec, trackbr.maxbr, trackbr.isScreenShare);
|
|
18851
|
+
if (codecPayload === undefined) {
|
|
18733
18852
|
return false;
|
|
18734
|
-
});
|
|
18735
|
-
if (codecPayload === 0) {
|
|
18736
|
-
return true;
|
|
18737
|
-
}
|
|
18738
|
-
if (isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
18739
|
-
this.ensureVideoDDExtensionForSVC(media, sdpParsed);
|
|
18740
18853
|
}
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
// Screen share is not capped since text/UI clarity requires high bitrate from the start
|
|
18744
|
-
// TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
|
|
18745
|
-
const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
18746
|
-
const startBitrate = trackbr.isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
18747
|
-
let fmtpFound = false;
|
|
18748
|
-
for (const fmtp of media.fmtp) {
|
|
18749
|
-
if (fmtp.payload === codecPayload) {
|
|
18750
|
-
fmtpFound = true;
|
|
18751
|
-
// if another track's fmtp already is set, we cannot override the bitrate
|
|
18752
|
-
// this has the unfortunate consequence of being forced to use the
|
|
18753
|
-
// initial track's bitrate for all tracks
|
|
18754
|
-
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
|
18755
|
-
fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
|
|
18756
|
-
}
|
|
18757
|
-
break;
|
|
18758
|
-
}
|
|
18759
|
-
}
|
|
18760
|
-
// VP8 and some codecs may not have an existing fmtp line - create one
|
|
18761
|
-
if (!fmtpFound) {
|
|
18762
|
-
media.fmtp.push({
|
|
18763
|
-
payload: codecPayload,
|
|
18764
|
-
config: "x-google-start-bitrate=".concat(startBitrate)
|
|
18765
|
-
});
|
|
18854
|
+
if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
18855
|
+
this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
|
|
18766
18856
|
}
|
|
18767
18857
|
return true;
|
|
18768
18858
|
});
|
|
@@ -18975,41 +19065,106 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18975
19065
|
}
|
|
18976
19066
|
});
|
|
18977
19067
|
}
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
19068
|
+
}
|
|
19069
|
+
/**
|
|
19070
|
+
* Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
|
|
19071
|
+
* returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
|
|
19072
|
+
* been chosen yet).
|
|
19073
|
+
*
|
|
19074
|
+
* A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
|
|
19075
|
+
* `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
|
|
19076
|
+
* sections it can send on, and an earlier offer may have munged it into others.
|
|
19077
|
+
* @internal
|
|
19078
|
+
*/
|
|
19079
|
+
function ensureVideoDDExtension(media, sdp, ddExtID) {
|
|
19080
|
+
var _a, _b;
|
|
19081
|
+
const id = ddExtensionIDFor(sdp, ddExtID);
|
|
19082
|
+
if (id === undefined) {
|
|
19083
|
+
return ddExtID;
|
|
19084
|
+
}
|
|
19085
|
+
if (!((_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.uri === ddExtensionURI))) {
|
|
19086
|
+
(_b = media.ext) !== null && _b !== void 0 ? _b : media.ext = [];
|
|
19087
|
+
media.ext.push({
|
|
19088
|
+
value: id,
|
|
19089
|
+
uri: ddExtensionURI
|
|
18985
19090
|
});
|
|
18986
|
-
|
|
18987
|
-
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19091
|
+
}
|
|
19092
|
+
return id;
|
|
19093
|
+
}
|
|
19094
|
+
/**
|
|
19095
|
+
* The id to map the dependency descriptor to throughout `sdp`, or undefined when no id would be
|
|
19096
|
+
* consistent for the whole bundle and the extension therefore has to be left out.
|
|
19097
|
+
*/
|
|
19098
|
+
function ddExtensionIDFor(sdp, cachedID) {
|
|
19099
|
+
const mapped = mappedExtensionID(sdp, ddExtensionURI);
|
|
19100
|
+
if (mapped !== undefined) {
|
|
19101
|
+
// Adopting an id that also stands for another URI is what the browser rejects the bundle
|
|
19102
|
+
// over, and its own half of the map is not ours to renumber, so give up on this offer.
|
|
19103
|
+
return usedForOtherURI(sdp, mapped, ddExtensionURI) ? undefined : mapped;
|
|
19104
|
+
}
|
|
19105
|
+
// Reusing the id from the last offer keeps the mapping stable across renegotiations, but only
|
|
19106
|
+
// while nothing else has taken it: the browser assigns ids to its own extensions without
|
|
19107
|
+
// knowing about ours, so an id that was free when we picked it can since have been claimed —
|
|
19108
|
+
// typically by the fuller extension set that arrives with the first section we send on.
|
|
19109
|
+
if (cachedID !== 0 && !usedForOtherURI(sdp, cachedID, ddExtensionURI)) {
|
|
19110
|
+
return cachedID;
|
|
19111
|
+
}
|
|
19112
|
+
return unusedExtensionID(sdp);
|
|
19113
|
+
}
|
|
19114
|
+
/** The id `uri` is mapped to in `sdp`, if any section maps it. */
|
|
19115
|
+
function mappedExtensionID(sdp, uri) {
|
|
19116
|
+
var _a;
|
|
19117
|
+
for (const media of sdp.media) {
|
|
19118
|
+
const ext = (_a = media.ext) === null || _a === void 0 ? void 0 : _a.find(candidate => candidate.uri === uri);
|
|
19119
|
+
if (ext) {
|
|
19120
|
+
return ext.value;
|
|
19003
19121
|
}
|
|
19004
19122
|
}
|
|
19123
|
+
return undefined;
|
|
19124
|
+
}
|
|
19125
|
+
/** Whether `id` stands for anything in `sdp` other than `uri`. */
|
|
19126
|
+
function usedForOtherURI(sdp, id, uri) {
|
|
19127
|
+
return sdp.media.some(media => {
|
|
19128
|
+
var _a;
|
|
19129
|
+
return (_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.value === id && ext.uri !== uri);
|
|
19130
|
+
});
|
|
19005
19131
|
}
|
|
19132
|
+
/**
|
|
19133
|
+
* An id no extension in `sdp` uses. Stays above every id in use rather than filling gaps, so it
|
|
19134
|
+
* is less likely to be an id the browser goes on to allocate to another extension, and steps
|
|
19135
|
+
* over 15, which RFC 8285 reserves.
|
|
19136
|
+
*/
|
|
19137
|
+
function unusedExtensionID(sdp) {
|
|
19138
|
+
let maxID = 0;
|
|
19139
|
+
sdp.media.forEach(media => {
|
|
19140
|
+
var _a;
|
|
19141
|
+
(_a = media.ext) === null || _a === void 0 ? void 0 : _a.forEach(ext => {
|
|
19142
|
+
if (ext.value > maxID) {
|
|
19143
|
+
maxID = ext.value;
|
|
19144
|
+
}
|
|
19145
|
+
});
|
|
19146
|
+
});
|
|
19147
|
+
return maxID + 1 === 15 ? 16 : maxID + 1;
|
|
19148
|
+
}
|
|
19149
|
+
/**
|
|
19150
|
+
* Checks whether an fmtp config declares `param` as an exact, `;`-delimited
|
|
19151
|
+
* token. A plain substring check conflates distinct opus parameters — e.g.
|
|
19152
|
+
* `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
|
|
19153
|
+
* parameter, not appear anywhere within the config string.
|
|
19154
|
+
* @internal
|
|
19155
|
+
*/
|
|
19156
|
+
function fmtpConfigHasParam(config, param) {
|
|
19157
|
+
return config.split(';').some(entry => entry.trim() === param);
|
|
19158
|
+
}
|
|
19159
|
+
/** @internal */
|
|
19006
19160
|
function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
|
|
19007
19161
|
// sdp-transform types don't include number however the parser outputs mids as numbers in some cases
|
|
19008
19162
|
const mid = getMidString(media.mid);
|
|
19009
19163
|
// found opus codec to add nack fb
|
|
19010
19164
|
let opusPayload = 0;
|
|
19011
19165
|
media.rtp.some(rtp => {
|
|
19012
|
-
|
|
19166
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
19167
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
19013
19168
|
opusPayload = rtp.payload;
|
|
19014
19169
|
return true;
|
|
19015
19170
|
}
|
|
@@ -19029,7 +19184,7 @@ function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
|
|
|
19029
19184
|
if (stereoMids.includes(mid) || stereoMids.length === 1 && stereoMids[0] === 'all') {
|
|
19030
19185
|
media.fmtp.some(fmtp => {
|
|
19031
19186
|
if (fmtp.payload === opusPayload) {
|
|
19032
|
-
if (!fmtp.config
|
|
19187
|
+
if (!fmtpConfigHasParam(fmtp.config, 'stereo=1')) {
|
|
19033
19188
|
fmtp.config += ';stereo=1';
|
|
19034
19189
|
}
|
|
19035
19190
|
return true;
|
|
@@ -19112,6 +19267,7 @@ function conformBundledCodecFmtp(media, isPlaceholder) {
|
|
|
19112
19267
|
}
|
|
19113
19268
|
}
|
|
19114
19269
|
}
|
|
19270
|
+
/** @internal */
|
|
19115
19271
|
function extractStereoAndNackAudioFromOffer(offer) {
|
|
19116
19272
|
var _a;
|
|
19117
19273
|
const stereoMids = [];
|
|
@@ -19123,7 +19279,8 @@ function extractStereoAndNackAudioFromOffer(offer) {
|
|
|
19123
19279
|
const mid = getMidString(media.mid);
|
|
19124
19280
|
if (media.type === 'audio') {
|
|
19125
19281
|
media.rtp.some(rtp => {
|
|
19126
|
-
|
|
19282
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
19283
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
19127
19284
|
opusPayload = rtp.payload;
|
|
19128
19285
|
return true;
|
|
19129
19286
|
}
|
|
@@ -19134,7 +19291,7 @@ function extractStereoAndNackAudioFromOffer(offer) {
|
|
|
19134
19291
|
}
|
|
19135
19292
|
media.fmtp.some(fmtp => {
|
|
19136
19293
|
if (fmtp.payload === opusPayload) {
|
|
19137
|
-
if (fmtp.config
|
|
19294
|
+
if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
|
|
19138
19295
|
stereoMids.push(mid);
|
|
19139
19296
|
}
|
|
19140
19297
|
return true;
|
|
@@ -21927,17 +22084,37 @@ class LocalVideoTrack extends LocalTrack {
|
|
|
21927
22084
|
setDegradationPreference(preference) {
|
|
21928
22085
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21929
22086
|
this.degradationPreference = preference;
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
|
|
22087
|
+
// applied one sender at a time on purpose, see applyDegradationPreference
|
|
22088
|
+
yield this.applyDegradationPreference(this.sender);
|
|
22089
|
+
for (const sc of this.simulcastCodecs.values()) {
|
|
22090
|
+
yield this.applyDegradationPreference(sc.sender);
|
|
22091
|
+
}
|
|
22092
|
+
});
|
|
22093
|
+
}
|
|
22094
|
+
/**
|
|
22095
|
+
* Degradation preference is a property of the sender, not of the track, so every sender
|
|
22096
|
+
* publishing this track needs it applied separately. A backup codec publishes over its
|
|
22097
|
+
* own sender, which would otherwise let the browser resolve a preference implicitly and
|
|
22098
|
+
* diverge from the primary encoder.
|
|
22099
|
+
*
|
|
22100
|
+
* Callers apply this sequentially rather than concurrently: `setParameters` is only valid
|
|
22101
|
+
* against the parameters most recently returned by `getParameters`, which is why this file
|
|
22102
|
+
* serializes other sender parameter updates through `senderLock`.
|
|
22103
|
+
*/
|
|
22104
|
+
applyDegradationPreference(sender) {
|
|
22105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
22106
|
+
if (!sender) {
|
|
22107
|
+
return;
|
|
22108
|
+
}
|
|
22109
|
+
try {
|
|
22110
|
+
this.log.debug("setting degradationPreference to ".concat(this.degradationPreference), this.logContext);
|
|
22111
|
+
const params = sender.getParameters();
|
|
22112
|
+
params.degradationPreference = this.degradationPreference;
|
|
22113
|
+
yield sender.setParameters(params);
|
|
22114
|
+
} catch (e) {
|
|
22115
|
+
this.log.warn("failed to set degradationPreference", Object.assign({
|
|
22116
|
+
error: e
|
|
22117
|
+
}, this.logContext));
|
|
21941
22118
|
}
|
|
21942
22119
|
});
|
|
21943
22120
|
}
|
|
@@ -21956,18 +22133,23 @@ class LocalVideoTrack extends LocalTrack {
|
|
|
21956
22133
|
return simulcastCodecInfo;
|
|
21957
22134
|
}
|
|
21958
22135
|
setSimulcastTrackSender(codec, sender) {
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
21963
|
-
simulcastCodecInfo.sender = sender;
|
|
21964
|
-
// browser will reenable disabled codec/layers after new codec has been published,
|
|
21965
|
-
// so refresh subscribedCodecs after publish a new codec
|
|
21966
|
-
setTimeout(() => {
|
|
21967
|
-
if (this.subscribedCodecs) {
|
|
21968
|
-
this.setPublishingCodecs(this.subscribedCodecs);
|
|
22136
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
22137
|
+
const simulcastCodecInfo = this.simulcastCodecs.get(codec);
|
|
22138
|
+
if (!simulcastCodecInfo) {
|
|
22139
|
+
return;
|
|
21969
22140
|
}
|
|
21970
|
-
|
|
22141
|
+
simulcastCodecInfo.sender = sender;
|
|
22142
|
+
// the backup codec publishes over its own sender, so it needs the same degradation
|
|
22143
|
+
// preference the primary sender resolved to.
|
|
22144
|
+
yield this.applyDegradationPreference(sender);
|
|
22145
|
+
// browser will reenable disabled codec/layers after new codec has been published,
|
|
22146
|
+
// so refresh subscribedCodecs after publish a new codec
|
|
22147
|
+
setTimeout(() => {
|
|
22148
|
+
if (this.subscribedCodecs) {
|
|
22149
|
+
this.setPublishingCodecs(this.subscribedCodecs);
|
|
22150
|
+
}
|
|
22151
|
+
}, refreshSubscribedCodecAfterNewCodec);
|
|
22152
|
+
});
|
|
21971
22153
|
}
|
|
21972
22154
|
/**
|
|
21973
22155
|
* @internal
|
|
@@ -22239,6 +22421,11 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
|
|
|
22239
22421
|
});
|
|
22240
22422
|
}const minReconnectWait = 2 * 1000;
|
|
22241
22423
|
const leaveReconnect = 'leave-reconnect';
|
|
22424
|
+
/**
|
|
22425
|
+
* How long local connection quality must stay `LOST` while connected and publishing before we
|
|
22426
|
+
* force a full reconnect — `LOST` is the server's verdict that it isn't receiving our media.
|
|
22427
|
+
*/
|
|
22428
|
+
const connectionQualityLostTimeout = 10 * 1000;
|
|
22242
22429
|
const reliabeReceiveStateTTL = 30000;
|
|
22243
22430
|
const initialMediaSectionsAudio = 3;
|
|
22244
22431
|
const initialMediaSectionsVideo = 3;
|
|
@@ -22528,7 +22715,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22528
22715
|
onBufferStatusChanged: (kind, isLow) => this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind)
|
|
22529
22716
|
});
|
|
22530
22717
|
this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
22531
|
-
this.client.onConnectionQuality = update =>
|
|
22718
|
+
this.client.onConnectionQuality = update => {
|
|
22719
|
+
this.handleLocalConnectionQuality(update);
|
|
22720
|
+
this.emit(EngineEvent.ConnectionQualityUpdate, update);
|
|
22721
|
+
};
|
|
22532
22722
|
this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
|
|
22533
22723
|
this.client.onSubscriptionError = resp => this.emit(EngineEvent.SubscriptionError, resp);
|
|
22534
22724
|
this.client.onSubscriptionPermissionUpdate = update => this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
|
|
@@ -22671,6 +22861,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22671
22861
|
this.removeAllListeners();
|
|
22672
22862
|
this.deregisterOnLineListener();
|
|
22673
22863
|
this.clearPendingReconnect();
|
|
22864
|
+
this.clearLostQualityTimeout();
|
|
22674
22865
|
this.cleanupLossyDataStats();
|
|
22675
22866
|
yield this.cleanupPeerConnections();
|
|
22676
22867
|
yield this.cleanupClient();
|
|
@@ -22685,6 +22876,8 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22685
22876
|
this.dataChannels.teardown();
|
|
22686
22877
|
yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
|
|
22687
22878
|
this.pcManager = undefined;
|
|
22879
|
+
// the connecting timestamp belongs to the transports we just tore down
|
|
22880
|
+
this.transportConnectingSince = undefined;
|
|
22688
22881
|
this.reliableReceivedState.clear();
|
|
22689
22882
|
});
|
|
22690
22883
|
}
|
|
@@ -22710,17 +22903,17 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22710
22903
|
throw new TrackInvalidError('a track with the same ID has already been published');
|
|
22711
22904
|
}
|
|
22712
22905
|
return new Promise((resolve, reject) => {
|
|
22713
|
-
const publicationTimeout = setTimeout(() => {
|
|
22906
|
+
const publicationTimeout = CriticalTimers.setTimeout(() => {
|
|
22714
22907
|
delete this.pendingTrackResolvers[req.cid];
|
|
22715
22908
|
reject(ConnectionError.timeout('publication of local track timed out, no response from server'));
|
|
22716
22909
|
}, 10000);
|
|
22717
22910
|
this.pendingTrackResolvers[req.cid] = {
|
|
22718
22911
|
resolve: info => {
|
|
22719
|
-
clearTimeout(publicationTimeout);
|
|
22912
|
+
CriticalTimers.clearTimeout(publicationTimeout);
|
|
22720
22913
|
resolve(info);
|
|
22721
22914
|
},
|
|
22722
22915
|
reject: () => {
|
|
22723
|
-
clearTimeout(publicationTimeout);
|
|
22916
|
+
CriticalTimers.clearTimeout(publicationTimeout);
|
|
22724
22917
|
reject(new Error('Cancelled publication by calling unpublish'));
|
|
22725
22918
|
}
|
|
22726
22919
|
};
|
|
@@ -22793,6 +22986,15 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22793
22986
|
this.pcManager.onDataChannel = this.handleDataChannel;
|
|
22794
22987
|
this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
|
|
22795
22988
|
this.log.debug("primary PC state changed ".concat(connectionState));
|
|
22989
|
+
// Record when the primary transport actually entered CONNECTING so
|
|
22990
|
+
// verifyTransport() can bound how long we tolerate it. Deriving it from the
|
|
22991
|
+
// real transition (this handler only fires on state changes) rather than from
|
|
22992
|
+
// observation time keeps it from going stale across peer-connection rebuilds.
|
|
22993
|
+
if (connectionState === PCTransportState.CONNECTING) {
|
|
22994
|
+
this.transportConnectingSince = Date.now();
|
|
22995
|
+
} else {
|
|
22996
|
+
this.transportConnectingSince = undefined;
|
|
22997
|
+
}
|
|
22796
22998
|
if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
|
|
22797
22999
|
// reset publisher connection promise
|
|
22798
23000
|
this.publisherConnectionPromise = undefined;
|
|
@@ -23001,15 +23203,23 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23001
23203
|
}
|
|
23002
23204
|
}
|
|
23003
23205
|
addMediaSections(numAudios, numVideos) {
|
|
23004
|
-
var _a, _b;
|
|
23206
|
+
var _a, _b, _c;
|
|
23005
23207
|
const transceiverInit = {
|
|
23006
23208
|
direction: 'recvonly'
|
|
23007
23209
|
};
|
|
23008
23210
|
for (let i = 0; i < numAudios; i++) {
|
|
23009
23211
|
(_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.addPublisherTransceiverOfKind('audio', transceiverInit);
|
|
23010
23212
|
}
|
|
23213
|
+
// media only arrives on these sections when there is no subscriber connection to arrive on
|
|
23214
|
+
const receivesMedia = ((_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.mode) === 'publisher-only';
|
|
23011
23215
|
for (let i = 0; i < numVideos; i++) {
|
|
23012
|
-
(
|
|
23216
|
+
const transceiver = (_c = this.pcManager) === null || _c === void 0 ? void 0 : _c.addPublisherTransceiverOfKind('video', transceiverInit);
|
|
23217
|
+
if (receivesMedia && transceiver) {
|
|
23218
|
+
const negotiated = negotiateDependencyDescriptor(transceiver);
|
|
23219
|
+
this.log.debug('dependency descriptor negotiated for received video', {
|
|
23220
|
+
negotiated
|
|
23221
|
+
});
|
|
23222
|
+
}
|
|
23013
23223
|
}
|
|
23014
23224
|
}
|
|
23015
23225
|
createDataChannels() {
|
|
@@ -23151,7 +23361,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23151
23361
|
if (!opts.videoCodec) {
|
|
23152
23362
|
return;
|
|
23153
23363
|
}
|
|
23154
|
-
track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
|
23364
|
+
yield track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
|
23155
23365
|
return transceiver.sender;
|
|
23156
23366
|
});
|
|
23157
23367
|
}
|
|
@@ -23163,6 +23373,63 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23163
23373
|
return this.pcManager.addPublisherTrack(track);
|
|
23164
23374
|
});
|
|
23165
23375
|
}
|
|
23376
|
+
/**
|
|
23377
|
+
* A sustained local `LOST` while connected and publishing means the server isn't receiving
|
|
23378
|
+
* our media, so force a full reconnect; any non-`LOST` value cancels a pending trigger.
|
|
23379
|
+
*/
|
|
23380
|
+
handleLocalConnectionQuality(update) {
|
|
23381
|
+
if (!this.participantSid) {
|
|
23382
|
+
return;
|
|
23383
|
+
}
|
|
23384
|
+
const localUpdate = update.updates.find(u => u.participantSid === this.participantSid);
|
|
23385
|
+
if (!localUpdate) {
|
|
23386
|
+
return;
|
|
23387
|
+
}
|
|
23388
|
+
if (localUpdate.quality === ConnectionQuality$1.LOST) {
|
|
23389
|
+
this.scheduleLostQualityReconnect();
|
|
23390
|
+
} else {
|
|
23391
|
+
this.clearLostQualityTimeout();
|
|
23392
|
+
}
|
|
23393
|
+
}
|
|
23394
|
+
scheduleLostQualityReconnect() {
|
|
23395
|
+
if (this.lostQualityTimeout) {
|
|
23396
|
+
// already counting down towards a reconnect
|
|
23397
|
+
return;
|
|
23398
|
+
}
|
|
23399
|
+
this.lostQualityTimeout = CriticalTimers.setTimeout(() => {
|
|
23400
|
+
this.lostQualityTimeout = undefined;
|
|
23401
|
+
if (this._isClosed || this.pcState !== PCState.Connected || this.attemptingReconnect) {
|
|
23402
|
+
return;
|
|
23403
|
+
}
|
|
23404
|
+
if (!this.hasActivePublisherSenders()) {
|
|
23405
|
+
return;
|
|
23406
|
+
}
|
|
23407
|
+
this.log.warn('local connection quality lost while publishing, triggering full reconnect', this.logContext);
|
|
23408
|
+
this.fullReconnectOnNext = true;
|
|
23409
|
+
this.handleDisconnect('connection quality lost', ReconnectReason.RR_PUBLISHER_FAILED);
|
|
23410
|
+
}, connectionQualityLostTimeout);
|
|
23411
|
+
}
|
|
23412
|
+
clearLostQualityTimeout() {
|
|
23413
|
+
if (this.lostQualityTimeout) {
|
|
23414
|
+
CriticalTimers.clearTimeout(this.lostQualityTimeout);
|
|
23415
|
+
this.lostQualityTimeout = undefined;
|
|
23416
|
+
}
|
|
23417
|
+
}
|
|
23418
|
+
/** Whether the publisher currently has any sender with a live track. */
|
|
23419
|
+
hasActivePublisherSenders() {
|
|
23420
|
+
var _a, _b;
|
|
23421
|
+
return (_b = (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.publisher.getSenders().some(sender => !!sender.track && sender.track.readyState === 'live')) !== null && _b !== void 0 ? _b : false;
|
|
23422
|
+
}
|
|
23423
|
+
/**
|
|
23424
|
+
* Forces a full reconnect while keeping the engine (and its saved credentials) alive. Used by
|
|
23425
|
+
* Room's connection-reconcile safety net when the transport silently died but we looked connected.
|
|
23426
|
+
* @internal
|
|
23427
|
+
*/
|
|
23428
|
+
reconnect() {
|
|
23429
|
+
let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ReconnectReason.RR_UNKNOWN;
|
|
23430
|
+
this.fullReconnectOnNext = true;
|
|
23431
|
+
this.handleDisconnect('reconcile', reason);
|
|
23432
|
+
}
|
|
23166
23433
|
attemptReconnect(reason) {
|
|
23167
23434
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23168
23435
|
var _a, _b, _c;
|
|
@@ -23174,21 +23441,32 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23174
23441
|
this.log.warn('already attempting reconnect, returning early');
|
|
23175
23442
|
return;
|
|
23176
23443
|
}
|
|
23444
|
+
// A pending Lost-quality countdown belongs to the session we're now leaving; cancel it so
|
|
23445
|
+
// it can't fire against the reconnected session before the server has evaluated it. (A resume
|
|
23446
|
+
// keeps the peer connections, so cleanupPeerConnections wouldn't cover this path.)
|
|
23447
|
+
this.clearLostQualityTimeout();
|
|
23177
23448
|
if (((_a = this.clientConfiguration) === null || _a === void 0 ? void 0 : _a.resumeConnection) === ClientConfigSetting.DISABLED ||
|
|
23178
23449
|
// signaling state could change to closed due to hardware sleep
|
|
23179
23450
|
// those connections cannot be resumed
|
|
23180
23451
|
((_c = (_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.currentState) !== null && _c !== void 0 ? _c : PCTransportState.NEW) === PCTransportState.NEW) {
|
|
23181
23452
|
this.fullReconnectOnNext = true;
|
|
23182
23453
|
}
|
|
23454
|
+
// Consume the flag up front: capture whether this attempt is a full reconnect, then reset
|
|
23455
|
+
// it. From here on a `true` value unambiguously represents a *new* full-reconnect request
|
|
23456
|
+
// that arrived while this attempt was running (e.g. a server RECONNECT leave), which the
|
|
23457
|
+
// finally block dispatches — for both the resume and full-reconnect paths.
|
|
23458
|
+
const fullReconnect = this.fullReconnectOnNext;
|
|
23459
|
+
this.fullReconnectOnNext = false;
|
|
23460
|
+
let succeeded = false;
|
|
23183
23461
|
try {
|
|
23184
23462
|
this.attemptingReconnect = true;
|
|
23185
|
-
if (
|
|
23463
|
+
if (fullReconnect) {
|
|
23186
23464
|
yield this.restartConnection();
|
|
23187
23465
|
} else {
|
|
23188
23466
|
yield this.resumeConnection(reason);
|
|
23189
23467
|
}
|
|
23190
23468
|
this.clearPendingReconnect();
|
|
23191
|
-
|
|
23469
|
+
succeeded = true;
|
|
23192
23470
|
} catch (e) {
|
|
23193
23471
|
this.reconnectAttempts += 1;
|
|
23194
23472
|
let recoverable = true;
|
|
@@ -23198,8 +23476,9 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23198
23476
|
});
|
|
23199
23477
|
// unrecoverable
|
|
23200
23478
|
recoverable = false;
|
|
23201
|
-
} else if (!(e instanceof SignalReconnectError)) {
|
|
23202
|
-
//
|
|
23479
|
+
} else if (fullReconnect || !(e instanceof SignalReconnectError)) {
|
|
23480
|
+
// a failed full reconnect stays a full reconnect; a failed resume can only be
|
|
23481
|
+
// resumed again for a signal-level error, otherwise it escalates
|
|
23203
23482
|
this.fullReconnectOnNext = true;
|
|
23204
23483
|
}
|
|
23205
23484
|
if (recoverable) {
|
|
@@ -23211,6 +23490,13 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23211
23490
|
}
|
|
23212
23491
|
} finally {
|
|
23213
23492
|
this.attemptingReconnect = false;
|
|
23493
|
+
// A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
|
|
23494
|
+
// during a resume or a restart) that a successful attempt didn't act on; dispatch it now
|
|
23495
|
+
// (the failure path already retries).
|
|
23496
|
+
if (succeeded && this.fullReconnectOnNext && !this._isClosed) {
|
|
23497
|
+
this.log.debug('full reconnect requested during in-progress attempt, dispatching');
|
|
23498
|
+
this.handleDisconnect('reconnect');
|
|
23499
|
+
}
|
|
23214
23500
|
}
|
|
23215
23501
|
});
|
|
23216
23502
|
}
|
|
@@ -23529,14 +23815,23 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23529
23815
|
if (!this.pcManager) {
|
|
23530
23816
|
return false;
|
|
23531
23817
|
}
|
|
23818
|
+
const state = this.pcManager.currentState;
|
|
23532
23819
|
const allowedConnectionStates = [PCTransportState.CONNECTING, PCTransportState.CONNECTED];
|
|
23533
|
-
if (!allowedConnectionStates.includes(
|
|
23820
|
+
if (!allowedConnectionStates.includes(state)) {
|
|
23534
23821
|
return false;
|
|
23535
23822
|
}
|
|
23536
23823
|
// ensure signal is connected
|
|
23537
23824
|
if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
|
|
23538
23825
|
return false;
|
|
23539
23826
|
}
|
|
23827
|
+
// A transport stuck in CONNECTING never reaches CONNECTED nor reports FAILED, so it would
|
|
23828
|
+
// otherwise look healthy forever; bound how long we tolerate it. The entry time is recorded
|
|
23829
|
+
// in the pcManager state-change handler (see configure()), so this is a pure read — an
|
|
23830
|
+
// unrecorded CONNECTING fails open rather than measuring against a stale timestamp.
|
|
23831
|
+
if (state === PCTransportState.CONNECTING && this.transportConnectingSince !== undefined && Date.now() - this.transportConnectingSince > this.peerConnectionTimeout) {
|
|
23832
|
+
this.log.warn('transport stuck in connecting state', this.logContext);
|
|
23833
|
+
return false;
|
|
23834
|
+
}
|
|
23540
23835
|
return true;
|
|
23541
23836
|
}
|
|
23542
23837
|
/** @internal */
|
|
@@ -24178,9 +24473,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24178
24473
|
// Suppress unhandled rejection on reader.closed — errors are
|
|
24179
24474
|
// already propagated through reader.read() to the consumer.
|
|
24180
24475
|
reader.closed.catch(() => {});
|
|
24181
|
-
const decoder = new TextDecoder('utf-8'
|
|
24182
|
-
fatal: true
|
|
24183
|
-
});
|
|
24476
|
+
const decoder = new TextDecoder('utf-8');
|
|
24184
24477
|
const signal = this.signal;
|
|
24185
24478
|
const cleanup = () => {
|
|
24186
24479
|
reader.releaseLock();
|
|
@@ -24658,9 +24951,7 @@ function bytesToChunks(streamId) {
|
|
|
24658
24951
|
* synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
|
|
24659
24952
|
*/
|
|
24660
24953
|
function bytesToDecodedUtf8(streamId) {
|
|
24661
|
-
const decoder = new TextDecoder('utf-8'
|
|
24662
|
-
fatal: true
|
|
24663
|
-
});
|
|
24954
|
+
const decoder = new TextDecoder('utf-8');
|
|
24664
24955
|
const encoder = new TextEncoder();
|
|
24665
24956
|
let outIndex = 0;
|
|
24666
24957
|
const decodeOrThrow = bytes => {
|
|
@@ -31475,6 +31766,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31475
31766
|
this.log = livekitLogger;
|
|
31476
31767
|
this.bufferedEvents = [];
|
|
31477
31768
|
this.isResuming = false;
|
|
31769
|
+
this.pendingTrackAddedCallbacks = new Map();
|
|
31478
31770
|
this.connect = (url, token, opts) => __awaiter(this, void 0, void 0, function* () {
|
|
31479
31771
|
var _a;
|
|
31480
31772
|
if (!isBrowserSupported()) {
|
|
@@ -31990,8 +32282,9 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31990
32282
|
return;
|
|
31991
32283
|
}
|
|
31992
32284
|
const newStreamState = Track.streamStateFromProto(streamState.state);
|
|
32285
|
+
const prevStreamState = pub.track.streamState;
|
|
31993
32286
|
pub.track.setStreamState(newStreamState);
|
|
31994
|
-
if (newStreamState !==
|
|
32287
|
+
if (newStreamState !== prevStreamState) {
|
|
31995
32288
|
participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
|
|
31996
32289
|
this.emitWhenConnected(RoomEvent.TrackStreamStateChanged, pub, pub.track.streamState, participant);
|
|
31997
32290
|
}
|
|
@@ -32009,6 +32302,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32009
32302
|
pub.setAllowed(update.allowed);
|
|
32010
32303
|
};
|
|
32011
32304
|
this.handleSubscriptionError = update => {
|
|
32305
|
+
this.cancelPendingTrackAdded(update.trackSid);
|
|
32012
32306
|
const participant = Array.from(this.remoteParticipants.values()).find(p => p.trackPublications.has(update.trackSid));
|
|
32013
32307
|
if (!participant) {
|
|
32014
32308
|
return;
|
|
@@ -32521,10 +32815,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32521
32815
|
}
|
|
32522
32816
|
this.emitBufferedEvents();
|
|
32523
32817
|
}).on(EngineEvent.SignalResumed, () => {
|
|
32524
|
-
this.bufferedEvents = [];
|
|
32525
32818
|
if (this.state === ConnectionState.Reconnecting || this.isResuming) {
|
|
32526
32819
|
this.sendSyncState();
|
|
32527
32820
|
}
|
|
32821
|
+
this.emitBufferedEvents();
|
|
32528
32822
|
}).on(EngineEvent.Restarting, this.handleRestarting).on(EngineEvent.Restarted, this.handleRestarted).on(EngineEvent.SignalRestarted, this.handleSignalRestarted).on(EngineEvent.Offline, () => {
|
|
32529
32823
|
if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
|
|
32530
32824
|
this.emit(RoomEvent.Reconnecting);
|
|
@@ -32941,29 +33235,43 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32941
33235
|
this.maybeCreateEngine();
|
|
32942
33236
|
}
|
|
32943
33237
|
onTrackAdded(mediaTrack, stream, receiver) {
|
|
33238
|
+
var _a, _b;
|
|
32944
33239
|
// don't fire onSubscribed when connecting
|
|
32945
33240
|
// WebRTC fires onTrack as soon as setRemoteDescription is called on the offer
|
|
32946
33241
|
// at that time, ICE connectivity has not been established so the track is not
|
|
32947
33242
|
// technically subscribed.
|
|
32948
33243
|
// We'll defer these events until when the room is connected or eventually disconnected.
|
|
32949
|
-
if (
|
|
33244
|
+
if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
|
|
33245
|
+
const pendingTrackSid = extractTrackSid(mediaTrack, stream);
|
|
33246
|
+
this.log.debug('deferring on track for later', {
|
|
33247
|
+
mediaTrackId: mediaTrack.id,
|
|
33248
|
+
mediaStreamId: stream.id,
|
|
33249
|
+
tracksInStream: stream.getTracks().map(track => track.id)
|
|
33250
|
+
});
|
|
32950
33251
|
const reconnectedHandler = () => {
|
|
32951
|
-
this.log.debug('deferring on track for later', {
|
|
32952
|
-
mediaTrackId: mediaTrack.id,
|
|
32953
|
-
mediaStreamId: stream.id,
|
|
32954
|
-
tracksInStream: stream.getTracks().map(track => track.id)
|
|
32955
|
-
});
|
|
32956
|
-
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
32957
33252
|
cleanup();
|
|
33253
|
+
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
32958
33254
|
};
|
|
32959
33255
|
const cleanup = () => {
|
|
32960
33256
|
this.off(RoomEvent.Reconnected, reconnectedHandler);
|
|
32961
33257
|
this.off(RoomEvent.Connected, reconnectedHandler);
|
|
32962
33258
|
this.off(RoomEvent.Disconnected, cleanup);
|
|
33259
|
+
if (pendingTrackSid) {
|
|
33260
|
+
const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
|
|
33261
|
+
pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.delete(cleanup);
|
|
33262
|
+
if ((pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.size) === 0) {
|
|
33263
|
+
this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
|
|
33264
|
+
}
|
|
33265
|
+
}
|
|
32963
33266
|
};
|
|
32964
33267
|
this.once(RoomEvent.Reconnected, reconnectedHandler);
|
|
32965
33268
|
this.once(RoomEvent.Connected, reconnectedHandler);
|
|
32966
33269
|
this.once(RoomEvent.Disconnected, cleanup);
|
|
33270
|
+
if (pendingTrackSid) {
|
|
33271
|
+
const pendingCallbacks = (_a = this.pendingTrackAddedCallbacks.get(pendingTrackSid)) !== null && _a !== void 0 ? _a : new Set();
|
|
33272
|
+
pendingCallbacks.add(cleanup);
|
|
33273
|
+
this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
|
|
33274
|
+
}
|
|
32967
33275
|
return;
|
|
32968
33276
|
}
|
|
32969
33277
|
if (this.state === ConnectionState.Disconnected) {
|
|
@@ -32976,11 +33284,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32976
33284
|
}
|
|
32977
33285
|
const parts = unpackStreamId(stream.id);
|
|
32978
33286
|
const participantSid = parts[0];
|
|
32979
|
-
|
|
32980
|
-
let trackId = mediaTrack.id;
|
|
33287
|
+
const streamId = parts[1];
|
|
32981
33288
|
// firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
|
|
32982
33289
|
// and generates its own track id instead of infer from sdp track id.
|
|
32983
|
-
|
|
33290
|
+
let trackId = (_b = extractTrackSid(mediaTrack, stream)) !== null && _b !== void 0 ? _b : mediaTrack.id;
|
|
32984
33291
|
if (participantSid === this.localParticipant.sid) {
|
|
32985
33292
|
this.log.warn('tried to create RemoteParticipant for local participant');
|
|
32986
33293
|
return;
|
|
@@ -33024,6 +33331,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33024
33331
|
this.emit(RoomEvent.EncryptionError, new Error("Encrypted ".concat(publication.source, " track received from participant ").concat(participant.sid, ", but room does not have encryption enabled!")));
|
|
33025
33332
|
}
|
|
33026
33333
|
}
|
|
33334
|
+
cancelPendingTrackAdded(trackSid) {
|
|
33335
|
+
var _a;
|
|
33336
|
+
(_a = this.pendingTrackAddedCallbacks.get(trackSid)) === null || _a === void 0 ? void 0 : _a.forEach(cleanup => cleanup());
|
|
33337
|
+
}
|
|
33027
33338
|
handleLocalTrackSubscribed(subscribedSid) {
|
|
33028
33339
|
const findPublication = () => this.localParticipant.getTrackPublications().find(_ref6 => {
|
|
33029
33340
|
let trackSid = _ref6.trackSid;
|
|
@@ -33287,6 +33598,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33287
33598
|
}
|
|
33288
33599
|
this.emitWhenConnected(RoomEvent.TrackSubscribed, track, publication, participant);
|
|
33289
33600
|
}).on(ParticipantEvent.TrackUnpublished, publication => {
|
|
33601
|
+
this.cancelPendingTrackAdded(publication.trackSid);
|
|
33290
33602
|
this.emit(RoomEvent.TrackUnpublished, publication, participant);
|
|
33291
33603
|
}).on(ParticipantEvent.TrackUnsubscribed, (track, publication) => {
|
|
33292
33604
|
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
|
|
@@ -33404,8 +33716,17 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33404
33716
|
} : undefined
|
|
33405
33717
|
});
|
|
33406
33718
|
if (consecutiveFailures >= 3) {
|
|
33407
|
-
this.
|
|
33408
|
-
this.
|
|
33719
|
+
this.clearConnectionReconcile();
|
|
33720
|
+
if (this.engine && !this.engine.isClosed) {
|
|
33721
|
+
// The transport silently died while we still looked connected. Try a full reconnect
|
|
33722
|
+
// (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
|
|
33723
|
+
this.log.warn('detected connection state mismatch, attempting full reconnect');
|
|
33724
|
+
this.engine.reconnect();
|
|
33725
|
+
} else {
|
|
33726
|
+
// No usable engine to reconnect with; tear down.
|
|
33727
|
+
this.recreateEngine();
|
|
33728
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.STATE_MISMATCH);
|
|
33729
|
+
}
|
|
33409
33730
|
}
|
|
33410
33731
|
} else {
|
|
33411
33732
|
consecutiveFailures = 0;
|
|
@@ -34802,24 +35123,26 @@ class TokenSourceEndpoint extends TokenSourceCached {
|
|
|
34802
35123
|
const body = yield response.json();
|
|
34803
35124
|
return TokenSourceResponse.fromJson(body, {
|
|
34804
35125
|
// NOTE: it could be possible that the response body could contain more fields than just
|
|
34805
|
-
// what's in TokenSourceResponse depending on the implementation (ie,
|
|
35126
|
+
// what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
|
|
34806
35127
|
ignoreUnknownFields: true
|
|
34807
35128
|
});
|
|
34808
35129
|
});
|
|
34809
35130
|
}
|
|
34810
35131
|
}
|
|
34811
|
-
class
|
|
34812
|
-
constructor(
|
|
35132
|
+
class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
|
|
35133
|
+
constructor(tokenServerId, options) {
|
|
34813
35134
|
const _options$baseUrl = options.baseUrl,
|
|
34814
35135
|
baseUrl = _options$baseUrl === void 0 ? 'https://cloud-api.livekit.io' : _options$baseUrl,
|
|
34815
35136
|
rest = __rest(options, ["baseUrl"]);
|
|
34816
35137
|
super("".concat(baseUrl, "/api/v2/sandbox/connection-details"), Object.assign(Object.assign({}, rest), {
|
|
34817
35138
|
headers: {
|
|
34818
|
-
'X-Sandbox-ID':
|
|
35139
|
+
'X-Sandbox-ID': tokenServerId
|
|
34819
35140
|
}
|
|
34820
35141
|
}));
|
|
34821
35142
|
}
|
|
34822
35143
|
}
|
|
35144
|
+
/** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
|
|
35145
|
+
class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
|
|
34823
35146
|
const TokenSource = {
|
|
34824
35147
|
/** TokenSource.literal contains a single, literal set of {@link TokenSourceResponseObject}
|
|
34825
35148
|
* credentials, either provided directly or returned from a provided function. */
|
|
@@ -34838,24 +35161,31 @@ const TokenSource = {
|
|
|
34838
35161
|
/**
|
|
34839
35162
|
* TokenSource.endpoint creates a token source that fetches credentials from a given URL using
|
|
34840
35163
|
* the standard endpoint format:
|
|
34841
|
-
* @see https://
|
|
35164
|
+
* @see https://docs.livekit.io/frontends/build/authentication/endpoint/
|
|
34842
35165
|
*/
|
|
34843
35166
|
endpoint(url) {
|
|
34844
35167
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
34845
35168
|
return new TokenSourceEndpoint(url, options);
|
|
34846
35169
|
},
|
|
34847
35170
|
/**
|
|
34848
|
-
*
|
|
35171
|
+
* @deprecated Use {@link TokenSource.developmentTokenServer} instead
|
|
35172
|
+
*/
|
|
35173
|
+
sandboxTokenServer(sandboxId) {
|
|
35174
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
35175
|
+
return new TokenSourceSandboxTokenServer(sandboxId, options);
|
|
35176
|
+
},
|
|
35177
|
+
/**
|
|
35178
|
+
* TokenSource.developmentTokenServer queries a development token server for credentials,
|
|
34849
35179
|
* which supports quick prototyping / getting started types of use cases.
|
|
34850
35180
|
*
|
|
34851
35181
|
* This token provider is INSECURE and should NOT be used in production.
|
|
34852
35182
|
*
|
|
34853
35183
|
* For more info:
|
|
34854
|
-
* @see https://
|
|
35184
|
+
* @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
|
|
34855
35185
|
*/
|
|
34856
|
-
|
|
35186
|
+
developmentTokenServer(tokenServerId) {
|
|
34857
35187
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
34858
|
-
return new
|
|
35188
|
+
return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
|
|
34859
35189
|
}
|
|
34860
35190
|
};/**
|
|
34861
35191
|
* Try to analyze the local track to determine the facing mode of a track.
|