livekit-client 2.21.0 → 2.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +189 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2243 -270
- 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 +189 -2
- 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 +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +51 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +27 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- 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/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.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 +10 -0
- 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/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +51 -3
- package/dist/ts4.2/room/RTCEngine.d.ts +27 -3
- package/dist/ts4.2/room/Room.d.ts +10 -1
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- 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/track/Track.d.ts +10 -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/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +19 -12
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +260 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +183 -81
- package/src/room/RTCEngine.test.ts +339 -2
- package/src/room/RTCEngine.ts +168 -18
- package/src/room/Room.test.ts +134 -3
- package/src/room/Room.ts +122 -22
- 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/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- 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 +29 -1
- package/src/room/utils.test.ts +94 -2
- package/src/room/utils.ts +53 -1
- 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
- package/src/utils/machineInspector.ts +90 -0
|
@@ -946,6 +946,9 @@ function _defineProperty(e, r, t) {
|
|
|
946
946
|
writable: true
|
|
947
947
|
}) : e[r] = t, e;
|
|
948
948
|
}
|
|
949
|
+
function _iterableToArray(r) {
|
|
950
|
+
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
|
|
951
|
+
}
|
|
949
952
|
function _iterableToArrayLimit(r, l) {
|
|
950
953
|
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
951
954
|
if (null != t) {
|
|
@@ -976,9 +979,53 @@ function _iterableToArrayLimit(r, l) {
|
|
|
976
979
|
function _nonIterableRest() {
|
|
977
980
|
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
978
981
|
}
|
|
982
|
+
function ownKeys(e, r) {
|
|
983
|
+
var t = Object.keys(e);
|
|
984
|
+
if (Object.getOwnPropertySymbols) {
|
|
985
|
+
var o = Object.getOwnPropertySymbols(e);
|
|
986
|
+
r && (o = o.filter(function (r) {
|
|
987
|
+
return Object.getOwnPropertyDescriptor(e, r).enumerable;
|
|
988
|
+
})), t.push.apply(t, o);
|
|
989
|
+
}
|
|
990
|
+
return t;
|
|
991
|
+
}
|
|
992
|
+
function _objectSpread2(e) {
|
|
993
|
+
for (var r = 1; r < arguments.length; r++) {
|
|
994
|
+
var t = null != arguments[r] ? arguments[r] : {};
|
|
995
|
+
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
|
|
996
|
+
_defineProperty(e, r, t[r]);
|
|
997
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
|
|
998
|
+
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
return e;
|
|
1002
|
+
}
|
|
1003
|
+
function _objectWithoutProperties(e, t) {
|
|
1004
|
+
if (null == e) return {};
|
|
1005
|
+
var o,
|
|
1006
|
+
r,
|
|
1007
|
+
i = _objectWithoutPropertiesLoose(e, t);
|
|
1008
|
+
if (Object.getOwnPropertySymbols) {
|
|
1009
|
+
var n = Object.getOwnPropertySymbols(e);
|
|
1010
|
+
for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
|
|
1011
|
+
}
|
|
1012
|
+
return i;
|
|
1013
|
+
}
|
|
1014
|
+
function _objectWithoutPropertiesLoose(r, e) {
|
|
1015
|
+
if (null == r) return {};
|
|
1016
|
+
var t = {};
|
|
1017
|
+
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
|
|
1018
|
+
if (-1 !== e.indexOf(n)) continue;
|
|
1019
|
+
t[n] = r[n];
|
|
1020
|
+
}
|
|
1021
|
+
return t;
|
|
1022
|
+
}
|
|
979
1023
|
function _slicedToArray(r, e) {
|
|
980
1024
|
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
|
|
981
1025
|
}
|
|
1026
|
+
function _toArray(r) {
|
|
1027
|
+
return _arrayWithHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableRest();
|
|
1028
|
+
}
|
|
982
1029
|
function _toPrimitive(t, r) {
|
|
983
1030
|
if ("object" != typeof t || !t) return t;
|
|
984
1031
|
var e = t[Symbol.toPrimitive];
|
|
@@ -8505,6 +8552,7 @@ var LoggerNames;
|
|
|
8505
8552
|
LoggerNames["DataTracks"] = "livekit-data-tracks";
|
|
8506
8553
|
LoggerNames["Region"] = "livekit-region";
|
|
8507
8554
|
LoggerNames["ICE"] = "livekit-ice";
|
|
8555
|
+
LoggerNames["Stats"] = "livekit-stats";
|
|
8508
8556
|
})(LoggerNames || (LoggerNames = {}));
|
|
8509
8557
|
let livekitLogger = loglevelExports.getLogger(LoggerNames.Default);
|
|
8510
8558
|
const livekitLoggers = Object.values(LoggerNames).map(name => loglevelExports.getLogger(name));
|
|
@@ -12160,7 +12208,7 @@ function getMatch(exp, ua) {
|
|
|
12160
12208
|
}
|
|
12161
12209
|
function getOSVersion(ua) {
|
|
12162
12210
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
|
12163
|
-
}var version$1 = "2.
|
|
12211
|
+
}var version$1 = "2.22.1";const version = version$1;
|
|
12164
12212
|
const protocolVersion = 17;
|
|
12165
12213
|
/** Initial client protocol. */
|
|
12166
12214
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
@@ -12446,7 +12494,7 @@ var RoomEvent;
|
|
|
12446
12494
|
* When a [[RemoteParticipant]] leaves *after* the local
|
|
12447
12495
|
* participant has joined.
|
|
12448
12496
|
*
|
|
12449
|
-
* args: ([[RemoteParticipant]])
|
|
12497
|
+
* args: ([[RemoteParticipant]], [[DisconnectReason]] | undefined)
|
|
12450
12498
|
*/
|
|
12451
12499
|
RoomEvent["ParticipantDisconnected"] = "participantDisconnected";
|
|
12452
12500
|
/**
|
|
@@ -13014,7 +13062,171 @@ var TrackEvent;
|
|
|
13014
13062
|
* @internal
|
|
13015
13063
|
*/
|
|
13016
13064
|
TrackEvent["PreConnectBufferFlushed"] = "preConnectBufferFlushed";
|
|
13017
|
-
})(TrackEvent || (TrackEvent = {}));function
|
|
13065
|
+
})(TrackEvent || (TrackEvent = {}));function compact(summary) {
|
|
13066
|
+
const compacted = {};
|
|
13067
|
+
for (const _ref of Object.entries(summary)) {
|
|
13068
|
+
var _ref2 = _slicedToArray(_ref, 2);
|
|
13069
|
+
const key = _ref2[0];
|
|
13070
|
+
const value = _ref2[1];
|
|
13071
|
+
if (value !== undefined) {
|
|
13072
|
+
compacted[key] = value;
|
|
13073
|
+
}
|
|
13074
|
+
}
|
|
13075
|
+
return compacted;
|
|
13076
|
+
}
|
|
13077
|
+
function resolution(width, height) {
|
|
13078
|
+
return width && height ? "".concat(width, "x").concat(height) : undefined;
|
|
13079
|
+
}
|
|
13080
|
+
/** keeps the derived durations readable; reported values are logged as they are */
|
|
13081
|
+
function round(seconds) {
|
|
13082
|
+
return Math.round(seconds * 10000) / 10000;
|
|
13083
|
+
}
|
|
13084
|
+
/**
|
|
13085
|
+
* Average time media spent in the jitter buffer, in s. Both counters are
|
|
13086
|
+
* cumulative, so this is the average over the lifetime of the stream.
|
|
13087
|
+
*/
|
|
13088
|
+
function jitterBuffer(stat) {
|
|
13089
|
+
const delay = stat.jitterBufferDelay;
|
|
13090
|
+
const emitted = stat.jitterBufferEmittedCount;
|
|
13091
|
+
return delay !== undefined && emitted ? round(delay / emitted) : undefined;
|
|
13092
|
+
}
|
|
13093
|
+
/**
|
|
13094
|
+
* Playout delay in s. Video receive reports it directly, the audio path keeps it
|
|
13095
|
+
* in the linked `media-playout` stats summed over the samples played out. Pairs
|
|
13096
|
+
* with `RemoteTrack.setPlayoutDelay`.
|
|
13097
|
+
*/
|
|
13098
|
+
function playoutDelay(stat, playout) {
|
|
13099
|
+
if (stat.playoutDelay !== undefined) {
|
|
13100
|
+
return round(stat.playoutDelay);
|
|
13101
|
+
}
|
|
13102
|
+
const total = playout === null || playout === void 0 ? void 0 : playout.totalPlayoutDelay;
|
|
13103
|
+
const samples = playout === null || playout === void 0 ? void 0 : playout.totalSamplesCount;
|
|
13104
|
+
return total !== undefined && samples ? round(total / samples) : undefined;
|
|
13105
|
+
}
|
|
13106
|
+
/**
|
|
13107
|
+
* Picks the interesting fields out of a `getStats()` report and groups them by
|
|
13108
|
+
* RTP stream, so a stats dump can be read without unfolding the raw report.
|
|
13109
|
+
*/
|
|
13110
|
+
function summarizeStatsReport(report) {
|
|
13111
|
+
var _a, _b;
|
|
13112
|
+
const byId = new Map();
|
|
13113
|
+
const candidatePairs = [];
|
|
13114
|
+
const inbound = [];
|
|
13115
|
+
const outbound = [];
|
|
13116
|
+
let transport;
|
|
13117
|
+
report.forEach(stat => byId.set(stat.id, stat));
|
|
13118
|
+
const codecOf = stat => {
|
|
13119
|
+
var _a;
|
|
13120
|
+
return stat.codecId ? (_a = byId.get(stat.codecId)) === null || _a === void 0 ? void 0 : _a.mimeType : undefined;
|
|
13121
|
+
};
|
|
13122
|
+
const relatedOf = (stat, key) => stat[key] ? byId.get(stat[key]) : undefined;
|
|
13123
|
+
report.forEach(stat => {
|
|
13124
|
+
switch (stat.type) {
|
|
13125
|
+
case 'inbound-rtp':
|
|
13126
|
+
{
|
|
13127
|
+
const playout = relatedOf(stat, 'playoutId');
|
|
13128
|
+
inbound.push(compact({
|
|
13129
|
+
kind: stat.kind,
|
|
13130
|
+
ssrc: stat.ssrc,
|
|
13131
|
+
mid: stat.mid,
|
|
13132
|
+
// matches `streamTrackID` in the track's own log context
|
|
13133
|
+
trackId: stat.trackIdentifier,
|
|
13134
|
+
codec: codecOf(stat),
|
|
13135
|
+
decoder: stat.decoderImplementation,
|
|
13136
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
13137
|
+
fps: stat.framesPerSecond,
|
|
13138
|
+
bytesReceived: stat.bytesReceived,
|
|
13139
|
+
packetsReceived: stat.packetsReceived,
|
|
13140
|
+
packetsLost: stat.packetsLost,
|
|
13141
|
+
packetsDiscarded: stat.packetsDiscarded,
|
|
13142
|
+
// frames received without frames decoded is a decode failure
|
|
13143
|
+
framesReceived: stat.framesReceived,
|
|
13144
|
+
framesDecoded: stat.framesDecoded,
|
|
13145
|
+
framesDropped: stat.framesDropped,
|
|
13146
|
+
keyFramesDecoded: stat.keyFramesDecoded,
|
|
13147
|
+
freezeCount: stat.freezeCount,
|
|
13148
|
+
totalFreezesDuration: stat.totalFreezesDuration,
|
|
13149
|
+
pauseCount: stat.pauseCount,
|
|
13150
|
+
nackCount: stat.nackCount,
|
|
13151
|
+
pliCount: stat.pliCount,
|
|
13152
|
+
firCount: stat.firCount,
|
|
13153
|
+
jitter: stat.jitter,
|
|
13154
|
+
jitterBuffer: jitterBuffer(stat),
|
|
13155
|
+
playoutDelay: playoutDelay(stat, playout),
|
|
13156
|
+
audioLevel: stat.audioLevel,
|
|
13157
|
+
totalSamplesReceived: stat.totalSamplesReceived,
|
|
13158
|
+
concealedSamples: stat.concealedSamples
|
|
13159
|
+
}));
|
|
13160
|
+
break;
|
|
13161
|
+
}
|
|
13162
|
+
case 'outbound-rtp':
|
|
13163
|
+
{
|
|
13164
|
+
const remote = relatedOf(stat, 'remoteId');
|
|
13165
|
+
const source = relatedOf(stat, 'mediaSourceId');
|
|
13166
|
+
outbound.push(compact({
|
|
13167
|
+
kind: stat.kind,
|
|
13168
|
+
ssrc: stat.ssrc,
|
|
13169
|
+
mid: stat.mid,
|
|
13170
|
+
rid: stat.rid,
|
|
13171
|
+
trackId: source === null || source === void 0 ? void 0 : source.trackIdentifier,
|
|
13172
|
+
active: stat.active,
|
|
13173
|
+
codec: codecOf(stat),
|
|
13174
|
+
encoder: stat.encoderImplementation,
|
|
13175
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
13176
|
+
fps: stat.framesPerSecond,
|
|
13177
|
+
// what the source produces, to tell a stalled capture from a stalled encoder
|
|
13178
|
+
captureResolution: resolution(source === null || source === void 0 ? void 0 : source.width, source === null || source === void 0 ? void 0 : source.height),
|
|
13179
|
+
captureFps: source === null || source === void 0 ? void 0 : source.framesPerSecond,
|
|
13180
|
+
audioLevel: source === null || source === void 0 ? void 0 : source.audioLevel,
|
|
13181
|
+
targetBitrate: stat.targetBitrate,
|
|
13182
|
+
bytesSent: stat.bytesSent,
|
|
13183
|
+
packetsSent: stat.packetsSent,
|
|
13184
|
+
retransmittedPacketsSent: stat.retransmittedPacketsSent,
|
|
13185
|
+
framesEncoded: stat.framesEncoded,
|
|
13186
|
+
keyFramesEncoded: stat.keyFramesEncoded,
|
|
13187
|
+
limitedBy: stat.qualityLimitationReason === 'none' ? undefined : stat.qualityLimitationReason,
|
|
13188
|
+
nackCount: stat.nackCount,
|
|
13189
|
+
pliCount: stat.pliCount,
|
|
13190
|
+
firCount: stat.firCount,
|
|
13191
|
+
// loss, jitter and RTT are only known from what the remote reports
|
|
13192
|
+
remotePacketsLost: remote === null || remote === void 0 ? void 0 : remote.packetsLost,
|
|
13193
|
+
remoteFractionLost: remote === null || remote === void 0 ? void 0 : remote.fractionLost,
|
|
13194
|
+
remoteJitter: remote === null || remote === void 0 ? void 0 : remote.jitter,
|
|
13195
|
+
remoteRoundTripTime: remote === null || remote === void 0 ? void 0 : remote.roundTripTime
|
|
13196
|
+
}));
|
|
13197
|
+
break;
|
|
13198
|
+
}
|
|
13199
|
+
case 'transport':
|
|
13200
|
+
transport = stat;
|
|
13201
|
+
break;
|
|
13202
|
+
case 'candidate-pair':
|
|
13203
|
+
candidatePairs.push(stat);
|
|
13204
|
+
break;
|
|
13205
|
+
}
|
|
13206
|
+
});
|
|
13207
|
+
const selectedPairId = transport === null || transport === void 0 ? void 0 : transport.selectedCandidatePairId;
|
|
13208
|
+
const pair = (_b = (_a = selectedPairId ? byId.get(selectedPairId) : undefined) !== null && _a !== void 0 ? _a : candidatePairs.find(candidate => candidate.selected)) !== null && _b !== void 0 ? _b : candidatePairs.find(candidate => candidate.nominated);
|
|
13209
|
+
const local = (pair === null || pair === void 0 ? void 0 : pair.localCandidateId) ? byId.get(pair.localCandidateId) : undefined;
|
|
13210
|
+
const remote = (pair === null || pair === void 0 ? void 0 : pair.remoteCandidateId) ? byId.get(pair.remoteCandidateId) : undefined;
|
|
13211
|
+
const connection = compact({
|
|
13212
|
+
ice: transport === null || transport === void 0 ? void 0 : transport.iceState,
|
|
13213
|
+
dtls: transport === null || transport === void 0 ? void 0 : transport.dtlsState,
|
|
13214
|
+
route: local && remote ? "".concat(local.candidateType, "/").concat(local.protocol, " -> ").concat(remote.candidateType) : undefined,
|
|
13215
|
+
network: local === null || local === void 0 ? void 0 : local.networkType,
|
|
13216
|
+
currentRoundTripTime: pair === null || pair === void 0 ? void 0 : pair.currentRoundTripTime,
|
|
13217
|
+
// the send bandwidth estimate; no RTP stream reports it
|
|
13218
|
+
availableOutgoingBitrate: pair === null || pair === void 0 ? void 0 : pair.availableOutgoingBitrate,
|
|
13219
|
+
availableIncomingBitrate: pair === null || pair === void 0 ? void 0 : pair.availableIncomingBitrate,
|
|
13220
|
+
bytesSent: pair === null || pair === void 0 ? void 0 : pair.bytesSent,
|
|
13221
|
+
bytesReceived: pair === null || pair === void 0 ? void 0 : pair.bytesReceived,
|
|
13222
|
+
candidatePairChanges: transport === null || transport === void 0 ? void 0 : transport.selectedCandidatePairChanges
|
|
13223
|
+
});
|
|
13224
|
+
return {
|
|
13225
|
+
connection: Object.keys(connection).length > 0 ? connection : undefined,
|
|
13226
|
+
outbound: outbound.length > 0 ? outbound : undefined,
|
|
13227
|
+
inbound: inbound.length > 0 ? inbound : undefined
|
|
13228
|
+
};
|
|
13229
|
+
}function cloneDeep$1(value) {
|
|
13018
13230
|
if (typeof value === 'undefined') {
|
|
13019
13231
|
return value;
|
|
13020
13232
|
}
|
|
@@ -13439,7 +13651,7 @@ function extractProcessorsFromOptions(options) {
|
|
|
13439
13651
|
return {
|
|
13440
13652
|
audioProcessor,
|
|
13441
13653
|
videoProcessor,
|
|
13442
|
-
optionsWithoutProcessor: cloneDeep(newOptions)
|
|
13654
|
+
optionsWithoutProcessor: cloneDeep$1(newOptions)
|
|
13443
13655
|
};
|
|
13444
13656
|
}
|
|
13445
13657
|
function getTrackSourceFromProto(source) {
|
|
@@ -13496,6 +13708,7 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13496
13708
|
this._streamState = Track.StreamState.Active;
|
|
13497
13709
|
this.isInBackground = false;
|
|
13498
13710
|
this._currentBitrate = 0;
|
|
13711
|
+
this.finalStatsLogged = false;
|
|
13499
13712
|
this.log = livekitLogger;
|
|
13500
13713
|
this.appVisibilityChangedListener = () => {
|
|
13501
13714
|
if (this.backgroundTimeout) {
|
|
@@ -13640,9 +13853,28 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13640
13853
|
if (this.monitorInterval) {
|
|
13641
13854
|
clearInterval(this.monitorInterval);
|
|
13642
13855
|
}
|
|
13643
|
-
if (this.timeSyncHandle) {
|
|
13856
|
+
if (this.timeSyncHandle !== undefined) {
|
|
13644
13857
|
cancelAnimationFrame(this.timeSyncHandle);
|
|
13858
|
+
this.timeSyncHandle = undefined;
|
|
13645
13859
|
}
|
|
13860
|
+
this.logFinalStats();
|
|
13861
|
+
}
|
|
13862
|
+
/**
|
|
13863
|
+
* Dumps the raw stats of the track as it ends, once: a track that stops
|
|
13864
|
+
* between two of the room's stats dumps is gone by the time the next one runs.
|
|
13865
|
+
*/
|
|
13866
|
+
logFinalStats() {
|
|
13867
|
+
if (this.finalStatsLogged) {
|
|
13868
|
+
return;
|
|
13869
|
+
}
|
|
13870
|
+
this.finalStatsLogged = true;
|
|
13871
|
+
this.getRTCStatsReport().then(report => {
|
|
13872
|
+
if (report) {
|
|
13873
|
+
this.log.info('final track stats', summarizeStatsReport(report));
|
|
13874
|
+
}
|
|
13875
|
+
}).catch(error => this.log.debug('could not collect final track stats', {
|
|
13876
|
+
error
|
|
13877
|
+
}));
|
|
13646
13878
|
}
|
|
13647
13879
|
/** @internal */
|
|
13648
13880
|
updateLoggerOptions(loggerOptions) {
|
|
@@ -13868,7 +14100,7 @@ function supportsAddTrack() {
|
|
|
13868
14100
|
return 'addTrack' in RTCPeerConnection.prototype;
|
|
13869
14101
|
}
|
|
13870
14102
|
function supportsAdaptiveStream() {
|
|
13871
|
-
return typeof ResizeObserver !== undefined && typeof IntersectionObserver !== undefined;
|
|
14103
|
+
return typeof ResizeObserver !== 'undefined' && typeof IntersectionObserver !== 'undefined';
|
|
13872
14104
|
}
|
|
13873
14105
|
function supportsDynacast() {
|
|
13874
14106
|
return supportsTransceiver();
|
|
@@ -13926,9 +14158,64 @@ function supportsVP9() {
|
|
|
13926
14158
|
}
|
|
13927
14159
|
return hasVP9;
|
|
13928
14160
|
}
|
|
14161
|
+
function supportsH265() {
|
|
14162
|
+
if (!('getCapabilities' in RTCRtpSender)) {
|
|
14163
|
+
return false;
|
|
14164
|
+
}
|
|
14165
|
+
const capabilities = RTCRtpSender.getCapabilities('video');
|
|
14166
|
+
let hasH265 = false;
|
|
14167
|
+
if (capabilities) {
|
|
14168
|
+
for (const codec of capabilities.codecs) {
|
|
14169
|
+
if (codec.mimeType.toLowerCase() === 'video/h265') {
|
|
14170
|
+
hasH265 = true;
|
|
14171
|
+
break;
|
|
14172
|
+
}
|
|
14173
|
+
}
|
|
14174
|
+
}
|
|
14175
|
+
return hasH265;
|
|
14176
|
+
}
|
|
13929
14177
|
function isSVCCodec(codec) {
|
|
13930
14178
|
return codec === 'av1' || codec === 'vp9';
|
|
13931
14179
|
}
|
|
14180
|
+
/**
|
|
14181
|
+
* Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
|
|
14182
|
+
*
|
|
14183
|
+
* Chrome only offers the extension on transceivers that can send, so one we create to receive on
|
|
14184
|
+
* never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
|
|
14185
|
+
* assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
|
|
14186
|
+
* transceiver rather than munging the extension into the SDP leaves the browser owning the
|
|
14187
|
+
* extension id, which is what keeps that id consistent across the bundle and across
|
|
14188
|
+
* renegotiations.
|
|
14189
|
+
*
|
|
14190
|
+
* A no-op where the browser offers no such control, or does not know the extension at all.
|
|
14191
|
+
* @internal
|
|
14192
|
+
*/
|
|
14193
|
+
function negotiateDependencyDescriptor(transceiver) {
|
|
14194
|
+
var _a;
|
|
14195
|
+
const extensions = (_a = transceiver.getHeaderExtensionsToNegotiate) === null || _a === void 0 ? void 0 : _a.call(transceiver);
|
|
14196
|
+
if (!extensions || !transceiver.setHeaderExtensionsToNegotiate) {
|
|
14197
|
+
return false;
|
|
14198
|
+
}
|
|
14199
|
+
const dd = extensions.find(ext => ext.uri === ddExtensionURI);
|
|
14200
|
+
if (!dd) {
|
|
14201
|
+
return false;
|
|
14202
|
+
}
|
|
14203
|
+
if (dd.direction !== 'stopped') {
|
|
14204
|
+
return true;
|
|
14205
|
+
}
|
|
14206
|
+
// sendrecv rather than recvonly, so that the extension is written as a plain `a=extmap` line —
|
|
14207
|
+
// the form the server already emits where it is the one offering — rather than one carrying a
|
|
14208
|
+
// `/recvonly` suffix that its parser may not expect
|
|
14209
|
+
dd.direction = 'sendrecv';
|
|
14210
|
+
try {
|
|
14211
|
+
transceiver.setHeaderExtensionsToNegotiate(extensions);
|
|
14212
|
+
return true;
|
|
14213
|
+
} catch (e) {
|
|
14214
|
+
// a rejected direction throws. Negotiating without the extension is what happened before this
|
|
14215
|
+
// existed, so it is not worth failing the connection over
|
|
14216
|
+
return false;
|
|
14217
|
+
}
|
|
14218
|
+
}
|
|
13932
14219
|
function supportsSetSinkId(elm) {
|
|
13933
14220
|
if (!document || isSafariBased()) {
|
|
13934
14221
|
return false;
|
|
@@ -14470,6 +14757,17 @@ function isCompressionStreamSupported() {
|
|
|
14470
14757
|
function isPublisherOfferWithJoinSupported() {
|
|
14471
14758
|
// we have connectivity issue about publisher offer with join on firefox #1919
|
|
14472
14759
|
return isCompressionStreamSupported() && !isFireFox();
|
|
14760
|
+
}
|
|
14761
|
+
function extractTrackSid(mediaTrack, stream) {
|
|
14762
|
+
const _unpackStreamId = unpackStreamId(stream.id),
|
|
14763
|
+
_unpackStreamId2 = _slicedToArray(_unpackStreamId, 2),
|
|
14764
|
+
streamId = _unpackStreamId2[1];
|
|
14765
|
+
if (streamId === null || streamId === void 0 ? void 0 : streamId.startsWith('TR')) {
|
|
14766
|
+
return streamId;
|
|
14767
|
+
}
|
|
14768
|
+
if (mediaTrack.id.startsWith('TR')) {
|
|
14769
|
+
return mediaTrack.id;
|
|
14770
|
+
}
|
|
14473
14771
|
}function createRtcUrl(url, searchParams) {
|
|
14474
14772
|
let useV0Path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
14475
14773
|
const v0Url = createV0RtcUrl(url, searchParams);
|
|
@@ -14509,6 +14807,11 @@ function parseSignalResponse(value) {
|
|
|
14509
14807
|
}
|
|
14510
14808
|
function getAbortReasonAsString(signal) {
|
|
14511
14809
|
let defaultMessage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Unknown reason';
|
|
14810
|
+
// the connect timeout hands this an Error rather than a signal, and its message is the only
|
|
14811
|
+
// description of what went wrong — without this a timeout reports itself as a generic abort
|
|
14812
|
+
if (signal instanceof Error) {
|
|
14813
|
+
return signal.message;
|
|
14814
|
+
}
|
|
14512
14815
|
if (!(signal instanceof AbortSignal)) {
|
|
14513
14816
|
return defaultMessage;
|
|
14514
14817
|
}
|
|
@@ -14968,6 +15271,34 @@ function computeBitrate(currentStats, prevStats) {
|
|
|
14968
15271
|
}class RemoteTrack extends Track {
|
|
14969
15272
|
constructor(mediaTrack, sid, kind, receiver, loggerOptions) {
|
|
14970
15273
|
super(mediaTrack, kind, loggerOptions);
|
|
15274
|
+
this.timeSyncLoop = () => {
|
|
15275
|
+
var _a;
|
|
15276
|
+
if (this.listenerCount(TrackEvent.TimeSyncUpdate) === 0) {
|
|
15277
|
+
// nobody is listening anymore, pause the loop until a new listener subscribes
|
|
15278
|
+
this.timeSyncHandle = undefined;
|
|
15279
|
+
return;
|
|
15280
|
+
}
|
|
15281
|
+
this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
|
|
15282
|
+
const sources = (_a = this.receiver) === null || _a === void 0 ? void 0 : _a.getSynchronizationSources()[0];
|
|
15283
|
+
if (sources) {
|
|
15284
|
+
const timestamp = sources.timestamp,
|
|
15285
|
+
rtpTimestamp = sources.rtpTimestamp;
|
|
15286
|
+
if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
|
|
15287
|
+
this.emit(TrackEvent.TimeSyncUpdate, {
|
|
15288
|
+
timestamp,
|
|
15289
|
+
rtpTimestamp
|
|
15290
|
+
});
|
|
15291
|
+
this.rtpTimestamp = rtpTimestamp;
|
|
15292
|
+
}
|
|
15293
|
+
}
|
|
15294
|
+
};
|
|
15295
|
+
this.onTimeSyncListenerAdded = event => {
|
|
15296
|
+
if (event === TrackEvent.TimeSyncUpdate && this.timeSyncHandle === undefined) {
|
|
15297
|
+
// `newListener` fires before the listener is registered, so schedule the
|
|
15298
|
+
// next frame instead of entering the loop (which would see a count of 0)
|
|
15299
|
+
this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
|
|
15300
|
+
}
|
|
15301
|
+
};
|
|
14971
15302
|
this.sid = sid;
|
|
14972
15303
|
this.receiver = receiver;
|
|
14973
15304
|
}
|
|
@@ -15065,24 +15396,19 @@ function computeBitrate(currentStats, prevStats) {
|
|
|
15065
15396
|
this.registerTimeSyncUpdate();
|
|
15066
15397
|
}
|
|
15067
15398
|
}
|
|
15399
|
+
/* @internal */
|
|
15400
|
+
stopMonitor() {
|
|
15401
|
+
super.stopMonitor();
|
|
15402
|
+
this.off('newListener', this.onTimeSyncListenerAdded);
|
|
15403
|
+
}
|
|
15068
15404
|
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();
|
|
15405
|
+
// `newListener` isn't part of the typed event map, hence the cast
|
|
15406
|
+
const emitter = this;
|
|
15407
|
+
emitter.off('newListener', this.onTimeSyncListenerAdded);
|
|
15408
|
+
emitter.on('newListener', this.onTimeSyncListenerAdded);
|
|
15409
|
+
if (this.timeSyncHandle === undefined) {
|
|
15410
|
+
this.timeSyncLoop();
|
|
15411
|
+
}
|
|
15086
15412
|
}
|
|
15087
15413
|
}const REACTION_DELAY = 100;
|
|
15088
15414
|
class RemoteVideoTrack extends RemoteTrack {
|
|
@@ -16546,6 +16872,1140 @@ class AsyncQueue {
|
|
|
16546
16872
|
snapshot() {
|
|
16547
16873
|
return Array.from(this.pendingTasks.values());
|
|
16548
16874
|
}
|
|
16875
|
+
}/**
|
|
16876
|
+
* Opt-in registry that lets a development tool observe the state machines driving the connection
|
|
16877
|
+
* layer.
|
|
16878
|
+
*
|
|
16879
|
+
* The machines are private to the objects that own them and, more importantly, do not outlive them:
|
|
16880
|
+
* a full reconnect replaces the whole engine along with its `SignalClient`, so anything watching a
|
|
16881
|
+
* single machine reference goes blind exactly when the interesting part starts. Machines therefore
|
|
16882
|
+
* announce themselves here as they are constructed, and a subscriber sees the whole succession.
|
|
16883
|
+
*
|
|
16884
|
+
* Nothing is recorded until {@link enableMachineInspector} is called, which no shipping code does —
|
|
16885
|
+
* with the registry off, announcing is a comparison and a return.
|
|
16886
|
+
*/
|
|
16887
|
+
/** Kept small: enough for a panel opened mid-session to see how the connection got where it is. */
|
|
16888
|
+
/** Announces a machine to whatever is watching. A no-op unless the inspector was enabled. */
|
|
16889
|
+
function announceMachine(label, machine) {
|
|
16890
|
+
{
|
|
16891
|
+
return;
|
|
16892
|
+
}
|
|
16893
|
+
}const _excluded = ["client"];
|
|
16894
|
+
//#region src/emitter.ts
|
|
16895
|
+
/**
|
|
16896
|
+
* Minimal typed event emitter used internally by Fsm and BehavioralFsm.
|
|
16897
|
+
*
|
|
16898
|
+
* Two listener categories exist at runtime: named listeners (keyed by event
|
|
16899
|
+
* name) and wildcard listeners (keyed as `"*"`). The wildcard fires on EVERY
|
|
16900
|
+
* emit, regardless of event name, and its callback receives both the event
|
|
16901
|
+
* name and the data. Named listeners receive only the data.
|
|
16902
|
+
*
|
|
16903
|
+
* @typeParam TEventMap - Record mapping event names to their payload types.
|
|
16904
|
+
* Constrains `on()` and `emit()` to matching event/payload pairs.
|
|
16905
|
+
*/
|
|
16906
|
+
var Emitter = class Emitter {
|
|
16907
|
+
constructor() {
|
|
16908
|
+
_defineProperty(this, "listeners", /* @__PURE__ */new Map());
|
|
16909
|
+
}
|
|
16910
|
+
on(event, cb) {
|
|
16911
|
+
let set = this.listeners.get(event);
|
|
16912
|
+
if (!set) {
|
|
16913
|
+
set = /* @__PURE__ */new Set();
|
|
16914
|
+
this.listeners.set(event, set);
|
|
16915
|
+
}
|
|
16916
|
+
set.add(cb);
|
|
16917
|
+
return {
|
|
16918
|
+
off: () => {
|
|
16919
|
+
set.delete(cb);
|
|
16920
|
+
}
|
|
16921
|
+
};
|
|
16922
|
+
}
|
|
16923
|
+
/**
|
|
16924
|
+
* Emit a named event, notifying wildcard listeners first, then named listeners.
|
|
16925
|
+
*
|
|
16926
|
+
* Wildcard listeners receive `(eventName, data)`; named listeners receive
|
|
16927
|
+
* only `data`. The firing order (wildcards before named) is intentional —
|
|
16928
|
+
* it lets relay listeners (like Fsm's bfsm proxy) observe all events
|
|
16929
|
+
* before specific subscribers react.
|
|
16930
|
+
*/
|
|
16931
|
+
emit(event, data) {
|
|
16932
|
+
const wildcards = this.listeners.get("*");
|
|
16933
|
+
if (wildcards) for (const cb of wildcards) cb(event, data);
|
|
16934
|
+
const named = this.listeners.get(event);
|
|
16935
|
+
if (named) for (const cb of named) cb(data);
|
|
16936
|
+
}
|
|
16937
|
+
/**
|
|
16938
|
+
* Remove all listeners (named and wildcard).
|
|
16939
|
+
*
|
|
16940
|
+
* Existing `Subscription` objects remain valid — their `off()` closures
|
|
16941
|
+
* hold a reference to the now-empty Set, so calling them is harmless.
|
|
16942
|
+
* Called by `dispose()` to prevent memory retention after an FSM shuts down.
|
|
16943
|
+
*/
|
|
16944
|
+
clear() {
|
|
16945
|
+
this.listeners.clear();
|
|
16946
|
+
}
|
|
16947
|
+
};
|
|
16948
|
+
|
|
16949
|
+
//#endregion
|
|
16950
|
+
//#region src/json-safe.ts
|
|
16951
|
+
/**
|
|
16952
|
+
* Thrown internally when the walk hits a value that can't survive a
|
|
16953
|
+
* serialization boundary. Callers catch this to attach FSM/input context
|
|
16954
|
+
* before re-throwing a fully descriptive error — see `path`/`label` below.
|
|
16955
|
+
*/
|
|
16956
|
+
var NonSerializableValueError = class extends Error {
|
|
16957
|
+
constructor(path, label) {
|
|
16958
|
+
super("non-serializable value at ".concat(path, " (").concat(label, ")"));
|
|
16959
|
+
this.path = path;
|
|
16960
|
+
this.label = label;
|
|
16961
|
+
}
|
|
16962
|
+
};
|
|
16963
|
+
/**
|
|
16964
|
+
* Deep-clones `value`, throwing `NonSerializableValueError` the moment it
|
|
16965
|
+
* finds anything that isn't `null`, a boolean, a finite number, a string, a
|
|
16966
|
+
* plain array, or a plain object. `rootPath` seeds the path used in error
|
|
16967
|
+
* messages — e.g. `"args"` so a nested failure reads as `args[1].onComplete`.
|
|
16968
|
+
*
|
|
16969
|
+
* Cloning (rather than just validating) is what keeps a returned snapshot
|
|
16970
|
+
* from aliasing live FSM state: mutating the snapshot afterward — or the
|
|
16971
|
+
* original value passed into a deferred `handle()` call — can't reach back
|
|
16972
|
+
* into the FSM's internal deferred queue.
|
|
16973
|
+
*/
|
|
16974
|
+
const cloneJsonSafe = (value, rootPath) => {
|
|
16975
|
+
return cloneNode(value, rootPath, /* @__PURE__ */new Set());
|
|
16976
|
+
};
|
|
16977
|
+
const cloneNode = (value, path, ancestors) => {
|
|
16978
|
+
if (value === null) return null;
|
|
16979
|
+
switch (typeof value) {
|
|
16980
|
+
case "string":
|
|
16981
|
+
case "boolean":
|
|
16982
|
+
return value;
|
|
16983
|
+
case "number":
|
|
16984
|
+
if (!Number.isFinite(value)) throw new NonSerializableValueError(path, describeNonFiniteNumber(value));
|
|
16985
|
+
return value;
|
|
16986
|
+
case "object":
|
|
16987
|
+
return cloneObject(value, path, ancestors);
|
|
16988
|
+
default:
|
|
16989
|
+
throw new NonSerializableValueError(path, typeof value);
|
|
16990
|
+
}
|
|
16991
|
+
};
|
|
16992
|
+
const cloneObject = (obj, path, ancestors) => {
|
|
16993
|
+
var _obj$constructor$name, _obj$constructor;
|
|
16994
|
+
if (ancestors.has(obj)) throw new NonSerializableValueError(path, "circular reference");
|
|
16995
|
+
if (Array.isArray(obj)) {
|
|
16996
|
+
if (obj.length !== Object.keys(obj).length) throw new NonSerializableValueError(path, "sparse array or array with non-index properties");
|
|
16997
|
+
ancestors.add(obj);
|
|
16998
|
+
const cloned = obj.map((item, i) => cloneNode(item, "".concat(path, "[").concat(i, "]"), ancestors));
|
|
16999
|
+
ancestors.delete(obj);
|
|
17000
|
+
return cloned;
|
|
17001
|
+
}
|
|
17002
|
+
const proto = Object.getPrototypeOf(obj);
|
|
17003
|
+
if (proto !== Object.prototype && proto !== null) throw new NonSerializableValueError(path, (_obj$constructor$name = (_obj$constructor = obj.constructor) === null || _obj$constructor === void 0 ? void 0 : _obj$constructor.name) !== null && _obj$constructor$name !== void 0 ? _obj$constructor$name : "object");
|
|
17004
|
+
ancestors.add(obj);
|
|
17005
|
+
const cloned = {};
|
|
17006
|
+
for (const key of Object.keys(obj)) {
|
|
17007
|
+
const value = cloneNode(obj[key], "".concat(path, ".").concat(key), ancestors);
|
|
17008
|
+
Object.defineProperty(cloned, key, {
|
|
17009
|
+
value,
|
|
17010
|
+
enumerable: true,
|
|
17011
|
+
writable: true,
|
|
17012
|
+
configurable: true
|
|
17013
|
+
});
|
|
17014
|
+
}
|
|
17015
|
+
ancestors.delete(obj);
|
|
17016
|
+
return cloned;
|
|
17017
|
+
};
|
|
17018
|
+
const describeNonFiniteNumber = n => {
|
|
17019
|
+
if (Number.isNaN(n)) return "NaN";
|
|
17020
|
+
return n > 0 ? "Infinity" : "-Infinity";
|
|
17021
|
+
};
|
|
17022
|
+
/**
|
|
17023
|
+
* Deep-clones `value` WITHOUT validating it — unlike `cloneJsonSafe`, this
|
|
17024
|
+
* never throws. Plain objects/arrays are cloned recursively (so a caller
|
|
17025
|
+
* can't alias state back into whatever holds the result); anything else
|
|
17026
|
+
* (functions, `Date`/`Map`/class instances, symbols, non-finite numbers,
|
|
17027
|
+
* etc.) is passed through by reference as-is.
|
|
17028
|
+
*
|
|
17029
|
+
* This is `rehydrate()`'s side of the aliasing guarantee: `dehydrate()`
|
|
17030
|
+
* validates-and-clones on the way OUT (via `cloneJsonSafe`), but `rehydrate()`
|
|
17031
|
+
* trusts the snapshot it's given is already valid data — re-validating on
|
|
17032
|
+
* the way IN would be a redundant, unwanted asymmetry (see the build plan's
|
|
17033
|
+
* disclosed known gaps). We still need the clone so mutating the caller's
|
|
17034
|
+
* snapshot object after `rehydrate()` returns can't reach into the live
|
|
17035
|
+
* FSM's internal deferred queue.
|
|
17036
|
+
*
|
|
17037
|
+
* Cycle-safe: a value that contains itself is returned as-is (by reference)
|
|
17038
|
+
* rather than cloned infinitely — there's no validation step here to make
|
|
17039
|
+
* that throw, so silently keeping the shared reference is the only option
|
|
17040
|
+
* that doesn't hang.
|
|
17041
|
+
*/
|
|
17042
|
+
const cloneDeep = function (value) {
|
|
17043
|
+
let ancestors = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : /* @__PURE__ */new Set();
|
|
17044
|
+
if (value === null || typeof value !== "object") return value;
|
|
17045
|
+
if (ancestors.has(value)) return value;
|
|
17046
|
+
if (Array.isArray(value)) {
|
|
17047
|
+
ancestors.add(value);
|
|
17048
|
+
const cloned = value.map(item => cloneDeep(item, ancestors));
|
|
17049
|
+
ancestors.delete(value);
|
|
17050
|
+
return cloned;
|
|
17051
|
+
}
|
|
17052
|
+
const proto = Object.getPrototypeOf(value);
|
|
17053
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
17054
|
+
ancestors.add(value);
|
|
17055
|
+
const cloned = {};
|
|
17056
|
+
for (const key of Object.keys(value)) {
|
|
17057
|
+
const clonedValue = cloneDeep(value[key], ancestors);
|
|
17058
|
+
Object.defineProperty(cloned, key, {
|
|
17059
|
+
value: clonedValue,
|
|
17060
|
+
enumerable: true,
|
|
17061
|
+
writable: true,
|
|
17062
|
+
configurable: true
|
|
17063
|
+
});
|
|
17064
|
+
}
|
|
17065
|
+
ancestors.delete(value);
|
|
17066
|
+
return cloned;
|
|
17067
|
+
};
|
|
17068
|
+
|
|
17069
|
+
//#endregion
|
|
17070
|
+
//#region src/types.ts
|
|
17071
|
+
/**
|
|
17072
|
+
* Symbol used as a property key to identify machina FSM instances at runtime.
|
|
17073
|
+
* Each class stamps itself with a MachinaType value so the ChildLink adapter
|
|
17074
|
+
* can dispatch handle()/canHandle()/reset() correctly without circular imports.
|
|
17075
|
+
*/
|
|
17076
|
+
const MACHINA_TYPE = Symbol("machina.type");
|
|
17077
|
+
|
|
17078
|
+
//#endregion
|
|
17079
|
+
//#region src/behavioral-fsm.ts
|
|
17080
|
+
const MAX_TRANSITION_DEPTH = 20;
|
|
17081
|
+
/**
|
|
17082
|
+
* Defines FSM behavior (states + transitions) while tracking per-client state
|
|
17083
|
+
* in a `WeakMap`. A single `BehavioralFsm` instance can drive any number of
|
|
17084
|
+
* independent client objects simultaneously — each gets its own state,
|
|
17085
|
+
* deferred queue, and lifecycle.
|
|
17086
|
+
*
|
|
17087
|
+
* Prefer `createBehavioralFsm()` over constructing this directly — the factory
|
|
17088
|
+
* infers all generic parameters from the config object.
|
|
17089
|
+
*
|
|
17090
|
+
* All public methods silently no-op after `dispose()` is called.
|
|
17091
|
+
*
|
|
17092
|
+
* @typeParam TClient - The client object type. Must be an object (non-primitive)
|
|
17093
|
+
* so it can serve as a WeakMap key.
|
|
17094
|
+
* @typeParam TStateNames - String literal union of valid state names.
|
|
17095
|
+
* @typeParam TInputNames - String literal union of valid input names.
|
|
17096
|
+
* @typeParam TBubbles - String literal union of inputs this FSM declares via
|
|
17097
|
+
* `bubbles`. Type-only — carried so `BubblesOfInstance` can extract it from
|
|
17098
|
+
* a constructed instance; nothing at runtime reads this generic.
|
|
17099
|
+
*/
|
|
17100
|
+
var BehavioralFsm = class BehavioralFsm {
|
|
17101
|
+
constructor(config) {
|
|
17102
|
+
_defineProperty(this, "id", void 0);
|
|
17103
|
+
_defineProperty(this, "initialState", void 0);
|
|
17104
|
+
_defineProperty(this, MACHINA_TYPE, "BehavioralFsm");
|
|
17105
|
+
_defineProperty(this, "states", void 0);
|
|
17106
|
+
_defineProperty(this, "emitter", new Emitter());
|
|
17107
|
+
_defineProperty(this, "clients", /* @__PURE__ */new WeakMap());
|
|
17108
|
+
_defineProperty(this, "knownClients", /* @__PURE__ */new Set());
|
|
17109
|
+
_defineProperty(this, "childSubscriptions", []);
|
|
17110
|
+
_defineProperty(this, "disposed", false);
|
|
17111
|
+
_defineProperty(this, "transitionDepth", 0);
|
|
17112
|
+
this.id = config.id;
|
|
17113
|
+
this.initialState = config.initialState;
|
|
17114
|
+
this.states = config.states;
|
|
17115
|
+
this.wrapChildLinks();
|
|
17116
|
+
this.setupChildSubscriptions();
|
|
17117
|
+
}
|
|
17118
|
+
/**
|
|
17119
|
+
* Dispatch an input to the given client's current state handler.
|
|
17120
|
+
*
|
|
17121
|
+
* Delegation order: if the current state has a `_child` FSM that can
|
|
17122
|
+
* handle the input, it is dispatched there. If the child emits `nohandler`,
|
|
17123
|
+
* the input bubbles up to this FSM's local handler. If no handler exists
|
|
17124
|
+
* here either, `nohandler` is emitted on this FSM's emitter.
|
|
17125
|
+
*
|
|
17126
|
+
* No-ops silently when disposed.
|
|
17127
|
+
*/
|
|
17128
|
+
handle(client, inputName) {
|
|
17129
|
+
var _this$states$meta$sta;
|
|
17130
|
+
if (this.disposed) return;
|
|
17131
|
+
const meta = this.getOrCreateClientMeta(client);
|
|
17132
|
+
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
|
|
17133
|
+
args[_key - 2] = arguments[_key];
|
|
17134
|
+
}
|
|
17135
|
+
meta.currentActionArgs = args;
|
|
17136
|
+
const childLink = (_this$states$meta$sta = this.states[meta.state]) === null || _this$states$meta$sta === void 0 ? void 0 : _this$states$meta$sta._child;
|
|
17137
|
+
if (childLink) {
|
|
17138
|
+
if (childLink.canHandle(client, inputName)) {
|
|
17139
|
+
try {
|
|
17140
|
+
childLink.handle(client, inputName, ...args);
|
|
17141
|
+
} finally {
|
|
17142
|
+
meta.currentActionArgs = void 0;
|
|
17143
|
+
}
|
|
17144
|
+
return;
|
|
17145
|
+
}
|
|
17146
|
+
}
|
|
17147
|
+
this.handleLocally(client, inputName, args, meta);
|
|
17148
|
+
}
|
|
17149
|
+
/**
|
|
17150
|
+
* Returns true if the client's current state has a handler for `inputName`
|
|
17151
|
+
* (or a catch-all `"*"` handler), or if the current state's `_child` chain
|
|
17152
|
+
* can handle it — the check recurses to the same depth `handle()`'s
|
|
17153
|
+
* delegation can actually reach, so a grandchild-only input answers true
|
|
17154
|
+
* from the root. Does NOT initialize the client — no `_onEnter`, no
|
|
17155
|
+
* events, no side effects. Unseen clients are treated as if they were
|
|
17156
|
+
* already in `initialState`. Returns false when disposed.
|
|
17157
|
+
*/
|
|
17158
|
+
canHandle(client, inputName) {
|
|
17159
|
+
var _this$clients$get$sta, _this$clients$get, _stateObj$inputName;
|
|
17160
|
+
if (this.disposed) return false;
|
|
17161
|
+
const state = (_this$clients$get$sta = (_this$clients$get = this.clients.get(client)) === null || _this$clients$get === void 0 ? void 0 : _this$clients$get.state) !== null && _this$clients$get$sta !== void 0 ? _this$clients$get$sta : this.initialState;
|
|
17162
|
+
const stateObj = this.states[state];
|
|
17163
|
+
if ((_stateObj$inputName = stateObj === null || stateObj === void 0 ? void 0 : stateObj[inputName]) !== null && _stateObj$inputName !== void 0 ? _stateObj$inputName : stateObj === null || stateObj === void 0 ? void 0 : stateObj["*"]) return true;
|
|
17164
|
+
const childLink = stateObj === null || stateObj === void 0 ? void 0 : stateObj._child;
|
|
17165
|
+
return childLink ? childLink.canHandle(client, inputName) : false;
|
|
17166
|
+
}
|
|
17167
|
+
/**
|
|
17168
|
+
* Transition the client back to `initialState`, firing `_onEnter` and
|
|
17169
|
+
* lifecycle events as if entering it fresh. No-ops when disposed.
|
|
17170
|
+
*/
|
|
17171
|
+
reset(client) {
|
|
17172
|
+
if (this.disposed) return;
|
|
17173
|
+
this.transition(client, this.initialState);
|
|
17174
|
+
}
|
|
17175
|
+
/**
|
|
17176
|
+
* Returns the client's current state, or `undefined` if the client has
|
|
17177
|
+
* never been initialized (i.e. `handle()`, `transition()`, or `reset()`
|
|
17178
|
+
* have never been called for it). Does NOT trigger initialization.
|
|
17179
|
+
*/
|
|
17180
|
+
currentState(client) {
|
|
17181
|
+
var _this$clients$get2;
|
|
17182
|
+
return (_this$clients$get2 = this.clients.get(client)) === null || _this$clients$get2 === void 0 ? void 0 : _this$clients$get2.state;
|
|
17183
|
+
}
|
|
17184
|
+
/**
|
|
17185
|
+
* Directly transition `client` to `toState`, running the full lifecycle:
|
|
17186
|
+
* `_onExit` for the current state → `transitioning` event → update state →
|
|
17187
|
+
* `_onEnter` for new state → `transitioned` event → child reset → deferred
|
|
17188
|
+
* queue replay → bounce (if `_onEnter` returned a state name).
|
|
17189
|
+
*
|
|
17190
|
+
* Same-state transitions are silently ignored. Transitions to unknown state
|
|
17191
|
+
* names emit `invalidstate` instead of throwing. Throws if the transition
|
|
17192
|
+
* depth exceeds `MAX_TRANSITION_DEPTH` (likely an `_onEnter` → transition loop).
|
|
17193
|
+
*
|
|
17194
|
+
* No-ops when disposed.
|
|
17195
|
+
*/
|
|
17196
|
+
transition(client, toState) {
|
|
17197
|
+
if (this.disposed) return;
|
|
17198
|
+
const meta = this.getOrCreateClientMeta(client);
|
|
17199
|
+
const fromState = meta.state;
|
|
17200
|
+
if (toState === fromState) return;
|
|
17201
|
+
if (!Object.hasOwn(this.states, toState)) {
|
|
17202
|
+
this.emitter.emit("invalidstate", {
|
|
17203
|
+
stateName: toState,
|
|
17204
|
+
client
|
|
17205
|
+
});
|
|
17206
|
+
return;
|
|
17207
|
+
}
|
|
17208
|
+
this.transitionDepth++;
|
|
17209
|
+
if (this.transitionDepth > MAX_TRANSITION_DEPTH) {
|
|
17210
|
+
this.transitionDepth = 0;
|
|
17211
|
+
throw new Error("Max transition depth (".concat(MAX_TRANSITION_DEPTH, ") exceeded in FSM \"").concat(this.id, "\". Likely an infinite _onEnter \u2192 transition loop."));
|
|
17212
|
+
}
|
|
17213
|
+
try {
|
|
17214
|
+
const curStateObj = this.states[fromState];
|
|
17215
|
+
const newStateObj = this.states[toState];
|
|
17216
|
+
if (curStateObj !== null && curStateObj !== void 0 && curStateObj._onExit && typeof curStateObj._onExit === "function") {
|
|
17217
|
+
const exitArgs = this.buildHandlerArgs(client, "", meta);
|
|
17218
|
+
curStateObj._onExit(exitArgs);
|
|
17219
|
+
}
|
|
17220
|
+
meta.state = toState;
|
|
17221
|
+
const payload = {
|
|
17222
|
+
fromState,
|
|
17223
|
+
toState,
|
|
17224
|
+
client
|
|
17225
|
+
};
|
|
17226
|
+
this.emitter.emit("transitioning", payload);
|
|
17227
|
+
let bounceTarget = void 0;
|
|
17228
|
+
if (newStateObj !== null && newStateObj !== void 0 && newStateObj._onEnter && typeof newStateObj._onEnter === "function") {
|
|
17229
|
+
const enterArgs = this.buildHandlerArgs(client, "", meta);
|
|
17230
|
+
bounceTarget = newStateObj._onEnter(enterArgs);
|
|
17231
|
+
}
|
|
17232
|
+
this.emitter.emit("transitioned", payload);
|
|
17233
|
+
const childLink = newStateObj === null || newStateObj === void 0 ? void 0 : newStateObj._child;
|
|
17234
|
+
if (childLink) childLink.reset(client);
|
|
17235
|
+
this.processQueue(client, meta);
|
|
17236
|
+
if (typeof bounceTarget === "string" && meta.state === toState) this.transition(client, bounceTarget);
|
|
17237
|
+
} finally {
|
|
17238
|
+
this.transitionDepth--;
|
|
17239
|
+
}
|
|
17240
|
+
}
|
|
17241
|
+
/**
|
|
17242
|
+
* Returns the client's state as a dot-delimited path including any active
|
|
17243
|
+
* child FSM states (e.g. `"active.connecting.retrying"`). Returns just the
|
|
17244
|
+
* current state name when no child is active. Returns `""` for clients that
|
|
17245
|
+
* have never been initialized (unlike `currentState()` which returns `undefined`).
|
|
17246
|
+
*/
|
|
17247
|
+
compositeState(client) {
|
|
17248
|
+
var _this$states$meta$sta2;
|
|
17249
|
+
const meta = this.clients.get(client);
|
|
17250
|
+
if (!meta) return "";
|
|
17251
|
+
const childLink = (_this$states$meta$sta2 = this.states[meta.state]) === null || _this$states$meta$sta2 === void 0 ? void 0 : _this$states$meta$sta2._child;
|
|
17252
|
+
if (childLink) {
|
|
17253
|
+
const childComposite = childLink.compositeState(client);
|
|
17254
|
+
if (childComposite) return "".concat(meta.state, ".").concat(childComposite);
|
|
17255
|
+
}
|
|
17256
|
+
return meta.state;
|
|
17257
|
+
}
|
|
17258
|
+
rehydrate(client, input) {
|
|
17259
|
+
if (this.disposed) return;
|
|
17260
|
+
if (typeof input === "string") {
|
|
17261
|
+
this.rehydrateCompositePath(client, input);
|
|
17262
|
+
return;
|
|
17263
|
+
}
|
|
17264
|
+
const writes = this.planSnapshotWrites(client, input);
|
|
17265
|
+
for (const write of writes) write();
|
|
17266
|
+
}
|
|
17267
|
+
/**
|
|
17268
|
+
* Snapshot everything machina tracks for `client`: current state, pending
|
|
17269
|
+
* deferred inputs, and — recursively — the same for every `_child` that has
|
|
17270
|
+
* ever seen this client, active or not. Feed the result to the object form
|
|
17271
|
+
* of `rehydrate()` to restore it later, deferrals included.
|
|
17272
|
+
*
|
|
17273
|
+
* Returns `undefined` for a client this FSM has never seen (mirrors
|
|
17274
|
+
* `currentState()`) — the call does NOT trigger initialization.
|
|
17275
|
+
*
|
|
17276
|
+
* Throws if any deferred input's args contain a non-serializable value
|
|
17277
|
+
* (function, undefined, symbol, bigint, non-finite number, Date/Map/class
|
|
17278
|
+
* instance, or a circular reference) — naming the input, its `until` target
|
|
17279
|
+
* if any, the FSM id, and the exact value path. Throws for an Fsm child
|
|
17280
|
+
* that's on `client`'s active path *relative to the true root* (consistent
|
|
17281
|
+
* with `rehydrate()`'s conditional throw) — an Fsm owns its own context, so
|
|
17282
|
+
* there's nothing per-client to snapshot. An Fsm child declared at a state
|
|
17283
|
+
* `client` never visited, OR nested under a `BehavioralFsm` child that is
|
|
17284
|
+
* itself off-path from the root, is skipped rather than throwing — neither
|
|
17285
|
+
* has any per-client state to lose, so one Fsm child anywhere in the
|
|
17286
|
+
* hierarchy doesn't disable `dehydrate()` for clients that never reach that
|
|
17287
|
+
* branch, no matter how deeply nested the Fsm child is.
|
|
17288
|
+
*
|
|
17289
|
+
* Meant for clients at rest between `handle()` calls — `currentActionArgs`
|
|
17290
|
+
* (the in-flight args mid-handler) has no meaning here and is excluded.
|
|
17291
|
+
*
|
|
17292
|
+
* @param isOnActivePath - @internal Whether this FSM itself is currently
|
|
17293
|
+
* reachable from the true root's active path. Defaults to `true` for the
|
|
17294
|
+
* public entry point (this FSM IS the root from its own perspective); the
|
|
17295
|
+
* `ChildLink` adapter passes `false` down when recursing into a nested
|
|
17296
|
+
* `BehavioralFsm` child that is itself off-path, so that child's own
|
|
17297
|
+
* Fsm-child checks don't recompute reachability from its dormant local
|
|
17298
|
+
* state alone.
|
|
17299
|
+
*/
|
|
17300
|
+
dehydrate(client) {
|
|
17301
|
+
let isOnActivePath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
17302
|
+
const meta = this.clients.get(client);
|
|
17303
|
+
if (!meta) return;
|
|
17304
|
+
const snapshot = {
|
|
17305
|
+
state: meta.state,
|
|
17306
|
+
deferred: meta.deferredQueue.map(item => this.snapshotDeferredInput(item))
|
|
17307
|
+
};
|
|
17308
|
+
const children = this.collectChildSnapshots(client, meta.state, isOnActivePath);
|
|
17309
|
+
if (children) snapshot.children = children;
|
|
17310
|
+
return snapshot;
|
|
17311
|
+
}
|
|
17312
|
+
/**
|
|
17313
|
+
* @internal
|
|
17314
|
+
* Validates `snapshot` against this level's state graph and recurses into
|
|
17315
|
+
* every declared child, returning write thunks to run only once the WHOLE
|
|
17316
|
+
* tree — every level — validates successfully. Nothing is written here.
|
|
17317
|
+
* Called by the object-form of `rehydrate()` and, recursively, by ChildLink
|
|
17318
|
+
* so a nested BehavioralFsm participates in the same validate-then-write
|
|
17319
|
+
* pass. Not part of the public persistence API — call `rehydrate()` instead.
|
|
17320
|
+
*/
|
|
17321
|
+
planSnapshotWrites(client, snapshot) {
|
|
17322
|
+
if (this.disposed) return [];
|
|
17323
|
+
const state = snapshot.state,
|
|
17324
|
+
deferred = snapshot.deferred,
|
|
17325
|
+
children = snapshot.children;
|
|
17326
|
+
if (!Object.hasOwn(this.states, state)) throw new Error("rehydrate: unknown state \"".concat(state, "\" in FSM \"").concat(this.id, "\". Valid states: ").concat(Object.keys(this.states).join(", ")));
|
|
17327
|
+
const writes = [];
|
|
17328
|
+
if (children) for (const stateName of Object.keys(children)) {
|
|
17329
|
+
var _this$states$stateNam;
|
|
17330
|
+
if (!Object.hasOwn(this.states, stateName)) throw new Error("rehydrate: unknown state \"".concat(stateName, "\" in FSM \"").concat(this.id, "\" referenced by snapshot.children."));
|
|
17331
|
+
const childLink = (_this$states$stateNam = this.states[stateName]) === null || _this$states$stateNam === void 0 ? void 0 : _this$states$stateNam._child;
|
|
17332
|
+
if (!childLink) throw new Error("rehydrate: state \"".concat(stateName, "\" in FSM \"").concat(this.id, "\" has no _child, but the snapshot has a children[\"").concat(stateName, "\"] entry."));
|
|
17333
|
+
writes.push(...childLink.planRehydrate(client, children[stateName]));
|
|
17334
|
+
}
|
|
17335
|
+
const deferredQueue = deferred.map(item => _objectSpread2({
|
|
17336
|
+
inputName: item.inputName,
|
|
17337
|
+
args: cloneDeep(item.args)
|
|
17338
|
+
}, item.untilState !== void 0 ? {
|
|
17339
|
+
untilState: item.untilState
|
|
17340
|
+
} : {}));
|
|
17341
|
+
writes.push(() => {
|
|
17342
|
+
if (!this.clients.has(client)) this.knownClients.add(new WeakRef(client));
|
|
17343
|
+
this.clients.set(client, {
|
|
17344
|
+
state,
|
|
17345
|
+
deferredQueue
|
|
17346
|
+
});
|
|
17347
|
+
});
|
|
17348
|
+
return writes;
|
|
17349
|
+
}
|
|
17350
|
+
rehydrateCompositePath(client, compositeState) {
|
|
17351
|
+
const _compositeState$split = compositeState.split("."),
|
|
17352
|
+
_compositeState$split2 = _toArray(_compositeState$split),
|
|
17353
|
+
state = _compositeState$split2[0],
|
|
17354
|
+
rest = _arrayLikeToArray(_compositeState$split2).slice(1);
|
|
17355
|
+
if (!Object.hasOwn(this.states, state)) throw new Error("rehydrate: unknown state \"".concat(state, "\" in FSM \"").concat(this.id, "\". Valid states: ").concat(Object.keys(this.states).join(", ")));
|
|
17356
|
+
if (rest.length > 0) {
|
|
17357
|
+
var _this$states$state;
|
|
17358
|
+
const childPath = rest.join(".");
|
|
17359
|
+
const childLink = (_this$states$state = this.states[state]) === null || _this$states$state === void 0 ? void 0 : _this$states$state._child;
|
|
17360
|
+
if (!childLink) throw new Error("rehydrate: state \"".concat(state, "\" in FSM \"").concat(this.id, "\" has no _child, but composite path \"").concat(compositeState, "\" requires one."));
|
|
17361
|
+
childLink.rehydrate(client, childPath);
|
|
17362
|
+
}
|
|
17363
|
+
if (!this.clients.has(client)) this.knownClients.add(new WeakRef(client));
|
|
17364
|
+
this.clients.set(client, {
|
|
17365
|
+
state,
|
|
17366
|
+
deferredQueue: []
|
|
17367
|
+
});
|
|
17368
|
+
}
|
|
17369
|
+
/**
|
|
17370
|
+
* Walks every declared state's `_child`, dehydrating each one that has ever
|
|
17371
|
+
* seen `client`. The same child instance can be declared under multiple
|
|
17372
|
+
* state names (shared child) — it's dehydrated once and the result is
|
|
17373
|
+
* reused under every declaring state name, matching how the engine already
|
|
17374
|
+
* treats shared children elsewhere (dispose, event subscriptions).
|
|
17375
|
+
*
|
|
17376
|
+
* Declaring names are grouped by `childLink.instance` (the actual FSM
|
|
17377
|
+
* instance, not the `ChildLink` wrapper — `wrapChildLinks()` mints a fresh
|
|
17378
|
+
* wrapper per declaring state, so grouping by wrapper would never hit for a
|
|
17379
|
+
* shared child) BEFORE any `onPath` is computed or any child is dehydrated.
|
|
17380
|
+
* A shared child's `onPath` is the OR of `stateName === activeState` across
|
|
17381
|
+
* every declaring name in its group: at most one declaring name can ever
|
|
17382
|
+
* equal `activeState`, so this combined flag is correct and, critically,
|
|
17383
|
+
* independent of `Object.keys(this.states)` iteration order — computing
|
|
17384
|
+
* `onPath` per-declaring-name and caching whichever one happened to run
|
|
17385
|
+
* first would silently launder an on-path client's Fsm-child throw through
|
|
17386
|
+
* an unrelated off-path declaring name.
|
|
17387
|
+
*
|
|
17388
|
+
* An off-path Fsm child (declared only at states other than `activeState`,
|
|
17389
|
+
* OR nested anywhere under a `BehavioralFsm` child that is itself off-path
|
|
17390
|
+
* relative to the true root) is skipped entirely rather than dehydrated:
|
|
17391
|
+
* Fsm state isn't tracked per-client to begin with, so an off-path Fsm
|
|
17392
|
+
* child has nothing to lose by being skipped — unlike a BehavioralFsm
|
|
17393
|
+
* child's off-path meta, which is real per-client data. `isOnActivePath`
|
|
17394
|
+
* is the inherited "am I even reachable from the root" flag; combining it
|
|
17395
|
+
* with the group's `stateNames.includes(activeState)` check (rather than
|
|
17396
|
+
* using that check alone) is what keeps a nested Fsm grandchild from
|
|
17397
|
+
* throwing when its immediate BehavioralFsm parent is itself off-path —
|
|
17398
|
+
* the parent's own dormant `activeState` is irrelevant once the parent
|
|
17399
|
+
* isn't reachable. This keeps one Fsm child anywhere in the hierarchy from
|
|
17400
|
+
* disabling `dehydrate()` for every client, only for clients actually on
|
|
17401
|
+
* that branch.
|
|
17402
|
+
*/
|
|
17403
|
+
collectChildSnapshots(client, activeState, isOnActivePath) {
|
|
17404
|
+
const declaringNamesByInstance = /* @__PURE__ */new Map();
|
|
17405
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17406
|
+
var _this$states$stateNam2;
|
|
17407
|
+
const childLink = (_this$states$stateNam2 = this.states[stateName]) === null || _this$states$stateNam2 === void 0 ? void 0 : _this$states$stateNam2._child;
|
|
17408
|
+
if (!childLink) continue;
|
|
17409
|
+
const entry = declaringNamesByInstance.get(childLink.instance);
|
|
17410
|
+
if (entry) entry.stateNames.push(stateName);else declaringNamesByInstance.set(childLink.instance, {
|
|
17411
|
+
childLink,
|
|
17412
|
+
stateNames: [stateName]
|
|
17413
|
+
});
|
|
17414
|
+
}
|
|
17415
|
+
let children;
|
|
17416
|
+
for (const _ref of declaringNamesByInstance.values()) {
|
|
17417
|
+
const childLink = _ref.childLink;
|
|
17418
|
+
const stateNames = _ref.stateNames;
|
|
17419
|
+
const onPath = isOnActivePath && stateNames.includes(activeState);
|
|
17420
|
+
if (childLink.instance[MACHINA_TYPE] === "Fsm" && !onPath) continue;
|
|
17421
|
+
const childSnapshot = childLink.dehydrate(client, onPath);
|
|
17422
|
+
if (childSnapshot) {
|
|
17423
|
+
children !== null && children !== void 0 ? children : children = {};
|
|
17424
|
+
for (const stateName of stateNames) children[stateName] = childSnapshot;
|
|
17425
|
+
}
|
|
17426
|
+
}
|
|
17427
|
+
return children;
|
|
17428
|
+
}
|
|
17429
|
+
snapshotDeferredInput(item) {
|
|
17430
|
+
let clonedArgs;
|
|
17431
|
+
try {
|
|
17432
|
+
clonedArgs = cloneJsonSafe(item.args, "args");
|
|
17433
|
+
} catch (err) {
|
|
17434
|
+
if (!(err instanceof NonSerializableValueError)) throw err;
|
|
17435
|
+
const untilPart = item.untilState ? " (until \"".concat(item.untilState, "\")") : "";
|
|
17436
|
+
throw new Error("dehydrate: deferred input \"".concat(item.inputName, "\"").concat(untilPart, " in FSM \"").concat(this.id, "\" has a non-serializable value at ").concat(err.path, " (").concat(err.label, ")"));
|
|
17437
|
+
}
|
|
17438
|
+
const snapshot = {
|
|
17439
|
+
inputName: item.inputName,
|
|
17440
|
+
args: clonedArgs
|
|
17441
|
+
};
|
|
17442
|
+
if (item.untilState !== void 0) snapshot.untilState = item.untilState;
|
|
17443
|
+
return snapshot;
|
|
17444
|
+
}
|
|
17445
|
+
on(eventName, callback) {
|
|
17446
|
+
if (this.disposed) return {
|
|
17447
|
+
off() {}
|
|
17448
|
+
};
|
|
17449
|
+
return this.emitter.on(eventName, callback);
|
|
17450
|
+
}
|
|
17451
|
+
/**
|
|
17452
|
+
* Emit a custom event through the FSM. Built-in lifecycle events are
|
|
17453
|
+
* emitted automatically — this is for user-defined events from handlers.
|
|
17454
|
+
* No-ops when disposed.
|
|
17455
|
+
*/
|
|
17456
|
+
emit(eventName, data) {
|
|
17457
|
+
if (this.disposed) return;
|
|
17458
|
+
this.emitter.emit(eventName, data);
|
|
17459
|
+
}
|
|
17460
|
+
/**
|
|
17461
|
+
* Permanently shut down this FSM. Irreversible — all subsequent method
|
|
17462
|
+
* calls become silent no-ops. Tears down child subscriptions, clears all
|
|
17463
|
+
* listeners, and cascades disposal to child FSMs (unless `preserveChildren`
|
|
17464
|
+
* is set). The same child appearing in multiple states is disposed once.
|
|
17465
|
+
*/
|
|
17466
|
+
dispose(options) {
|
|
17467
|
+
this.disposed = true;
|
|
17468
|
+
for (const sub of this.childSubscriptions) sub.off();
|
|
17469
|
+
if (!(options !== null && options !== void 0 && options.preserveChildren)) {
|
|
17470
|
+
const seen = /* @__PURE__ */new Set();
|
|
17471
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17472
|
+
var _this$states$stateNam3;
|
|
17473
|
+
const childLink = (_this$states$stateNam3 = this.states[stateName]) === null || _this$states$stateNam3 === void 0 ? void 0 : _this$states$stateNam3._child;
|
|
17474
|
+
if (childLink && !seen.has(childLink.instance)) {
|
|
17475
|
+
seen.add(childLink.instance);
|
|
17476
|
+
childLink.dispose();
|
|
17477
|
+
}
|
|
17478
|
+
}
|
|
17479
|
+
}
|
|
17480
|
+
this.emitter.clear();
|
|
17481
|
+
}
|
|
17482
|
+
/**
|
|
17483
|
+
* Walks all states at construction time, detects raw FSM instances assigned
|
|
17484
|
+
* to _child, and wraps them into ChildLink adapters via createChildLink().
|
|
17485
|
+
* Must run BEFORE setupChildSubscriptions() so the subscriptions see
|
|
17486
|
+
* ChildLink objects, not raw FSM instances.
|
|
17487
|
+
*/
|
|
17488
|
+
wrapChildLinks() {
|
|
17489
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17490
|
+
const stateObj = this.states[stateName];
|
|
17491
|
+
const rawChild = stateObj === null || stateObj === void 0 ? void 0 : stateObj._child;
|
|
17492
|
+
if (!rawChild) continue;
|
|
17493
|
+
if (typeof rawChild !== "object") throw new Error("State \"".concat(stateName, "\"._child: expected an Fsm or BehavioralFsm instance, got ").concat(String(rawChild)));
|
|
17494
|
+
if (!(MACHINA_TYPE in rawChild)) throw new Error("State \"".concat(stateName, "\"._child: expected an Fsm or BehavioralFsm instance, got a plain object"));
|
|
17495
|
+
stateObj._child = createChildLink(rawChild);
|
|
17496
|
+
}
|
|
17497
|
+
}
|
|
17498
|
+
/**
|
|
17499
|
+
* Walks all states at construction time, finds states with _child, and
|
|
17500
|
+
* subscribes once to each unique child's wildcard events. Subscriptions are
|
|
17501
|
+
* stored for cleanup in dispose(). We deduplicate by `childLink.instance`
|
|
17502
|
+
* (the underlying Fsm/BehavioralFsm instance), not the `ChildLink` wrapper —
|
|
17503
|
+
* `wrapChildLinks()` mints a fresh wrapper per declaring state, so a child
|
|
17504
|
+
* shared across states would otherwise get one subscription PER declaring
|
|
17505
|
+
* state, each independently walking known clients and relaying events. That
|
|
17506
|
+
* silently double-fires client-less relays (Fsm-child events, or a
|
|
17507
|
+
* BehavioralFsm child's custom `emit()` with no `client` in the payload)
|
|
17508
|
+
* whenever two different clients are active on two different declaring
|
|
17509
|
+
* names at once — the same wrapper-vs-instance identity bug
|
|
17510
|
+
* `collectChildSnapshots()` had before its #184 fix.
|
|
17511
|
+
*/
|
|
17512
|
+
setupChildSubscriptions() {
|
|
17513
|
+
const seenInstances = /* @__PURE__ */new Set();
|
|
17514
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17515
|
+
var _this$states$stateNam4;
|
|
17516
|
+
const childLink = (_this$states$stateNam4 = this.states[stateName]) === null || _this$states$stateNam4 === void 0 ? void 0 : _this$states$stateNam4._child;
|
|
17517
|
+
if (!childLink || seenInstances.has(childLink.instance)) continue;
|
|
17518
|
+
seenInstances.add(childLink.instance);
|
|
17519
|
+
const sub = childLink.onAny((eventName, data) => {
|
|
17520
|
+
if (eventName === "nohandler") {
|
|
17521
|
+
var _payload$args;
|
|
17522
|
+
const payload = data;
|
|
17523
|
+
if (payload.client !== void 0) this.bubbleNohandler(payload.client, childLink, payload.inputName, (_payload$args = payload.args) !== null && _payload$args !== void 0 ? _payload$args : []);else for (const ref of this.knownClients) {
|
|
17524
|
+
var _payload$args2;
|
|
17525
|
+
const client = ref.deref();
|
|
17526
|
+
if (client === void 0) {
|
|
17527
|
+
this.knownClients.delete(ref);
|
|
17528
|
+
continue;
|
|
17529
|
+
}
|
|
17530
|
+
this.bubbleNohandler(client, childLink, payload.inputName, (_payload$args2 = payload.args) !== null && _payload$args2 !== void 0 ? _payload$args2 : []);
|
|
17531
|
+
}
|
|
17532
|
+
return;
|
|
17533
|
+
}
|
|
17534
|
+
const payload = data;
|
|
17535
|
+
if (payload && typeof payload === "object" && "client" in payload) {
|
|
17536
|
+
if (this.isChildActiveForClient(payload.client, childLink)) this.emitter.emit(eventName, data);
|
|
17537
|
+
} else for (const ref of this.knownClients) {
|
|
17538
|
+
const client = ref.deref();
|
|
17539
|
+
if (!client) {
|
|
17540
|
+
this.knownClients.delete(ref);
|
|
17541
|
+
continue;
|
|
17542
|
+
}
|
|
17543
|
+
if (this.isChildActiveForClient(client, childLink)) {
|
|
17544
|
+
this.emitter.emit(eventName, data);
|
|
17545
|
+
break;
|
|
17546
|
+
}
|
|
17547
|
+
}
|
|
17548
|
+
});
|
|
17549
|
+
this.childSubscriptions.push(sub);
|
|
17550
|
+
}
|
|
17551
|
+
}
|
|
17552
|
+
/**
|
|
17553
|
+
* Bubbles a child nohandler to the parent for the given client.
|
|
17554
|
+
* Only fires if the client is currently in a state that has this childLink.
|
|
17555
|
+
* Extracted from the lambda in setupChildSubscriptions to keep it readable.
|
|
17556
|
+
*/
|
|
17557
|
+
bubbleNohandler(client, childLink, inputName, args) {
|
|
17558
|
+
if (!this.isChildActiveForClient(client, childLink)) return;
|
|
17559
|
+
const meta = this.clients.get(client);
|
|
17560
|
+
meta.currentActionArgs = args;
|
|
17561
|
+
this.handleLocally(client, inputName, args, meta);
|
|
17562
|
+
}
|
|
17563
|
+
/**
|
|
17564
|
+
* Returns true if the given client is currently in a parent state whose
|
|
17565
|
+
* _child resolves to the same underlying instance as childLink. Returns
|
|
17566
|
+
* false if the client has no meta (never initialized) or is in a state
|
|
17567
|
+
* with a different (or no) child.
|
|
17568
|
+
*
|
|
17569
|
+
* Compares `.instance`, not the `ChildLink` wrapper itself — setupChildSubscriptions()
|
|
17570
|
+
* dedupes subscriptions by instance and keeps only ONE representative wrapper
|
|
17571
|
+
* per shared child, so the client's actual active declaring state may hold a
|
|
17572
|
+
* DIFFERENT wrapper for that same instance (wrapChildLinks() mints one per
|
|
17573
|
+
* declaring state). Comparing wrappers directly would only ever match the one
|
|
17574
|
+
* declaring state whose wrapper happened to be kept for the subscription,
|
|
17575
|
+
* silently breaking relay for every other declaring name of a shared child.
|
|
17576
|
+
*/
|
|
17577
|
+
isChildActiveForClient(client, childLink) {
|
|
17578
|
+
var _this$states$meta$sta3;
|
|
17579
|
+
const meta = this.clients.get(client);
|
|
17580
|
+
if (!meta) return false;
|
|
17581
|
+
return ((_this$states$meta$sta3 = this.states[meta.state]) === null || _this$states$meta$sta3 === void 0 || (_this$states$meta$sta3 = _this$states$meta$sta3._child) === null || _this$states$meta$sta3 === void 0 ? void 0 : _this$states$meta$sta3.instance) === childLink.instance;
|
|
17582
|
+
}
|
|
17583
|
+
/**
|
|
17584
|
+
* The inner handler dispatch — no delegation, no initialization side effects
|
|
17585
|
+
* beyond what getOrCreateClientMeta already did. Called by handle() after
|
|
17586
|
+
* the delegation check, and by the nohandler child listener for bubbling.
|
|
17587
|
+
*/
|
|
17588
|
+
handleLocally(client, inputName, args, meta) {
|
|
17589
|
+
var _stateObj$inputName2;
|
|
17590
|
+
const stateObj = this.states[meta.state];
|
|
17591
|
+
const handler = (_stateObj$inputName2 = stateObj === null || stateObj === void 0 ? void 0 : stateObj[inputName]) !== null && _stateObj$inputName2 !== void 0 ? _stateObj$inputName2 : stateObj === null || stateObj === void 0 ? void 0 : stateObj["*"];
|
|
17592
|
+
if (!handler) {
|
|
17593
|
+
this.emitter.emit("nohandler", {
|
|
17594
|
+
inputName,
|
|
17595
|
+
args,
|
|
17596
|
+
client
|
|
17597
|
+
});
|
|
17598
|
+
meta.currentActionArgs = void 0;
|
|
17599
|
+
return;
|
|
17600
|
+
}
|
|
17601
|
+
try {
|
|
17602
|
+
this.emitter.emit("handling", {
|
|
17603
|
+
inputName,
|
|
17604
|
+
client
|
|
17605
|
+
});
|
|
17606
|
+
const handlerArgs = this.buildHandlerArgs(client, inputName, meta);
|
|
17607
|
+
let targetState = void 0;
|
|
17608
|
+
if (typeof handler === "string") targetState = handler;else if (typeof handler === "function") targetState = handler(handlerArgs, ...args);
|
|
17609
|
+
this.emitter.emit("handled", {
|
|
17610
|
+
inputName,
|
|
17611
|
+
client
|
|
17612
|
+
});
|
|
17613
|
+
if (typeof targetState === "string") this.transition(client, targetState);
|
|
17614
|
+
} finally {
|
|
17615
|
+
meta.currentActionArgs = void 0;
|
|
17616
|
+
}
|
|
17617
|
+
}
|
|
17618
|
+
getOrCreateClientMeta(client) {
|
|
17619
|
+
let meta = this.clients.get(client);
|
|
17620
|
+
if (meta) return meta;
|
|
17621
|
+
meta = {
|
|
17622
|
+
state: void 0,
|
|
17623
|
+
deferredQueue: []
|
|
17624
|
+
};
|
|
17625
|
+
this.clients.set(client, meta);
|
|
17626
|
+
this.knownClients.add(new WeakRef(client));
|
|
17627
|
+
this.transition(client, this.initialState);
|
|
17628
|
+
return meta;
|
|
17629
|
+
}
|
|
17630
|
+
buildHandlerArgs(client, inputName, meta) {
|
|
17631
|
+
return {
|
|
17632
|
+
ctx: client,
|
|
17633
|
+
inputName,
|
|
17634
|
+
defer: opts => {
|
|
17635
|
+
if (!meta.currentActionArgs) return;
|
|
17636
|
+
const deferred = {
|
|
17637
|
+
inputName,
|
|
17638
|
+
args: [...meta.currentActionArgs],
|
|
17639
|
+
untilState: opts === null || opts === void 0 ? void 0 : opts.until
|
|
17640
|
+
};
|
|
17641
|
+
meta.deferredQueue.push(deferred);
|
|
17642
|
+
this.emitter.emit("deferred", {
|
|
17643
|
+
inputName,
|
|
17644
|
+
client
|
|
17645
|
+
});
|
|
17646
|
+
},
|
|
17647
|
+
emit: (evtName, evtData) => {
|
|
17648
|
+
this.emitter.emit(evtName, evtData);
|
|
17649
|
+
}
|
|
17650
|
+
};
|
|
17651
|
+
}
|
|
17652
|
+
processQueue(client, meta) {
|
|
17653
|
+
const toReplay = [];
|
|
17654
|
+
const remaining = [];
|
|
17655
|
+
for (const item of meta.deferredQueue) if (item.untilState === void 0 || item.untilState === meta.state) toReplay.push(item);else remaining.push(item);
|
|
17656
|
+
meta.deferredQueue = remaining;
|
|
17657
|
+
for (const item of toReplay) this.handle(client, item.inputName, ...item.args);
|
|
17658
|
+
}
|
|
17659
|
+
};
|
|
17660
|
+
/**
|
|
17661
|
+
* Internal factory called by `wrapChildLinks()` during construction.
|
|
17662
|
+
* Wraps a raw Fsm or BehavioralFsm instance in a uniform `ChildLink` adapter
|
|
17663
|
+
* so the parent engine doesn't need to know which type it's talking to.
|
|
17664
|
+
*
|
|
17665
|
+
* Users never call this directly — they assign an FSM instance to `_child`
|
|
17666
|
+
* in their state config and `wrapChildLinks()` handles the wrapping.
|
|
17667
|
+
*/
|
|
17668
|
+
function createChildLink(child) {
|
|
17669
|
+
if (!child || typeof child !== "object") throw new Error("createChildLink: expected an Fsm or BehavioralFsm instance, got ".concat(String(child)));
|
|
17670
|
+
const childType = child[MACHINA_TYPE];
|
|
17671
|
+
if (childType === "BehavioralFsm") return {
|
|
17672
|
+
instance: child,
|
|
17673
|
+
canHandle(client, inputName) {
|
|
17674
|
+
return child.canHandle(client, inputName);
|
|
17675
|
+
},
|
|
17676
|
+
handle(client, inputName) {
|
|
17677
|
+
for (var _len2 = arguments.length, args = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
|
|
17678
|
+
args[_key2 - 2] = arguments[_key2];
|
|
17679
|
+
}
|
|
17680
|
+
child.handle(client, inputName, ...args);
|
|
17681
|
+
},
|
|
17682
|
+
reset(client) {
|
|
17683
|
+
child.transition(client, child.initialState);
|
|
17684
|
+
},
|
|
17685
|
+
onAny(callback) {
|
|
17686
|
+
return child.on("*", callback);
|
|
17687
|
+
},
|
|
17688
|
+
compositeState(client) {
|
|
17689
|
+
return child.compositeState(client);
|
|
17690
|
+
},
|
|
17691
|
+
rehydrate(client, compositeState) {
|
|
17692
|
+
child.rehydrate(client, compositeState);
|
|
17693
|
+
},
|
|
17694
|
+
dehydrate(client, isOnActivePath) {
|
|
17695
|
+
return child.dehydrate(client, isOnActivePath);
|
|
17696
|
+
},
|
|
17697
|
+
planRehydrate(client, snapshot) {
|
|
17698
|
+
return child.planSnapshotWrites(client, snapshot);
|
|
17699
|
+
},
|
|
17700
|
+
dispose() {
|
|
17701
|
+
child.dispose();
|
|
17702
|
+
}
|
|
17703
|
+
};
|
|
17704
|
+
if (childType === "Fsm") return {
|
|
17705
|
+
instance: child,
|
|
17706
|
+
canHandle(_client, inputName) {
|
|
17707
|
+
return child.canHandle(inputName);
|
|
17708
|
+
},
|
|
17709
|
+
handle(_client, inputName) {
|
|
17710
|
+
for (var _len3 = arguments.length, args = new Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) {
|
|
17711
|
+
args[_key3 - 2] = arguments[_key3];
|
|
17712
|
+
}
|
|
17713
|
+
child.handle(inputName, ...args);
|
|
17714
|
+
},
|
|
17715
|
+
reset(_client) {
|
|
17716
|
+
child.reset();
|
|
17717
|
+
},
|
|
17718
|
+
onAny(callback) {
|
|
17719
|
+
return child.on("*", callback);
|
|
17720
|
+
},
|
|
17721
|
+
compositeState(_client) {
|
|
17722
|
+
return child.compositeState();
|
|
17723
|
+
},
|
|
17724
|
+
rehydrate(_client, _compositeState) {
|
|
17725
|
+
throw new Error("rehydrate: cannot rehydrate an Fsm child. Fsm owns its own context; rehydrate is only valid for BehavioralFsm hierarchies.");
|
|
17726
|
+
},
|
|
17727
|
+
dehydrate(_client, _isOnActivePath) {
|
|
17728
|
+
throw new Error("dehydrate: cannot dehydrate an Fsm child. Fsm owns its own context; dehydrate is only valid for BehavioralFsm hierarchies.");
|
|
17729
|
+
},
|
|
17730
|
+
planRehydrate(_client, _snapshot) {
|
|
17731
|
+
throw new Error("rehydrate: cannot rehydrate an Fsm child. Fsm owns its own context; rehydrate is only valid for BehavioralFsm hierarchies.");
|
|
17732
|
+
},
|
|
17733
|
+
dispose() {
|
|
17734
|
+
child.dispose();
|
|
17735
|
+
}
|
|
17736
|
+
};
|
|
17737
|
+
throw new Error("createChildLink: expected an Fsm or BehavioralFsm instance, got [MACHINA_TYPE] = ".concat(String(childType !== null && childType !== void 0 ? childType : "undefined")));
|
|
17738
|
+
}
|
|
17739
|
+
|
|
17740
|
+
//#endregion
|
|
17741
|
+
//#region src/fsm.ts
|
|
17742
|
+
/**
|
|
17743
|
+
* Single-client FSM. Wraps a BehavioralFsm and uses the config's `context`
|
|
17744
|
+
* object as the implicit client, so callers never pass a client argument.
|
|
17745
|
+
*
|
|
17746
|
+
* Prefer `createFsm()` over constructing this directly — the factory infers
|
|
17747
|
+
* all generic parameters from the config object.
|
|
17748
|
+
*
|
|
17749
|
+
* All public methods silently no-op after `dispose()` is called.
|
|
17750
|
+
*
|
|
17751
|
+
* @typeParam TCtx - The context type, inferred from `config.context`.
|
|
17752
|
+
* @typeParam TStateNames - String literal union of valid state names.
|
|
17753
|
+
* @typeParam TInputNames - String literal union of valid input names.
|
|
17754
|
+
* @typeParam TBubbles - String literal union of inputs this FSM declares via
|
|
17755
|
+
* `bubbles`. Type-only — carried so `BubblesOfInstance` can extract it from
|
|
17756
|
+
* a constructed instance; nothing at runtime reads this generic.
|
|
17757
|
+
*/
|
|
17758
|
+
var Fsm = class Fsm {
|
|
17759
|
+
constructor(config) {
|
|
17760
|
+
var _config$context;
|
|
17761
|
+
_defineProperty(this, "id", void 0);
|
|
17762
|
+
_defineProperty(this, "initialState", void 0);
|
|
17763
|
+
_defineProperty(this, MACHINA_TYPE, "Fsm");
|
|
17764
|
+
_defineProperty(this, "states", void 0);
|
|
17765
|
+
_defineProperty(this, "bfsm", void 0);
|
|
17766
|
+
_defineProperty(this, "context", void 0);
|
|
17767
|
+
_defineProperty(this, "emitter", new Emitter());
|
|
17768
|
+
_defineProperty(this, "disposed", false);
|
|
17769
|
+
this.id = config.id;
|
|
17770
|
+
this.initialState = config.initialState;
|
|
17771
|
+
this.context = (_config$context = config.context) !== null && _config$context !== void 0 ? _config$context : {};
|
|
17772
|
+
this.bfsm = new BehavioralFsm(config);
|
|
17773
|
+
this.states = config.states;
|
|
17774
|
+
this.bfsm.on("*", (eventName, data) => {
|
|
17775
|
+
if (data && typeof data === "object" && "client" in data) {
|
|
17776
|
+
data.client;
|
|
17777
|
+
const payload = _objectWithoutProperties(data, _excluded);
|
|
17778
|
+
this.emitter.emit(eventName, payload);
|
|
17779
|
+
} else this.emitter.emit(eventName, data);
|
|
17780
|
+
});
|
|
17781
|
+
this.bfsm.transition(this.context, config.initialState);
|
|
17782
|
+
}
|
|
17783
|
+
/**
|
|
17784
|
+
* Dispatch an input to the current state's handler.
|
|
17785
|
+
* If a `_child` FSM in the current state can handle it, delegation occurs
|
|
17786
|
+
* there first; unhandled inputs bubble up to the parent.
|
|
17787
|
+
* No-ops silently when disposed.
|
|
17788
|
+
*/
|
|
17789
|
+
handle(inputName) {
|
|
17790
|
+
if (this.disposed) return;
|
|
17791
|
+
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
|
|
17792
|
+
args[_key4 - 1] = arguments[_key4];
|
|
17793
|
+
}
|
|
17794
|
+
this.bfsm.handle(this.context, inputName, ...args);
|
|
17795
|
+
}
|
|
17796
|
+
/**
|
|
17797
|
+
* Returns true if the current state has a handler for `inputName`
|
|
17798
|
+
* (or a catch-all `"*"` handler), or if the current state's `_child`
|
|
17799
|
+
* chain can handle it (checked recursively, matching `handle()`'s
|
|
17800
|
+
* delegation reach). Does not trigger initialization or any side
|
|
17801
|
+
* effects. Returns false when disposed.
|
|
17802
|
+
*/
|
|
17803
|
+
canHandle(inputName) {
|
|
17804
|
+
if (this.disposed) return false;
|
|
17805
|
+
return this.bfsm.canHandle(this.context, inputName);
|
|
17806
|
+
}
|
|
17807
|
+
/**
|
|
17808
|
+
* Transition back to `initialState`, firing `_onEnter` and lifecycle
|
|
17809
|
+
* events as if entering it fresh. No-ops silently when disposed.
|
|
17810
|
+
*/
|
|
17811
|
+
reset() {
|
|
17812
|
+
if (this.disposed) return;
|
|
17813
|
+
this.bfsm.reset(this.context);
|
|
17814
|
+
}
|
|
17815
|
+
/**
|
|
17816
|
+
* Returns the current state name. Always defined — Fsm eagerly
|
|
17817
|
+
* initializes into `initialState` during construction.
|
|
17818
|
+
*/
|
|
17819
|
+
currentState() {
|
|
17820
|
+
return this.bfsm.currentState(this.context);
|
|
17821
|
+
}
|
|
17822
|
+
/**
|
|
17823
|
+
* Directly transition to `toState`, firing `_onExit`, `_onEnter`, and
|
|
17824
|
+
* lifecycle events. Same-state transitions are silently ignored.
|
|
17825
|
+
* No-ops when disposed.
|
|
17826
|
+
*/
|
|
17827
|
+
transition(toState) {
|
|
17828
|
+
if (this.disposed) return;
|
|
17829
|
+
this.bfsm.transition(this.context, toState);
|
|
17830
|
+
}
|
|
17831
|
+
/**
|
|
17832
|
+
* Returns the current state as a dot-delimited path that includes
|
|
17833
|
+
* any active child FSM states (e.g. `"active.connecting.retrying"`).
|
|
17834
|
+
* Returns just the current state name when no child is active.
|
|
17835
|
+
*/
|
|
17836
|
+
compositeState() {
|
|
17837
|
+
return this.bfsm.compositeState(this.context);
|
|
17838
|
+
}
|
|
17839
|
+
on(eventName, callback) {
|
|
17840
|
+
if (this.disposed) return {
|
|
17841
|
+
off() {}
|
|
17842
|
+
};
|
|
17843
|
+
return this.emitter.on(eventName, callback);
|
|
17844
|
+
}
|
|
17845
|
+
/**
|
|
17846
|
+
* Emit a custom event through the FSM. Built-in lifecycle events are
|
|
17847
|
+
* emitted automatically — this is for user-defined events from handlers.
|
|
17848
|
+
* Routes through the BehavioralFsm so all relay paths are consistent.
|
|
17849
|
+
* No-ops when disposed.
|
|
17850
|
+
*/
|
|
17851
|
+
emit(eventName, data) {
|
|
17852
|
+
if (this.disposed) return;
|
|
17853
|
+
this.bfsm.emit(eventName, data);
|
|
17854
|
+
}
|
|
17855
|
+
/**
|
|
17856
|
+
* Permanently shut down this FSM. Irreversible — all subsequent method
|
|
17857
|
+
* calls become silent no-ops. Clears all listeners and cascades disposal
|
|
17858
|
+
* to child FSMs (unless `preserveChildren` is set).
|
|
17859
|
+
*/
|
|
17860
|
+
dispose(options) {
|
|
17861
|
+
this.disposed = true;
|
|
17862
|
+
this.bfsm.dispose(options);
|
|
17863
|
+
this.emitter.clear();
|
|
17864
|
+
}
|
|
17865
|
+
};
|
|
17866
|
+
/**
|
|
17867
|
+
* Create a single-client FSM from a config object.
|
|
17868
|
+
*
|
|
17869
|
+
* Generic parameters are inferred automatically:
|
|
17870
|
+
* - `TCtx` comes from `config.context` (defaults to `{}` if omitted).
|
|
17871
|
+
* - `TStates` is captured with `const` inference to preserve string literal
|
|
17872
|
+
* types, enabling compile-time validation of transition targets and `handle()`
|
|
17873
|
+
* input names.
|
|
17874
|
+
*
|
|
17875
|
+
* State names, input names, and all handler signatures derive from `TStates`.
|
|
17876
|
+
*
|
|
17877
|
+
* @example
|
|
17878
|
+
* ```ts
|
|
17879
|
+
* const light = createFsm({
|
|
17880
|
+
* id: "traffic-light",
|
|
17881
|
+
* initialState: "green",
|
|
17882
|
+
* context: { tickCount: 0 },
|
|
17883
|
+
* states: {
|
|
17884
|
+
* green: { timeout: "yellow" },
|
|
17885
|
+
* yellow: { timeout: "red" },
|
|
17886
|
+
* red: { timeout: "green" },
|
|
17887
|
+
* },
|
|
17888
|
+
* });
|
|
17889
|
+
*
|
|
17890
|
+
* light.handle("timeout"); // transitions green → yellow
|
|
17891
|
+
* ```
|
|
17892
|
+
*/
|
|
17893
|
+
function createFsm(config) {
|
|
17894
|
+
return new Fsm(config);
|
|
17895
|
+
}/**
|
|
17896
|
+
* Declares a handler for one input, restoring the payload typing that machina's `...unknown[]`
|
|
17897
|
+
* handler arguments give up.
|
|
17898
|
+
*/
|
|
17899
|
+
function on(handler) {
|
|
17900
|
+
return handler;
|
|
17901
|
+
}
|
|
17902
|
+
/**
|
|
17903
|
+
* Whether a transport-originated input belongs to the attempt that currently owns the session.
|
|
17904
|
+
* Inputs from a superseded attempt are dropped: its transport is already being replaced, so it can
|
|
17905
|
+
* neither declare the session live nor take it down.
|
|
17906
|
+
*/
|
|
17907
|
+
function isCurrentAttempt(ctx, event) {
|
|
17908
|
+
return event.attemptId === ctx.attemptId;
|
|
17909
|
+
}
|
|
17910
|
+
const attemptEstablished = on((_ref, event) => {
|
|
17911
|
+
let ctx = _ref.ctx;
|
|
17912
|
+
if (!isCurrentAttempt(ctx, event)) {
|
|
17913
|
+
return;
|
|
17914
|
+
}
|
|
17915
|
+
return 'connected';
|
|
17916
|
+
});
|
|
17917
|
+
const startConnect = on(_ref2 => {
|
|
17918
|
+
let ctx = _ref2.ctx;
|
|
17919
|
+
ctx.attemptId += 1;
|
|
17920
|
+
ctx.lastError = undefined;
|
|
17921
|
+
return 'connecting';
|
|
17922
|
+
});
|
|
17923
|
+
const startReconnect = on(_ref3 => {
|
|
17924
|
+
let ctx = _ref3.ctx;
|
|
17925
|
+
ctx.attemptId += 1;
|
|
17926
|
+
ctx.lastError = undefined;
|
|
17927
|
+
return 'reconnecting';
|
|
17928
|
+
});
|
|
17929
|
+
const requestClose = on((_ref4, event) => {
|
|
17930
|
+
let ctx = _ref4.ctx;
|
|
17931
|
+
ctx.closeReason = event.reason;
|
|
17932
|
+
return 'disconnecting';
|
|
17933
|
+
});
|
|
17934
|
+
const signalStates = {
|
|
17935
|
+
new: {
|
|
17936
|
+
connect: startConnect,
|
|
17937
|
+
close: requestClose
|
|
17938
|
+
},
|
|
17939
|
+
connecting: {
|
|
17940
|
+
connectComplete: attemptEstablished,
|
|
17941
|
+
// An initial connect has no session to fall back on, so failure is terminal.
|
|
17942
|
+
connectFailed: on((_ref5, event) => {
|
|
17943
|
+
let ctx = _ref5.ctx;
|
|
17944
|
+
ctx.lastError = event.error;
|
|
17945
|
+
return 'closed';
|
|
17946
|
+
}),
|
|
17947
|
+
close: requestClose
|
|
17948
|
+
},
|
|
17949
|
+
connected: {
|
|
17950
|
+
reconnect: startReconnect,
|
|
17951
|
+
transportFailed: on((_ref6, event) => {
|
|
17952
|
+
let ctx = _ref6.ctx;
|
|
17953
|
+
if (!isCurrentAttempt(ctx, event)) {
|
|
17954
|
+
// a transport that has already been replaced, reporting its close late
|
|
17955
|
+
return;
|
|
17956
|
+
}
|
|
17957
|
+
ctx.lastError = event.reason;
|
|
17958
|
+
return 'offline';
|
|
17959
|
+
}),
|
|
17960
|
+
close: requestClose
|
|
17961
|
+
},
|
|
17962
|
+
offline: {
|
|
17963
|
+
connect: startConnect,
|
|
17964
|
+
reconnect: startReconnect,
|
|
17965
|
+
close: requestClose
|
|
17966
|
+
},
|
|
17967
|
+
reconnecting: {
|
|
17968
|
+
reconnectComplete: attemptEstablished,
|
|
17969
|
+
reconnectFailed: on((_ref7, event) => {
|
|
17970
|
+
let ctx = _ref7.ctx;
|
|
17971
|
+
ctx.lastError = event.error;
|
|
17972
|
+
return event.recoverable ? 'offline' : 'closed';
|
|
17973
|
+
}),
|
|
17974
|
+
close: requestClose
|
|
17975
|
+
},
|
|
17976
|
+
// Owns the transport until the close handshake settles. Every path into this state is followed
|
|
17977
|
+
// by a `closeComplete`, so it cannot become a trap.
|
|
17978
|
+
disconnecting: {
|
|
17979
|
+
closeComplete: 'closed'
|
|
17980
|
+
},
|
|
17981
|
+
// No transport, but the session may still be resumable: the engine resumes after an unexpected
|
|
17982
|
+
// close just as it does from `offline`.
|
|
17983
|
+
closed: {
|
|
17984
|
+
connect: startConnect,
|
|
17985
|
+
reconnect: startReconnect
|
|
17986
|
+
}
|
|
17987
|
+
};
|
|
17988
|
+
/**
|
|
17989
|
+
* Lifecycle model of the signal connection.
|
|
17990
|
+
*
|
|
17991
|
+
* The machine deliberately does not own connection attempts: `SignalClient` performs the
|
|
17992
|
+
* asynchronous work and reports the outcome. It also does not decide *whether* to reconnect —
|
|
17993
|
+
* that policy (backoff, resume vs. full reconnect, region failover, giving up) belongs to
|
|
17994
|
+
* `RTCEngine`, so transport loss lands in `offline` rather than starting a reconnect on its own.
|
|
17995
|
+
*
|
|
17996
|
+
* Each client gets its own instance: the context is mutable and per-connection.
|
|
17997
|
+
*/
|
|
17998
|
+
function createSignalMachine() {
|
|
17999
|
+
let initialState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'new';
|
|
18000
|
+
const context = {
|
|
18001
|
+
attemptId: 0
|
|
18002
|
+
};
|
|
18003
|
+
return createFsm({
|
|
18004
|
+
id: 'signal',
|
|
18005
|
+
initialState: initialState,
|
|
18006
|
+
context,
|
|
18007
|
+
states: signalStates
|
|
18008
|
+
});
|
|
16549
18009
|
}/**
|
|
16550
18010
|
* [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
|
|
16551
18011
|
*
|
|
@@ -16583,7 +18043,14 @@ class WebSocketStream {
|
|
|
16583
18043
|
let data = _ref2.data;
|
|
16584
18044
|
return controller.enqueue(data);
|
|
16585
18045
|
};
|
|
16586
|
-
ws.onerror = e => controller.error(e);
|
|
18046
|
+
ws.onerror = e => controller.error(ConnectionError.websocket(e instanceof Error ? "".concat(e.name, ": ").concat(e.message) : "Encountered unknown websocket error: ".concat(String(e))));
|
|
18047
|
+
ws.onclose = ev => {
|
|
18048
|
+
if (ev.wasClean) {
|
|
18049
|
+
controller.close();
|
|
18050
|
+
} else {
|
|
18051
|
+
controller.error(ConnectionError.websocket("WS closed unexpectedly with code ".concat(ev.code)));
|
|
18052
|
+
}
|
|
18053
|
+
};
|
|
16587
18054
|
},
|
|
16588
18055
|
cancel: closeWithInfo
|
|
16589
18056
|
}),
|
|
@@ -16622,7 +18089,7 @@ class WebSocketStream {
|
|
|
16622
18089
|
resolve(reason);
|
|
16623
18090
|
}
|
|
16624
18091
|
});
|
|
16625
|
-
ws.
|
|
18092
|
+
ws.addEventListener('close', _ref3 => {
|
|
16626
18093
|
let code = _ref3.code,
|
|
16627
18094
|
reason = _ref3.reason;
|
|
16628
18095
|
resolve({
|
|
@@ -16630,7 +18097,7 @@ class WebSocketStream {
|
|
|
16630
18097
|
reason
|
|
16631
18098
|
});
|
|
16632
18099
|
ws.removeEventListener('error', rejectHandler);
|
|
16633
|
-
};
|
|
18100
|
+
});
|
|
16634
18101
|
ws.addEventListener('error', rejectHandler);
|
|
16635
18102
|
});
|
|
16636
18103
|
if (options.signal) {
|
|
@@ -16639,6 +18106,16 @@ class WebSocketStream {
|
|
|
16639
18106
|
this.close = closeWithInfo;
|
|
16640
18107
|
}
|
|
16641
18108
|
}const passThroughQueueSignals = ['syncState', 'trickle', 'offer', 'answer', 'simulate', 'leave'];
|
|
18109
|
+
/**
|
|
18110
|
+
* Whether a failed resume may be followed by another one. Mirrors how `RTCEngine` classifies these
|
|
18111
|
+
* errors: a server leave ends the session, and an expired token cannot be recovered by retrying.
|
|
18112
|
+
*/
|
|
18113
|
+
function isRecoverableReconnectError(error) {
|
|
18114
|
+
if (error instanceof ConnectionError) {
|
|
18115
|
+
return error.reason !== ConnectionErrorReason.LeaveRequest && error.reason !== ConnectionErrorReason.NotAllowed;
|
|
18116
|
+
}
|
|
18117
|
+
return true;
|
|
18118
|
+
}
|
|
16642
18119
|
function canPassThroughQueue(req) {
|
|
16643
18120
|
const canPass = passThroughQueueSignals.indexOf(req.case) >= 0;
|
|
16644
18121
|
livekitLogger.trace('request allowed to bypass queue:', {
|
|
@@ -16655,6 +18132,24 @@ var SignalConnectionState;
|
|
|
16655
18132
|
SignalConnectionState[SignalConnectionState["DISCONNECTING"] = 3] = "DISCONNECTING";
|
|
16656
18133
|
SignalConnectionState[SignalConnectionState["DISCONNECTED"] = 4] = "DISCONNECTED";
|
|
16657
18134
|
})(SignalConnectionState || (SignalConnectionState = {}));
|
|
18135
|
+
/**
|
|
18136
|
+
* Public projection of the lifecycle machine's states. `new`, `offline` and `closed` are all
|
|
18137
|
+
* reported as `DISCONNECTED`: they differ in what may happen next
|
|
18138
|
+
*/
|
|
18139
|
+
function lifecycleToConnectionState(lifecycle) {
|
|
18140
|
+
switch (lifecycle) {
|
|
18141
|
+
case 'connected':
|
|
18142
|
+
return SignalConnectionState.CONNECTED;
|
|
18143
|
+
case 'connecting':
|
|
18144
|
+
return SignalConnectionState.CONNECTING;
|
|
18145
|
+
case 'reconnecting':
|
|
18146
|
+
return SignalConnectionState.RECONNECTING;
|
|
18147
|
+
case 'disconnecting':
|
|
18148
|
+
return SignalConnectionState.DISCONNECTING;
|
|
18149
|
+
default:
|
|
18150
|
+
return SignalConnectionState.DISCONNECTED;
|
|
18151
|
+
}
|
|
18152
|
+
}
|
|
16658
18153
|
/** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
|
|
16659
18154
|
const MAX_WS_CLOSE_TIME = 250;
|
|
16660
18155
|
/**
|
|
@@ -16664,13 +18159,41 @@ const JOIN_RESPONSE_TIMEOUT = 5000;
|
|
|
16664
18159
|
/** @internal */
|
|
16665
18160
|
class SignalClient {
|
|
16666
18161
|
get currentState() {
|
|
16667
|
-
return this.
|
|
18162
|
+
return lifecycleToConnectionState(this.lifecycleState);
|
|
16668
18163
|
}
|
|
16669
18164
|
get isDisconnected() {
|
|
16670
|
-
|
|
18165
|
+
const state = this.currentState;
|
|
18166
|
+
return state === SignalConnectionState.DISCONNECTING || state === SignalConnectionState.DISCONNECTED;
|
|
18167
|
+
}
|
|
18168
|
+
/** Runtime lifecycle state, finer grained than the public {@link currentState} projection. */
|
|
18169
|
+
get lifecycleState() {
|
|
18170
|
+
return this.machine.currentState();
|
|
18171
|
+
}
|
|
18172
|
+
/** Id of the current connection attempt and of the transport it owns. */
|
|
18173
|
+
get attemptId() {
|
|
18174
|
+
return this.machine.context.attemptId;
|
|
18175
|
+
}
|
|
18176
|
+
/**
|
|
18177
|
+
* Applies a lifecycle input and reports whether it moved the machine
|
|
18178
|
+
*/
|
|
18179
|
+
sendLifecycleInput(input) {
|
|
18180
|
+
const before = this.lifecycleState;
|
|
18181
|
+
this.machine.handle(input.type, input);
|
|
18182
|
+
return this.lifecycleState !== before;
|
|
18183
|
+
}
|
|
18184
|
+
/**
|
|
18185
|
+
* Waits out a close that is already in flight. Establishing a session while one is tearing down
|
|
18186
|
+
* would race it for the transport — the teardown can close the socket the new attempt just
|
|
18187
|
+
* opened.
|
|
18188
|
+
*/
|
|
18189
|
+
settleInFlightClose() {
|
|
18190
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18191
|
+
this.log.debug('waiting for an in-flight close to settle before establishing a session');
|
|
18192
|
+
(yield this.closingLock.lock())();
|
|
18193
|
+
});
|
|
16671
18194
|
}
|
|
16672
18195
|
get isEstablishingConnection() {
|
|
16673
|
-
return this.
|
|
18196
|
+
return this.lifecycleState === 'connecting' || this.lifecycleState === 'reconnecting';
|
|
16674
18197
|
}
|
|
16675
18198
|
getNextRequestId() {
|
|
16676
18199
|
this._requestId += 1;
|
|
@@ -16682,7 +18205,6 @@ class SignalClient {
|
|
|
16682
18205
|
var _a;
|
|
16683
18206
|
/** signal rtt in milliseconds */
|
|
16684
18207
|
this.rtt = 0;
|
|
16685
|
-
this.state = SignalConnectionState.DISCONNECTED;
|
|
16686
18208
|
this.log = livekitLogger;
|
|
16687
18209
|
this._requestId = 0;
|
|
16688
18210
|
this.useV0SignalPath = false;
|
|
@@ -16708,7 +18230,18 @@ class SignalClient {
|
|
|
16708
18230
|
this.queuedRequests = [];
|
|
16709
18231
|
this.closingLock = new _();
|
|
16710
18232
|
this.connectionLock = new _();
|
|
16711
|
-
this.
|
|
18233
|
+
this.machine = createSignalMachine();
|
|
18234
|
+
this.machine.on('transitioned', _ref => {
|
|
18235
|
+
let fromState = _ref.fromState,
|
|
18236
|
+
toState = _ref.toState;
|
|
18237
|
+
this.log.debug("signal lifecycle: ".concat(fromState, " -> ").concat(toState));
|
|
18238
|
+
});
|
|
18239
|
+
this.machine.on('nohandler', _ref2 => {
|
|
18240
|
+
let inputName = _ref2.inputName;
|
|
18241
|
+
this.log.debug("ignoring signal lifecycle input ".concat(inputName, " in state ").concat(this.lifecycleState));
|
|
18242
|
+
});
|
|
18243
|
+
// no-op unless a development tool asked for it; see utils/machineInspector
|
|
18244
|
+
announceMachine('signal', this.machine);
|
|
16712
18245
|
}
|
|
16713
18246
|
get logContext() {
|
|
16714
18247
|
var _a, _b;
|
|
@@ -16720,12 +18253,30 @@ class SignalClient {
|
|
|
16720
18253
|
let useV0Path = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
|
|
16721
18254
|
let publisherOffer = arguments.length > 5 ? arguments[5] : undefined;
|
|
16722
18255
|
return function* () {
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
18256
|
+
if (_this.lifecycleState === 'disconnecting') {
|
|
18257
|
+
yield _this.settleInFlightClose();
|
|
18258
|
+
}
|
|
18259
|
+
if (!_this.sendLifecycleInput({
|
|
18260
|
+
type: 'connect'
|
|
18261
|
+
})) {
|
|
18262
|
+
// Proceeding would open a transport the lifecycle does not own: its completion would be
|
|
18263
|
+
// discarded and the session left claiming a transport that had been torn down. Every caller
|
|
18264
|
+
// that restarts a session closes the previous one first (see RTCEngine.restartConnection).
|
|
18265
|
+
throw ConnectionError.internal("cannot establish a signal session from '".concat(_this.lifecycleState, "', close the current one first"));
|
|
18266
|
+
}
|
|
16726
18267
|
_this.options = opts;
|
|
16727
|
-
|
|
16728
|
-
|
|
18268
|
+
try {
|
|
18269
|
+
const res = yield _this.connect(url, token, opts, abortSignal, useV0Path, publisherOffer);
|
|
18270
|
+
return res;
|
|
18271
|
+
} catch (e) {
|
|
18272
|
+
// reported here rather than at each rejection site inside connect(), so that no failure
|
|
18273
|
+
// path can leave the machine stuck in `connecting`
|
|
18274
|
+
_this.sendLifecycleInput({
|
|
18275
|
+
type: 'connectFailed',
|
|
18276
|
+
error: e
|
|
18277
|
+
});
|
|
18278
|
+
throw e;
|
|
18279
|
+
}
|
|
16729
18280
|
}();
|
|
16730
18281
|
});
|
|
16731
18282
|
}
|
|
@@ -16735,15 +18286,31 @@ class SignalClient {
|
|
|
16735
18286
|
this.log.warn('attempted to reconnect without signal options being set, ignoring');
|
|
16736
18287
|
return;
|
|
16737
18288
|
}
|
|
16738
|
-
this.
|
|
18289
|
+
if (this.lifecycleState === 'disconnecting') {
|
|
18290
|
+
yield this.settleInFlightClose();
|
|
18291
|
+
}
|
|
18292
|
+
if (!this.sendLifecycleInput({
|
|
18293
|
+
type: 'reconnect'
|
|
18294
|
+
})) {
|
|
18295
|
+
throw ConnectionError.internal("cannot resume the signal session from '".concat(this.lifecycleState, "'"));
|
|
18296
|
+
}
|
|
16739
18297
|
// clear ping interval and restart it once reconnected
|
|
16740
18298
|
this.clearPingInterval();
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
18299
|
+
try {
|
|
18300
|
+
const res = yield this.connect(url, token, Object.assign(Object.assign({}, this.options), {
|
|
18301
|
+
reconnect: true,
|
|
18302
|
+
sid,
|
|
18303
|
+
reconnectReason: reason
|
|
18304
|
+
}), undefined, this.useV0SignalPath);
|
|
18305
|
+
return res;
|
|
18306
|
+
} catch (e) {
|
|
18307
|
+
this.sendLifecycleInput({
|
|
18308
|
+
type: 'reconnectFailed',
|
|
18309
|
+
error: e,
|
|
18310
|
+
recoverable: isRecoverableReconnectError(e)
|
|
18311
|
+
});
|
|
18312
|
+
throw e;
|
|
18313
|
+
}
|
|
16747
18314
|
});
|
|
16748
18315
|
}
|
|
16749
18316
|
connect(url_1, token_1, opts_1, abortSignal_1) {
|
|
@@ -16780,7 +18347,11 @@ class SignalClient {
|
|
|
16780
18347
|
this.close();
|
|
16781
18348
|
}
|
|
16782
18349
|
cleanupAbortHandlers();
|
|
16783
|
-
|
|
18350
|
+
// The caller may already have classified this failure: the connect timeout hands us a
|
|
18351
|
+
// Timeout error. Only a genuine abort is a cancellation, and reporting a stalled connect as
|
|
18352
|
+
// one makes the engine read it as user intent — skipping region failover and never
|
|
18353
|
+
// recording the attempt against the backoff strategy (see Room.connect).
|
|
18354
|
+
reject(eventOrError instanceof ConnectionError ? eventOrError : ConnectionError.cancelled(reason));
|
|
16784
18355
|
});
|
|
16785
18356
|
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', abortHandler);
|
|
16786
18357
|
const cleanupAbortHandlers = () => {
|
|
@@ -16790,48 +18361,62 @@ class SignalClient {
|
|
|
16790
18361
|
const wsTimeout = setTimeout(() => {
|
|
16791
18362
|
abortHandler(ConnectionError.timeout('room connection has timed out (signal)'));
|
|
16792
18363
|
}, opts.websocketTimeout);
|
|
16793
|
-
const handleSignalConnected = (connection, firstMessage) => {
|
|
16794
|
-
this.handleSignalConnected(connection, wsTimeout, firstMessage);
|
|
16795
|
-
};
|
|
16796
18364
|
const redactedUrl = new URL(rtcUrl);
|
|
16797
18365
|
if (redactedUrl.searchParams.has('access_token')) {
|
|
16798
18366
|
redactedUrl.searchParams.set('access_token', '<redacted>');
|
|
16799
18367
|
}
|
|
16800
18368
|
if (this.ws) {
|
|
16801
18369
|
const startClose = performance.now();
|
|
16802
|
-
yield this.
|
|
18370
|
+
yield this.teardownTransport('replaced by a new connection attempt');
|
|
16803
18371
|
this.log.debug("closed previous ws connection in ".concat(performance.now() - startClose, "ms"));
|
|
16804
18372
|
}
|
|
18373
|
+
// the transport created below belongs to this attempt; events arriving from it after a
|
|
18374
|
+
// newer attempt has started are dropped by the machine
|
|
18375
|
+
const attemptId = this.attemptId;
|
|
16805
18376
|
this.log.info("signal connecting to ".concat(redactedUrl), {
|
|
16806
18377
|
reconnect: opts.reconnect,
|
|
16807
18378
|
reconnectReason: opts.reconnectReason
|
|
16808
18379
|
});
|
|
16809
18380
|
this.ws = new WebSocketStream(rtcUrl);
|
|
18381
|
+
// A failed upgrade closes the socket, so `opened` and `closed` settle for the same cause — but
|
|
18382
|
+
// only the `opened` path can classify it (a 401 is known after asking the validate endpoint).
|
|
18383
|
+
// Noting the failure *before* the close handler is registered means it runs first, so a close
|
|
18384
|
+
// that merely reflects a failed upgrade stands down and lets the classified error reach the
|
|
18385
|
+
// caller. Any other close still fails the attempt straight away: after a successful upgrade
|
|
18386
|
+
// nothing else would, short of the first-message timeout.
|
|
18387
|
+
let upgradeFailed = false;
|
|
18388
|
+
this.ws.opened.catch(() => {
|
|
18389
|
+
upgradeFailed = true;
|
|
18390
|
+
});
|
|
16810
18391
|
try {
|
|
16811
18392
|
this.ws.closed.then(closeInfo => {
|
|
16812
|
-
if (this.isEstablishingConnection) {
|
|
18393
|
+
if (this.isEstablishingConnection && !upgradeFailed) {
|
|
16813
18394
|
reject(ConnectionError.internal("Websocket got closed during a (re)connection attempt: ".concat(closeInfo.reason)));
|
|
16814
18395
|
}
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
18396
|
+
this.log.debug('websocket closed', {
|
|
18397
|
+
reason: closeInfo.reason,
|
|
18398
|
+
code: closeInfo.closeCode,
|
|
18399
|
+
attemptId,
|
|
18400
|
+
state: this.lifecycleState
|
|
18401
|
+
});
|
|
18402
|
+
// Every close of the live transport is reported, including a clean 1000 one: a server
|
|
18403
|
+
// that drops signalling closes cleanly — a migration that never sends its
|
|
18404
|
+
// `Leave{action=RESUME}` does exactly that — and treating that as "nothing happened"
|
|
18405
|
+
// leaves the client believing it is still connected until something else notices.
|
|
18406
|
+
// Closes we caused ourselves, and closes from a transport that has since been
|
|
18407
|
+
// replaced, are dropped by the machine's state and attempt guards below rather than by
|
|
18408
|
+
// a close-code test.
|
|
18409
|
+
this.handleOnClose(closeInfo.reason || (closeInfo.closeCode === 1000 ? 'server closed the signal connection' : 'Unexpected WS error'), attemptId);
|
|
16826
18410
|
return;
|
|
16827
18411
|
}).catch(reason => {
|
|
16828
|
-
if (this.isEstablishingConnection) {
|
|
18412
|
+
if (this.isEstablishingConnection && !upgradeFailed) {
|
|
16829
18413
|
reject(ConnectionError.internal("Websocket error during a (re)connection attempt: ".concat(reason)));
|
|
16830
18414
|
}
|
|
16831
18415
|
});
|
|
16832
18416
|
const connection = yield this.ws.opened.catch(reason => __awaiter(this, void 0, void 0, function* () {
|
|
16833
|
-
if (this.
|
|
16834
|
-
|
|
18417
|
+
if (this.lifecycleState !== 'connected') {
|
|
18418
|
+
// claimed synchronously, before the await below: the socket's close event is already
|
|
18419
|
+
// on its way and would otherwise reject first with a less useful error
|
|
16835
18420
|
clearTimeout(wsTimeout);
|
|
16836
18421
|
const error = yield this.handleConnectionError(reason, validateUrl);
|
|
16837
18422
|
reject(error);
|
|
@@ -16896,7 +18481,7 @@ class SignalClient {
|
|
|
16896
18481
|
}
|
|
16897
18482
|
// Handle successful connection
|
|
16898
18483
|
const firstMessageToProcess = validation.shouldProcessFirstMessage ? firstSignalResponse : undefined;
|
|
16899
|
-
handleSignalConnected(connection, firstMessageToProcess);
|
|
18484
|
+
this.handleSignalConnected(connection, wsTimeout, attemptId, firstMessageToProcess);
|
|
16900
18485
|
resolve(validation.response);
|
|
16901
18486
|
} catch (e) {
|
|
16902
18487
|
reject(e);
|
|
@@ -16919,14 +18504,22 @@ class SignalClient {
|
|
|
16919
18504
|
if (this.signalLatency) {
|
|
16920
18505
|
yield sleep(this.signalLatency);
|
|
16921
18506
|
}
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
18507
|
+
try {
|
|
18508
|
+
const _yield$signalReader$r = yield signalReader.read(),
|
|
18509
|
+
done = _yield$signalReader$r.done,
|
|
18510
|
+
value = _yield$signalReader$r.value;
|
|
18511
|
+
if (done) {
|
|
18512
|
+
break;
|
|
18513
|
+
}
|
|
18514
|
+
const resp = parseSignalResponse(value);
|
|
18515
|
+
this.handleSignalResponse(resp);
|
|
18516
|
+
} catch (e) {
|
|
18517
|
+
this.log.error("error reading from signal stream", {
|
|
18518
|
+
error: e
|
|
18519
|
+
});
|
|
18520
|
+
yield this.close(false, 'error in reading loop');
|
|
16926
18521
|
break;
|
|
16927
18522
|
}
|
|
16928
|
-
const resp = parseSignalResponse(value);
|
|
16929
|
-
this.handleSignalResponse(resp);
|
|
16930
18523
|
}
|
|
16931
18524
|
});
|
|
16932
18525
|
}
|
|
@@ -16936,40 +18529,53 @@ class SignalClient {
|
|
|
16936
18529
|
let updateState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
|
|
16937
18530
|
let reason = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Close method called on signal client';
|
|
16938
18531
|
return function* () {
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
18532
|
+
// when the lifecycle is already shutting down (or another close owns it), only the transport
|
|
18533
|
+
// teardown below still applies — it is idempotent, so callers can always await a close
|
|
18534
|
+
const drivesLifecycle = updateState && _this3.sendLifecycleInput({
|
|
18535
|
+
type: 'close',
|
|
18536
|
+
reason
|
|
18537
|
+
});
|
|
16943
18538
|
const unlock = yield _this3.closingLock.lock();
|
|
16944
18539
|
try {
|
|
16945
|
-
_this3.
|
|
16946
|
-
if (updateState) {
|
|
16947
|
-
_this3.state = SignalConnectionState.DISCONNECTING;
|
|
16948
|
-
}
|
|
16949
|
-
if (_this3.ws) {
|
|
16950
|
-
_this3.ws.close({
|
|
16951
|
-
closeCode: 1000,
|
|
16952
|
-
reason
|
|
16953
|
-
});
|
|
16954
|
-
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
|
|
16955
|
-
const closePromise = _this3.ws.closed;
|
|
16956
|
-
_this3.ws = undefined;
|
|
16957
|
-
_this3.streamWriter = undefined;
|
|
16958
|
-
yield Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
16959
|
-
}
|
|
16960
|
-
} catch (e) {
|
|
16961
|
-
_this3.log.debug('websocket error while closing', {
|
|
16962
|
-
error: e
|
|
16963
|
-
});
|
|
18540
|
+
yield _this3.teardownTransport(reason);
|
|
16964
18541
|
} finally {
|
|
16965
|
-
if (
|
|
16966
|
-
_this3.
|
|
18542
|
+
if (drivesLifecycle) {
|
|
18543
|
+
_this3.sendLifecycleInput({
|
|
18544
|
+
type: 'closeComplete'
|
|
18545
|
+
});
|
|
16967
18546
|
}
|
|
16968
18547
|
unlock();
|
|
16969
18548
|
}
|
|
16970
18549
|
}();
|
|
16971
18550
|
});
|
|
16972
18551
|
}
|
|
18552
|
+
/**
|
|
18553
|
+
* Releases the transport and everything tied to it, without touching the lifecycle state. Used
|
|
18554
|
+
* both by {@link close} and by paths that replace the transport under a live lifecycle (a new
|
|
18555
|
+
* attempt, or an unexpected close that leaves the client in `offline`).
|
|
18556
|
+
*/
|
|
18557
|
+
teardownTransport(reason) {
|
|
18558
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18559
|
+
try {
|
|
18560
|
+
this.clearPingInterval();
|
|
18561
|
+
if (this.ws) {
|
|
18562
|
+
this.ws.close({
|
|
18563
|
+
closeCode: 1000,
|
|
18564
|
+
reason
|
|
18565
|
+
});
|
|
18566
|
+
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
|
|
18567
|
+
const closePromise = this.ws.closed;
|
|
18568
|
+
this.ws = undefined;
|
|
18569
|
+
this.streamWriter = undefined;
|
|
18570
|
+
yield Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
18571
|
+
}
|
|
18572
|
+
} catch (e) {
|
|
18573
|
+
this.log.debug('websocket error while closing', {
|
|
18574
|
+
error: e
|
|
18575
|
+
});
|
|
18576
|
+
}
|
|
18577
|
+
});
|
|
18578
|
+
}
|
|
16973
18579
|
// initial offer after joining
|
|
16974
18580
|
sendOffer(offer, offerId) {
|
|
16975
18581
|
this.log.debug('sending offer', {
|
|
@@ -17148,7 +18754,8 @@ class SignalClient {
|
|
|
17148
18754
|
// capture all requests while reconnecting and put them in a queue
|
|
17149
18755
|
// unless the request originates from the queue, then don't enqueue again
|
|
17150
18756
|
const canQueue = !fromQueue && !canPassThroughQueue(message);
|
|
17151
|
-
|
|
18757
|
+
const isHoldingRequests = _this5.lifecycleState === 'reconnecting' || _this5.queuedRequests.length > 0;
|
|
18758
|
+
if (canQueue && isHoldingRequests) {
|
|
17152
18759
|
_this5.queuedRequests.push(() => __awaiter(_this5, void 0, void 0, function* () {
|
|
17153
18760
|
yield this.sendRequest(message, true);
|
|
17154
18761
|
}));
|
|
@@ -17161,7 +18768,11 @@ class SignalClient {
|
|
|
17161
18768
|
if (_this5.signalLatency) {
|
|
17162
18769
|
yield sleep(_this5.signalLatency);
|
|
17163
18770
|
}
|
|
17164
|
-
|
|
18771
|
+
// `leave` is the one request whose purpose is to be sent on the way out (an aborted connect
|
|
18772
|
+
// attempt tells the server before tearing down), so it is allowed through for as long as the
|
|
18773
|
+
// transport is still there
|
|
18774
|
+
const isLeaveOnShutdown = message.case === 'leave' && !!_this5.streamWriter;
|
|
18775
|
+
if (_this5.isDisconnected && !isLeaveOnShutdown) {
|
|
17165
18776
|
// Skip requests if the signal layer is disconnected
|
|
17166
18777
|
// This can happen if an event is sent in the mist of room.connect() initializing
|
|
17167
18778
|
_this5.log.debug("skipping signal request (type: ".concat(message.case, ") - SignalClient disconnected"));
|
|
@@ -17315,17 +18926,39 @@ class SignalClient {
|
|
|
17315
18926
|
}
|
|
17316
18927
|
}
|
|
17317
18928
|
}
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
onCloseCallback
|
|
17328
|
-
|
|
18929
|
+
/**
|
|
18930
|
+
* Handles a transport we lost without asking to. The client goes to `offline` rather than
|
|
18931
|
+
* `closed`: whether this session gets resumed, restarted or given up on is the engine's call.
|
|
18932
|
+
*/
|
|
18933
|
+
handleOnClose(reason_1) {
|
|
18934
|
+
return __awaiter(this, arguments, void 0, function (reason) {
|
|
18935
|
+
var _this6 = this;
|
|
18936
|
+
let attemptId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.attemptId;
|
|
18937
|
+
return function* () {
|
|
18938
|
+
const onCloseCallback = _this6.onClose;
|
|
18939
|
+
if (!_this6.sendLifecycleInput({
|
|
18940
|
+
type: 'transportFailed',
|
|
18941
|
+
attemptId,
|
|
18942
|
+
reason
|
|
18943
|
+
})) {
|
|
18944
|
+
// a close we caused ourselves, or one from a transport that has since been replaced: logged
|
|
18945
|
+
// rather than dropped silently, because a close that goes unnoticed leaves the client writing
|
|
18946
|
+
// to a dead socket
|
|
18947
|
+
_this6.log.debug("ignoring transport close in state ".concat(_this6.lifecycleState), {
|
|
18948
|
+
reason,
|
|
18949
|
+
attemptId,
|
|
18950
|
+
currentAttemptId: _this6.attemptId
|
|
18951
|
+
});
|
|
18952
|
+
return;
|
|
18953
|
+
}
|
|
18954
|
+
yield _this6.teardownTransport(reason);
|
|
18955
|
+
_this6.log.info("websocket connection closed: ".concat(reason), {
|
|
18956
|
+
reason
|
|
18957
|
+
});
|
|
18958
|
+
if (onCloseCallback) {
|
|
18959
|
+
onCloseCallback(reason);
|
|
18960
|
+
}
|
|
18961
|
+
}();
|
|
17329
18962
|
});
|
|
17330
18963
|
}
|
|
17331
18964
|
handleWSError(error) {
|
|
@@ -17382,10 +19015,27 @@ class SignalClient {
|
|
|
17382
19015
|
* @param firstMessage Optional first message to process
|
|
17383
19016
|
* @internal
|
|
17384
19017
|
*/
|
|
17385
|
-
handleSignalConnected(connection, timeoutHandle, firstMessage) {
|
|
17386
|
-
this.state = SignalConnectionState.CONNECTED;
|
|
17387
|
-
this.log.info('signal connected');
|
|
19018
|
+
handleSignalConnected(connection, timeoutHandle, attemptId, firstMessage) {
|
|
17388
19019
|
clearTimeout(timeoutHandle);
|
|
19020
|
+
const established = this.sendLifecycleInput(this.lifecycleState === 'reconnecting' ? {
|
|
19021
|
+
type: 'reconnectComplete',
|
|
19022
|
+
attemptId
|
|
19023
|
+
} : {
|
|
19024
|
+
type: 'connectComplete',
|
|
19025
|
+
attemptId
|
|
19026
|
+
});
|
|
19027
|
+
if (!established) {
|
|
19028
|
+
// The attempt was abandoned while we waited for the server's first message: it was closed, or
|
|
19029
|
+
// a newer attempt superseded it. Arming the ping interval and the read loop here would outlive
|
|
19030
|
+
// the session that owns them, so leave the transport to whoever holds the lifecycle now.
|
|
19031
|
+
this.log.debug('discarding a connection whose attempt no longer owns the session', {
|
|
19032
|
+
attemptId,
|
|
19033
|
+
currentAttemptId: this.attemptId,
|
|
19034
|
+
state: this.lifecycleState
|
|
19035
|
+
});
|
|
19036
|
+
return;
|
|
19037
|
+
}
|
|
19038
|
+
this.log.info('signal connected');
|
|
17389
19039
|
this.startPingInterval();
|
|
17390
19040
|
this.startReadingLoop(connection.readable.getReader(), firstMessage);
|
|
17391
19041
|
}
|
|
@@ -17403,7 +19053,7 @@ class SignalClient {
|
|
|
17403
19053
|
isValid: true,
|
|
17404
19054
|
response: firstSignalResponse.message.value
|
|
17405
19055
|
};
|
|
17406
|
-
} else if (this.
|
|
19056
|
+
} else if (this.lifecycleState === 'reconnecting' && ((_b = firstSignalResponse.message) === null || _b === void 0 ? void 0 : _b.case) !== 'leave') {
|
|
17407
19057
|
if (((_c = firstSignalResponse.message) === null || _c === void 0 ? void 0 : _c.case) === 'reconnect') {
|
|
17408
19058
|
return {
|
|
17409
19059
|
isValid: true,
|
|
@@ -18392,6 +20042,51 @@ const startBitrateMultiplier = 0.9;
|
|
|
18392
20042
|
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
18393
20043
|
const maxStartBitrateKbps = 1000;
|
|
18394
20044
|
const debounceInterval = 20;
|
|
20045
|
+
/**
|
|
20046
|
+
* Applies the configured start bitrate when this media section belongs to `cid`.
|
|
20047
|
+
* This SDP munging is used for a bitrate setting that cannot be applied through
|
|
20048
|
+
* `RTCRtpEncodingParameters`.
|
|
20049
|
+
*
|
|
20050
|
+
* Returns `undefined` when the section does not belong to the track, `0` when
|
|
20051
|
+
* it does but does not offer the requested codec, and the codec payload when the
|
|
20052
|
+
* requested codec is present (whether the bitrate was added or already set).
|
|
20053
|
+
*
|
|
20054
|
+
* @internal
|
|
20055
|
+
*/
|
|
20056
|
+
function applyVideoStartBitrate(media, cid, codec, maxbr) {
|
|
20057
|
+
let isScreenShare = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
|
|
20058
|
+
var _a, _b, _c;
|
|
20059
|
+
if (!((_a = media.msid) === null || _a === void 0 ? void 0 : _a.includes(cid))) {
|
|
20060
|
+
return undefined;
|
|
20061
|
+
}
|
|
20062
|
+
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;
|
|
20063
|
+
if (codecPayload === 0) {
|
|
20064
|
+
return 0;
|
|
20065
|
+
}
|
|
20066
|
+
// Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
|
|
20067
|
+
// from starting too aggressively. Screen share is not capped since text/UI
|
|
20068
|
+
// clarity requires high bitrate from the start.
|
|
20069
|
+
// TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
|
|
20070
|
+
const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
|
|
20071
|
+
const startBitrate = isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
20072
|
+
const fmtp = media.fmtp.find(entry => entry.payload === codecPayload);
|
|
20073
|
+
if (fmtp) {
|
|
20074
|
+
// If another track's fmtp already has a start bitrate, it cannot be
|
|
20075
|
+
// overridden here because the payload type is shared across the bundle.
|
|
20076
|
+
// This forces every track sharing that payload to use the initial track's
|
|
20077
|
+
// start bitrate.
|
|
20078
|
+
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
|
20079
|
+
fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
|
|
20080
|
+
}
|
|
20081
|
+
} else {
|
|
20082
|
+
// VP8 and some codecs may not have an existing fmtp line.
|
|
20083
|
+
media.fmtp.push({
|
|
20084
|
+
payload: codecPayload,
|
|
20085
|
+
config: "x-google-start-bitrate=".concat(startBitrate)
|
|
20086
|
+
});
|
|
20087
|
+
}
|
|
20088
|
+
return codecPayload;
|
|
20089
|
+
}
|
|
18395
20090
|
const PCEvents = {
|
|
18396
20091
|
NegotiationStarted: 'negotiationStarted',
|
|
18397
20092
|
NegotiationComplete: 'negotiationComplete',
|
|
@@ -18693,9 +20388,13 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18693
20388
|
// the only exception to this is when ICE restart is needed
|
|
18694
20389
|
const currentSD = this._pc.remoteDescription;
|
|
18695
20390
|
if ((options === null || options === void 0 ? void 0 : options.iceRestart) && currentSD) {
|
|
18696
|
-
//
|
|
18697
|
-
//
|
|
20391
|
+
// roll the remote description back in so createOffer produces a valid
|
|
20392
|
+
// ICE-restart offer on top of the already-negotiated state
|
|
18698
20393
|
yield this._pc.setRemoteDescription(currentSD);
|
|
20394
|
+
} else if (options === null || options === void 0 ? void 0 : options.iceRestart) {
|
|
20395
|
+
// ICE restart with no remote description to restart on: `renegotiate` would stall
|
|
20396
|
+
// (the pending offer is never answered), so throw for the caller to recreate the PC.
|
|
20397
|
+
throw new NegotiationError('ICE restart requested without a remote description, peer connection must be recreated');
|
|
18699
20398
|
} else {
|
|
18700
20399
|
this.renegotiate = true;
|
|
18701
20400
|
this.log.debug('requesting renegotiation');
|
|
@@ -18721,48 +20420,15 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18721
20420
|
ensureAudioNackAndStereo(media, ['all'], []);
|
|
18722
20421
|
} else if (media.type === 'video') {
|
|
18723
20422
|
this.trackBitrates.some(trackbr => {
|
|
18724
|
-
if (!
|
|
20423
|
+
if (!trackbr.cid) {
|
|
18725
20424
|
return false;
|
|
18726
20425
|
}
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
|
|
18730
|
-
codecPayload = rtp.payload;
|
|
18731
|
-
return true;
|
|
18732
|
-
}
|
|
20426
|
+
const codecPayload = applyVideoStartBitrate(media, trackbr.cid, trackbr.codec, trackbr.maxbr, trackbr.isScreenShare);
|
|
20427
|
+
if (codecPayload === undefined) {
|
|
18733
20428
|
return false;
|
|
18734
|
-
});
|
|
18735
|
-
if (codecPayload === 0) {
|
|
18736
|
-
return true;
|
|
18737
|
-
}
|
|
18738
|
-
if (isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
18739
|
-
this.ensureVideoDDExtensionForSVC(media, sdpParsed);
|
|
18740
20429
|
}
|
|
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
|
-
});
|
|
20430
|
+
if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
20431
|
+
this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
|
|
18766
20432
|
}
|
|
18767
20433
|
return true;
|
|
18768
20434
|
});
|
|
@@ -18878,8 +20544,10 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18878
20544
|
var _a;
|
|
18879
20545
|
return (_a = this.pc) === null || _a === void 0 ? void 0 : _a.remoteDescription;
|
|
18880
20546
|
}
|
|
20547
|
+
/** stats of the underlying connection, `undefined` when there is none */
|
|
18881
20548
|
getStats() {
|
|
18882
|
-
|
|
20549
|
+
var _a;
|
|
20550
|
+
return (_a = this._pc) === null || _a === void 0 ? void 0 : _a.getStats();
|
|
18883
20551
|
}
|
|
18884
20552
|
getMaxMessageSize() {
|
|
18885
20553
|
var _a, _b;
|
|
@@ -18975,41 +20643,106 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18975
20643
|
}
|
|
18976
20644
|
});
|
|
18977
20645
|
}
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
20646
|
+
}
|
|
20647
|
+
/**
|
|
20648
|
+
* Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
|
|
20649
|
+
* returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
|
|
20650
|
+
* been chosen yet).
|
|
20651
|
+
*
|
|
20652
|
+
* A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
|
|
20653
|
+
* `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
|
|
20654
|
+
* sections it can send on, and an earlier offer may have munged it into others.
|
|
20655
|
+
* @internal
|
|
20656
|
+
*/
|
|
20657
|
+
function ensureVideoDDExtension(media, sdp, ddExtID) {
|
|
20658
|
+
var _a, _b;
|
|
20659
|
+
const id = ddExtensionIDFor(sdp, ddExtID);
|
|
20660
|
+
if (id === undefined) {
|
|
20661
|
+
return ddExtID;
|
|
20662
|
+
}
|
|
20663
|
+
if (!((_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.uri === ddExtensionURI))) {
|
|
20664
|
+
(_b = media.ext) !== null && _b !== void 0 ? _b : media.ext = [];
|
|
20665
|
+
media.ext.push({
|
|
20666
|
+
value: id,
|
|
20667
|
+
uri: ddExtensionURI
|
|
18985
20668
|
});
|
|
18986
|
-
|
|
18987
|
-
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
20669
|
+
}
|
|
20670
|
+
return id;
|
|
20671
|
+
}
|
|
20672
|
+
/**
|
|
20673
|
+
* The id to map the dependency descriptor to throughout `sdp`, or undefined when no id would be
|
|
20674
|
+
* consistent for the whole bundle and the extension therefore has to be left out.
|
|
20675
|
+
*/
|
|
20676
|
+
function ddExtensionIDFor(sdp, cachedID) {
|
|
20677
|
+
const mapped = mappedExtensionID(sdp, ddExtensionURI);
|
|
20678
|
+
if (mapped !== undefined) {
|
|
20679
|
+
// Adopting an id that also stands for another URI is what the browser rejects the bundle
|
|
20680
|
+
// over, and its own half of the map is not ours to renumber, so give up on this offer.
|
|
20681
|
+
return usedForOtherURI(sdp, mapped, ddExtensionURI) ? undefined : mapped;
|
|
20682
|
+
}
|
|
20683
|
+
// Reusing the id from the last offer keeps the mapping stable across renegotiations, but only
|
|
20684
|
+
// while nothing else has taken it: the browser assigns ids to its own extensions without
|
|
20685
|
+
// knowing about ours, so an id that was free when we picked it can since have been claimed —
|
|
20686
|
+
// typically by the fuller extension set that arrives with the first section we send on.
|
|
20687
|
+
if (cachedID !== 0 && !usedForOtherURI(sdp, cachedID, ddExtensionURI)) {
|
|
20688
|
+
return cachedID;
|
|
20689
|
+
}
|
|
20690
|
+
return unusedExtensionID(sdp);
|
|
20691
|
+
}
|
|
20692
|
+
/** The id `uri` is mapped to in `sdp`, if any section maps it. */
|
|
20693
|
+
function mappedExtensionID(sdp, uri) {
|
|
20694
|
+
var _a;
|
|
20695
|
+
for (const media of sdp.media) {
|
|
20696
|
+
const ext = (_a = media.ext) === null || _a === void 0 ? void 0 : _a.find(candidate => candidate.uri === uri);
|
|
20697
|
+
if (ext) {
|
|
20698
|
+
return ext.value;
|
|
19003
20699
|
}
|
|
19004
20700
|
}
|
|
20701
|
+
return undefined;
|
|
19005
20702
|
}
|
|
20703
|
+
/** Whether `id` stands for anything in `sdp` other than `uri`. */
|
|
20704
|
+
function usedForOtherURI(sdp, id, uri) {
|
|
20705
|
+
return sdp.media.some(media => {
|
|
20706
|
+
var _a;
|
|
20707
|
+
return (_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.value === id && ext.uri !== uri);
|
|
20708
|
+
});
|
|
20709
|
+
}
|
|
20710
|
+
/**
|
|
20711
|
+
* An id no extension in `sdp` uses. Stays above every id in use rather than filling gaps, so it
|
|
20712
|
+
* is less likely to be an id the browser goes on to allocate to another extension, and steps
|
|
20713
|
+
* over 15, which RFC 8285 reserves.
|
|
20714
|
+
*/
|
|
20715
|
+
function unusedExtensionID(sdp) {
|
|
20716
|
+
let maxID = 0;
|
|
20717
|
+
sdp.media.forEach(media => {
|
|
20718
|
+
var _a;
|
|
20719
|
+
(_a = media.ext) === null || _a === void 0 ? void 0 : _a.forEach(ext => {
|
|
20720
|
+
if (ext.value > maxID) {
|
|
20721
|
+
maxID = ext.value;
|
|
20722
|
+
}
|
|
20723
|
+
});
|
|
20724
|
+
});
|
|
20725
|
+
return maxID + 1 === 15 ? 16 : maxID + 1;
|
|
20726
|
+
}
|
|
20727
|
+
/**
|
|
20728
|
+
* Checks whether an fmtp config declares `param` as an exact, `;`-delimited
|
|
20729
|
+
* token. A plain substring check conflates distinct opus parameters — e.g.
|
|
20730
|
+
* `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
|
|
20731
|
+
* parameter, not appear anywhere within the config string.
|
|
20732
|
+
* @internal
|
|
20733
|
+
*/
|
|
20734
|
+
function fmtpConfigHasParam(config, param) {
|
|
20735
|
+
return config.split(';').some(entry => entry.trim() === param);
|
|
20736
|
+
}
|
|
20737
|
+
/** @internal */
|
|
19006
20738
|
function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
|
|
19007
20739
|
// sdp-transform types don't include number however the parser outputs mids as numbers in some cases
|
|
19008
20740
|
const mid = getMidString(media.mid);
|
|
19009
20741
|
// found opus codec to add nack fb
|
|
19010
20742
|
let opusPayload = 0;
|
|
19011
20743
|
media.rtp.some(rtp => {
|
|
19012
|
-
|
|
20744
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
20745
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
19013
20746
|
opusPayload = rtp.payload;
|
|
19014
20747
|
return true;
|
|
19015
20748
|
}
|
|
@@ -19029,7 +20762,7 @@ function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
|
|
|
19029
20762
|
if (stereoMids.includes(mid) || stereoMids.length === 1 && stereoMids[0] === 'all') {
|
|
19030
20763
|
media.fmtp.some(fmtp => {
|
|
19031
20764
|
if (fmtp.payload === opusPayload) {
|
|
19032
|
-
if (!fmtp.config
|
|
20765
|
+
if (!fmtpConfigHasParam(fmtp.config, 'stereo=1')) {
|
|
19033
20766
|
fmtp.config += ';stereo=1';
|
|
19034
20767
|
}
|
|
19035
20768
|
return true;
|
|
@@ -19112,6 +20845,7 @@ function conformBundledCodecFmtp(media, isPlaceholder) {
|
|
|
19112
20845
|
}
|
|
19113
20846
|
}
|
|
19114
20847
|
}
|
|
20848
|
+
/** @internal */
|
|
19115
20849
|
function extractStereoAndNackAudioFromOffer(offer) {
|
|
19116
20850
|
var _a;
|
|
19117
20851
|
const stereoMids = [];
|
|
@@ -19123,7 +20857,8 @@ function extractStereoAndNackAudioFromOffer(offer) {
|
|
|
19123
20857
|
const mid = getMidString(media.mid);
|
|
19124
20858
|
if (media.type === 'audio') {
|
|
19125
20859
|
media.rtp.some(rtp => {
|
|
19126
|
-
|
|
20860
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
20861
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
19127
20862
|
opusPayload = rtp.payload;
|
|
19128
20863
|
return true;
|
|
19129
20864
|
}
|
|
@@ -19134,7 +20869,7 @@ function extractStereoAndNackAudioFromOffer(offer) {
|
|
|
19134
20869
|
}
|
|
19135
20870
|
media.fmtp.some(fmtp => {
|
|
19136
20871
|
if (fmtp.payload === opusPayload) {
|
|
19137
|
-
if (fmtp.config
|
|
20872
|
+
if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
|
|
19138
20873
|
stereoMids.push(mid);
|
|
19139
20874
|
}
|
|
19140
20875
|
return true;
|
|
@@ -21102,12 +22837,17 @@ class LocalTrack extends Track {
|
|
|
21102
22837
|
type: 'audio',
|
|
21103
22838
|
streamId: v.id,
|
|
21104
22839
|
packetsSent: v.packetsSent,
|
|
21105
|
-
packetsLost: v.packetsLost,
|
|
21106
22840
|
bytesSent: v.bytesSent,
|
|
21107
|
-
timestamp: v.timestamp
|
|
21108
|
-
roundTripTime: v.roundTripTime,
|
|
21109
|
-
jitter: v.jitter
|
|
22841
|
+
timestamp: v.timestamp
|
|
21110
22842
|
};
|
|
22843
|
+
// loss, jitter and RTT are only known from what the remote reports back,
|
|
22844
|
+
// the same way the video sender picks them up
|
|
22845
|
+
const remote = stats.get(v.remoteId);
|
|
22846
|
+
if (remote) {
|
|
22847
|
+
audioStats.packetsLost = remote.packetsLost;
|
|
22848
|
+
audioStats.jitter = remote.jitter;
|
|
22849
|
+
audioStats.roundTripTime = remote.roundTripTime;
|
|
22850
|
+
}
|
|
21111
22851
|
}
|
|
21112
22852
|
});
|
|
21113
22853
|
return audioStats;
|
|
@@ -21927,17 +23667,37 @@ class LocalVideoTrack extends LocalTrack {
|
|
|
21927
23667
|
setDegradationPreference(preference) {
|
|
21928
23668
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21929
23669
|
this.degradationPreference = preference;
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
|
|
23670
|
+
// applied one sender at a time on purpose, see applyDegradationPreference
|
|
23671
|
+
yield this.applyDegradationPreference(this.sender);
|
|
23672
|
+
for (const sc of this.simulcastCodecs.values()) {
|
|
23673
|
+
yield this.applyDegradationPreference(sc.sender);
|
|
23674
|
+
}
|
|
23675
|
+
});
|
|
23676
|
+
}
|
|
23677
|
+
/**
|
|
23678
|
+
* Degradation preference is a property of the sender, not of the track, so every sender
|
|
23679
|
+
* publishing this track needs it applied separately. A backup codec publishes over its
|
|
23680
|
+
* own sender, which would otherwise let the browser resolve a preference implicitly and
|
|
23681
|
+
* diverge from the primary encoder.
|
|
23682
|
+
*
|
|
23683
|
+
* Callers apply this sequentially rather than concurrently: `setParameters` is only valid
|
|
23684
|
+
* against the parameters most recently returned by `getParameters`, which is why this file
|
|
23685
|
+
* serializes other sender parameter updates through `senderLock`.
|
|
23686
|
+
*/
|
|
23687
|
+
applyDegradationPreference(sender) {
|
|
23688
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23689
|
+
if (!sender) {
|
|
23690
|
+
return;
|
|
23691
|
+
}
|
|
23692
|
+
try {
|
|
23693
|
+
this.log.debug("setting degradationPreference to ".concat(this.degradationPreference), this.logContext);
|
|
23694
|
+
const params = sender.getParameters();
|
|
23695
|
+
params.degradationPreference = this.degradationPreference;
|
|
23696
|
+
yield sender.setParameters(params);
|
|
23697
|
+
} catch (e) {
|
|
23698
|
+
this.log.warn("failed to set degradationPreference", Object.assign({
|
|
23699
|
+
error: e
|
|
23700
|
+
}, this.logContext));
|
|
21941
23701
|
}
|
|
21942
23702
|
});
|
|
21943
23703
|
}
|
|
@@ -21956,18 +23716,23 @@ class LocalVideoTrack extends LocalTrack {
|
|
|
21956
23716
|
return simulcastCodecInfo;
|
|
21957
23717
|
}
|
|
21958
23718
|
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);
|
|
23719
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23720
|
+
const simulcastCodecInfo = this.simulcastCodecs.get(codec);
|
|
23721
|
+
if (!simulcastCodecInfo) {
|
|
23722
|
+
return;
|
|
21969
23723
|
}
|
|
21970
|
-
|
|
23724
|
+
simulcastCodecInfo.sender = sender;
|
|
23725
|
+
// the backup codec publishes over its own sender, so it needs the same degradation
|
|
23726
|
+
// preference the primary sender resolved to.
|
|
23727
|
+
yield this.applyDegradationPreference(sender);
|
|
23728
|
+
// browser will reenable disabled codec/layers after new codec has been published,
|
|
23729
|
+
// so refresh subscribedCodecs after publish a new codec
|
|
23730
|
+
setTimeout(() => {
|
|
23731
|
+
if (this.subscribedCodecs) {
|
|
23732
|
+
this.setPublishingCodecs(this.subscribedCodecs);
|
|
23733
|
+
}
|
|
23734
|
+
}, refreshSubscribedCodecAfterNewCodec);
|
|
23735
|
+
});
|
|
21971
23736
|
}
|
|
21972
23737
|
/**
|
|
21973
23738
|
* @internal
|
|
@@ -22239,6 +24004,11 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
|
|
|
22239
24004
|
});
|
|
22240
24005
|
}const minReconnectWait = 2 * 1000;
|
|
22241
24006
|
const leaveReconnect = 'leave-reconnect';
|
|
24007
|
+
/**
|
|
24008
|
+
* How long local connection quality must stay `LOST` while connected and publishing before we
|
|
24009
|
+
* force a full reconnect — `LOST` is the server's verdict that it isn't receiving our media.
|
|
24010
|
+
*/
|
|
24011
|
+
const connectionQualityLostTimeout = 10 * 1000;
|
|
22242
24012
|
const reliabeReceiveStateTTL = 30000;
|
|
22243
24013
|
const initialMediaSectionsAudio = 3;
|
|
22244
24014
|
const initialMediaSectionsVideo = 3;
|
|
@@ -22419,7 +24189,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22419
24189
|
const disconnect = duration => {
|
|
22420
24190
|
this.log.warn("could not recover connection after ".concat(this.reconnectAttempts, " attempts, ").concat(duration, "ms. giving up"));
|
|
22421
24191
|
this.emit(EngineEvent.Disconnected);
|
|
22422
|
-
this.close();
|
|
24192
|
+
this.close("gave up reconnecting after ".concat(this.reconnectAttempts, " attempts, ").concat(duration, "ms"));
|
|
22423
24193
|
};
|
|
22424
24194
|
const duration = Date.now() - this.reconnectStart;
|
|
22425
24195
|
let delay = this.getNextRetryDelay({
|
|
@@ -22528,7 +24298,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22528
24298
|
onBufferStatusChanged: (kind, isLow) => this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind)
|
|
22529
24299
|
});
|
|
22530
24300
|
this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
22531
|
-
this.client.onConnectionQuality = update =>
|
|
24301
|
+
this.client.onConnectionQuality = update => {
|
|
24302
|
+
this.handleLocalConnectionQuality(update);
|
|
24303
|
+
this.emit(EngineEvent.ConnectionQualityUpdate, update);
|
|
24304
|
+
};
|
|
22532
24305
|
this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
|
|
22533
24306
|
this.client.onSubscriptionError = resp => this.emit(EngineEvent.SubscriptionError, resp);
|
|
22534
24307
|
this.client.onSubscriptionPermissionUpdate = update => this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
|
|
@@ -22657,7 +24430,12 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22657
24430
|
}();
|
|
22658
24431
|
});
|
|
22659
24432
|
}
|
|
22660
|
-
|
|
24433
|
+
/**
|
|
24434
|
+
* @param reason why the session is ending, recorded by the signal lifecycle. Worth passing
|
|
24435
|
+
* wherever the caller knows more than "someone called close" — the server's leave reason, or
|
|
24436
|
+
* having given up on reconnecting.
|
|
24437
|
+
*/
|
|
24438
|
+
close(reason) {
|
|
22661
24439
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22662
24440
|
const unlock = yield this.closingLock.lock();
|
|
22663
24441
|
if (this.isClosed) {
|
|
@@ -22671,9 +24449,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22671
24449
|
this.removeAllListeners();
|
|
22672
24450
|
this.deregisterOnLineListener();
|
|
22673
24451
|
this.clearPendingReconnect();
|
|
24452
|
+
this.clearLostQualityTimeout();
|
|
22674
24453
|
this.cleanupLossyDataStats();
|
|
22675
24454
|
yield this.cleanupPeerConnections();
|
|
22676
|
-
yield this.cleanupClient();
|
|
24455
|
+
yield this.cleanupClient(reason);
|
|
22677
24456
|
} finally {
|
|
22678
24457
|
unlock();
|
|
22679
24458
|
}
|
|
@@ -22685,15 +24464,17 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22685
24464
|
this.dataChannels.teardown();
|
|
22686
24465
|
yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
|
|
22687
24466
|
this.pcManager = undefined;
|
|
24467
|
+
// the connecting timestamp belongs to the transports we just tore down
|
|
24468
|
+
this.transportConnectingSince = undefined;
|
|
22688
24469
|
this.reliableReceivedState.clear();
|
|
22689
24470
|
});
|
|
22690
24471
|
}
|
|
22691
24472
|
cleanupLossyDataStats() {
|
|
22692
24473
|
this.lossyChannel.stopThresholdTuning();
|
|
22693
24474
|
}
|
|
22694
|
-
cleanupClient() {
|
|
24475
|
+
cleanupClient(reason) {
|
|
22695
24476
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22696
|
-
yield this.client.close();
|
|
24477
|
+
yield this.client.close(true, reason);
|
|
22697
24478
|
this.client.resetCallbacks();
|
|
22698
24479
|
// Any in-flight addTrack requests are orphaned by the signal reconnect — the new session
|
|
22699
24480
|
// won't deliver `trackPublishedResponse` for them, so reject the pending resolvers and
|
|
@@ -22710,17 +24491,17 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22710
24491
|
throw new TrackInvalidError('a track with the same ID has already been published');
|
|
22711
24492
|
}
|
|
22712
24493
|
return new Promise((resolve, reject) => {
|
|
22713
|
-
const publicationTimeout = setTimeout(() => {
|
|
24494
|
+
const publicationTimeout = CriticalTimers.setTimeout(() => {
|
|
22714
24495
|
delete this.pendingTrackResolvers[req.cid];
|
|
22715
24496
|
reject(ConnectionError.timeout('publication of local track timed out, no response from server'));
|
|
22716
24497
|
}, 10000);
|
|
22717
24498
|
this.pendingTrackResolvers[req.cid] = {
|
|
22718
24499
|
resolve: info => {
|
|
22719
|
-
clearTimeout(publicationTimeout);
|
|
24500
|
+
CriticalTimers.clearTimeout(publicationTimeout);
|
|
22720
24501
|
resolve(info);
|
|
22721
24502
|
},
|
|
22722
24503
|
reject: () => {
|
|
22723
|
-
clearTimeout(publicationTimeout);
|
|
24504
|
+
CriticalTimers.clearTimeout(publicationTimeout);
|
|
22724
24505
|
reject(new Error('Cancelled publication by calling unpublish'));
|
|
22725
24506
|
}
|
|
22726
24507
|
};
|
|
@@ -22793,6 +24574,15 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22793
24574
|
this.pcManager.onDataChannel = this.handleDataChannel;
|
|
22794
24575
|
this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
|
|
22795
24576
|
this.log.debug("primary PC state changed ".concat(connectionState));
|
|
24577
|
+
// Record when the primary transport actually entered CONNECTING so
|
|
24578
|
+
// verifyTransport() can bound how long we tolerate it. Deriving it from the
|
|
24579
|
+
// real transition (this handler only fires on state changes) rather than from
|
|
24580
|
+
// observation time keeps it from going stale across peer-connection rebuilds.
|
|
24581
|
+
if (connectionState === PCTransportState.CONNECTING) {
|
|
24582
|
+
this.transportConnectingSince = Date.now();
|
|
24583
|
+
} else {
|
|
24584
|
+
this.transportConnectingSince = undefined;
|
|
24585
|
+
}
|
|
22796
24586
|
if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
|
|
22797
24587
|
// reset publisher connection promise
|
|
22798
24588
|
this.publisherConnectionPromise = undefined;
|
|
@@ -22919,6 +24709,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22919
24709
|
this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
|
|
22920
24710
|
};
|
|
22921
24711
|
this.client.onLeave = leave => {
|
|
24712
|
+
var _a;
|
|
22922
24713
|
this.log.info("client leave request received (action=".concat(leave === null || leave === void 0 ? void 0 : leave.action, ")"), {
|
|
22923
24714
|
reason: leave === null || leave === void 0 ? void 0 : leave.reason
|
|
22924
24715
|
});
|
|
@@ -22929,7 +24720,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22929
24720
|
switch (leave.action) {
|
|
22930
24721
|
case LeaveRequest_Action.DISCONNECT:
|
|
22931
24722
|
this.emit(EngineEvent.Disconnected, leave === null || leave === void 0 ? void 0 : leave.reason);
|
|
22932
|
-
this.close();
|
|
24723
|
+
this.close("server leave: ".concat((_a = DisconnectReason[leave.reason]) !== null && _a !== void 0 ? _a : leave.reason));
|
|
22933
24724
|
break;
|
|
22934
24725
|
case LeaveRequest_Action.RECONNECT:
|
|
22935
24726
|
this.fullReconnectOnNext = true;
|
|
@@ -23001,15 +24792,23 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23001
24792
|
}
|
|
23002
24793
|
}
|
|
23003
24794
|
addMediaSections(numAudios, numVideos) {
|
|
23004
|
-
var _a, _b;
|
|
24795
|
+
var _a, _b, _c;
|
|
23005
24796
|
const transceiverInit = {
|
|
23006
24797
|
direction: 'recvonly'
|
|
23007
24798
|
};
|
|
23008
24799
|
for (let i = 0; i < numAudios; i++) {
|
|
23009
24800
|
(_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.addPublisherTransceiverOfKind('audio', transceiverInit);
|
|
23010
24801
|
}
|
|
24802
|
+
// media only arrives on these sections when there is no subscriber connection to arrive on
|
|
24803
|
+
const receivesMedia = ((_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.mode) === 'publisher-only';
|
|
23011
24804
|
for (let i = 0; i < numVideos; i++) {
|
|
23012
|
-
(
|
|
24805
|
+
const transceiver = (_c = this.pcManager) === null || _c === void 0 ? void 0 : _c.addPublisherTransceiverOfKind('video', transceiverInit);
|
|
24806
|
+
if (receivesMedia && transceiver) {
|
|
24807
|
+
const negotiated = negotiateDependencyDescriptor(transceiver);
|
|
24808
|
+
this.log.debug('dependency descriptor negotiated for received video', {
|
|
24809
|
+
negotiated
|
|
24810
|
+
});
|
|
24811
|
+
}
|
|
23013
24812
|
}
|
|
23014
24813
|
}
|
|
23015
24814
|
createDataChannels() {
|
|
@@ -23151,7 +24950,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23151
24950
|
if (!opts.videoCodec) {
|
|
23152
24951
|
return;
|
|
23153
24952
|
}
|
|
23154
|
-
track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
|
24953
|
+
yield track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
|
23155
24954
|
return transceiver.sender;
|
|
23156
24955
|
});
|
|
23157
24956
|
}
|
|
@@ -23163,6 +24962,63 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23163
24962
|
return this.pcManager.addPublisherTrack(track);
|
|
23164
24963
|
});
|
|
23165
24964
|
}
|
|
24965
|
+
/**
|
|
24966
|
+
* A sustained local `LOST` while connected and publishing means the server isn't receiving
|
|
24967
|
+
* our media, so force a full reconnect; any non-`LOST` value cancels a pending trigger.
|
|
24968
|
+
*/
|
|
24969
|
+
handleLocalConnectionQuality(update) {
|
|
24970
|
+
if (!this.participantSid) {
|
|
24971
|
+
return;
|
|
24972
|
+
}
|
|
24973
|
+
const localUpdate = update.updates.find(u => u.participantSid === this.participantSid);
|
|
24974
|
+
if (!localUpdate) {
|
|
24975
|
+
return;
|
|
24976
|
+
}
|
|
24977
|
+
if (localUpdate.quality === ConnectionQuality$1.LOST) {
|
|
24978
|
+
this.scheduleLostQualityReconnect();
|
|
24979
|
+
} else {
|
|
24980
|
+
this.clearLostQualityTimeout();
|
|
24981
|
+
}
|
|
24982
|
+
}
|
|
24983
|
+
scheduleLostQualityReconnect() {
|
|
24984
|
+
if (this.lostQualityTimeout) {
|
|
24985
|
+
// already counting down towards a reconnect
|
|
24986
|
+
return;
|
|
24987
|
+
}
|
|
24988
|
+
this.lostQualityTimeout = CriticalTimers.setTimeout(() => {
|
|
24989
|
+
this.lostQualityTimeout = undefined;
|
|
24990
|
+
if (this._isClosed || this.pcState !== PCState.Connected || this.attemptingReconnect) {
|
|
24991
|
+
return;
|
|
24992
|
+
}
|
|
24993
|
+
if (!this.hasActivePublisherSenders()) {
|
|
24994
|
+
return;
|
|
24995
|
+
}
|
|
24996
|
+
this.log.warn('local connection quality lost while publishing, triggering full reconnect', this.logContext);
|
|
24997
|
+
this.fullReconnectOnNext = true;
|
|
24998
|
+
this.handleDisconnect('connection quality lost', ReconnectReason.RR_PUBLISHER_FAILED);
|
|
24999
|
+
}, connectionQualityLostTimeout);
|
|
25000
|
+
}
|
|
25001
|
+
clearLostQualityTimeout() {
|
|
25002
|
+
if (this.lostQualityTimeout) {
|
|
25003
|
+
CriticalTimers.clearTimeout(this.lostQualityTimeout);
|
|
25004
|
+
this.lostQualityTimeout = undefined;
|
|
25005
|
+
}
|
|
25006
|
+
}
|
|
25007
|
+
/** Whether the publisher currently has any sender with a live track. */
|
|
25008
|
+
hasActivePublisherSenders() {
|
|
25009
|
+
var _a, _b;
|
|
25010
|
+
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;
|
|
25011
|
+
}
|
|
25012
|
+
/**
|
|
25013
|
+
* Forces a full reconnect while keeping the engine (and its saved credentials) alive. Used by
|
|
25014
|
+
* Room's connection-reconcile safety net when the transport silently died but we looked connected.
|
|
25015
|
+
* @internal
|
|
25016
|
+
*/
|
|
25017
|
+
reconnect() {
|
|
25018
|
+
let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ReconnectReason.RR_UNKNOWN;
|
|
25019
|
+
this.fullReconnectOnNext = true;
|
|
25020
|
+
this.handleDisconnect('reconcile', reason);
|
|
25021
|
+
}
|
|
23166
25022
|
attemptReconnect(reason) {
|
|
23167
25023
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23168
25024
|
var _a, _b, _c;
|
|
@@ -23174,21 +25030,32 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23174
25030
|
this.log.warn('already attempting reconnect, returning early');
|
|
23175
25031
|
return;
|
|
23176
25032
|
}
|
|
25033
|
+
// A pending Lost-quality countdown belongs to the session we're now leaving; cancel it so
|
|
25034
|
+
// it can't fire against the reconnected session before the server has evaluated it. (A resume
|
|
25035
|
+
// keeps the peer connections, so cleanupPeerConnections wouldn't cover this path.)
|
|
25036
|
+
this.clearLostQualityTimeout();
|
|
23177
25037
|
if (((_a = this.clientConfiguration) === null || _a === void 0 ? void 0 : _a.resumeConnection) === ClientConfigSetting.DISABLED ||
|
|
23178
25038
|
// signaling state could change to closed due to hardware sleep
|
|
23179
25039
|
// those connections cannot be resumed
|
|
23180
25040
|
((_c = (_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.currentState) !== null && _c !== void 0 ? _c : PCTransportState.NEW) === PCTransportState.NEW) {
|
|
23181
25041
|
this.fullReconnectOnNext = true;
|
|
23182
25042
|
}
|
|
25043
|
+
// Consume the flag up front: capture whether this attempt is a full reconnect, then reset
|
|
25044
|
+
// it. From here on a `true` value unambiguously represents a *new* full-reconnect request
|
|
25045
|
+
// that arrived while this attempt was running (e.g. a server RECONNECT leave), which the
|
|
25046
|
+
// finally block dispatches — for both the resume and full-reconnect paths.
|
|
25047
|
+
const fullReconnect = this.fullReconnectOnNext;
|
|
25048
|
+
this.fullReconnectOnNext = false;
|
|
25049
|
+
let succeeded = false;
|
|
23183
25050
|
try {
|
|
23184
25051
|
this.attemptingReconnect = true;
|
|
23185
|
-
if (
|
|
25052
|
+
if (fullReconnect) {
|
|
23186
25053
|
yield this.restartConnection();
|
|
23187
25054
|
} else {
|
|
23188
25055
|
yield this.resumeConnection(reason);
|
|
23189
25056
|
}
|
|
23190
25057
|
this.clearPendingReconnect();
|
|
23191
|
-
|
|
25058
|
+
succeeded = true;
|
|
23192
25059
|
} catch (e) {
|
|
23193
25060
|
this.reconnectAttempts += 1;
|
|
23194
25061
|
let recoverable = true;
|
|
@@ -23198,8 +25065,9 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23198
25065
|
});
|
|
23199
25066
|
// unrecoverable
|
|
23200
25067
|
recoverable = false;
|
|
23201
|
-
} else if (!(e instanceof SignalReconnectError)) {
|
|
23202
|
-
//
|
|
25068
|
+
} else if (fullReconnect || !(e instanceof SignalReconnectError)) {
|
|
25069
|
+
// a failed full reconnect stays a full reconnect; a failed resume can only be
|
|
25070
|
+
// resumed again for a signal-level error, otherwise it escalates
|
|
23203
25071
|
this.fullReconnectOnNext = true;
|
|
23204
25072
|
}
|
|
23205
25073
|
if (recoverable) {
|
|
@@ -23207,10 +25075,17 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23207
25075
|
} else {
|
|
23208
25076
|
this.log.info("could not recover connection after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms. giving up"));
|
|
23209
25077
|
this.emit(EngineEvent.Disconnected);
|
|
23210
|
-
yield this.close();
|
|
25078
|
+
yield this.close("gave up reconnecting after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms"));
|
|
23211
25079
|
}
|
|
23212
25080
|
} finally {
|
|
23213
25081
|
this.attemptingReconnect = false;
|
|
25082
|
+
// A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
|
|
25083
|
+
// during a resume or a restart) that a successful attempt didn't act on; dispatch it now
|
|
25084
|
+
// (the failure path already retries).
|
|
25085
|
+
if (succeeded && this.fullReconnectOnNext && !this._isClosed) {
|
|
25086
|
+
this.log.debug('full reconnect requested during in-progress attempt, dispatching');
|
|
25087
|
+
this.handleDisconnect('reconnect');
|
|
25088
|
+
}
|
|
23214
25089
|
}
|
|
23215
25090
|
});
|
|
23216
25091
|
}
|
|
@@ -23529,14 +25404,23 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23529
25404
|
if (!this.pcManager) {
|
|
23530
25405
|
return false;
|
|
23531
25406
|
}
|
|
25407
|
+
const state = this.pcManager.currentState;
|
|
23532
25408
|
const allowedConnectionStates = [PCTransportState.CONNECTING, PCTransportState.CONNECTED];
|
|
23533
|
-
if (!allowedConnectionStates.includes(
|
|
25409
|
+
if (!allowedConnectionStates.includes(state)) {
|
|
23534
25410
|
return false;
|
|
23535
25411
|
}
|
|
23536
25412
|
// ensure signal is connected
|
|
23537
25413
|
if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
|
|
23538
25414
|
return false;
|
|
23539
25415
|
}
|
|
25416
|
+
// A transport stuck in CONNECTING never reaches CONNECTED nor reports FAILED, so it would
|
|
25417
|
+
// otherwise look healthy forever; bound how long we tolerate it. The entry time is recorded
|
|
25418
|
+
// in the pcManager state-change handler (see configure()), so this is a pure read — an
|
|
25419
|
+
// unrecorded CONNECTING fails open rather than measuring against a stale timestamp.
|
|
25420
|
+
if (state === PCTransportState.CONNECTING && this.transportConnectingSince !== undefined && Date.now() - this.transportConnectingSince > this.peerConnectionTimeout) {
|
|
25421
|
+
this.log.warn('transport stuck in connecting state', this.logContext);
|
|
25422
|
+
return false;
|
|
25423
|
+
}
|
|
23540
25424
|
return true;
|
|
23541
25425
|
}
|
|
23542
25426
|
/** @internal */
|
|
@@ -24178,9 +26062,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24178
26062
|
// Suppress unhandled rejection on reader.closed — errors are
|
|
24179
26063
|
// already propagated through reader.read() to the consumer.
|
|
24180
26064
|
reader.closed.catch(() => {});
|
|
24181
|
-
const decoder = new TextDecoder('utf-8'
|
|
24182
|
-
fatal: true
|
|
24183
|
-
});
|
|
26065
|
+
const decoder = new TextDecoder('utf-8');
|
|
24184
26066
|
const signal = this.signal;
|
|
24185
26067
|
const cleanup = () => {
|
|
24186
26068
|
reader.releaseLock();
|
|
@@ -24658,9 +26540,7 @@ function bytesToChunks(streamId) {
|
|
|
24658
26540
|
* synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
|
|
24659
26541
|
*/
|
|
24660
26542
|
function bytesToDecodedUtf8(streamId) {
|
|
24661
|
-
const decoder = new TextDecoder('utf-8'
|
|
24662
|
-
fatal: true
|
|
24663
|
-
});
|
|
26543
|
+
const decoder = new TextDecoder('utf-8');
|
|
24664
26544
|
const encoder = new TextEncoder();
|
|
24665
26545
|
let outIndex = 0;
|
|
24666
26546
|
const decodeOrThrow = bytes => {
|
|
@@ -31440,6 +33320,7 @@ class DeferrableMap extends Map {
|
|
|
31440
33320
|
ConnectionState["SignalReconnecting"] = "signalReconnecting";
|
|
31441
33321
|
})(ConnectionState || (ConnectionState = {}));
|
|
31442
33322
|
const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
|
|
33323
|
+
const STATS_LOG_FREQUENCY_MS = 30 * 1000;
|
|
31443
33324
|
/**
|
|
31444
33325
|
* In LiveKit, a room is the logical grouping for a list of participants.
|
|
31445
33326
|
* Participants in a room can publish tracks, and subscribe to others' tracks.
|
|
@@ -31458,7 +33339,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31458
33339
|
*/
|
|
31459
33340
|
constructor(options) {
|
|
31460
33341
|
var _this;
|
|
31461
|
-
var _a, _b, _c, _d, _e, _f;
|
|
33342
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
31462
33343
|
super();
|
|
31463
33344
|
_this = this;
|
|
31464
33345
|
this.state = ConnectionState.Disconnected;
|
|
@@ -31473,8 +33354,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31473
33354
|
this.e2eeStateMutex = new _();
|
|
31474
33355
|
this.isVideoPlaybackBlocked = false;
|
|
31475
33356
|
this.log = livekitLogger;
|
|
33357
|
+
this.statsLog = livekitLogger;
|
|
31476
33358
|
this.bufferedEvents = [];
|
|
31477
33359
|
this.isResuming = false;
|
|
33360
|
+
this.pendingTrackAddedCallbacks = new Map();
|
|
31478
33361
|
this.connect = (url, token, opts) => __awaiter(this, void 0, void 0, function* () {
|
|
31479
33362
|
var _a;
|
|
31480
33363
|
if (!isBrowserSupported()) {
|
|
@@ -31904,7 +33787,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31904
33787
|
let remoteParticipant = this.remoteParticipants.get(info.identity);
|
|
31905
33788
|
// when it's disconnected, send updates
|
|
31906
33789
|
if (info.state === ParticipantInfo_State.DISCONNECTED) {
|
|
31907
|
-
this.handleParticipantDisconnected(info.identity, remoteParticipant);
|
|
33790
|
+
this.handleParticipantDisconnected(info.identity, remoteParticipant, info.disconnectReason === DisconnectReason.UNKNOWN_REASON ? undefined : info.disconnectReason);
|
|
31908
33791
|
} else {
|
|
31909
33792
|
// create participant if doesn't exist
|
|
31910
33793
|
this.getOrCreateParticipant(info.identity, info);
|
|
@@ -31990,8 +33873,9 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31990
33873
|
return;
|
|
31991
33874
|
}
|
|
31992
33875
|
const newStreamState = Track.streamStateFromProto(streamState.state);
|
|
33876
|
+
const prevStreamState = pub.track.streamState;
|
|
31993
33877
|
pub.track.setStreamState(newStreamState);
|
|
31994
|
-
if (newStreamState !==
|
|
33878
|
+
if (newStreamState !== prevStreamState) {
|
|
31995
33879
|
participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
|
|
31996
33880
|
this.emitWhenConnected(RoomEvent.TrackStreamStateChanged, pub, pub.track.streamState, participant);
|
|
31997
33881
|
}
|
|
@@ -32009,6 +33893,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32009
33893
|
pub.setAllowed(update.allowed);
|
|
32010
33894
|
};
|
|
32011
33895
|
this.handleSubscriptionError = update => {
|
|
33896
|
+
this.cancelPendingTrackAdded(update.trackSid);
|
|
32012
33897
|
const participant = Array.from(this.remoteParticipants.values()).find(p => p.trackPublications.has(update.trackSid));
|
|
32013
33898
|
if (!participant) {
|
|
32014
33899
|
return;
|
|
@@ -32154,6 +34039,34 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32154
34039
|
this.getAllRemoteParticipantIdentities = () => {
|
|
32155
34040
|
return Array.from(this.remoteParticipants.keys());
|
|
32156
34041
|
};
|
|
34042
|
+
/**
|
|
34043
|
+
* Dumps stats of both peer connections.
|
|
34044
|
+
*/
|
|
34045
|
+
this.logWebRTCStats = () => __awaiter(this, void 0, void 0, function* () {
|
|
34046
|
+
var _a, _b, _c, _d;
|
|
34047
|
+
const pcManager = (_a = this.engine) === null || _a === void 0 ? void 0 : _a.pcManager;
|
|
34048
|
+
if (!pcManager) {
|
|
34049
|
+
return;
|
|
34050
|
+
}
|
|
34051
|
+
try {
|
|
34052
|
+
const _yield$Promise$all = yield Promise.all([pcManager.publisher.getStats(), (_b = pcManager.subscriber) === null || _b === void 0 ? void 0 : _b.getStats()]),
|
|
34053
|
+
_yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2),
|
|
34054
|
+
publisher = _yield$Promise$all2[0],
|
|
34055
|
+
subscriber = _yield$Promise$all2[1];
|
|
34056
|
+
const publisherStats = publisher && summarizeStatsReport(publisher);
|
|
34057
|
+
const subscriberStats = subscriber && summarizeStatsReport(subscriber);
|
|
34058
|
+
this.statsLog.info("webrtc stats", {
|
|
34059
|
+
publisher: publisherStats === null || publisherStats === void 0 ? void 0 : publisherStats.connection,
|
|
34060
|
+
subscriber: subscriberStats === null || subscriberStats === void 0 ? void 0 : subscriberStats.connection,
|
|
34061
|
+
inbound: [...((_c = publisherStats === null || publisherStats === void 0 ? void 0 : publisherStats.inbound) !== null && _c !== void 0 ? _c : []), ...((_d = subscriberStats === null || subscriberStats === void 0 ? void 0 : subscriberStats.inbound) !== null && _d !== void 0 ? _d : [])],
|
|
34062
|
+
outbound: publisherStats === null || publisherStats === void 0 ? void 0 : publisherStats.outbound
|
|
34063
|
+
});
|
|
34064
|
+
} catch (error) {
|
|
34065
|
+
this.statsLog.debug('could not collect webrtc stats', {
|
|
34066
|
+
error
|
|
34067
|
+
});
|
|
34068
|
+
}
|
|
34069
|
+
});
|
|
32157
34070
|
this.onLocalParticipantMetadataChanged = metadata => {
|
|
32158
34071
|
this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
|
|
32159
34072
|
};
|
|
@@ -32224,12 +34137,15 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32224
34137
|
this.sidToIdentity = new Map();
|
|
32225
34138
|
this.options = Object.assign(Object.assign({}, roomOptionDefaults), options);
|
|
32226
34139
|
this.log = getLogger((_a = this.options.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Room, () => this.logContext);
|
|
34140
|
+
// its own logger name, so the stats dumps can be silenced or routed
|
|
34141
|
+
// separately from the rest of the room's logs
|
|
34142
|
+
this.statsLog = getLogger(LoggerNames.Stats, () => this.logContext);
|
|
32227
34143
|
this.transcriptionReceivedTimes = new Map();
|
|
32228
34144
|
this.options.audioCaptureDefaults = Object.assign(Object.assign({}, audioDefaults), options === null || options === void 0 ? void 0 : options.audioCaptureDefaults);
|
|
32229
34145
|
this.options.videoCaptureDefaults = Object.assign(Object.assign({}, videoDefaults), options === null || options === void 0 ? void 0 : options.videoCaptureDefaults);
|
|
32230
34146
|
this.options.publishDefaults = Object.assign(Object.assign({}, publishDefaults), options === null || options === void 0 ? void 0 : options.publishDefaults);
|
|
32231
34147
|
this.maybeCreateEngine();
|
|
32232
|
-
this.incomingDataStreamManager = new IncomingDataStreamManager();
|
|
34148
|
+
this.incomingDataStreamManager = new IncomingDataStreamManager((_b = this.options.dataStream) === null || _b === void 0 ? void 0 : _b.maxPayloadByteLength);
|
|
32233
34149
|
this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log, this.getRemoteParticipantClientProtocol, this.getRemoteParticipantCapabilities, this.getAllRemoteParticipantIdentities);
|
|
32234
34150
|
this.incomingDataTrackManager = new IncomingDataTrackManager({
|
|
32235
34151
|
e2eeManager: this.e2eeManager
|
|
@@ -32292,15 +34208,15 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32292
34208
|
this.setupE2EE();
|
|
32293
34209
|
}
|
|
32294
34210
|
this.engine.e2eeManager = this.e2eeManager;
|
|
32295
|
-
this.incomingDataTrackManager.updateE2eeManager((
|
|
32296
|
-
this.outgoingDataTrackManager.updateE2eeManager((
|
|
34211
|
+
this.incomingDataTrackManager.updateE2eeManager((_c = this.e2eeManager) !== null && _c !== void 0 ? _c : null);
|
|
34212
|
+
this.outgoingDataTrackManager.updateE2eeManager((_d = this.e2eeManager) !== null && _d !== void 0 ? _d : null);
|
|
32297
34213
|
if (this.options.videoCaptureDefaults.deviceId) {
|
|
32298
34214
|
this.localParticipant.activeDeviceMap.set('videoinput', unwrapConstraint(this.options.videoCaptureDefaults.deviceId));
|
|
32299
34215
|
}
|
|
32300
34216
|
if (this.options.audioCaptureDefaults.deviceId) {
|
|
32301
34217
|
this.localParticipant.activeDeviceMap.set('audioinput', unwrapConstraint(this.options.audioCaptureDefaults.deviceId));
|
|
32302
34218
|
}
|
|
32303
|
-
if ((
|
|
34219
|
+
if ((_e = this.options.audioOutput) === null || _e === void 0 ? void 0 : _e.deviceId) {
|
|
32304
34220
|
this.switchActiveDevice('audiooutput', unwrapConstraint(this.options.audioOutput.deviceId)).catch(e => this.log.warn("Could not set audio output: ".concat(e.message)));
|
|
32305
34221
|
}
|
|
32306
34222
|
if (isWeb()) {
|
|
@@ -32327,7 +34243,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32327
34243
|
onDeviceChange = this.handleDeviceChange;
|
|
32328
34244
|
}
|
|
32329
34245
|
// in order to catch device changes prior to room connection we need to register the event in the constructor
|
|
32330
|
-
(
|
|
34246
|
+
(_g = (_f = navigator.mediaDevices) === null || _f === void 0 ? void 0 : _f.addEventListener) === null || _g === void 0 ? void 0 : _g.call(_f, 'devicechange', onDeviceChange, {
|
|
32331
34247
|
signal: cleanupController.signal
|
|
32332
34248
|
});
|
|
32333
34249
|
}
|
|
@@ -32521,10 +34437,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32521
34437
|
}
|
|
32522
34438
|
this.emitBufferedEvents();
|
|
32523
34439
|
}).on(EngineEvent.SignalResumed, () => {
|
|
32524
|
-
this.bufferedEvents = [];
|
|
32525
34440
|
if (this.state === ConnectionState.Reconnecting || this.isResuming) {
|
|
32526
34441
|
this.sendSyncState();
|
|
32527
34442
|
}
|
|
34443
|
+
this.emitBufferedEvents();
|
|
32528
34444
|
}).on(EngineEvent.Restarting, this.handleRestarting).on(EngineEvent.Restarted, this.handleRestarted).on(EngineEvent.SignalRestarted, this.handleSignalRestarted).on(EngineEvent.Offline, () => {
|
|
32529
34445
|
if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
|
|
32530
34446
|
this.emit(RoomEvent.Reconnecting);
|
|
@@ -32941,29 +34857,43 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32941
34857
|
this.maybeCreateEngine();
|
|
32942
34858
|
}
|
|
32943
34859
|
onTrackAdded(mediaTrack, stream, receiver) {
|
|
34860
|
+
var _a, _b;
|
|
32944
34861
|
// don't fire onSubscribed when connecting
|
|
32945
34862
|
// WebRTC fires onTrack as soon as setRemoteDescription is called on the offer
|
|
32946
34863
|
// at that time, ICE connectivity has not been established so the track is not
|
|
32947
34864
|
// technically subscribed.
|
|
32948
34865
|
// We'll defer these events until when the room is connected or eventually disconnected.
|
|
32949
|
-
if (
|
|
34866
|
+
if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
|
|
34867
|
+
const pendingTrackSid = extractTrackSid(mediaTrack, stream);
|
|
34868
|
+
this.log.debug('deferring on track for later', {
|
|
34869
|
+
mediaTrackId: mediaTrack.id,
|
|
34870
|
+
mediaStreamId: stream.id,
|
|
34871
|
+
tracksInStream: stream.getTracks().map(track => track.id)
|
|
34872
|
+
});
|
|
32950
34873
|
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
34874
|
cleanup();
|
|
34875
|
+
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
32958
34876
|
};
|
|
32959
34877
|
const cleanup = () => {
|
|
32960
34878
|
this.off(RoomEvent.Reconnected, reconnectedHandler);
|
|
32961
34879
|
this.off(RoomEvent.Connected, reconnectedHandler);
|
|
32962
34880
|
this.off(RoomEvent.Disconnected, cleanup);
|
|
34881
|
+
if (pendingTrackSid) {
|
|
34882
|
+
const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
|
|
34883
|
+
pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.delete(cleanup);
|
|
34884
|
+
if ((pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.size) === 0) {
|
|
34885
|
+
this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
|
|
34886
|
+
}
|
|
34887
|
+
}
|
|
32963
34888
|
};
|
|
32964
34889
|
this.once(RoomEvent.Reconnected, reconnectedHandler);
|
|
32965
34890
|
this.once(RoomEvent.Connected, reconnectedHandler);
|
|
32966
34891
|
this.once(RoomEvent.Disconnected, cleanup);
|
|
34892
|
+
if (pendingTrackSid) {
|
|
34893
|
+
const pendingCallbacks = (_a = this.pendingTrackAddedCallbacks.get(pendingTrackSid)) !== null && _a !== void 0 ? _a : new Set();
|
|
34894
|
+
pendingCallbacks.add(cleanup);
|
|
34895
|
+
this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
|
|
34896
|
+
}
|
|
32967
34897
|
return;
|
|
32968
34898
|
}
|
|
32969
34899
|
if (this.state === ConnectionState.Disconnected) {
|
|
@@ -32976,11 +34906,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32976
34906
|
}
|
|
32977
34907
|
const parts = unpackStreamId(stream.id);
|
|
32978
34908
|
const participantSid = parts[0];
|
|
32979
|
-
|
|
32980
|
-
let trackId = mediaTrack.id;
|
|
34909
|
+
const streamId = parts[1];
|
|
32981
34910
|
// firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
|
|
32982
34911
|
// and generates its own track id instead of infer from sdp track id.
|
|
32983
|
-
|
|
34912
|
+
let trackId = (_b = extractTrackSid(mediaTrack, stream)) !== null && _b !== void 0 ? _b : mediaTrack.id;
|
|
32984
34913
|
if (participantSid === this.localParticipant.sid) {
|
|
32985
34914
|
this.log.warn('tried to create RemoteParticipant for local participant');
|
|
32986
34915
|
return;
|
|
@@ -33024,6 +34953,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33024
34953
|
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
34954
|
}
|
|
33026
34955
|
}
|
|
34956
|
+
cancelPendingTrackAdded(trackSid) {
|
|
34957
|
+
var _a;
|
|
34958
|
+
(_a = this.pendingTrackAddedCallbacks.get(trackSid)) === null || _a === void 0 ? void 0 : _a.forEach(cleanup => cleanup());
|
|
34959
|
+
}
|
|
33027
34960
|
handleLocalTrackSubscribed(subscribedSid) {
|
|
33028
34961
|
const findPublication = () => this.localParticipant.getTrackPublications().find(_ref6 => {
|
|
33029
34962
|
let trackSid = _ref6.trackSid;
|
|
@@ -33131,7 +35064,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33131
35064
|
this.emit(RoomEvent.Disconnected, reason);
|
|
33132
35065
|
}
|
|
33133
35066
|
}
|
|
33134
|
-
handleParticipantDisconnected(identity, participant) {
|
|
35067
|
+
handleParticipantDisconnected(identity, participant, disconnectReason) {
|
|
33135
35068
|
// remove and send event
|
|
33136
35069
|
this.remoteParticipants.delete(identity);
|
|
33137
35070
|
if (!participant) {
|
|
@@ -33142,7 +35075,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33142
35075
|
participant.trackPublications.forEach(publication => {
|
|
33143
35076
|
participant.unpublishTrack(publication.trackSid, true);
|
|
33144
35077
|
});
|
|
33145
|
-
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
|
35078
|
+
this.emit(RoomEvent.ParticipantDisconnected, participant, disconnectReason);
|
|
33146
35079
|
participant.setDisconnected();
|
|
33147
35080
|
this.rpcClientManager.handleParticipantDisconnected(participant.identity);
|
|
33148
35081
|
}
|
|
@@ -33287,6 +35220,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33287
35220
|
}
|
|
33288
35221
|
this.emitWhenConnected(RoomEvent.TrackSubscribed, track, publication, participant);
|
|
33289
35222
|
}).on(ParticipantEvent.TrackUnpublished, publication => {
|
|
35223
|
+
this.cancelPendingTrackAdded(publication.trackSid);
|
|
33290
35224
|
this.emit(RoomEvent.TrackUnpublished, publication, participant);
|
|
33291
35225
|
}).on(ParticipantEvent.TrackUnsubscribed, (track, publication) => {
|
|
33292
35226
|
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
|
|
@@ -33384,6 +35318,19 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33384
35318
|
}();
|
|
33385
35319
|
}));
|
|
33386
35320
|
}
|
|
35321
|
+
setStatsLogging(enabled) {
|
|
35322
|
+
if (enabled) {
|
|
35323
|
+
if (!this.statsLogInterval) {
|
|
35324
|
+
this.statsLogInterval = CriticalTimers.setInterval(() => {
|
|
35325
|
+
// logWebRTCStats handles its own errors, nothing to await here
|
|
35326
|
+
this.logWebRTCStats();
|
|
35327
|
+
}, STATS_LOG_FREQUENCY_MS);
|
|
35328
|
+
}
|
|
35329
|
+
} else if (this.statsLogInterval) {
|
|
35330
|
+
CriticalTimers.clearInterval(this.statsLogInterval);
|
|
35331
|
+
this.statsLogInterval = undefined;
|
|
35332
|
+
}
|
|
35333
|
+
}
|
|
33387
35334
|
registerConnectionReconcile() {
|
|
33388
35335
|
this.clearConnectionReconcile();
|
|
33389
35336
|
let consecutiveFailures = 0;
|
|
@@ -33404,8 +35351,17 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33404
35351
|
} : undefined
|
|
33405
35352
|
});
|
|
33406
35353
|
if (consecutiveFailures >= 3) {
|
|
33407
|
-
this.
|
|
33408
|
-
this.
|
|
35354
|
+
this.clearConnectionReconcile();
|
|
35355
|
+
if (this.engine && !this.engine.isClosed) {
|
|
35356
|
+
// The transport silently died while we still looked connected. Try a full reconnect
|
|
35357
|
+
// (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
|
|
35358
|
+
this.log.warn('detected connection state mismatch, attempting full reconnect');
|
|
35359
|
+
this.engine.reconnect();
|
|
35360
|
+
} else {
|
|
35361
|
+
// No usable engine to reconnect with; tear down.
|
|
35362
|
+
this.recreateEngine();
|
|
35363
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.STATE_MISMATCH);
|
|
35364
|
+
}
|
|
33409
35365
|
}
|
|
33410
35366
|
} else {
|
|
33411
35367
|
consecutiveFailures = 0;
|
|
@@ -33425,6 +35381,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33425
35381
|
this.log.info("connection state changed: ".concat(this.state, " -> ").concat(state));
|
|
33426
35382
|
this.state = state;
|
|
33427
35383
|
this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
|
|
35384
|
+
this.setStatsLogging(state === ConnectionState.Connected);
|
|
33428
35385
|
this.emit(RoomEvent.ConnectionStateChanged, this.state);
|
|
33429
35386
|
return true;
|
|
33430
35387
|
}
|
|
@@ -34597,15 +36554,22 @@ function isObject(input) {
|
|
|
34597
36554
|
const ONE_MINUTE_IN_MILLISECONDS = 60 * ONE_SECOND_IN_MILLISECONDS;
|
|
34598
36555
|
function isResponseTokenValid(response) {
|
|
34599
36556
|
const jwtPayload = decodeTokenPayload(response.participantToken);
|
|
34600
|
-
|
|
34601
|
-
|
|
36557
|
+
// Missing exp: TokenSourceCached would otherwise return this response forever.
|
|
36558
|
+
// nbf is optional (RFC 7519); do not skip the exp check when it is absent.
|
|
36559
|
+
if (!(jwtPayload === null || jwtPayload === void 0 ? void 0 : jwtPayload.exp)) {
|
|
36560
|
+
return false;
|
|
34602
36561
|
}
|
|
34603
36562
|
const now = new Date();
|
|
34604
|
-
|
|
34605
|
-
|
|
36563
|
+
if (jwtPayload.nbf) {
|
|
36564
|
+
const nbfInMilliseconds = jwtPayload.nbf * ONE_SECOND_IN_MILLISECONDS;
|
|
36565
|
+
const nbfDate = new Date(nbfInMilliseconds);
|
|
36566
|
+
if (nbfDate > now) {
|
|
36567
|
+
return false;
|
|
36568
|
+
}
|
|
36569
|
+
}
|
|
34606
36570
|
const expInMilliseconds = jwtPayload.exp * ONE_SECOND_IN_MILLISECONDS;
|
|
34607
36571
|
const expDate = new Date(expInMilliseconds - ONE_MINUTE_IN_MILLISECONDS);
|
|
34608
|
-
return
|
|
36572
|
+
return expDate > now;
|
|
34609
36573
|
}
|
|
34610
36574
|
/** Given a LiveKit generated participant token, decodes and returns the associated {@link TokenPayload} data. */
|
|
34611
36575
|
function decodeTokenPayload(token) {
|
|
@@ -34802,24 +36766,26 @@ class TokenSourceEndpoint extends TokenSourceCached {
|
|
|
34802
36766
|
const body = yield response.json();
|
|
34803
36767
|
return TokenSourceResponse.fromJson(body, {
|
|
34804
36768
|
// 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,
|
|
36769
|
+
// what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
|
|
34806
36770
|
ignoreUnknownFields: true
|
|
34807
36771
|
});
|
|
34808
36772
|
});
|
|
34809
36773
|
}
|
|
34810
36774
|
}
|
|
34811
|
-
class
|
|
34812
|
-
constructor(
|
|
36775
|
+
class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
|
|
36776
|
+
constructor(tokenServerId, options) {
|
|
34813
36777
|
const _options$baseUrl = options.baseUrl,
|
|
34814
36778
|
baseUrl = _options$baseUrl === void 0 ? 'https://cloud-api.livekit.io' : _options$baseUrl,
|
|
34815
36779
|
rest = __rest(options, ["baseUrl"]);
|
|
34816
36780
|
super("".concat(baseUrl, "/api/v2/sandbox/connection-details"), Object.assign(Object.assign({}, rest), {
|
|
34817
36781
|
headers: {
|
|
34818
|
-
'X-Sandbox-ID':
|
|
36782
|
+
'X-Sandbox-ID': tokenServerId
|
|
34819
36783
|
}
|
|
34820
36784
|
}));
|
|
34821
36785
|
}
|
|
34822
36786
|
}
|
|
36787
|
+
/** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
|
|
36788
|
+
class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
|
|
34823
36789
|
const TokenSource = {
|
|
34824
36790
|
/** TokenSource.literal contains a single, literal set of {@link TokenSourceResponseObject}
|
|
34825
36791
|
* credentials, either provided directly or returned from a provided function. */
|
|
@@ -34838,24 +36804,31 @@ const TokenSource = {
|
|
|
34838
36804
|
/**
|
|
34839
36805
|
* TokenSource.endpoint creates a token source that fetches credentials from a given URL using
|
|
34840
36806
|
* the standard endpoint format:
|
|
34841
|
-
* @see https://
|
|
36807
|
+
* @see https://docs.livekit.io/frontends/build/authentication/endpoint/
|
|
34842
36808
|
*/
|
|
34843
36809
|
endpoint(url) {
|
|
34844
36810
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
34845
36811
|
return new TokenSourceEndpoint(url, options);
|
|
34846
36812
|
},
|
|
34847
36813
|
/**
|
|
34848
|
-
*
|
|
36814
|
+
* @deprecated Use {@link TokenSource.developmentTokenServer} instead
|
|
36815
|
+
*/
|
|
36816
|
+
sandboxTokenServer(sandboxId) {
|
|
36817
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
36818
|
+
return new TokenSourceSandboxTokenServer(sandboxId, options);
|
|
36819
|
+
},
|
|
36820
|
+
/**
|
|
36821
|
+
* TokenSource.developmentTokenServer queries a development token server for credentials,
|
|
34849
36822
|
* which supports quick prototyping / getting started types of use cases.
|
|
34850
36823
|
*
|
|
34851
36824
|
* This token provider is INSECURE and should NOT be used in production.
|
|
34852
36825
|
*
|
|
34853
36826
|
* For more info:
|
|
34854
|
-
* @see https://
|
|
36827
|
+
* @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
|
|
34855
36828
|
*/
|
|
34856
|
-
|
|
36829
|
+
developmentTokenServer(tokenServerId) {
|
|
34857
36830
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
34858
|
-
return new
|
|
36831
|
+
return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
|
|
34859
36832
|
}
|
|
34860
36833
|
};/**
|
|
34861
36834
|
* Try to analyze the local track to determine the facing mode of a track.
|
|
@@ -34983,4 +36956,4 @@ const serializers = {
|
|
|
34983
36956
|
json,
|
|
34984
36957
|
raw,
|
|
34985
36958
|
custom
|
|
34986
|
-
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|
|
36959
|
+
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isSVCCodec,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsH265,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|