@blockcast/mmt-container 0.1.0-main.dc6b2132f53c → 0.1.0-main.dd5ce397d768
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-runtime.js +2519 -251
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.d.ts +7 -7
- package/dist/carp-catalog.d.ts.map +1 -1
- package/dist/carp-catalog.js +336 -12
- package/dist/carp-catalog.js.map +4 -4
- package/dist/catalog-fec-params.d.ts +105 -0
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +495 -4
- package/dist/catalog-fec-params.js.map +4 -4
- package/dist/cmaf-block-assembler.d.ts +8 -0
- package/dist/cmaf-block-assembler.d.ts.map +1 -1
- package/dist/cmaf-block-assembler.js +13 -9
- package/dist/cmaf-block-assembler.js.map +2 -2
- package/dist/cmaf-group-batcher.js.map +1 -1
- package/dist/fec-manager.d.ts +46 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +129 -34
- package/dist/fec-manager.js.map +3 -3
- package/dist/fec-worker-setup.d.ts +6 -2
- package/dist/fec-worker-setup.d.ts.map +1 -1
- package/dist/fec-worker-setup.js +45 -36
- package/dist/fec-worker-setup.js.map +3 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -1
- package/dist/index.js.map +2 -2
- package/dist/media-router.d.ts +12 -0
- package/dist/media-router.d.ts.map +1 -1
- package/dist/media-router.js +5 -19
- package/dist/media-router.js.map +2 -2
- package/dist/mfu-reassembler.d.ts +112 -1
- package/dist/mfu-reassembler.d.ts.map +1 -1
- package/dist/mfu-reassembler.js +219 -61
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.js +206 -61
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +545 -75
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.d.ts +34 -0
- package/dist/mmtp-decode-pipeline.d.ts.map +1 -1
- package/dist/mmtp-decode-pipeline.js +267 -85
- package/dist/mmtp-decode-pipeline.js.map +3 -3
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +249 -76
- package/dist/mmtp-router.js.map +3 -3
- package/dist/mmtp-to-cmaf.d.ts.map +1 -1
- package/dist/mmtp-to-cmaf.js +2 -1
- package/dist/mmtp-to-cmaf.js.map +2 -2
- package/dist/multicast-manager.d.ts +30 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +1862 -241
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +1 -2
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +1335 -23
- package/dist/multicast-transport.js.map +4 -4
- package/dist/no-modules.js +545 -75
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +10 -0
- package/dist/repair-fetch.js.map +2 -2
- package/dist/repair-layers.d.ts +141 -0
- package/dist/repair-layers.d.ts.map +1 -0
- package/dist/repair-layers.js +303 -0
- package/dist/repair-layers.js.map +7 -0
- package/dist/track-session.d.ts +64 -24
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +138 -65
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.d.ts +11 -0
- package/dist/wasm-mfu-reassembler.d.ts.map +1 -1
- package/dist/wasm-mfu-reassembler.js +42 -27
- package/dist/wasm-mfu-reassembler.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +207 -62
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +7 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +14 -5
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -1
- package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +15 -5
- package/package.json +6 -6
- package/schemas/catalog.schema.json +82 -5
- package/src/carp-catalog.ts +80 -11
- package/src/catalog-fec-params.ts +342 -2
- package/src/cmaf-block-assembler.ts +23 -1
- package/src/fec-manager.ts +168 -18
- package/src/fec-worker-setup.ts +37 -11
- package/src/index.ts +38 -0
- package/src/media-router.ts +13 -6
- package/src/mfu-reassembler.ts +431 -81
- package/src/mmtp-decode-pipeline.ts +93 -24
- package/src/mmtp-router.ts +55 -4
- package/src/mmtp-to-cmaf.ts +33 -1
- package/src/multicast-manager.ts +106 -22
- package/src/multicast-transport.ts +13 -27
- package/src/repair-fetch.ts +21 -0
- package/src/repair-layers.ts +490 -0
- package/src/track-session.ts +201 -119
- package/src/wasm-mfu-reassembler.ts +18 -1
- package/test-vectors/catalog/README.md +16 -1
- package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
- package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
- package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
- package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
- package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
- package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
- package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
- package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
- package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +23 -0
- package/test-vectors/catalog/negative/repair-track-legacy-shape.json +2 -1
- package/test-vectors/catalog/positive/fec-layered-repair.json +104 -0
package/dist/browser-runtime.js
CHANGED
|
@@ -126,7 +126,7 @@ var init_fec_worker_client = __esm({
|
|
|
126
126
|
*/
|
|
127
127
|
feedSource(ssId, data, ts, alta) {
|
|
128
128
|
if (__privateGet(this, _disposed5))
|
|
129
|
-
|
|
129
|
+
throw new Error("[FecWorkerClient] feedSource on a disposed client");
|
|
130
130
|
const cmd = alta ? {
|
|
131
131
|
type: "feedSource",
|
|
132
132
|
ssId,
|
|
@@ -149,7 +149,7 @@ var init_fec_worker_client = __esm({
|
|
|
149
149
|
*/
|
|
150
150
|
feedRepair(ssStart, ssbLength, rsId, data, ts) {
|
|
151
151
|
if (__privateGet(this, _disposed5))
|
|
152
|
-
|
|
152
|
+
throw new Error("[FecWorkerClient] feedRepair on a disposed client");
|
|
153
153
|
__privateGet(this, _worker).postMessage({
|
|
154
154
|
type: "feedRepair",
|
|
155
155
|
ssStart,
|
|
@@ -164,7 +164,7 @@ var init_fec_worker_client = __esm({
|
|
|
164
164
|
*/
|
|
165
165
|
relayBlockSig(sbn, sig, hash) {
|
|
166
166
|
if (__privateGet(this, _disposed5))
|
|
167
|
-
|
|
167
|
+
throw new Error("[FecWorkerClient] relayBlockSig on a disposed client");
|
|
168
168
|
__privateGet(this, _worker).postMessage({ type: "relayBlockSig", sbn, sig, hash }, [sig, hash]);
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
@@ -175,7 +175,7 @@ var init_fec_worker_client = __esm({
|
|
|
175
175
|
*/
|
|
176
176
|
cleanup(currentSbn) {
|
|
177
177
|
if (__privateGet(this, _disposed5))
|
|
178
|
-
|
|
178
|
+
throw new Error("[FecWorkerClient] cleanup on a disposed client");
|
|
179
179
|
if (!Number.isInteger(currentSbn) || currentSbn < 0 || currentSbn > 4294967295) {
|
|
180
180
|
throw new Error("[FecWorkerClient] cleanup: SBN must be a uint32");
|
|
181
181
|
}
|
|
@@ -188,7 +188,7 @@ var init_fec_worker_client = __esm({
|
|
|
188
188
|
/** Reset this track's decoder epoch while preserving Worker configuration. */
|
|
189
189
|
reset() {
|
|
190
190
|
if (__privateGet(this, _disposed5))
|
|
191
|
-
|
|
191
|
+
throw new Error("[FecWorkerClient] reset on a disposed client");
|
|
192
192
|
__privateWrapper(this, _pendingResets)._++;
|
|
193
193
|
__privateSet(this, _cleanupInFlight, null);
|
|
194
194
|
__privateGet(this, _queuedCleanupSbns).clear();
|
|
@@ -218,6 +218,15 @@ var init_fec_worker_client = __esm({
|
|
|
218
218
|
onError(cb) {
|
|
219
219
|
__privateSet(this, _onError, cb);
|
|
220
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* True once dispose() ran. A disposed client cannot decode, fill, or
|
|
223
|
+
* acknowledge anything, so routing symbols to it discards them; every
|
|
224
|
+
* command method (feedSource, feedRepair, relayBlockSig, cleanup, reset)
|
|
225
|
+
* throws rather than pretend otherwise. Callers check this first.
|
|
226
|
+
*/
|
|
227
|
+
get disposed() {
|
|
228
|
+
return __privateGet(this, _disposed5);
|
|
229
|
+
}
|
|
221
230
|
/**
|
|
222
231
|
* Dispose the Worker. Sends dispose command then terminates.
|
|
223
232
|
* Idempotent — safe to call multiple times.
|
|
@@ -1020,7 +1029,10 @@ function checkedSampleEnd(offset, byteLength) {
|
|
|
1020
1029
|
return Number.isSafeInteger(end) && end <= 4294967295 ? end : null;
|
|
1021
1030
|
}
|
|
1022
1031
|
var COMPLETED_ADDRESS_WINDOW_MULTIPLE = 2;
|
|
1023
|
-
|
|
1032
|
+
function parkedRunHoldsASlot(hasOpenRun, fragmentCount) {
|
|
1033
|
+
return !hasOpenRun && fragmentCount > 0;
|
|
1034
|
+
}
|
|
1035
|
+
var _buffers, _timeouts, _activeDataUnits, _pendingContinuations, _emitted, _emittedOrder, _completed, _completedOrder, _timeoutMs, _maxBuffered, _nextAdmissionSeq, _MFUReassembler_instances, addAssociatedFragment_fn, parkContinuation_fn, drainPendingContinuations_fn, unopenedParkedRunCount_fn, incompleteRunCount_fn, evictLeastRecentlyAdmitted_fn, releaseSlot_fn, reject_fn, associateDataUnit_fn, tryReassemble_fn, assemble_fn, setTimeout_fn, clearPendingTimeout_fn, armHeadlessPendingTimeout_fn, deletePendingRun_fn, onPendingTimeout_fn, clearTimeout_fn, markEmitted_fn, markCompleted_fn, orderValue_fn, removeActiveDataUnitForBuffer_fn, openRunAt_fn, onTimeout_fn, pendingDebugSamples_fn;
|
|
1024
1036
|
var MFUReassembler = class {
|
|
1025
1037
|
constructor(options) {
|
|
1026
1038
|
__privateAdd(this, _MFUReassembler_instances);
|
|
@@ -1048,6 +1060,11 @@ var MFUReassembler = class {
|
|
|
1048
1060
|
__privateAdd(this, _completedOrder, []);
|
|
1049
1061
|
__privateAdd(this, _timeoutMs);
|
|
1050
1062
|
__privateAdd(this, _maxBuffered);
|
|
1063
|
+
/**
|
|
1064
|
+
* Monotonic admission ordinal. Counts slot admissions, never reads a clock,
|
|
1065
|
+
* so eviction order survives on hosts that run `timeoutMs: null`.
|
|
1066
|
+
*/
|
|
1067
|
+
__privateAdd(this, _nextAdmissionSeq, 0);
|
|
1051
1068
|
/** Statistics for debugging */
|
|
1052
1069
|
this.stats = {
|
|
1053
1070
|
totalFragments: 0,
|
|
@@ -1081,12 +1098,9 @@ var MFUReassembler = class {
|
|
|
1081
1098
|
state.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onTimeout_fn).call(this, bufferKey), Math.max(0, state.startedAt + ms - now));
|
|
1082
1099
|
}
|
|
1083
1100
|
}
|
|
1084
|
-
for (const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (ms !== null && __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) === void 0) {
|
|
1088
|
-
pending.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address), Math.max(0, pending.startedAt + ms - now));
|
|
1089
|
-
}
|
|
1101
|
+
for (const address of __privateGet(this, _pendingContinuations).keys()) {
|
|
1102
|
+
__privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
|
|
1103
|
+
__privateMethod(this, _MFUReassembler_instances, armHeadlessPendingTimeout_fn).call(this, address);
|
|
1090
1104
|
}
|
|
1091
1105
|
}
|
|
1092
1106
|
get timeoutMs() {
|
|
@@ -1250,14 +1264,26 @@ var MFUReassembler = class {
|
|
|
1250
1264
|
this.stats.outOfOrderFragments = 0;
|
|
1251
1265
|
}
|
|
1252
1266
|
/**
|
|
1253
|
-
* Get number of pending MFUs
|
|
1267
|
+
* Get number of pending MFUs: engine buffers plus every headless parked data
|
|
1268
|
+
* unit that holds a slot.
|
|
1269
|
+
*
|
|
1270
|
+
* This is not one of the four charging sites, but it shares their second term,
|
|
1271
|
+
* so it takes the same rule — by delegating to `#unopenedParkedRunCount`, the
|
|
1272
|
+
* one site that applies it. Rust's public `buffered_count()` is
|
|
1273
|
+
* `inner.buffered_count() + unopened_parked_run_count()`, and that count is
|
|
1274
|
+
* `parked_run_holds_a_slot` — so filtering here on the open run alone would
|
|
1275
|
+
* report a phantom slot for an empty published queue in TypeScript and not in
|
|
1276
|
+
* Rust, out of a *host-observable* counter (`getStatus().bufferedMFUs`). Same
|
|
1277
|
+
* state, two numbers, decided by which engine the host loaded: the divergence
|
|
1278
|
+
* class this file exists to close, so the public counters have to agree too.
|
|
1279
|
+
*
|
|
1280
|
+
* Distinct from `#incompleteRunCount`, which sums `#activeDataUnits` rather
|
|
1281
|
+
* than `#buffers`. Those are different quantities by design and are not being
|
|
1282
|
+
* merged; only the parked term they share is required to agree, which is what
|
|
1283
|
+
* routing both through one method makes structural rather than conventional.
|
|
1254
1284
|
*/
|
|
1255
1285
|
get pendingCount() {
|
|
1256
|
-
|
|
1257
|
-
for (const address of __privateGet(this, _pendingContinuations).keys()) {
|
|
1258
|
-
if (__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) === void 0) count++;
|
|
1259
|
-
}
|
|
1260
|
-
return count;
|
|
1286
|
+
return __privateGet(this, _buffers).size + __privateMethod(this, _MFUReassembler_instances, unopenedParkedRunCount_fn).call(this);
|
|
1261
1287
|
}
|
|
1262
1288
|
/**
|
|
1263
1289
|
* Get current buffer status
|
|
@@ -1288,6 +1314,7 @@ _completed = new WeakMap();
|
|
|
1288
1314
|
_completedOrder = new WeakMap();
|
|
1289
1315
|
_timeoutMs = new WeakMap();
|
|
1290
1316
|
_maxBuffered = new WeakMap();
|
|
1317
|
+
_nextAdmissionSeq = new WeakMap();
|
|
1291
1318
|
_MFUReassembler_instances = new WeakSet();
|
|
1292
1319
|
addAssociatedFragment_fn = function(fragment, runKey, address, startedAt) {
|
|
1293
1320
|
const association = __privateMethod(this, _MFUReassembler_instances, associateDataUnit_fn).call(this, fragment, runKey, address);
|
|
@@ -1317,22 +1344,33 @@ addAssociatedFragment_fn = function(fragment, runKey, address, startedAt) {
|
|
|
1317
1344
|
};
|
|
1318
1345
|
parkContinuation_fn = function(address, fragment) {
|
|
1319
1346
|
let pending = __privateGet(this, _pendingContinuations).get(address);
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1347
|
+
const open = __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address);
|
|
1348
|
+
const alreadyCharged = open !== void 0 || parkedRunHoldsASlot(open !== void 0, pending?.fragments.size ?? 0);
|
|
1349
|
+
if (!alreadyCharged && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
|
|
1350
|
+
if (!__privateMethod(this, _MFUReassembler_instances, evictLeastRecentlyAdmitted_fn).call(this)) {
|
|
1323
1351
|
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
|
|
1324
1352
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1353
|
+
}
|
|
1354
|
+
let armAfterInsert = false;
|
|
1355
|
+
if (pending === void 0) {
|
|
1356
|
+
pending = {
|
|
1357
|
+
fragments: /* @__PURE__ */ new Map(),
|
|
1358
|
+
startedAt: Date.now(),
|
|
1359
|
+
admissionSeq: open !== void 0 ? open[1].admissionSeq : __privateWrapper(this, _nextAdmissionSeq)._++
|
|
1360
|
+
};
|
|
1330
1361
|
__privateGet(this, _pendingContinuations).set(address, pending);
|
|
1362
|
+
armAfterInsert = true;
|
|
1363
|
+
} else if (!alreadyCharged) {
|
|
1364
|
+
pending.admissionSeq = __privateWrapper(this, _nextAdmissionSeq)._++;
|
|
1365
|
+
pending.startedAt = Date.now();
|
|
1366
|
+
__privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
|
|
1367
|
+
armAfterInsert = true;
|
|
1331
1368
|
}
|
|
1332
1369
|
if (pending.fragments.has(fragment.fragmentCounter)) {
|
|
1333
1370
|
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "duplicate queued continuation counter would replace retained media");
|
|
1334
1371
|
}
|
|
1335
1372
|
pending.fragments.set(fragment.fragmentCounter, fragment);
|
|
1373
|
+
if (armAfterInsert) __privateMethod(this, _MFUReassembler_instances, armHeadlessPendingTimeout_fn).call(this, address);
|
|
1336
1374
|
};
|
|
1337
1375
|
drainPendingContinuations_fn = function(address, runKey) {
|
|
1338
1376
|
while (true) {
|
|
@@ -1355,15 +1393,92 @@ drainPendingContinuations_fn = function(address, runKey) {
|
|
|
1355
1393
|
}
|
|
1356
1394
|
}
|
|
1357
1395
|
};
|
|
1358
|
-
/**
|
|
1359
|
-
*
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1396
|
+
/** Parked data units that hold a `maxBuffered` slot of their own. Membership
|
|
1397
|
+
* is `parkedRunHoldsASlot`, the same rule #evictLeastRecentlyAdmitted offers
|
|
1398
|
+
* as candidates, so nothing can be counted here that eviction cannot reclaim.
|
|
1399
|
+
*
|
|
1400
|
+
* One site, two callers — `#incompleteRunCount` and `pendingCount` — mirroring
|
|
1401
|
+
* Rust's `unopened_parked_run_count`, which `incomplete_run_count` and
|
|
1402
|
+
* `buffered_count` both delegate to. Both callers used to inline this loop
|
|
1403
|
+
* verbatim, so converging the *rule* moved one copy and left the other as the
|
|
1404
|
+
* divergence (Ally's review of #400). The rule can no longer drift now that
|
|
1405
|
+
* every site routes through `parkedRunHoldsASlot`; keeping the loop itself at
|
|
1406
|
+
* one site is what stops the scaffolding drifting the next time this question
|
|
1407
|
+
* grows an argument. */
|
|
1408
|
+
unopenedParkedRunCount_fn = function() {
|
|
1409
|
+
let count = 0;
|
|
1410
|
+
for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
|
|
1411
|
+
if (parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
|
|
1412
|
+
count++;
|
|
1413
|
+
}
|
|
1364
1414
|
}
|
|
1365
1415
|
return count;
|
|
1366
1416
|
};
|
|
1417
|
+
/** Every open run plus every headless parked data unit that holds a slot,
|
|
1418
|
+
* one slot each. This is the quantity maxBuffered bounds. The parked term is
|
|
1419
|
+
* `#unopenedParkedRunCount`, whose membership is `parkedRunHoldsASlot` — the
|
|
1420
|
+
* same rule #evictLeastRecentlyAdmitted offers as candidates, so nothing can
|
|
1421
|
+
* be counted here that eviction cannot reclaim. */
|
|
1422
|
+
incompleteRunCount_fn = function() {
|
|
1423
|
+
return __privateGet(this, _activeDataUnits).size + __privateMethod(this, _MFUReassembler_instances, unopenedParkedRunCount_fn).call(this);
|
|
1424
|
+
};
|
|
1425
|
+
/**
|
|
1426
|
+
* Drop the incomplete data unit admitted longest ago so a newly arriving one
|
|
1427
|
+
* can take its slot. Selection is by monotonic admission ordinal, never by
|
|
1428
|
+
* wall clock, so this is a bounded-capacity replacement policy and not a
|
|
1429
|
+
* timeout: it behaves identically on hosts that never drive `cleanup_timeouts`
|
|
1430
|
+
* (`timeoutMs: null`). Only incomplete data units are candidates — a data
|
|
1431
|
+
* unit that completed has already left both maps.
|
|
1432
|
+
*
|
|
1433
|
+
* Returns false only if no slot is charged, which cannot happen at the bound
|
|
1434
|
+
* (`maxBuffered` is a positive integer); admission then falls back to the
|
|
1435
|
+
* fail-closed `active-data-unit-limit` reject, which stays a stable host code.
|
|
1436
|
+
* That holds because the candidate set here is built from the same
|
|
1437
|
+
* `parkedRunHoldsASlot` rule `#incompleteRunCount` counts by, so reaching the
|
|
1438
|
+
* bound implies at least one candidate. Filtering one and not the other would
|
|
1439
|
+
* make the claim false: a queue counted but not offered charges a slot no
|
|
1440
|
+
* eviction can reclaim, and the receiver refuses every later data unit
|
|
1441
|
+
* permanently.
|
|
1442
|
+
*/
|
|
1443
|
+
evictLeastRecentlyAdmitted_fn = function() {
|
|
1444
|
+
let victimAddress;
|
|
1445
|
+
let victimSeq = Number.POSITIVE_INFINITY;
|
|
1446
|
+
for (const [, active] of __privateGet(this, _activeDataUnits)) {
|
|
1447
|
+
if (active.admissionSeq < victimSeq) {
|
|
1448
|
+
victimSeq = active.admissionSeq;
|
|
1449
|
+
victimAddress = active.address;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
|
|
1453
|
+
if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (pending.admissionSeq < victimSeq) {
|
|
1457
|
+
victimSeq = pending.admissionSeq;
|
|
1458
|
+
victimAddress = address;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (victimAddress === void 0) return false;
|
|
1462
|
+
__privateMethod(this, _MFUReassembler_instances, releaseSlot_fn).call(this, victimAddress);
|
|
1463
|
+
this.stats.evictedMemoryLimit++;
|
|
1464
|
+
return true;
|
|
1465
|
+
};
|
|
1466
|
+
/**
|
|
1467
|
+
* Release every form of state one address holds: its open run, that run's
|
|
1468
|
+
* buffered fragments and deadline, and its continuation queue. The address is
|
|
1469
|
+
* deliberately not marked completed — the data unit never emitted, so a later
|
|
1470
|
+
* fragment of it may open a fresh slot rather than being rejected as stale.
|
|
1471
|
+
*/
|
|
1472
|
+
releaseSlot_fn = function(address) {
|
|
1473
|
+
const open = __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address);
|
|
1474
|
+
if (open !== void 0) {
|
|
1475
|
+
const [runKey, active] = open;
|
|
1476
|
+
__privateGet(this, _activeDataUnits).delete(runKey);
|
|
1477
|
+
__privateMethod(this, _MFUReassembler_instances, clearTimeout_fn).call(this, active.bufferKey);
|
|
1478
|
+
__privateGet(this, _buffers).delete(active.bufferKey);
|
|
1479
|
+
}
|
|
1480
|
+
__privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, address);
|
|
1481
|
+
};
|
|
1367
1482
|
reject_fn = function(code, message) {
|
|
1368
1483
|
this.stats.rejectedFragments++;
|
|
1369
1484
|
throw new MfuReassemblyError(code, message);
|
|
@@ -1371,10 +1486,11 @@ reject_fn = function(code, message) {
|
|
|
1371
1486
|
associateDataUnit_fn = function(fragment, runKey, address) {
|
|
1372
1487
|
const fi = fragment.fragmentationIndicator;
|
|
1373
1488
|
if (fi === 1) {
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
__privateMethod(this, _MFUReassembler_instances,
|
|
1377
|
-
|
|
1489
|
+
const parked = __privateGet(this, _pendingContinuations).get(address);
|
|
1490
|
+
const inheritsParkedSlot = parkedRunHoldsASlot(
|
|
1491
|
+
__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0,
|
|
1492
|
+
parked?.fragments.size ?? 0
|
|
1493
|
+
);
|
|
1378
1494
|
const sampleIdentity = hasSampleIdentity(fragment);
|
|
1379
1495
|
const hasSampleFields = hasAnySampleIdentityField(fragment);
|
|
1380
1496
|
const hasItemIdentity = fragment.itemId !== void 0;
|
|
@@ -1394,18 +1510,28 @@ associateDataUnit_fn = function(fragment, runKey, address) {
|
|
|
1394
1510
|
if (__privateGet(this, _buffers).has(bufferKey)) {
|
|
1395
1511
|
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "overlapping-data-unit", "intrinsic data-unit buffer already exists");
|
|
1396
1512
|
}
|
|
1513
|
+
let sampleEnd = null;
|
|
1514
|
+
if (sampleIdentity) {
|
|
1515
|
+
sampleEnd = checkedSampleEnd(fragment.sampleOffset, fragment.data.byteLength);
|
|
1516
|
+
if (sampleEnd === null) __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "offset-overflow", "sample byte offset overflow");
|
|
1517
|
+
}
|
|
1518
|
+
if (!inheritsParkedSlot && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
|
|
1519
|
+
if (!__privateMethod(this, _MFUReassembler_instances, evictLeastRecentlyAdmitted_fn).call(this)) {
|
|
1520
|
+
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const admissionSeq = inheritsParkedSlot ? parked.admissionSeq : __privateWrapper(this, _nextAdmissionSeq)._++;
|
|
1397
1524
|
let active2;
|
|
1398
1525
|
if (sampleIdentity) {
|
|
1399
|
-
const nextOffset = checkedSampleEnd(fragment.sampleOffset, fragment.data.byteLength);
|
|
1400
|
-
if (nextOffset === null) __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "offset-overflow", "sample byte offset overflow");
|
|
1401
1526
|
active2 = {
|
|
1402
1527
|
address,
|
|
1403
1528
|
kind: "sample",
|
|
1404
1529
|
bufferKey,
|
|
1405
1530
|
nextFragmentCounter: fragment.fragmentCounter - 1,
|
|
1531
|
+
admissionSeq,
|
|
1406
1532
|
movieFragmentSequenceNumber: fragment.movieFragmentSequenceNumber,
|
|
1407
1533
|
sampleNumber: fragment.sampleNumber,
|
|
1408
|
-
nextOffset,
|
|
1534
|
+
nextOffset: sampleEnd,
|
|
1409
1535
|
...fragment.catalogTrackName === void 0 ? {} : { catalogTrackName: fragment.catalogTrackName }
|
|
1410
1536
|
};
|
|
1411
1537
|
} else {
|
|
@@ -1414,6 +1540,7 @@ associateDataUnit_fn = function(fragment, runKey, address) {
|
|
|
1414
1540
|
kind: "item",
|
|
1415
1541
|
bufferKey,
|
|
1416
1542
|
nextFragmentCounter: fragment.fragmentCounter - 1,
|
|
1543
|
+
admissionSeq,
|
|
1417
1544
|
itemId: fragment.itemId,
|
|
1418
1545
|
...fragment.catalogTrackName === void 0 ? {} : { catalogTrackName: fragment.catalogTrackName }
|
|
1419
1546
|
};
|
|
@@ -1534,11 +1661,19 @@ clearPendingTimeout_fn = function(address) {
|
|
|
1534
1661
|
/**
|
|
1535
1662
|
* A queue whose run closed without completing is headless again and expires
|
|
1536
1663
|
* from its own first arrival, exactly as it would had the head never come.
|
|
1664
|
+
*
|
|
1665
|
+
* An empty queue is not armed: it holds no data unit that could fail to
|
|
1666
|
+
* complete, so there is nothing for a deadline to expire. Every arming site
|
|
1667
|
+
* reaches this function, which takes that test through `parkedRunHoldsASlot`
|
|
1668
|
+
* rather than filtering on the open run alone, which is how this engine
|
|
1669
|
+
* reaches Rust's answer: its
|
|
1670
|
+
* `next_timeout_at` derives candidate deadlines from the queue's live
|
|
1671
|
+
* entries, so an empty queue yields none (BLO-32420).
|
|
1537
1672
|
*/
|
|
1538
1673
|
armHeadlessPendingTimeout_fn = function(address) {
|
|
1539
1674
|
const pending = __privateGet(this, _pendingContinuations).get(address);
|
|
1540
1675
|
if (pending === void 0 || pending.timeoutId !== void 0 || __privateGet(this, _timeoutMs) === null) return;
|
|
1541
|
-
if (__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0) return;
|
|
1676
|
+
if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) return;
|
|
1542
1677
|
pending.timeoutId = setTimeout(
|
|
1543
1678
|
() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address),
|
|
1544
1679
|
Math.max(0, pending.startedAt + __privateGet(this, _timeoutMs) - Date.now())
|
|
@@ -1551,6 +1686,10 @@ deletePendingRun_fn = function(address) {
|
|
|
1551
1686
|
onPendingTimeout_fn = function(address) {
|
|
1552
1687
|
const pending = __privateGet(this, _pendingContinuations).get(address);
|
|
1553
1688
|
if (pending === void 0) return;
|
|
1689
|
+
if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
|
|
1690
|
+
__privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1554
1693
|
this.stats.timedOutMFUs++;
|
|
1555
1694
|
console.warn(
|
|
1556
1695
|
`[MfuReassembler] timeout: run=${address} fragments=${pending.fragments.size} (continuations awaiting first fragment, discarded)`
|
|
@@ -2815,8 +2954,9 @@ if (void 0) {
|
|
|
2815
2954
|
packetSequenceNumber: testPacketSequence(1, 0)
|
|
2816
2955
|
})
|
|
2817
2956
|
).toBeNull();
|
|
2818
|
-
|
|
2819
|
-
|
|
2957
|
+
expect(reassembler.pendingCount).toBe(1);
|
|
2958
|
+
expect(
|
|
2959
|
+
reassembler.addFragment({
|
|
2820
2960
|
fragmentationIndicator: 3,
|
|
2821
2961
|
mpuSequenceNumber: 2,
|
|
2822
2962
|
fragmentCounter: 0,
|
|
@@ -2824,24 +2964,24 @@ if (void 0) {
|
|
|
2824
2964
|
timestamp: 1000n,
|
|
2825
2965
|
rapFlag: false,
|
|
2826
2966
|
packetSequenceNumber: testPacketSequence(2, 0)
|
|
2827
|
-
})
|
|
2828
|
-
|
|
2829
|
-
);
|
|
2830
|
-
expectRejection(
|
|
2831
|
-
() => reassembler.addFragment({
|
|
2832
|
-
fragmentationIndicator: 1,
|
|
2833
|
-
mpuSequenceNumber: 2,
|
|
2834
|
-
fragmentCounter: 1,
|
|
2835
|
-
itemId: 2,
|
|
2836
|
-
data: new Uint8Array([2]),
|
|
2837
|
-
timestamp: 1000n,
|
|
2838
|
-
rapFlag: true,
|
|
2839
|
-
packetSequenceNumber: testPacketSequence(2, 1)
|
|
2840
|
-
}),
|
|
2841
|
-
"active-data-unit-limit"
|
|
2842
|
-
);
|
|
2967
|
+
})
|
|
2968
|
+
).toBeNull();
|
|
2843
2969
|
expect(reassembler.pendingCount).toBe(1);
|
|
2844
2970
|
expect(reassembler.getStatus().pendingFragments).toBe(1);
|
|
2971
|
+
expect(reassembler.stats.evictedMemoryLimit).toBe(1);
|
|
2972
|
+
const completed = reassembler.addFragment({
|
|
2973
|
+
fragmentationIndicator: 1,
|
|
2974
|
+
mpuSequenceNumber: 2,
|
|
2975
|
+
fragmentCounter: 1,
|
|
2976
|
+
itemId: 2,
|
|
2977
|
+
data: new Uint8Array([4]),
|
|
2978
|
+
timestamp: 1000n,
|
|
2979
|
+
rapFlag: true,
|
|
2980
|
+
packetSequenceNumber: testPacketSequence(2, 1)
|
|
2981
|
+
});
|
|
2982
|
+
expect(completed?.data).toEqual(new Uint8Array([4, 2]));
|
|
2983
|
+
expect(reassembler.stats.evictedMemoryLimit).toBe(1);
|
|
2984
|
+
expect(reassembler.pendingCount).toBe(0);
|
|
2845
2985
|
const result = reassembler.addFragment({
|
|
2846
2986
|
fragmentationIndicator: 1,
|
|
2847
2987
|
mpuSequenceNumber: 1,
|
|
@@ -2852,7 +2992,8 @@ if (void 0) {
|
|
|
2852
2992
|
rapFlag: true,
|
|
2853
2993
|
packetSequenceNumber: testPacketSequence(1, 1)
|
|
2854
2994
|
});
|
|
2855
|
-
expect(result
|
|
2995
|
+
expect(result).toBeNull();
|
|
2996
|
+
expect(reassembler.pendingCount).toBe(1);
|
|
2856
2997
|
});
|
|
2857
2998
|
test("expires a continuation that never receives its first fragment", () => {
|
|
2858
2999
|
vi.useFakeTimers();
|
|
@@ -2917,7 +3058,7 @@ if (void 0) {
|
|
|
2917
3058
|
vi.useRealTimers();
|
|
2918
3059
|
}
|
|
2919
3060
|
});
|
|
2920
|
-
test("bounds active data units
|
|
3061
|
+
test("bounds active data units by evicting the oldest at maxBuffered", () => {
|
|
2921
3062
|
const reassembler = new MFUReassembler({
|
|
2922
3063
|
timeoutMs: 1e3,
|
|
2923
3064
|
maxBuffered: 2
|
|
@@ -2934,8 +3075,8 @@ if (void 0) {
|
|
|
2934
3075
|
packetSequenceNumber: testPacketSequence(i, 1)
|
|
2935
3076
|
});
|
|
2936
3077
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
3078
|
+
expect(
|
|
3079
|
+
reassembler.addFragment({
|
|
2939
3080
|
fragmentationIndicator: 1,
|
|
2940
3081
|
mpuSequenceNumber: 2,
|
|
2941
3082
|
fragmentCounter: 1,
|
|
@@ -2944,12 +3085,25 @@ if (void 0) {
|
|
|
2944
3085
|
timestamp: 200n,
|
|
2945
3086
|
rapFlag: false,
|
|
2946
3087
|
packetSequenceNumber: testPacketSequence(2, 1)
|
|
2947
|
-
})
|
|
2948
|
-
|
|
2949
|
-
);
|
|
3088
|
+
})
|
|
3089
|
+
).toBeNull();
|
|
3090
|
+
expect(reassembler.pendingCount).toBe(2);
|
|
3091
|
+
expect(reassembler.stats.timedOutMFUs).toBe(0);
|
|
3092
|
+
expect(reassembler.stats.evictedMemoryLimit).toBe(1);
|
|
3093
|
+
expect(
|
|
3094
|
+
reassembler.addFragment({
|
|
3095
|
+
fragmentationIndicator: 3,
|
|
3096
|
+
mpuSequenceNumber: 0,
|
|
3097
|
+
fragmentCounter: 0,
|
|
3098
|
+
data: new Uint8Array([9]),
|
|
3099
|
+
timestamp: 300n,
|
|
3100
|
+
rapFlag: false,
|
|
3101
|
+
packetSequenceNumber: testPacketSequence(0, 0)
|
|
3102
|
+
})
|
|
3103
|
+
).toBeNull();
|
|
2950
3104
|
expect(reassembler.pendingCount).toBe(2);
|
|
3105
|
+
expect(reassembler.stats.evictedMemoryLimit).toBe(2);
|
|
2951
3106
|
expect(reassembler.stats.timedOutMFUs).toBe(0);
|
|
2952
|
-
expect(reassembler.stats.evictedMemoryLimit).toBe(0);
|
|
2953
3107
|
});
|
|
2954
3108
|
test("should report stats correctly", () => {
|
|
2955
3109
|
const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
|
|
@@ -5649,6 +5803,289 @@ function resolveMulticastAuthCatalog(catalog, playerMode) {
|
|
|
5649
5803
|
};
|
|
5650
5804
|
}
|
|
5651
5805
|
|
|
5806
|
+
// src/repair-layers.ts
|
|
5807
|
+
var REPAIR_PRIORITY_MIN = 192;
|
|
5808
|
+
var REPAIR_PRIORITY_MAX = 255;
|
|
5809
|
+
var DEFAULT_REPAIR_PRIORITY = 240;
|
|
5810
|
+
var MAX_RAPTORQ_SOURCE_SYMBOLS = 56403;
|
|
5811
|
+
var MAX_REPAIR_SYMBOLS = 16777215;
|
|
5812
|
+
var MAX_ENCODING_SYMBOL_ID = 16777215;
|
|
5813
|
+
var RepairContractError = class extends Error {
|
|
5814
|
+
constructor(code, path, message) {
|
|
5815
|
+
super(`[repair-layers:${code}] ${path}: ${message}`);
|
|
5816
|
+
this.code = code;
|
|
5817
|
+
this.path = path;
|
|
5818
|
+
this.name = "RepairContractError";
|
|
5819
|
+
}
|
|
5820
|
+
};
|
|
5821
|
+
var throwRepairContractError = (code, path, message) => {
|
|
5822
|
+
throw new RepairContractError(code, path, message);
|
|
5823
|
+
};
|
|
5824
|
+
var REPAIR_FIELDS = ["depends", "repairLayer", "repairSymbols", "priority", "scope", "sourceSymbols"];
|
|
5825
|
+
var REPAIR_TRACK_GEOMETRY_KEYS = [
|
|
5826
|
+
"mmtpMode",
|
|
5827
|
+
"timescale",
|
|
5828
|
+
"groupDurationMs",
|
|
5829
|
+
"groupDurationTicks",
|
|
5830
|
+
"keyframeIntervalMs",
|
|
5831
|
+
"keyframeIntervalTicks",
|
|
5832
|
+
"fec",
|
|
5833
|
+
"symbolSize",
|
|
5834
|
+
"interleaveDepthMs"
|
|
5835
|
+
];
|
|
5836
|
+
var REPAIR_TRACK_ONLY_KEYS = ["repairLayer", "repairSymbols", "sourceSymbols", "scope"];
|
|
5837
|
+
function rejectRepairTrackGeometry(path, fields, fail) {
|
|
5838
|
+
for (const key of REPAIR_TRACK_GEOMETRY_KEYS) {
|
|
5839
|
+
if (fields[key] !== void 0) {
|
|
5840
|
+
fail(
|
|
5841
|
+
"track_repair_geometry",
|
|
5842
|
+
`${path}.${key}`,
|
|
5843
|
+
"a repair track carries no block geometry; it is shared with the source track's fec object"
|
|
5844
|
+
);
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
function repairReachability(fecObjects) {
|
|
5849
|
+
let declaresFecSource = false;
|
|
5850
|
+
const layerZeroNames = /* @__PURE__ */ new Set();
|
|
5851
|
+
for (const candidate of fecObjects) {
|
|
5852
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) continue;
|
|
5853
|
+
const fec = candidate;
|
|
5854
|
+
if (fec["algorithm"] === "none") continue;
|
|
5855
|
+
declaresFecSource = true;
|
|
5856
|
+
const repairTrack = fec["repairTrack"];
|
|
5857
|
+
if (typeof repairTrack === "string" && repairTrack.length > 0) layerZeroNames.add(repairTrack);
|
|
5858
|
+
}
|
|
5859
|
+
return { declaresFecSource, layerZeroNames };
|
|
5860
|
+
}
|
|
5861
|
+
function assertRepairTrackReachable(descriptor, reachability, fail) {
|
|
5862
|
+
if (dependsTarget(descriptor) !== void 0) return;
|
|
5863
|
+
if (reachability.declaresFecSource && !reachability.layerZeroNames.has(descriptor.name)) {
|
|
5864
|
+
fail(
|
|
5865
|
+
"track_repair_depends",
|
|
5866
|
+
`${descriptor.path}.depends`,
|
|
5867
|
+
"no source names this repair track in fec.repairTrack and it declares no depends"
|
|
5868
|
+
);
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
function rejectRepairFieldsOnMediaTrack(path, fields, fail) {
|
|
5872
|
+
for (const key of REPAIR_TRACK_ONLY_KEYS) {
|
|
5873
|
+
if (fields[key] !== void 0) {
|
|
5874
|
+
fail("track_repair_layer", `${path}.${key}`, `${key} is a repair track field`);
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
function describeRepairTrack(name, path, fields) {
|
|
5879
|
+
const descriptor = { name, path };
|
|
5880
|
+
for (const field of REPAIR_FIELDS) {
|
|
5881
|
+
if (field in fields) descriptor[field] = fields[field];
|
|
5882
|
+
}
|
|
5883
|
+
return descriptor;
|
|
5884
|
+
}
|
|
5885
|
+
function isSafeInteger(value) {
|
|
5886
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
5887
|
+
}
|
|
5888
|
+
function positiveInteger(value) {
|
|
5889
|
+
return isSafeInteger(value) && value > 0;
|
|
5890
|
+
}
|
|
5891
|
+
function dependsTarget(descriptor) {
|
|
5892
|
+
const depends = descriptor.depends;
|
|
5893
|
+
return Array.isArray(depends) && depends.length === 1 && typeof depends[0] === "string" && depends[0].length > 0 ? depends[0] : void 0;
|
|
5894
|
+
}
|
|
5895
|
+
function validateRepairTrackFields(descriptor, fail) {
|
|
5896
|
+
const { path } = descriptor;
|
|
5897
|
+
if (descriptor.depends !== void 0 && dependsTarget(descriptor) === void 0) {
|
|
5898
|
+
fail("track_repair_depends", `${path}.depends`, "must name exactly one source track");
|
|
5899
|
+
}
|
|
5900
|
+
if (descriptor.repairLayer !== void 0 && (!isSafeInteger(descriptor.repairLayer) || descriptor.repairLayer < 0)) {
|
|
5901
|
+
fail("track_repair_layer", `${path}.repairLayer`, "must be a non-negative integer");
|
|
5902
|
+
}
|
|
5903
|
+
if (descriptor.repairSymbols !== void 0 && (!positiveInteger(descriptor.repairSymbols) || descriptor.repairSymbols > MAX_REPAIR_SYMBOLS)) {
|
|
5904
|
+
fail("track_repair_layer", `${path}.repairSymbols`, `must be a positive integer at most ${MAX_REPAIR_SYMBOLS}`);
|
|
5905
|
+
}
|
|
5906
|
+
if (descriptor.priority !== void 0 && (!isSafeInteger(descriptor.priority) || descriptor.priority < REPAIR_PRIORITY_MIN || descriptor.priority > REPAIR_PRIORITY_MAX)) {
|
|
5907
|
+
fail(
|
|
5908
|
+
"track_repair_priority",
|
|
5909
|
+
`${path}.priority`,
|
|
5910
|
+
`must be an integer from ${REPAIR_PRIORITY_MIN} to ${REPAIR_PRIORITY_MAX} (MoQT scale, lower first)`
|
|
5911
|
+
);
|
|
5912
|
+
}
|
|
5913
|
+
if (descriptor.scope !== void 0) {
|
|
5914
|
+
if (descriptor.scope !== "keyframe") {
|
|
5915
|
+
fail("track_repair_overlay", `${path}.scope`, 'must be "keyframe"');
|
|
5916
|
+
}
|
|
5917
|
+
if (descriptor.repairLayer !== void 0) {
|
|
5918
|
+
fail("track_repair_overlay", `${path}.repairLayer`, "an overlay is a separate FEC instance and carries no repairLayer");
|
|
5919
|
+
}
|
|
5920
|
+
for (const field of ["depends", "sourceSymbols", "repairSymbols", "priority"]) {
|
|
5921
|
+
if (descriptor[field] === void 0) {
|
|
5922
|
+
fail("track_repair_overlay", `${path}.${field}`, `an overlay must declare ${field}`);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
if (descriptor.sourceSymbols !== void 0) {
|
|
5927
|
+
if (descriptor.scope === void 0) {
|
|
5928
|
+
fail("track_repair_overlay", `${path}.sourceSymbols`, "only an overlay (scope: keyframe) declares its own sourceSymbols");
|
|
5929
|
+
}
|
|
5930
|
+
if (!positiveInteger(descriptor.sourceSymbols) || descriptor.sourceSymbols > MAX_RAPTORQ_SOURCE_SYMBOLS) {
|
|
5931
|
+
fail(
|
|
5932
|
+
"track_fec_bounds",
|
|
5933
|
+
`${path}.sourceSymbols`,
|
|
5934
|
+
`must be a positive integer at most ${MAX_RAPTORQ_SOURCE_SYMBOLS} (RFC 6330 source block limit)`
|
|
5935
|
+
);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
if (isSafeInteger(descriptor.repairLayer) && descriptor.repairLayer >= 1) {
|
|
5939
|
+
for (const field of ["depends", "repairSymbols", "priority"]) {
|
|
5940
|
+
if (descriptor[field] === void 0) {
|
|
5941
|
+
fail("track_repair_layer", `${path}.${field}`, `repair layer ${descriptor.repairLayer} must declare ${field}`);
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
function assembleRepairLayers(source, descriptors, fail = throwRepairContractError, options = {}) {
|
|
5947
|
+
const layerZeroName = typeof source.repairTrack === "string" && source.repairTrack.length > 0 ? source.repairTrack : void 0;
|
|
5948
|
+
const layerZero = layerZeroName === void 0 ? void 0 : descriptors.find((d) => d.name === layerZeroName);
|
|
5949
|
+
if (layerZeroName !== void 0 && layerZero === void 0 && options.layerZeroMustBeDeclared) {
|
|
5950
|
+
fail("track_repair_track", `${source.path}.repairTrack`, `unknown FEC repair track ${JSON.stringify(layerZeroName)}`);
|
|
5951
|
+
}
|
|
5952
|
+
const dependents = descriptors.filter((d) => d.name !== layerZeroName && dependsTarget(d) === source.name);
|
|
5953
|
+
const k = positiveInteger(source.sourceSymbols) ? source.sourceSymbols : 0;
|
|
5954
|
+
if (source.algorithm === "raptorq" && k > MAX_RAPTORQ_SOURCE_SYMBOLS) {
|
|
5955
|
+
fail(
|
|
5956
|
+
"track_fec_bounds",
|
|
5957
|
+
`${source.path}.sourceSymbols`,
|
|
5958
|
+
`must be at most ${MAX_RAPTORQ_SOURCE_SYMBOLS} (RFC 6330 source block limit)`
|
|
5959
|
+
);
|
|
5960
|
+
}
|
|
5961
|
+
if (layerZeroName === void 0) {
|
|
5962
|
+
if (dependents.length === 0) return void 0;
|
|
5963
|
+
fail(
|
|
5964
|
+
"track_repair_layer",
|
|
5965
|
+
`${dependents[0].path}.depends`,
|
|
5966
|
+
`repair layers of ${JSON.stringify(source.name)} require a layer 0 (fec.repairTrack) on the source`
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
const layered = dependents.length > 0;
|
|
5970
|
+
let layerZeroSymbols = 0;
|
|
5971
|
+
if (positiveInteger(source.repairSymbols)) {
|
|
5972
|
+
layerZeroSymbols = source.repairSymbols;
|
|
5973
|
+
} else if (layered) {
|
|
5974
|
+
fail("track_fec", `${source.path}.repairSymbols`, "layered or overlay repair needs a positive integer repairSymbols on layer 0");
|
|
5975
|
+
}
|
|
5976
|
+
if (layered && !positiveInteger(source.sourceSymbols)) {
|
|
5977
|
+
fail("track_fec", `${source.path}.sourceSymbols`, "layered or overlay repair needs a positive integer sourceSymbols on the source");
|
|
5978
|
+
}
|
|
5979
|
+
if (layered && source.algorithm !== "raptorq") {
|
|
5980
|
+
fail(
|
|
5981
|
+
"track_fec",
|
|
5982
|
+
`${source.path}.algorithm`,
|
|
5983
|
+
"repair layers and overlays are defined only for raptorq: a block code fixes its codeword length, so symbols of a larger P are not symbols of the code a receiver holding fewer layers decodes"
|
|
5984
|
+
);
|
|
5985
|
+
}
|
|
5986
|
+
let layerZeroPriority = DEFAULT_REPAIR_PRIORITY;
|
|
5987
|
+
if (layerZero !== void 0) {
|
|
5988
|
+
if (layerZero.depends !== void 0 && dependsTarget(layerZero) !== source.name) {
|
|
5989
|
+
fail(
|
|
5990
|
+
"track_repair_depends",
|
|
5991
|
+
`${layerZero.path}.depends`,
|
|
5992
|
+
`layer 0 of ${JSON.stringify(source.name)} must depend on that source`
|
|
5993
|
+
);
|
|
5994
|
+
}
|
|
5995
|
+
if (layerZero.repairLayer !== void 0 && layerZero.repairLayer !== 0) {
|
|
5996
|
+
fail("track_repair_layer", `${layerZero.path}.repairLayer`, "the track named by fec.repairTrack is layer 0");
|
|
5997
|
+
}
|
|
5998
|
+
if (layerZero.scope !== void 0) {
|
|
5999
|
+
fail("track_repair_overlay", `${layerZero.path}.scope`, "the track named by fec.repairTrack is base layer 0, not an overlay");
|
|
6000
|
+
}
|
|
6001
|
+
if (layerZero.repairSymbols !== void 0 && layerZero.repairSymbols !== layerZeroSymbols) {
|
|
6002
|
+
fail(
|
|
6003
|
+
"track_repair_layer",
|
|
6004
|
+
`${layerZero.path}.repairSymbols`,
|
|
6005
|
+
`must agree with the source's fec.repairSymbols (${layerZeroSymbols})`
|
|
6006
|
+
);
|
|
6007
|
+
}
|
|
6008
|
+
if (typeof layerZero.priority === "number") layerZeroPriority = layerZero.priority;
|
|
6009
|
+
}
|
|
6010
|
+
const baseLayers = [
|
|
6011
|
+
{ layer: 0, repairTrackName: layerZeroName, repairSymbols: layerZeroSymbols, rsIdOffset: 0, priority: layerZeroPriority }
|
|
6012
|
+
];
|
|
6013
|
+
const overlays = [];
|
|
6014
|
+
for (const d of dependents) {
|
|
6015
|
+
if (d.scope === void 0 && d.repairLayer === void 0) {
|
|
6016
|
+
fail(
|
|
6017
|
+
"track_repair_layer",
|
|
6018
|
+
`${d.path}.repairLayer`,
|
|
6019
|
+
`a repair track that depends on ${JSON.stringify(source.name)} must declare repairLayer (1..n) or scope: "keyframe"`
|
|
6020
|
+
);
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
const layerTracks = dependents.filter((d) => d.scope === void 0).sort((a, b) => a.repairLayer - b.repairLayer);
|
|
6024
|
+
for (const d of dependents) {
|
|
6025
|
+
if (d.scope !== "keyframe") continue;
|
|
6026
|
+
if (overlays.length === 1) {
|
|
6027
|
+
fail(
|
|
6028
|
+
"track_repair_overlay",
|
|
6029
|
+
`${d.path}.scope`,
|
|
6030
|
+
`${JSON.stringify(source.name)} already has keyframe overlay ${JSON.stringify(overlays[0].repairTrackName)}: a RAP packet carries exactly two Source FEC Payload IDs (base, then overlay), so one overlay per source`
|
|
6031
|
+
);
|
|
6032
|
+
}
|
|
6033
|
+
const sourceSymbols = d.sourceSymbols;
|
|
6034
|
+
const repairSymbols = d.repairSymbols;
|
|
6035
|
+
if (sourceSymbols + repairSymbols - 1 > MAX_ENCODING_SYMBOL_ID) {
|
|
6036
|
+
fail(
|
|
6037
|
+
"track_fec_bounds",
|
|
6038
|
+
`${d.path}.repairSymbols`,
|
|
6039
|
+
`overlay sourceSymbols + repairSymbols - 1 exceeds the 24-bit ESI space (${MAX_ENCODING_SYMBOL_ID})`
|
|
6040
|
+
);
|
|
6041
|
+
}
|
|
6042
|
+
overlays.push({ repairTrackName: d.name, scope: "keyframe", sourceSymbols, repairSymbols, priority: d.priority });
|
|
6043
|
+
}
|
|
6044
|
+
let offset = layerZeroSymbols;
|
|
6045
|
+
let previousPriority = layerZeroPriority;
|
|
6046
|
+
layerTracks.forEach((d, i) => {
|
|
6047
|
+
const expected = i + 1;
|
|
6048
|
+
if (d.repairLayer === 0) {
|
|
6049
|
+
fail("track_repair_layer", `${d.path}.repairLayer`, `layer 0 of ${JSON.stringify(source.name)} is ${JSON.stringify(layerZeroName)}, named by fec.repairTrack`);
|
|
6050
|
+
}
|
|
6051
|
+
if (d.repairLayer !== expected) {
|
|
6052
|
+
fail(
|
|
6053
|
+
"track_repair_layer",
|
|
6054
|
+
`${d.path}.repairLayer`,
|
|
6055
|
+
i > 0 && d.repairLayer === layerTracks[i - 1].repairLayer ? `repair layer ${d.repairLayer} is declared twice for ${JSON.stringify(source.name)}` : `repair layers must be contiguous from 1: expected ${expected}, found ${d.repairLayer}`
|
|
6056
|
+
);
|
|
6057
|
+
}
|
|
6058
|
+
const priority = d.priority;
|
|
6059
|
+
if (priority < previousPriority) {
|
|
6060
|
+
fail(
|
|
6061
|
+
"track_repair_priority",
|
|
6062
|
+
`${d.path}.priority`,
|
|
6063
|
+
`must be at least ${previousPriority}, the priority of layer ${expected - 1}: priority is monotonic non-decreasing with layer index`
|
|
6064
|
+
);
|
|
6065
|
+
}
|
|
6066
|
+
previousPriority = priority;
|
|
6067
|
+
const repairSymbols = d.repairSymbols;
|
|
6068
|
+
baseLayers.push({ layer: expected, repairTrackName: d.name, repairSymbols, rsIdOffset: offset, priority });
|
|
6069
|
+
offset += repairSymbols;
|
|
6070
|
+
});
|
|
6071
|
+
const totalBaseRepairSymbols = offset;
|
|
6072
|
+
if (totalBaseRepairSymbols > MAX_REPAIR_SYMBOLS) {
|
|
6073
|
+
fail(
|
|
6074
|
+
"track_fec_bounds",
|
|
6075
|
+
`${layerTracks.length === 0 ? source.path : layerTracks[layerTracks.length - 1].path}.repairSymbols`,
|
|
6076
|
+
`total repair symbols across base layers exceeds the 24-bit RS_ID space (${MAX_REPAIR_SYMBOLS})`
|
|
6077
|
+
);
|
|
6078
|
+
}
|
|
6079
|
+
if (k + totalBaseRepairSymbols - 1 > MAX_ENCODING_SYMBOL_ID) {
|
|
6080
|
+
fail(
|
|
6081
|
+
"track_fec_bounds",
|
|
6082
|
+
`${source.path}.sourceSymbols`,
|
|
6083
|
+
`sourceSymbols + total base repair symbols - 1 exceeds the 24-bit ESI space (${MAX_ENCODING_SYMBOL_ID})`
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
return { sourceTrackName: source.name, baseLayers, overlays, totalBaseRepairSymbols };
|
|
6087
|
+
}
|
|
6088
|
+
|
|
5652
6089
|
// src/carp-catalog.ts
|
|
5653
6090
|
var VALID_ROLE_SUFFIXES = /* @__PURE__ */ new Set(["base", "delta", "repair"]);
|
|
5654
6091
|
function parseTrackRole(trackName) {
|
|
@@ -5775,7 +6212,7 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5775
6212
|
const tracks = catalog["tracks"];
|
|
5776
6213
|
const trackRecords = [];
|
|
5777
6214
|
const effective = (track, field) => track[field] ?? common[field];
|
|
5778
|
-
const
|
|
6215
|
+
const positiveInteger2 = (input) => Number.isSafeInteger(input) && input > 0;
|
|
5779
6216
|
const positiveNumber = (input) => typeof input === "number" && Number.isFinite(input) && input > 0;
|
|
5780
6217
|
for (let index = 0; index < tracks.length; index++) {
|
|
5781
6218
|
const raw = tracks[index];
|
|
@@ -5811,21 +6248,27 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5811
6248
|
const groupDurationTicks = effective(track, "groupDurationTicks");
|
|
5812
6249
|
const keyframeIntervalMs = effective(track, "keyframeIntervalMs");
|
|
5813
6250
|
const keyframeIntervalTicks = effective(track, "keyframeIntervalTicks");
|
|
5814
|
-
if (groupDurationMs !== void 0 && !
|
|
6251
|
+
if (groupDurationMs !== void 0 && !positiveInteger2(groupDurationMs)) {
|
|
5815
6252
|
catalogError("track_group_duration_ms", `${path}.groupDurationMs`, "must be a positive integer");
|
|
5816
6253
|
}
|
|
5817
|
-
if (groupDurationTicks !== void 0 && !
|
|
6254
|
+
if (groupDurationTicks !== void 0 && !positiveInteger2(groupDurationTicks)) {
|
|
5818
6255
|
catalogError("track_group_duration_ticks", `${path}.groupDurationTicks`, "must be a positive integer");
|
|
5819
6256
|
}
|
|
5820
|
-
if (keyframeIntervalMs !== void 0 && !positiveNumber(keyframeIntervalMs) || keyframeIntervalTicks !== void 0 && !
|
|
6257
|
+
if (keyframeIntervalMs !== void 0 && !positiveNumber(keyframeIntervalMs) || keyframeIntervalTicks !== void 0 && !positiveInteger2(keyframeIntervalTicks)) {
|
|
5821
6258
|
catalogError("track_keyframe_interval", path, "keyframe interval fields must be positive numbers");
|
|
5822
6259
|
}
|
|
6260
|
+
if (packaging === "fec-repair") {
|
|
6261
|
+
rejectRepairTrackGeometry(path, track, catalogError);
|
|
6262
|
+
validateRepairTrackFields(describeRepairTrack(name, path, track), catalogError);
|
|
6263
|
+
} else {
|
|
6264
|
+
rejectRepairFieldsOnMediaTrack(path, track, catalogError);
|
|
6265
|
+
}
|
|
5823
6266
|
if (packaging === "mmtp") {
|
|
5824
6267
|
const mmtpMode = effective(track, "mmtpMode");
|
|
5825
6268
|
if (mmtpMode !== "mpu" && mmtpMode !== "mfu") {
|
|
5826
6269
|
catalogError("track_mmtp_mode", `${path}.mmtpMode`, "must be mpu or mfu");
|
|
5827
6270
|
}
|
|
5828
|
-
if (!
|
|
6271
|
+
if (!positiveInteger2(timescale)) {
|
|
5829
6272
|
catalogError("track_timescale", `${path}.timescale`, "must be a positive integer");
|
|
5830
6273
|
}
|
|
5831
6274
|
if (groupDurationMs === void 0 && groupDurationTicks === void 0) {
|
|
@@ -5852,12 +6295,19 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5852
6295
|
if ("interleaveDepth" in config || "interleaveMs" in config) {
|
|
5853
6296
|
catalogError("track_fec", `${path}.fec`, "interleaveDepthMs is the only supported interleave field");
|
|
5854
6297
|
}
|
|
5855
|
-
if (
|
|
6298
|
+
if ("enhancementRepair" in config) {
|
|
6299
|
+
catalogError(
|
|
6300
|
+
"track_fec",
|
|
6301
|
+
`${path}.fec.enhancementRepair`,
|
|
6302
|
+
"repair layers are self-describing fec-repair tracks (depends, repairLayer); fec.enhancementRepair is not supported"
|
|
6303
|
+
);
|
|
6304
|
+
}
|
|
6305
|
+
if (config["interleaveDepthMs"] !== void 0 && !positiveInteger2(config["interleaveDepthMs"])) {
|
|
5856
6306
|
catalogError("track_fec", `${path}.fec.interleaveDepthMs`, "must be a positive integer");
|
|
5857
6307
|
}
|
|
5858
6308
|
if (algorithm !== "none") {
|
|
5859
6309
|
for (const field of ["sourceSymbols", "repairSymbols", "symbolSize"]) {
|
|
5860
|
-
if (!
|
|
6310
|
+
if (!positiveInteger2(config[field])) {
|
|
5861
6311
|
catalogError("track_fec", `${path}.fec.${field}`, "must be a positive integer");
|
|
5862
6312
|
}
|
|
5863
6313
|
}
|
|
@@ -5867,15 +6317,43 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5867
6317
|
}
|
|
5868
6318
|
}
|
|
5869
6319
|
}
|
|
6320
|
+
const repairDescriptors = trackRecords.flatMap(
|
|
6321
|
+
(track, index) => effective(track, "packaging") === "fec-repair" ? [describeRepairTrack(track["name"], `$.tracks[${index}]`, track)] : []
|
|
6322
|
+
);
|
|
6323
|
+
const reachability = repairReachability(
|
|
6324
|
+
trackRecords.filter((track) => effective(track, "packaging") !== "fec-repair").map((track) => track["fec"])
|
|
6325
|
+
);
|
|
6326
|
+
for (const descriptor of repairDescriptors) {
|
|
6327
|
+
assertRepairTrackReachable(descriptor, reachability, catalogError);
|
|
6328
|
+
const target = dependsTarget(descriptor);
|
|
6329
|
+
if (target === void 0) continue;
|
|
6330
|
+
const source = trackRecords.find((candidate) => candidate["name"] === target);
|
|
6331
|
+
const sourceFec = source?.["fec"];
|
|
6332
|
+
if (!source || effective(source, "packaging") !== "mmtp" || !sourceFec || sourceFec["algorithm"] === "none") {
|
|
6333
|
+
catalogError(
|
|
6334
|
+
"track_repair_depends",
|
|
6335
|
+
`${descriptor.path}.depends`,
|
|
6336
|
+
`${JSON.stringify(target)} is not an mmtp track with FEC`
|
|
6337
|
+
);
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
5870
6340
|
for (let index = 0; index < trackRecords.length; index++) {
|
|
5871
6341
|
const track = trackRecords[index];
|
|
5872
6342
|
const fec = track["fec"];
|
|
5873
6343
|
if (!fec || fec["algorithm"] === "none") continue;
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
6344
|
+
assembleRepairLayers(
|
|
6345
|
+
{
|
|
6346
|
+
name: track["name"],
|
|
6347
|
+
path: `$.tracks[${index}].fec`,
|
|
6348
|
+
algorithm: fec["algorithm"],
|
|
6349
|
+
sourceSymbols: fec["sourceSymbols"],
|
|
6350
|
+
repairSymbols: fec["repairSymbols"],
|
|
6351
|
+
repairTrack: fec["repairTrack"]
|
|
6352
|
+
},
|
|
6353
|
+
repairDescriptors,
|
|
6354
|
+
catalogError,
|
|
6355
|
+
{ layerZeroMustBeDeclared: true }
|
|
6356
|
+
);
|
|
5879
6357
|
}
|
|
5880
6358
|
const switchingSets = /* @__PURE__ */ new Map();
|
|
5881
6359
|
for (let index = 0; index < trackRecords.length; index++) {
|
|
@@ -7299,6 +7777,7 @@ var MmtpToCmaf = class {
|
|
|
7299
7777
|
const samplesPerFrame = __privateGet(this, _audioTimescale) !== 1e6 ? __privateGet(this, _aacFrameSamples) : sampleDuration;
|
|
7300
7778
|
const scaledTs = __privateGet(this, _audioTimescale) !== 1e6 ? __privateGet(this, _audioSampleCount) * samplesPerFrame : timestamp - __privateGet(this, _audioBaseTs);
|
|
7301
7779
|
__privateWrapper(this, _audioSampleCount)._++;
|
|
7780
|
+
const scaledCompositionTimeOffset = compositionTimeOffset === void 0 || __privateGet(this, _audioTimescale) === 1e6 ? compositionTimeOffset : Math.round(compositionTimeOffset * __privateGet(this, _audioTimescale) / 1e6);
|
|
7302
7781
|
return encodeDataSegment(
|
|
7303
7782
|
sampleData,
|
|
7304
7783
|
scaledTs,
|
|
@@ -7306,7 +7785,7 @@ var MmtpToCmaf = class {
|
|
|
7306
7785
|
true,
|
|
7307
7786
|
__privateWrapper(this, _audioSeq)._++,
|
|
7308
7787
|
1,
|
|
7309
|
-
|
|
7788
|
+
scaledCompositionTimeOffset
|
|
7310
7789
|
);
|
|
7311
7790
|
}
|
|
7312
7791
|
/** Convert an ordered audio batch using the caller-owned timeline. */
|
|
@@ -7577,7 +8056,7 @@ var CmafBlockAssembler = class {
|
|
|
7577
8056
|
__privateWrapper(this, _addFrameCount)._++;
|
|
7578
8057
|
__privateMethod(this, _CmafBlockAssembler_instances, startAggregateTimer_fn).call(this);
|
|
7579
8058
|
if (!__privateMethod(this, _CmafBlockAssembler_instances, ordersSamples_fn).call(this)) __privateMethod(this, _CmafBlockAssembler_instances, drainRecoveredUpTo_fn).call(this, timestamp);
|
|
7580
|
-
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, data, timestamp, __privateGet(this, _frameDuration2), keyframe, true, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber);
|
|
8059
|
+
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, data, timestamp, __privateGet(this, _frameDuration2), keyframe, true, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber, void 0);
|
|
7581
8060
|
__privateMethod(this, _CmafBlockAssembler_instances, dumpEmit_fn).call(this, "addFrame-direct", [{ data, timestamp, keyframe }]);
|
|
7582
8061
|
}
|
|
7583
8062
|
/** Mark a block as complete (all K source symbols received or FEC recovery succeeded).
|
|
@@ -7624,7 +8103,7 @@ var CmafBlockAssembler = class {
|
|
|
7624
8103
|
/** Emit a single frame immediately as a one-sample segment (for oversized packets bypassing FEC). */
|
|
7625
8104
|
addBypassFrame(data, timestamp, keyframe, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber) {
|
|
7626
8105
|
if (!__privateMethod(this, _CmafBlockAssembler_instances, ordersSamples_fn).call(this)) __privateMethod(this, _CmafBlockAssembler_instances, drainRecoveredUpTo_fn).call(this, timestamp);
|
|
7627
|
-
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, data, timestamp, __privateGet(this, _frameDuration2), keyframe, true, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber);
|
|
8106
|
+
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, data, timestamp, __privateGet(this, _frameDuration2), keyframe, true, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber, void 0);
|
|
7628
8107
|
}
|
|
7629
8108
|
/** Build and emit init segment from catalog config. */
|
|
7630
8109
|
emitInit() {
|
|
@@ -7774,7 +8253,7 @@ unwrapTimestamp_fn = function(timestamp) {
|
|
|
7774
8253
|
if (reference < 0) return timestamp;
|
|
7775
8254
|
return timestamp + Math.round((reference - timestamp) / wrapPeriod) * wrapPeriod;
|
|
7776
8255
|
};
|
|
7777
|
-
emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber) {
|
|
8256
|
+
emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber, synthetic) {
|
|
7778
8257
|
const scaledTiming = authoredTiming === void 0 ? null : scaleAuthoredTiming(authoredTiming, __privateGet(this, _config).timescale);
|
|
7779
8258
|
if (authoredTiming !== void 0 && scaledTiming === null) return;
|
|
7780
8259
|
const authoredTimestamp = scaledTiming?.timestamp ?? timestamp;
|
|
@@ -7812,7 +8291,8 @@ emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, c
|
|
|
7812
8291
|
...movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber },
|
|
7813
8292
|
...sampleNumber === void 0 ? {} : { sampleNumber },
|
|
7814
8293
|
...catalogTrackName === void 0 ? {} : { catalogTrackName },
|
|
7815
|
-
...deliveryProvenance === void 0 ? {} : { deliveryProvenance }
|
|
8294
|
+
...deliveryProvenance === void 0 ? {} : { deliveryProvenance },
|
|
8295
|
+
...synthetic === void 0 ? {} : { synthetic }
|
|
7816
8296
|
});
|
|
7817
8297
|
if (advancesHorizon && unwrappedTimestamp > __privateGet(this, _lastSourceTs)) {
|
|
7818
8298
|
__privateSet(this, _lastSourceTs, unwrappedTimestamp);
|
|
@@ -7950,7 +8430,8 @@ emitAudioGapFillNowBefore_fn = function(timestamp) {
|
|
|
7950
8430
|
data: __privateGet(this, _lastDecodableAudioFrame).slice(),
|
|
7951
8431
|
timestamp: fillTs,
|
|
7952
8432
|
duration: __privateGet(this, _frameDuration2),
|
|
7953
|
-
keyframe: true
|
|
8433
|
+
keyframe: true,
|
|
8434
|
+
synthetic: true
|
|
7954
8435
|
}, false);
|
|
7955
8436
|
}
|
|
7956
8437
|
};
|
|
@@ -7968,7 +8449,7 @@ emitPerSample_fn = function(frames, audioDurationOverride, advancesHorizon = fal
|
|
|
7968
8449
|
duration = audioDurationOverride ?? __privateGet(this, _frameDuration2);
|
|
7969
8450
|
}
|
|
7970
8451
|
if (duration <= 0) duration = audioDurationOverride ?? __privateGet(this, _frameDuration2);
|
|
7971
|
-
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, f.data, f.timestamp, duration, f.keyframe, advancesHorizon, f.catalogTrackName, f.mpuSequenceNumber, f.packetSequenceNumber, f.packetSequenceNumbers, f.repairedPacketSequenceNumbers, f.authoredTiming, f.deliveryProvenance, f.mmtpTimestamp, f.movieFragmentSequenceNumber, f.sampleNumber);
|
|
8452
|
+
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, f.data, f.timestamp, duration, f.keyframe, advancesHorizon, f.catalogTrackName, f.mpuSequenceNumber, f.packetSequenceNumber, f.packetSequenceNumbers, f.repairedPacketSequenceNumbers, f.authoredTiming, f.deliveryProvenance, f.mmtpTimestamp, f.movieFragmentSequenceNumber, f.sampleNumber, f.synthetic);
|
|
7972
8453
|
}
|
|
7973
8454
|
};
|
|
7974
8455
|
_dumpCount = new WeakMap();
|
|
@@ -7983,8 +8464,9 @@ _aggregateTimer = new WeakMap();
|
|
|
7983
8464
|
startAggregateTimer_fn = function() {
|
|
7984
8465
|
if (__privateGet(this, _aggregateTimer)) return;
|
|
7985
8466
|
__privateSet(this, _aggregateTimer, setInterval(() => {
|
|
8467
|
+
const order = __privateGet(this, _sampleOrderStats);
|
|
7986
8468
|
console.log(
|
|
7987
|
-
`[L4-AGG-${__privateGet(this, _config).trackType}] emits=${__privateGet(this, _emitCount)} samples=${__privateGet(this, _sampleCount)} kf=${__privateGet(this, _keyframeCount)} addFrame=${__privateGet(this, _addFrameCount)} recovered=${__privateGet(this, _recoveredBlockCount)} complete=${__privateGet(this, _completeBlockCount)} fail=${__privateGet(this, _failBlockCount)} pendingBlocks=${__privateGet(this, _blocks).size} pendingFlushes=${__privateGet(this, _pendingFlush).size} armedVideo=${__privateGet(this, _armedVideo)}`
|
|
8469
|
+
`[L4-AGG-${__privateGet(this, _config).trackType}] emits=${__privateGet(this, _emitCount)} samples=${__privateGet(this, _sampleCount)} kf=${__privateGet(this, _keyframeCount)} addFrame=${__privateGet(this, _addFrameCount)} recovered=${__privateGet(this, _recoveredBlockCount)} complete=${__privateGet(this, _completeBlockCount)} fail=${__privateGet(this, _failBlockCount)} pendingBlocks=${__privateGet(this, _blocks).size} pendingFlushes=${__privateGet(this, _pendingFlush).size} armedVideo=${__privateGet(this, _armedVideo)} forced=${order.forcedReleases} stale=${order.staleSamples} dup=${order.duplicateSamples} preRap=${order.preRapDeadlineDrops} noRap=${order.noRapFlushDrops}`
|
|
7988
8470
|
);
|
|
7989
8471
|
__privateSet(this, _emitCount, 0);
|
|
7990
8472
|
__privateSet(this, _sampleCount, 0);
|
|
@@ -8042,7 +8524,7 @@ drainRecoveredUpTo_fn = function(horizonTs) {
|
|
|
8042
8524
|
const ready = [...__privateGet(this, _pendingRecovered).values()].filter((frame) => frame.timestamp <= horizonTs).sort((a, b) => a.timestamp - b.timestamp);
|
|
8043
8525
|
for (const frame of ready) {
|
|
8044
8526
|
__privateGet(this, _pendingRecovered).delete(frame.timestamp);
|
|
8045
|
-
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, frame.data, frame.timestamp, __privateGet(this, _frameDuration2), frame.keyframe, false, frame.catalogTrackName, frame.mpuSequenceNumber, frame.packetSequenceNumber, frame.packetSequenceNumbers, frame.repairedPacketSequenceNumbers, frame.authoredTiming, frame.deliveryProvenance, frame.mmtpTimestamp, frame.movieFragmentSequenceNumber, frame.sampleNumber);
|
|
8527
|
+
__privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, frame.data, frame.timestamp, __privateGet(this, _frameDuration2), frame.keyframe, false, frame.catalogTrackName, frame.mpuSequenceNumber, frame.packetSequenceNumber, frame.packetSequenceNumbers, frame.repairedPacketSequenceNumbers, frame.authoredTiming, frame.deliveryProvenance, frame.mmtpTimestamp, frame.movieFragmentSequenceNumber, frame.sampleNumber, frame.synthetic);
|
|
8046
8528
|
__privateMethod(this, _CmafBlockAssembler_instances, dumpEmit_fn).call(this, "recovered-drain", [frame]);
|
|
8047
8529
|
}
|
|
8048
8530
|
};
|
|
@@ -8091,7 +8573,8 @@ flushBlockWithFill_fn = function(sbn) {
|
|
|
8091
8573
|
data: isVideo ? __privateGet(this, _fillFrame) : audioConcealmentFrame?.slice() ?? new Uint8Array(0),
|
|
8092
8574
|
timestamp: interpolatedTs,
|
|
8093
8575
|
// Video: first fill is keyframe (IDR). Audio: all fill marked keyframe.
|
|
8094
|
-
keyframe: isVideo ? !block.has(0) && esi === 0 : true
|
|
8576
|
+
keyframe: isVideo ? !block.has(0) && esi === 0 : true,
|
|
8577
|
+
synthetic: true
|
|
8095
8578
|
});
|
|
8096
8579
|
}
|
|
8097
8580
|
}
|
|
@@ -8414,6 +8897,15 @@ function deriveFecInterleaveDepth(interleaveDepthMs, mediaUnitDurationMs) {
|
|
|
8414
8897
|
}
|
|
8415
8898
|
return Math.max(1, Math.floor(interleaveDepthMs / mediaUnitDurationMs + 0.5));
|
|
8416
8899
|
}
|
|
8900
|
+
function fecBlockBudgetMs(interleaveDepthMs, reorderToleranceMs) {
|
|
8901
|
+
if (!Number.isFinite(interleaveDepthMs) || !(interleaveDepthMs > 0)) {
|
|
8902
|
+
throw new Error(`[FEC] interleaveDepthMs must be a finite value > 0 (got ${interleaveDepthMs}ms). Fix encoder config.`);
|
|
8903
|
+
}
|
|
8904
|
+
if (!(reorderToleranceMs >= 0) || !Number.isFinite(reorderToleranceMs)) {
|
|
8905
|
+
throw new Error(`[FEC] reorderToleranceMs must be a finite value >= 0 (got ${reorderToleranceMs}ms). It is a catalog value (fec.reorderToleranceMs), not a local default.`);
|
|
8906
|
+
}
|
|
8907
|
+
return Math.ceil(interleaveDepthMs + reorderToleranceMs);
|
|
8908
|
+
}
|
|
8417
8909
|
function parseRepairPacket(packet) {
|
|
8418
8910
|
const parsed = parseMoqRepairFrame(packet);
|
|
8419
8911
|
if (!parsed)
|
|
@@ -8515,32 +9007,18 @@ function computeFecLossWindowMs(fecInterleaveMs, fecK, jitterBufferMs) {
|
|
|
8515
9007
|
function computeFecDeliveryWindowMs(config) {
|
|
8516
9008
|
const fecInterleaveMs = config.interleaveDepthMs;
|
|
8517
9009
|
const fecK = config.sourceSymbols;
|
|
8518
|
-
if (fecInterleaveMs <= 0) {
|
|
9010
|
+
if (!Number.isFinite(fecInterleaveMs) || fecInterleaveMs <= 0) {
|
|
8519
9011
|
throw new Error(
|
|
8520
9012
|
`[TrackSession] fecInterleaveMs=${fecInterleaveMs}. Derive a positive FEC interleave window from the stream catalog.`
|
|
8521
9013
|
);
|
|
8522
9014
|
}
|
|
8523
|
-
if (fecK <= 0) {
|
|
9015
|
+
if (!Number.isFinite(fecK) || fecK <= 0) {
|
|
8524
9016
|
throw new Error(
|
|
8525
9017
|
`[TrackSession] fecK=${fecK}. Derive a positive FEC source-symbol count from the stream catalog.`
|
|
8526
9018
|
);
|
|
8527
9019
|
}
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
const mediaUnitMs = 1e3 / config.mediaUnitsPerSecond;
|
|
8531
|
-
const interleaveUnits = deriveFecInterleaveDepth(fecInterleaveMs, mediaUnitMs);
|
|
8532
|
-
const deliverySymbols = fecK + Math.max(0, config.repairSymbols ?? 0);
|
|
8533
|
-
spansMs.push(deliverySymbols * interleaveUnits * mediaUnitMs);
|
|
8534
|
-
}
|
|
8535
|
-
if (config.symbolSize && config.symbolSize > 0 && config.bandwidthBps && config.bandwidthBps > 0) {
|
|
8536
|
-
const repairSymbols = Math.max(0, config.repairSymbols ?? 0);
|
|
8537
|
-
spansMs.push(
|
|
8538
|
-
(fecK + repairSymbols) * config.symbolSize * 8 * 1e3 / config.bandwidthBps
|
|
8539
|
-
);
|
|
8540
|
-
}
|
|
8541
|
-
const coreMs = Math.max(...spansMs);
|
|
8542
|
-
const marginMs = config.jitterBufferMs !== void 0 && config.jitterBufferMs >= 0 ? config.jitterBufferMs : fecInterleaveMs;
|
|
8543
|
-
return Math.ceil(coreMs) + Math.ceil(marginMs);
|
|
9020
|
+
if (config.jitterBufferMs === void 0) return Math.ceil(fecInterleaveMs);
|
|
9021
|
+
return fecBlockBudgetMs(fecInterleaveMs, config.jitterBufferMs);
|
|
8544
9022
|
}
|
|
8545
9023
|
function computeFecDeliveryWindowMediaUnits(config, timestampTimescale) {
|
|
8546
9024
|
if (!Number.isFinite(timestampTimescale) || timestampTimescale <= 0) {
|
|
@@ -8556,7 +9034,7 @@ function computeFecDeliveryWindowMediaUnits(config, timestampTimescale) {
|
|
|
8556
9034
|
}
|
|
8557
9035
|
return deliveryWindow;
|
|
8558
9036
|
}
|
|
8559
|
-
var _state, _subscription, _hysteresisTimer, _joinDeadlineTimer, _previousPrimary, _manager, _suppressFired, _TrackSession_instances, transition_fn, clearHysteresis_fn, armJoinDeadline_fn, clearJoinDeadline_fn, suppress_fn, unsuppress_fn;
|
|
9037
|
+
var _state, _subscription, _hysteresisTimer, _joinDeadlineTimer, _previousPrimary, _manager, _suppressFired, _fecArmed, _TrackSession_instances, transition_fn, clearHysteresis_fn, armJoinDeadline_fn, clearJoinDeadline_fn, armFec_fn, disarmFec_fn, suppress_fn, unsuppress_fn;
|
|
8560
9038
|
var TrackSession = class {
|
|
8561
9039
|
constructor(config, manager) {
|
|
8562
9040
|
__privateAdd(this, _TrackSession_instances);
|
|
@@ -8573,6 +9051,12 @@ var TrackSession = class {
|
|
|
8573
9051
|
// from `failing` (exactly where a starving track sits) was re-subscribed to
|
|
8574
9052
|
// unicast with the host's media still muted: MoQ playing into a dead sink.
|
|
8575
9053
|
__privateAdd(this, _suppressFired, false);
|
|
9054
|
+
// Single FEC authority flag: set by the fec-setup edges (joinStarted,
|
|
9055
|
+
// enterUnicastFallback), cleared by the fec-teardown edge in #transition.
|
|
9056
|
+
// Setup and teardown are paired by construction, so a takeover timeout that
|
|
9057
|
+
// parks the session in `unicast` keeps the decoder the still-joined
|
|
9058
|
+
// transport is feeding, and the later unsubscribe still tears it down.
|
|
9059
|
+
__privateAdd(this, _fecArmed, false);
|
|
8576
9060
|
this.config = config;
|
|
8577
9061
|
__privateSet(this, _manager, manager);
|
|
8578
9062
|
}
|
|
@@ -8647,7 +9131,7 @@ var TrackSession = class {
|
|
|
8647
9131
|
joinStarted() {
|
|
8648
9132
|
if (__privateGet(this, _state) !== "unicast") return;
|
|
8649
9133
|
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
|
|
8650
|
-
|
|
9134
|
+
__privateMethod(this, _TrackSession_instances, armFec_fn).call(this);
|
|
8651
9135
|
}
|
|
8652
9136
|
/**
|
|
8653
9137
|
* any non-fallback state → unicast-fallback. Used when the SSM bridge is
|
|
@@ -8668,7 +9152,7 @@ var TrackSession = class {
|
|
|
8668
9152
|
__privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
|
|
8669
9153
|
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast-fallback");
|
|
8670
9154
|
__privateMethod(this, _TrackSession_instances, suppress_fn).call(this);
|
|
8671
|
-
|
|
9155
|
+
__privateMethod(this, _TrackSession_instances, armFec_fn).call(this);
|
|
8672
9156
|
}
|
|
8673
9157
|
/**
|
|
8674
9158
|
* unicast-fallback → unicast. Called when the SSM bridge appears mid-session
|
|
@@ -8699,7 +9183,11 @@ var TrackSession = class {
|
|
|
8699
9183
|
if (__privateGet(this, _state) !== "multicast" && __privateGet(this, _state) !== "amt") return;
|
|
8700
9184
|
__privateSet(this, _previousPrimary, __privateGet(this, _state));
|
|
8701
9185
|
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "failing");
|
|
8702
|
-
const hysteresisMs = computeFecLossWindowMs(
|
|
9186
|
+
const hysteresisMs = computeFecLossWindowMs(
|
|
9187
|
+
this.config.fecInterleaveMs,
|
|
9188
|
+
this.config.fecK,
|
|
9189
|
+
this.config.fecReorderToleranceMs
|
|
9190
|
+
);
|
|
8703
9191
|
__privateSet(this, _hysteresisTimer, setTimeout(() => {
|
|
8704
9192
|
this.hysteresisExpired();
|
|
8705
9193
|
}, hysteresisMs));
|
|
@@ -8723,23 +9211,19 @@ var TrackSession = class {
|
|
|
8723
9211
|
const prefs = this.config.preferredTransports;
|
|
8724
9212
|
const currentIdx = current ? prefs.indexOf(current) : -1;
|
|
8725
9213
|
const nextMode = prefs[currentIdx + 1];
|
|
8726
|
-
if (nextMode) {
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
8730
|
-
if (this.config.trackType === "audio") {
|
|
8731
|
-
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
|
|
8732
|
-
}
|
|
8733
|
-
} else {
|
|
8734
|
-
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
|
|
8735
|
-
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
8736
|
-
}
|
|
8737
|
-
} else {
|
|
8738
|
-
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
|
|
9214
|
+
if (nextMode !== void 0 && nextMode !== "moq") {
|
|
9215
|
+
__privateSet(this, _previousPrimary, nextMode);
|
|
9216
|
+
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
|
|
8739
9217
|
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
9218
|
+
return;
|
|
9219
|
+
}
|
|
9220
|
+
const from = __privateGet(this, _state);
|
|
9221
|
+
__privateSet(this, _previousPrimary, null);
|
|
9222
|
+
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
|
|
9223
|
+
__privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
|
|
9224
|
+
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
9225
|
+
if (from !== "unicast" && this.config.trackType === "audio") {
|
|
9226
|
+
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
|
|
8743
9227
|
}
|
|
8744
9228
|
}
|
|
8745
9229
|
/** Hysteresis expired → failback to next transport (Claim 9g). */
|
|
@@ -8763,11 +9247,20 @@ var TrackSession = class {
|
|
|
8763
9247
|
get isUnicastFallback() {
|
|
8764
9248
|
return __privateGet(this, _state) === "unicast-fallback";
|
|
8765
9249
|
}
|
|
9250
|
+
/**
|
|
9251
|
+
* True while this track owns a FEC decoder (fec-setup fired, fec-teardown
|
|
9252
|
+
* not yet). Stays true across a takeover timeout that parked the session in
|
|
9253
|
+
* `unicast`; SessionManager.failbackAll uses it to release such sessions.
|
|
9254
|
+
*/
|
|
9255
|
+
get isFecArmed() {
|
|
9256
|
+
return __privateGet(this, _fecArmed);
|
|
9257
|
+
}
|
|
8766
9258
|
dispose() {
|
|
8767
9259
|
__privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
|
|
8768
9260
|
__privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
|
|
8769
9261
|
__privateSet(this, _state, "idle");
|
|
8770
9262
|
__privateSet(this, _subscription, null);
|
|
9263
|
+
__privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
|
|
8771
9264
|
}
|
|
8772
9265
|
};
|
|
8773
9266
|
_state = new WeakMap();
|
|
@@ -8777,20 +9270,20 @@ _joinDeadlineTimer = new WeakMap();
|
|
|
8777
9270
|
_previousPrimary = new WeakMap();
|
|
8778
9271
|
_manager = new WeakMap();
|
|
8779
9272
|
_suppressFired = new WeakMap();
|
|
9273
|
+
_fecArmed = new WeakMap();
|
|
8780
9274
|
_TrackSession_instances = new WeakSet();
|
|
8781
9275
|
// -----------------------------------------------------------------------
|
|
8782
9276
|
// Internal
|
|
8783
9277
|
// -----------------------------------------------------------------------
|
|
8784
|
-
transition_fn = function(to) {
|
|
9278
|
+
transition_fn = function(to, opts) {
|
|
8785
9279
|
const from = __privateGet(this, _state);
|
|
8786
9280
|
if (from === to) return;
|
|
9281
|
+
const joinDeadlineMs = to === "joining" ? __privateGet(this, _manager).takeoverDeadlineMs : void 0;
|
|
8787
9282
|
__privateSet(this, _state, to);
|
|
8788
9283
|
if (from === "joining") __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
|
|
8789
|
-
if (
|
|
9284
|
+
if (joinDeadlineMs !== void 0) __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this, joinDeadlineMs);
|
|
8790
9285
|
const fecActive = (s) => s === "joining" || s === "multicast" || s === "amt" || s === "failing" || s === "unicast-fallback";
|
|
8791
|
-
if (fecActive(
|
|
8792
|
-
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-teardown" }, this.config);
|
|
8793
|
-
}
|
|
9286
|
+
if (!fecActive(to) && !opts?.retainFec) __privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
|
|
8794
9287
|
__privateGet(this, _manager).callbacks?.onStateChange?.(this.config.trackName, from, to);
|
|
8795
9288
|
};
|
|
8796
9289
|
clearHysteresis_fn = function() {
|
|
@@ -8799,14 +9292,13 @@ clearHysteresis_fn = function() {
|
|
|
8799
9292
|
__privateSet(this, _hysteresisTimer, null);
|
|
8800
9293
|
}
|
|
8801
9294
|
};
|
|
8802
|
-
armJoinDeadline_fn = function() {
|
|
9295
|
+
armJoinDeadline_fn = function(deadlineMs) {
|
|
8803
9296
|
__privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
|
|
8804
|
-
|
|
8805
|
-
if (deadlineMs <= 0) return;
|
|
9297
|
+
if (deadlineMs === 0) return;
|
|
8806
9298
|
__privateSet(this, _joinDeadlineTimer, setTimeout(() => {
|
|
8807
9299
|
__privateSet(this, _joinDeadlineTimer, null);
|
|
8808
9300
|
if (__privateGet(this, _state) !== "joining") return;
|
|
8809
|
-
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
|
|
9301
|
+
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast", { retainFec: true });
|
|
8810
9302
|
__privateGet(this, _manager).callbacks?.onTakeoverTimeout?.(this.config.trackName);
|
|
8811
9303
|
}, deadlineMs));
|
|
8812
9304
|
};
|
|
@@ -8816,6 +9308,18 @@ clearJoinDeadline_fn = function() {
|
|
|
8816
9308
|
__privateSet(this, _joinDeadlineTimer, null);
|
|
8817
9309
|
}
|
|
8818
9310
|
};
|
|
9311
|
+
/** Fire fec-setup exactly once per FEC epoch. */
|
|
9312
|
+
armFec_fn = function() {
|
|
9313
|
+
if (__privateGet(this, _fecArmed)) return;
|
|
9314
|
+
__privateSet(this, _fecArmed, true);
|
|
9315
|
+
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
|
|
9316
|
+
};
|
|
9317
|
+
/** Fire fec-teardown for an armed decoder; no-op when none is armed. */
|
|
9318
|
+
disarmFec_fn = function() {
|
|
9319
|
+
if (!__privateGet(this, _fecArmed)) return;
|
|
9320
|
+
__privateSet(this, _fecArmed, false);
|
|
9321
|
+
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-teardown" }, this.config);
|
|
9322
|
+
};
|
|
8819
9323
|
/** Fire onSuppress(true) exactly once per suppression epoch. */
|
|
8820
9324
|
suppress_fn = function() {
|
|
8821
9325
|
if (__privateGet(this, _suppressFired)) return;
|
|
@@ -8828,18 +9332,14 @@ unsuppress_fn = function() {
|
|
|
8828
9332
|
__privateSet(this, _suppressFired, false);
|
|
8829
9333
|
__privateGet(this, _manager).callbacks?.onSuppress?.(this.config.trackName, false);
|
|
8830
9334
|
};
|
|
9335
|
+
var MAX_TAKEOVER_DEADLINE_MS = 2147483647;
|
|
9336
|
+
var _takeoverDeadlineMs, _SessionManager_instances, requireTakeoverDeadlineMs_fn;
|
|
8831
9337
|
var SessionManager = class {
|
|
8832
9338
|
constructor(poolOptions) {
|
|
9339
|
+
__privateAdd(this, _SessionManager_instances);
|
|
8833
9340
|
this.sessions = /* @__PURE__ */ new Map();
|
|
8834
9341
|
this.callbacks = null;
|
|
8835
|
-
|
|
8836
|
-
* Takeover deadline (BLO-9413/D9): max time a session may sit in `joining`
|
|
8837
|
-
* without a RAP before returning to `unicast` and firing onTakeoverTimeout.
|
|
8838
|
-
* MoQ is never suppressed during `joining`, so expiry is a silent abandon —
|
|
8839
|
-
* playback never glitches. Hosts tune per stream (e.g. GOP length); 0
|
|
8840
|
-
* disables.
|
|
8841
|
-
*/
|
|
8842
|
-
this.takeoverDeadlineMs = 5e3;
|
|
9342
|
+
__privateAdd(this, _takeoverDeadlineMs);
|
|
8843
9343
|
this.pool = new TransportPool(poolOptions);
|
|
8844
9344
|
this.pool.onSubscriptionFailed((sub) => {
|
|
8845
9345
|
for (const trackName of sub.consumers) {
|
|
@@ -8848,18 +9348,56 @@ var SessionManager = class {
|
|
|
8848
9348
|
}
|
|
8849
9349
|
});
|
|
8850
9350
|
}
|
|
9351
|
+
/**
|
|
9352
|
+
* Takeover deadline (BLO-9413/D9): the time a session may sit in `joining`
|
|
9353
|
+
* without a RAP before returning to `unicast` and firing onTakeoverTimeout.
|
|
9354
|
+
* MoQ is never suppressed during `joining`, so expiry abandons the takeover
|
|
9355
|
+
* without a playback glitch. The host derives it from the catalog (RAP
|
|
9356
|
+
* cadence from groupDurationTicks/timescale plus the signaled FEC delivery
|
|
9357
|
+
* window or reorder tolerance) and sets it before addTrack(). There is no
|
|
9358
|
+
* default: an unset value fails addTrack() closed, and reading the getter
|
|
9359
|
+
* before assignment throws the same way, so no host ever holds a
|
|
9360
|
+
* `number | undefined` it could coalesce back into a default.
|
|
9361
|
+
* `hasTakeoverDeadline` answers whether one was admitted. A declared 0
|
|
9362
|
+
* disables the deadline. The value is an integer number of milliseconds:
|
|
9363
|
+
* setTimeout truncates fractions, so a host that divides ticks by a
|
|
9364
|
+
* timescale must round before assigning.
|
|
9365
|
+
*
|
|
9366
|
+
* Assignment validates, so the diagnostic lands at the host's own call site.
|
|
9367
|
+
* That is what bounds the blast radius: `joining` is entered from
|
|
9368
|
+
* joinStarted(), from the transportFailed() failover branch -- itself
|
|
9369
|
+
* reachable from a setTimeout via hysteresisExpired(), where no caller could
|
|
9370
|
+
* catch a throw -- and from the activateMulticast()/activateAmt()/pool-failure
|
|
9371
|
+
* loops, where a throw on one session would abandon its siblings mid-loop
|
|
9372
|
+
* (the split-brain those loops exist to prevent). Refusing a bad value at
|
|
9373
|
+
* assignment means no host mutation can make an already-admitted session
|
|
9374
|
+
* throw out of a transport edge.
|
|
9375
|
+
*/
|
|
9376
|
+
get takeoverDeadlineMs() {
|
|
9377
|
+
return __privateMethod(this, _SessionManager_instances, requireTakeoverDeadlineMs_fn).call(this);
|
|
9378
|
+
}
|
|
9379
|
+
set takeoverDeadlineMs(value) {
|
|
9380
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_TAKEOVER_DEADLINE_MS) {
|
|
9381
|
+
throw new Error(
|
|
9382
|
+
`[SessionManager] takeoverDeadlineMs must be an integer from 0 to ${MAX_TAKEOVER_DEADLINE_MS} (0 disables it, larger values overflow setTimeout); got ${String(value)}`
|
|
9383
|
+
);
|
|
9384
|
+
}
|
|
9385
|
+
__privateSet(this, _takeoverDeadlineMs, value);
|
|
9386
|
+
}
|
|
9387
|
+
/** True once the host has admitted a deadline; a declared 0 counts. */
|
|
9388
|
+
get hasTakeoverDeadline() {
|
|
9389
|
+
return __privateGet(this, _takeoverDeadlineMs) !== void 0;
|
|
9390
|
+
}
|
|
8851
9391
|
/** Narrow to MMTP callbacks (onTrackEvent). Safe — optional chaining handles PIM-only consumers. */
|
|
8852
9392
|
get mmtp() {
|
|
8853
9393
|
return this.callbacks;
|
|
8854
9394
|
}
|
|
8855
|
-
// -----------------------------------------------------------------------
|
|
8856
|
-
// Track management
|
|
8857
|
-
// -----------------------------------------------------------------------
|
|
8858
9395
|
/** Add a track session from catalog config. */
|
|
8859
9396
|
addTrack(config) {
|
|
8860
9397
|
if (this.sessions.has(config.trackName)) {
|
|
8861
9398
|
throw new Error(`[SessionManager] Track "${config.trackName}" already exists`);
|
|
8862
9399
|
}
|
|
9400
|
+
__privateMethod(this, _SessionManager_instances, requireTakeoverDeadlineMs_fn).call(this);
|
|
8863
9401
|
const session = new TrackSession(config, this);
|
|
8864
9402
|
this.sessions.set(config.trackName, session);
|
|
8865
9403
|
return session;
|
|
@@ -8883,8 +9421,9 @@ var SessionManager = class {
|
|
|
8883
9421
|
* Prevents split-brain between video and audio.
|
|
8884
9422
|
*
|
|
8885
9423
|
* A takeover deadline can return a track from `joining` to `unicast` while
|
|
8886
|
-
* the multicast socket keeps delivering packets
|
|
8887
|
-
* later,
|
|
9424
|
+
* the multicast socket keeps delivering packets and the track keeps its FEC
|
|
9425
|
+
* decoder armed. If a valid RAP arrives later, that late-RAP edge suppresses
|
|
9426
|
+
* unicast and promotes multicast without a second fec-setup.
|
|
8888
9427
|
*/
|
|
8889
9428
|
activateMulticast() {
|
|
8890
9429
|
for (const [, session] of this.sessions) {
|
|
@@ -8916,13 +9455,17 @@ var SessionManager = class {
|
|
|
8916
9455
|
* and they must drain uninterrupted across transport-state events.
|
|
8917
9456
|
*
|
|
8918
9457
|
* Coherence invariant (BLO-9413): after this returns, every non-fallback
|
|
8919
|
-
* session is `unicast` (or `idle`)
|
|
8920
|
-
* any outstanding suppression regardless of the state
|
|
8921
|
-
* (`failing` included; see TrackSession.#suppressFired)
|
|
9458
|
+
* session is `unicast` (or `idle`), unsuppressed, and without a FEC decoder.
|
|
9459
|
+
* unsubscribe() unwinds any outstanding suppression regardless of the state
|
|
9460
|
+
* it tears down from (`failing` included; see TrackSession.#suppressFired),
|
|
9461
|
+
* and a session that a takeover timeout parked in `unicast` with its decoder
|
|
9462
|
+
* still armed is cycled too, because this transport edge is what releases
|
|
9463
|
+
* that decoder (unsubscribe fires fec-teardown).
|
|
8922
9464
|
*/
|
|
8923
9465
|
failbackAll() {
|
|
8924
9466
|
for (const [, session] of this.sessions) {
|
|
8925
|
-
|
|
9467
|
+
const parkedWithFec = session.state === "unicast" && session.isFecArmed;
|
|
9468
|
+
if (parkedWithFec || session.state !== "idle" && session.state !== "unicast" && session.state !== "unicast-fallback") {
|
|
8926
9469
|
session.unsubscribe();
|
|
8927
9470
|
session.subscribe();
|
|
8928
9471
|
}
|
|
@@ -9006,6 +9549,27 @@ var SessionManager = class {
|
|
|
9006
9549
|
this.callbacks = null;
|
|
9007
9550
|
}
|
|
9008
9551
|
};
|
|
9552
|
+
_takeoverDeadlineMs = new WeakMap();
|
|
9553
|
+
_SessionManager_instances = new WeakSet();
|
|
9554
|
+
// -----------------------------------------------------------------------
|
|
9555
|
+
// Track management
|
|
9556
|
+
// -----------------------------------------------------------------------
|
|
9557
|
+
/**
|
|
9558
|
+
* The admitted takeover deadline. Throws only when the host never set one,
|
|
9559
|
+
* every other invalid value being refused by the setter, and returns a
|
|
9560
|
+
* declared 0 as 0. The getter and addTrack() both route through it, so a
|
|
9561
|
+
* session cannot exist without an admitted deadline, which is what makes
|
|
9562
|
+
* the #transition() read unable to throw on an already-admitted session.
|
|
9563
|
+
*/
|
|
9564
|
+
requireTakeoverDeadlineMs_fn = function() {
|
|
9565
|
+
const value = __privateGet(this, _takeoverDeadlineMs);
|
|
9566
|
+
if (value === void 0) {
|
|
9567
|
+
throw new Error(
|
|
9568
|
+
"[SessionManager] takeoverDeadlineMs is unset; set a catalog-derived takeover deadline (0 disables it) before adding tracks"
|
|
9569
|
+
);
|
|
9570
|
+
}
|
|
9571
|
+
return value;
|
|
9572
|
+
};
|
|
9009
9573
|
|
|
9010
9574
|
// src/fec-manager.ts
|
|
9011
9575
|
var MIN_INTERLEAVE_DEPTH_MS = 33;
|
|
@@ -9024,7 +9588,7 @@ function newFecCounters() {
|
|
|
9024
9588
|
greenFillFrames: 0
|
|
9025
9589
|
};
|
|
9026
9590
|
}
|
|
9027
|
-
var _fecDecoders, _workerClients, _syncDecoderBlockOwners, _decoderBlockOwners, _fecK, _fecSymbolSize, _fecTimeoutMs, _trackConfigs, _fecStats, _fecTrackStats, _fecWorkerStats, _fecStatsObservers, _activeBlocks, _activeBlockDeadlinesMs, _activeBlockTimeoutOwners, _lifecycleTrace, _pendingWorkerCleanups, _pendingWorkerCleanupClients, _blockSourceCounts, _blockSourceEsis, _blockRepairRsIds, _completedBlocks, _seenSymbols, _blockPathCounts, _blockCatalogTrackNames, _startupAlignedPackets, _startupPartialBlocks, _startupRequireRepairReady, _startupRepairReadyPackets, _startupRepairBoundaryCounts, _startupRepairFailOpenPackets, _cleanlyCompletedBlocks, _repairRequestedBlocks, _repairDeadlines, _workerRecoveredBlocks, _fpsDetected, _fecFps, _fecInterleaveDepthMs, _workerErrorCounts, _repairSymbolSizeMismatchCount, _FECManager_instances, streamKey_fn, belongsToPacket_fn, belongsToStream_fn, packetIdsForStreamKeys_fn, blockKey_fn, parseBlockKey_fn, decoderForPacket_fn, workerForPacket_fn, claimDecoderBlock_fn, decoderBlockCatalogTrackName_fn, resetDecoderBlockOwners_fn, discardFecDecoderClient_fn, assertExclusiveDecoderClient_fn, statsForPacket_fn, markPathBlockComplete_fn, clearRepairDeadline_fn, requestRepair_fn, traceLifecycle_fn, defaultTrackConfig_fn, resolveTrackConfig_fn, computeTimeout_fn, repairDeadlineMs_fn, repairGraceMs_fn, armBlockTimeout_fn, upsertTrackConfig_fn, normalizeSymbolData_fn, blockKeysFor_fn, observeBlockCatalogTrackName_fn, markStartupPartial_fn, routeSyncCleanup_fn, failBlock_fn;
|
|
9591
|
+
var _fecDecoders, _workerClients, _syncDecoderBlockOwners, _decoderBlockOwners, _fecK, _fecSymbolSize, _fecTimeoutMs, _trackConfigs, _fecStats, _fecTrackStats, _fecWorkerStats, _fecStatsObservers, _activeBlocks, _activeBlockDeadlinesMs, _activeBlockTimeoutOwners, _lifecycleTrace, _pendingWorkerCleanups, _pendingWorkerCleanupClients, _blockSourceCounts, _blockSourceEsis, _blockRepairRsIds, _completedBlocks, _seenSymbols, _blockPathCounts, _blockCatalogTrackNames, _startupAlignedPackets, _startupPartialBlocks, _startupRequireRepairReady, _startupRepairReadyPackets, _startupRepairBoundaryCounts, _startupRepairFailOpenPackets, _cleanlyCompletedBlocks, _repairRequestedBlocks, _repairDeadlines, _workerRecoveredBlocks, _fpsDetected, _fecFps, _fecInterleaveDepthMs, _workerErrorCounts, _decoderUnavailableEvents, _repairSymbolSizeMismatchCount, _FECManager_instances, streamKey_fn, belongsToPacket_fn, belongsToStream_fn, packetIdsForStreamKeys_fn, parseStreamKey_fn, blockKey_fn, parseBlockKey_fn, decoderForPacket_fn, workerForPacket_fn, routableWorkerForPacket_fn, claimDecoderBlock_fn, decoderBlockCatalogTrackName_fn, resetDecoderBlockOwners_fn, discardFecDecoderClient_fn, assertExclusiveDecoderClient_fn, statsForPacket_fn, markPathBlockComplete_fn, clearRepairDeadline_fn, requestRepair_fn, traceLifecycle_fn, defaultTrackConfig_fn, resolveTrackConfig_fn, computeTimeout_fn, repairDeadlineMs_fn, repairGraceMs_fn, armBlockTimeout_fn, upsertTrackConfig_fn, normalizeSymbolData_fn, blockKeysFor_fn, observeBlockCatalogTrackName_fn, markStartupPartial_fn, retireWorker_fn, releaseWorkerClient_fn, settleOwedCleanup_fn, evictDisposedWorker_fn, routeSyncCleanup_fn, failBlock_fn;
|
|
9028
9592
|
var FECManager = class {
|
|
9029
9593
|
constructor() {
|
|
9030
9594
|
__privateAdd(this, _FECManager_instances);
|
|
@@ -9128,6 +9692,7 @@ var FECManager = class {
|
|
|
9128
9692
|
// validation harness — which caps headless warnings at 500 — fails even
|
|
9129
9693
|
// though the errors are duplicates of a single underlying event.
|
|
9130
9694
|
__privateAdd(this, _workerErrorCounts, /* @__PURE__ */ new Map());
|
|
9695
|
+
__privateAdd(this, _decoderUnavailableEvents, 0);
|
|
9131
9696
|
__privateAdd(this, _repairSymbolSizeMismatchCount, 0);
|
|
9132
9697
|
// Event callbacks — block lifecycle notifications for MmtpRouter wiring.
|
|
9133
9698
|
// NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
|
|
@@ -9136,6 +9701,17 @@ var FECManager = class {
|
|
|
9136
9701
|
this.onBlockStarted = null;
|
|
9137
9702
|
this.onBlockComplete = null;
|
|
9138
9703
|
this.onBlockFailed = null;
|
|
9704
|
+
/**
|
|
9705
|
+
* Fired once per stream when its Worker decoder stops being routable.
|
|
9706
|
+
* `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when
|
|
9707
|
+
* a symbol or reset found the registered client already disposed and
|
|
9708
|
+
* evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder
|
|
9709
|
+
* registered for the same stream keeps decoding its symbols, and 'none'
|
|
9710
|
+
* when nothing does. Only 'none' leaves the stream unprotected: the host
|
|
9711
|
+
* must then register a decoder again or leave the stream, because counting
|
|
9712
|
+
* its symbols as handled would hide the loss.
|
|
9713
|
+
*/
|
|
9714
|
+
this.onDecoderUnavailable = null;
|
|
9139
9715
|
// D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
|
|
9140
9716
|
// block arms a one-shot repair deadline after the derived K-symbol source
|
|
9141
9717
|
// horizon; a block still short of K when it fires requests a per-SBN MoQ
|
|
@@ -9169,6 +9745,7 @@ var FECManager = class {
|
|
|
9169
9745
|
startupRepairReadyPackets: __privateMethod(this, _FECManager_instances, packetIdsForStreamKeys_fn).call(this, __privateGet(this, _startupRepairReadyPackets)),
|
|
9170
9746
|
startupRepairFailOpenPackets: __privateMethod(this, _FECManager_instances, packetIdsForStreamKeys_fn).call(this, __privateGet(this, _startupRepairFailOpenPackets)),
|
|
9171
9747
|
pendingWorkerCleanups: __privateGet(this, _pendingWorkerCleanups).size,
|
|
9748
|
+
decoderUnavailableEvents: __privateGet(this, _decoderUnavailableEvents),
|
|
9172
9749
|
repairTrackedBlocks: __privateGet(this, _blockRepairRsIds).size,
|
|
9173
9750
|
lifecycleTrace: __privateGet(this, _lifecycleTrace).map((entry) => ({ ...entry }))
|
|
9174
9751
|
};
|
|
@@ -9303,8 +9880,12 @@ var FECManager = class {
|
|
|
9303
9880
|
__privateMethod(this, _FECManager_instances, discardFecDecoderClient_fn).call(this, client);
|
|
9304
9881
|
}
|
|
9305
9882
|
}
|
|
9306
|
-
for (const [key, client] of __privateGet(this, _workerClients)) {
|
|
9883
|
+
for (const [key, client] of [...__privateGet(this, _workerClients)]) {
|
|
9307
9884
|
if (!__privateMethod(this, _FECManager_instances, belongsToStream_fn).call(this, key, packetId, catalogTrackName)) continue;
|
|
9885
|
+
if (client.disposed === true) {
|
|
9886
|
+
__privateMethod(this, _FECManager_instances, evictDisposedWorker_fn).call(this, client);
|
|
9887
|
+
continue;
|
|
9888
|
+
}
|
|
9308
9889
|
client.reset();
|
|
9309
9890
|
clients.add(client);
|
|
9310
9891
|
}
|
|
@@ -9524,6 +10105,12 @@ var FECManager = class {
|
|
|
9524
10105
|
}
|
|
9525
10106
|
const streamKey = __privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName);
|
|
9526
10107
|
__privateMethod(this, _FECManager_instances, assertExclusiveDecoderClient_fn).call(this, streamKey, client, __privateGet(this, _workerClients), "worker");
|
|
10108
|
+
const incumbent = __privateGet(this, _workerClients).get(streamKey);
|
|
10109
|
+
if (incumbent !== void 0 && incumbent !== client) {
|
|
10110
|
+
throw new Error(
|
|
10111
|
+
`[FECManager] stream ${streamKey} already routes to a Worker; call clearFecWorker before registering another`
|
|
10112
|
+
);
|
|
10113
|
+
}
|
|
9527
10114
|
if (config) __privateMethod(this, _FECManager_instances, upsertTrackConfig_fn).call(this, packetId, config);
|
|
9528
10115
|
const workerK = __privateMethod(this, _FECManager_instances, resolveTrackConfig_fn).call(this, packetId).k;
|
|
9529
10116
|
__privateGet(this, _workerClients).set(streamKey, client);
|
|
@@ -9610,14 +10197,7 @@ var FECManager = class {
|
|
|
9610
10197
|
const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
|
|
9611
10198
|
return block.packetId === packetId && block.sbn === sbn && __privateGet(this, _pendingWorkerCleanupClients).get(key) === client;
|
|
9612
10199
|
});
|
|
9613
|
-
for (const key of cleanupKeys)
|
|
9614
|
-
__privateGet(this, _pendingWorkerCleanups).delete(key);
|
|
9615
|
-
__privateGet(this, _pendingWorkerCleanupClients).delete(key);
|
|
9616
|
-
if (__privateGet(this, _completedBlocks).has(key)) continue;
|
|
9617
|
-
const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
|
|
9618
|
-
__privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, "cleanup-ack", sbn, packetId, block.catalogTrackName);
|
|
9619
|
-
__privateMethod(this, _FECManager_instances, failBlock_fn).call(this, sbn, packetId, false, block.catalogTrackName);
|
|
9620
|
-
}
|
|
10200
|
+
for (const key of cleanupKeys) __privateMethod(this, _FECManager_instances, settleOwedCleanup_fn).call(this, key, "cleanup-ack");
|
|
9621
10201
|
});
|
|
9622
10202
|
client.onError((msg, code) => {
|
|
9623
10203
|
const requestedKind = code ?? msg;
|
|
@@ -9633,7 +10213,26 @@ var FECManager = class {
|
|
|
9633
10213
|
}
|
|
9634
10214
|
});
|
|
9635
10215
|
}
|
|
9636
|
-
/**
|
|
10216
|
+
/**
|
|
10217
|
+
* Remove a stream's Worker decoder from routing.
|
|
10218
|
+
*
|
|
10219
|
+
* Returns false when no Worker is registered under that exact stream key.
|
|
10220
|
+
* On success every cleanup acknowledgement still owed by the client is
|
|
10221
|
+
* resolved by failing its block now (a Worker that is going away cannot
|
|
10222
|
+
* post it), the client's block ownership is released, and
|
|
10223
|
+
* onDecoderUnavailable fires once with reason 'cleared' and the decoder
|
|
10224
|
+
* that remains for the stream ('sync' or 'none'). Call this before
|
|
10225
|
+
* disposing the client; a client disposed while still registered is
|
|
10226
|
+
* evicted on its next symbol or reset with reason 'disposed'.
|
|
10227
|
+
*/
|
|
10228
|
+
clearFecWorker(packetId, catalogTrackName) {
|
|
10229
|
+
const streamKey = __privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName);
|
|
10230
|
+
const client = __privateGet(this, _workerClients).get(streamKey);
|
|
10231
|
+
if (!client) return false;
|
|
10232
|
+
__privateMethod(this, _FECManager_instances, retireWorker_fn).call(this, streamKey, client, "cleared");
|
|
10233
|
+
return true;
|
|
10234
|
+
}
|
|
10235
|
+
/** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
|
|
9637
10236
|
setFramerate(fps) {
|
|
9638
10237
|
if (__privateGet(this, _fpsDetected) || fps <= 0) return;
|
|
9639
10238
|
__privateSet(this, _fpsDetected, true);
|
|
@@ -9683,7 +10282,7 @@ var FECManager = class {
|
|
|
9683
10282
|
const sbn = Math.floor(ssId / this.kForPacket(packetId));
|
|
9684
10283
|
const blockKey = __privateMethod(this, _FECManager_instances, blockKey_fn).call(this, sbn, packetId, catalogTrackName);
|
|
9685
10284
|
const syncBlockOwner = __privateGet(this, _syncDecoderBlockOwners).get(blockKey);
|
|
9686
|
-
const workerClient = __privateMethod(this, _FECManager_instances,
|
|
10285
|
+
const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
9687
10286
|
if (workerClient && !syncBlockOwner) {
|
|
9688
10287
|
if (!__privateMethod(this, _FECManager_instances, claimDecoderBlock_fn).call(this, workerClient, sbn, catalogTrackName)) return null;
|
|
9689
10288
|
const buf = fecData.buffer.slice(
|
|
@@ -9760,7 +10359,7 @@ var FECManager = class {
|
|
|
9760
10359
|
if (__privateGet(this, _repairRequestedBlocks).has(blockKey)) {
|
|
9761
10360
|
__privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, "repair-arrived", sbn, packetId, catalogTrackName, rsId);
|
|
9762
10361
|
}
|
|
9763
|
-
const workerClient = __privateMethod(this, _FECManager_instances,
|
|
10362
|
+
const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
9764
10363
|
if (workerClient && !syncBlockOwner) {
|
|
9765
10364
|
if (!__privateMethod(this, _FECManager_instances, claimDecoderBlock_fn).call(this, workerClient, sbn, catalogTrackName)) return null;
|
|
9766
10365
|
const buf = fecData.buffer.slice(
|
|
@@ -10045,6 +10644,7 @@ _fpsDetected = new WeakMap();
|
|
|
10045
10644
|
_fecFps = new WeakMap();
|
|
10046
10645
|
_fecInterleaveDepthMs = new WeakMap();
|
|
10047
10646
|
_workerErrorCounts = new WeakMap();
|
|
10647
|
+
_decoderUnavailableEvents = new WeakMap();
|
|
10048
10648
|
_repairSymbolSizeMismatchCount = new WeakMap();
|
|
10049
10649
|
_FECManager_instances = new WeakSet();
|
|
10050
10650
|
streamKey_fn = function(packetId, catalogTrackName) {
|
|
@@ -10064,6 +10664,14 @@ belongsToStream_fn = function(key, packetId, catalogTrackName) {
|
|
|
10064
10664
|
packetIdsForStreamKeys_fn = function(keys) {
|
|
10065
10665
|
return [...new Set([...keys].map((key) => Number(key.split(":", 1)[0])))];
|
|
10066
10666
|
};
|
|
10667
|
+
parseStreamKey_fn = function(key) {
|
|
10668
|
+
const separator = key.indexOf(":");
|
|
10669
|
+
if (separator < 0) return { packetId: Number(key) || 0 };
|
|
10670
|
+
return {
|
|
10671
|
+
packetId: Number(key.slice(0, separator)) || 0,
|
|
10672
|
+
catalogTrackName: decodeURIComponent(key.slice(separator + 1))
|
|
10673
|
+
};
|
|
10674
|
+
};
|
|
10067
10675
|
blockKey_fn = function(sbn, packetId, catalogTrackName) {
|
|
10068
10676
|
const base = `${packetId}:${sbn}`;
|
|
10069
10677
|
return catalogTrackName === void 0 ? base : `${base}:${encodeURIComponent(catalogTrackName)}`;
|
|
@@ -10083,6 +10691,18 @@ decoderForPacket_fn = function(packetId, catalogTrackName) {
|
|
|
10083
10691
|
workerForPacket_fn = function(packetId, catalogTrackName) {
|
|
10084
10692
|
return __privateGet(this, _workerClients).get(__privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName)) ?? __privateGet(this, _workerClients).get(__privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId));
|
|
10085
10693
|
};
|
|
10694
|
+
/**
|
|
10695
|
+
* The stream's Worker client for routing, or undefined after evicting a
|
|
10696
|
+
* registered client that was already disposed. A disposed client throws on
|
|
10697
|
+
* every command, so it leaves routing here before a symbol reaches it and
|
|
10698
|
+
* the stream reports its decoder unavailable instead.
|
|
10699
|
+
*/
|
|
10700
|
+
routableWorkerForPacket_fn = function(packetId, catalogTrackName) {
|
|
10701
|
+
const client = __privateMethod(this, _FECManager_instances, workerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
10702
|
+
if (client === void 0 || client.disposed !== true) return client;
|
|
10703
|
+
__privateMethod(this, _FECManager_instances, evictDisposedWorker_fn).call(this, client);
|
|
10704
|
+
return void 0;
|
|
10705
|
+
};
|
|
10086
10706
|
claimDecoderBlock_fn = function(client, sbn, catalogTrackName) {
|
|
10087
10707
|
let blocks = __privateGet(this, _decoderBlockOwners).get(client);
|
|
10088
10708
|
if (!blocks) {
|
|
@@ -10250,7 +10870,7 @@ armBlockTimeout_fn = function(sbn, packetId, catalogTrackName, delayMs) {
|
|
|
10250
10870
|
__privateMethod(this, _FECManager_instances, failBlock_fn).call(this, sbn, packetId, false, catalogTrackName);
|
|
10251
10871
|
return;
|
|
10252
10872
|
}
|
|
10253
|
-
const workerClient = __privateMethod(this, _FECManager_instances,
|
|
10873
|
+
const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
10254
10874
|
if (workerClient) {
|
|
10255
10875
|
__privateGet(this, _pendingWorkerCleanups).add(key);
|
|
10256
10876
|
__privateGet(this, _pendingWorkerCleanupClients).set(key, workerClient);
|
|
@@ -10333,6 +10953,59 @@ markStartupPartial_fn = function(blockKey) {
|
|
|
10333
10953
|
));
|
|
10334
10954
|
}
|
|
10335
10955
|
};
|
|
10956
|
+
/**
|
|
10957
|
+
* Take a Worker client out of routing under one stream key, fail the blocks
|
|
10958
|
+
* whose completion it still owed, and report the stream's decoder
|
|
10959
|
+
* unavailable together with whatever still decodes the stream.
|
|
10960
|
+
*/
|
|
10961
|
+
retireWorker_fn = function(streamKey, client, reason) {
|
|
10962
|
+
__privateGet(this, _workerClients).delete(streamKey);
|
|
10963
|
+
__privateMethod(this, _FECManager_instances, releaseWorkerClient_fn).call(this, client);
|
|
10964
|
+
__privateWrapper(this, _decoderUnavailableEvents)._++;
|
|
10965
|
+
const stream = __privateMethod(this, _FECManager_instances, parseStreamKey_fn).call(this, streamKey);
|
|
10966
|
+
const fallbackDecoder = __privateMethod(this, _FECManager_instances, decoderForPacket_fn).call(this, stream.packetId, stream.catalogTrackName) ? "sync" : "none";
|
|
10967
|
+
this.onDecoderUnavailable?.({
|
|
10968
|
+
packetId: stream.packetId,
|
|
10969
|
+
...stream.catalogTrackName === void 0 ? {} : { catalogTrackName: stream.catalogTrackName },
|
|
10970
|
+
reason,
|
|
10971
|
+
fallbackDecoder
|
|
10972
|
+
});
|
|
10973
|
+
};
|
|
10974
|
+
/**
|
|
10975
|
+
* Fail every block whose Worker cleanup acknowledgement is owed by `client`
|
|
10976
|
+
* and drop its block ownership, so no block waits on a message the Worker
|
|
10977
|
+
* will never post.
|
|
10978
|
+
*/
|
|
10979
|
+
releaseWorkerClient_fn = function(client) {
|
|
10980
|
+
const owed = [...__privateGet(this, _pendingWorkerCleanupClients)].filter(([, owner]) => owner === client).map(([key]) => key);
|
|
10981
|
+
for (const key of owed) __privateMethod(this, _FECManager_instances, settleOwedCleanup_fn).call(this, key, "decoder-unavailable");
|
|
10982
|
+
__privateGet(this, _decoderBlockOwners).delete(client);
|
|
10983
|
+
};
|
|
10984
|
+
/**
|
|
10985
|
+
* Resolve one owed Worker cleanup: drop the pending entry and fail the block
|
|
10986
|
+
* unless it already completed. Shared by the cleanup acknowledgement and by
|
|
10987
|
+
* Worker retirement, so the two paths cannot drift in their bookkeeping.
|
|
10988
|
+
*/
|
|
10989
|
+
settleOwedCleanup_fn = function(key, event) {
|
|
10990
|
+
__privateGet(this, _pendingWorkerCleanups).delete(key);
|
|
10991
|
+
__privateGet(this, _pendingWorkerCleanupClients).delete(key);
|
|
10992
|
+
if (__privateGet(this, _completedBlocks).has(key)) return;
|
|
10993
|
+
const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
|
|
10994
|
+
__privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, event, block.sbn, block.packetId, block.catalogTrackName);
|
|
10995
|
+
__privateMethod(this, _FECManager_instances, failBlock_fn).call(this, block.sbn, block.packetId, false, block.catalogTrackName);
|
|
10996
|
+
};
|
|
10997
|
+
/** Evict a disposed client under every stream key that still maps to it. */
|
|
10998
|
+
evictDisposedWorker_fn = function(client) {
|
|
10999
|
+
for (const [key, registered] of [...__privateGet(this, _workerClients)]) {
|
|
11000
|
+
if (registered !== client) continue;
|
|
11001
|
+
const stream = __privateMethod(this, _FECManager_instances, parseStreamKey_fn).call(this, key);
|
|
11002
|
+
const track = stream.catalogTrackName === void 0 ? "" : ` track=${stream.catalogTrackName}`;
|
|
11003
|
+
console.warn(
|
|
11004
|
+
`[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`
|
|
11005
|
+
);
|
|
11006
|
+
__privateMethod(this, _FECManager_instances, retireWorker_fn).call(this, key, client, "disposed");
|
|
11007
|
+
}
|
|
11008
|
+
};
|
|
10336
11009
|
routeSyncCleanup_fn = function(decoder, sbn, packetId) {
|
|
10337
11010
|
let framed;
|
|
10338
11011
|
try {
|
|
@@ -11951,7 +12624,7 @@ buildFrameMeta_fn = function(assembled, packetId, seq, timing, recovered = false
|
|
|
11951
12624
|
}
|
|
11952
12625
|
return meta;
|
|
11953
12626
|
};
|
|
11954
|
-
sequencerFor_fn = function(packetId,
|
|
12627
|
+
sequencerFor_fn = function(packetId, _trackType2, catalogTrackName) {
|
|
11955
12628
|
const windowSize = __privateGet(this, _reassemblyPolicy) ? mmtpFrameReorderWindowFrames(__privateGet(this, _reassemblyPolicy), __privateGet(this, _reorderWindow)) : __privateGet(this, _reorderWindow);
|
|
11956
12629
|
if (windowSize <= 0) return null;
|
|
11957
12630
|
const streamKey = __privateMethod(this, _MmtpContainerParser_instances, streamKey_fn2).call(this, packetId, catalogTrackName);
|
|
@@ -13683,7 +14356,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13683
14356
|
let dedupedAgainstSource2 = 0;
|
|
13684
14357
|
let droppedBeforeSync2 = 0;
|
|
13685
14358
|
for (const sym of result.symbols) {
|
|
13686
|
-
if (sym.fragmentType === 0) continue;
|
|
14359
|
+
if (sym.fragmentType === 0 || sym.fragmentType === 1) continue;
|
|
13687
14360
|
const fi = sym.fragment.fragmentationIndicator;
|
|
13688
14361
|
if (fi === 0) {
|
|
13689
14362
|
if (__privateGet(this, _sourcePktSeqs).has(
|
|
@@ -13693,6 +14366,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13693
14366
|
continue;
|
|
13694
14367
|
}
|
|
13695
14368
|
const keyframe = isVideoTrack ? __privateMethod(this, _MmtpRouter_instances, detectKeyframe_fn).call(this, ownerPacketId, sym.fragment.data, isMfuKeyframeMetadata2(sym.fragment.rapFlag, sym.fragment.priority)) : sym.fragment.rapFlag;
|
|
14369
|
+
const sourceDiscontinuity = __privateMethod(this, _MmtpRouter_instances, opensFecSync_fn).call(this, ownerPacketId, keyframe, catalogTrackName);
|
|
13696
14370
|
if (!__privateMethod(this, _MmtpRouter_instances, shouldEmitFrame_fn).call(this, ownerPacketId, keyframe, catalogTrackName)) {
|
|
13697
14371
|
droppedBeforeSync2++;
|
|
13698
14372
|
continue;
|
|
@@ -13717,7 +14391,8 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13717
14391
|
// reassembler fold (`assembled.deliveryProvenance`); this branch
|
|
13718
14392
|
// never touches the reassembler, so it must carry the block-level
|
|
13719
14393
|
// verdict directly or the sample reaches MSE unattributed.
|
|
13720
|
-
deliveryProvenance
|
|
14394
|
+
deliveryProvenance,
|
|
14395
|
+
...sourceDiscontinuity ? { sourceDiscontinuity: true } : {}
|
|
13721
14396
|
});
|
|
13722
14397
|
__privateMethod(this, _MmtpRouter_instances, noteEmittedKeyframe_fn).call(this, ownerPacketId, keyframe);
|
|
13723
14398
|
} else {
|
|
@@ -13758,11 +14433,12 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13758
14433
|
}
|
|
13759
14434
|
if (assembled) {
|
|
13760
14435
|
const keyframe = isVideoTrack ? __privateMethod(this, _MmtpRouter_instances, detectKeyframe_fn).call(this, ownerPacketId, assembled.data, isMfuKeyframeMetadata2(assembled.rapFlag, assembled.priority)) : assembled.rapFlag;
|
|
13761
|
-
|
|
14436
|
+
const recoveredCatalogTrackName = assembled.catalogTrackName ?? catalogTrackName;
|
|
14437
|
+
const sourceDiscontinuity = __privateMethod(this, _MmtpRouter_instances, opensFecSync_fn).call(this, ownerPacketId, keyframe, recoveredCatalogTrackName);
|
|
14438
|
+
if (!__privateMethod(this, _MmtpRouter_instances, shouldEmitFrame_fn).call(this, ownerPacketId, keyframe, recoveredCatalogTrackName)) {
|
|
13762
14439
|
droppedBeforeSync2++;
|
|
13763
14440
|
continue;
|
|
13764
14441
|
}
|
|
13765
|
-
const recoveredCatalogTrackName = assembled.catalogTrackName ?? catalogTrackName;
|
|
13766
14442
|
const recoveredTiming = __privateMethod(this, _MmtpRouter_instances, resolveRecoveredTiming_fn).call(this, ownerPacketId, assembled.mpuSequenceNumber, assembled.movieFragmentSequenceNumber, assembled.sampleNumber, assembled.timestamp, recoveredCatalogTrackName);
|
|
13767
14443
|
if (!recoveredTiming) continue;
|
|
13768
14444
|
frames.push({
|
|
@@ -13782,7 +14458,8 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13782
14458
|
...assembled.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber },
|
|
13783
14459
|
...assembled.sampleNumber === void 0 ? {} : { sampleNumber: assembled.sampleNumber },
|
|
13784
14460
|
...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName },
|
|
13785
|
-
deliveryProvenance: assembled.deliveryProvenance
|
|
14461
|
+
deliveryProvenance: assembled.deliveryProvenance,
|
|
14462
|
+
...sourceDiscontinuity ? { sourceDiscontinuity: true } : {}
|
|
13786
14463
|
});
|
|
13787
14464
|
__privateMethod(this, _MmtpRouter_instances, noteEmittedKeyframe_fn).call(this, ownerPacketId, keyframe);
|
|
13788
14465
|
}
|
|
@@ -13790,6 +14467,26 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13790
14467
|
}
|
|
13791
14468
|
if (frames.length > 0) {
|
|
13792
14469
|
assembler.addRecoveredBlock(blockId, frames);
|
|
14470
|
+
const cb2 = __privateMethod(this, _MmtpRouter_instances, getFrameCallback_fn).call(this, ownerPacketId);
|
|
14471
|
+
for (const frame of frames) {
|
|
14472
|
+
cb2?.(frame.data, {
|
|
14473
|
+
timestamp: frame.timestamp,
|
|
14474
|
+
...frame.mmtpTimestamp === void 0 ? {} : { mmtpTimestamp: frame.mmtpTimestamp },
|
|
14475
|
+
keyframe: frame.keyframe,
|
|
14476
|
+
...frame.authoredTiming === void 0 ? {} : { authoredTiming: frame.authoredTiming },
|
|
14477
|
+
packetId: ownerPacketId,
|
|
14478
|
+
...frame.packetSequenceNumber === void 0 ? {} : { packetSequenceNumber: frame.packetSequenceNumber },
|
|
14479
|
+
...frame.packetSequenceNumbers === void 0 ? {} : { packetSequenceNumbers: frame.packetSequenceNumbers },
|
|
14480
|
+
...frame.repairedPacketSequenceNumbers === void 0 ? {} : { repairedPacketSequenceNumbers: frame.repairedPacketSequenceNumbers },
|
|
14481
|
+
...frame.mpuSequenceNumber === void 0 ? {} : { mpuSequenceNumber: frame.mpuSequenceNumber },
|
|
14482
|
+
...frame.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: frame.movieFragmentSequenceNumber },
|
|
14483
|
+
...frame.sampleNumber === void 0 ? {} : { sampleNumber: frame.sampleNumber },
|
|
14484
|
+
...frame.catalogTrackName === void 0 ? {} : { catalogTrackName: frame.catalogTrackName },
|
|
14485
|
+
recovered: true,
|
|
14486
|
+
deliveryProvenance: frame.deliveryProvenance,
|
|
14487
|
+
...frame.sourceDiscontinuity ? { sourceDiscontinuity: true } : {}
|
|
14488
|
+
});
|
|
14489
|
+
}
|
|
13793
14490
|
debugRouter(
|
|
13794
14491
|
`[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols \u2192 ${frames.length} reassembled frames \u2192 MSE assembler (packetId=${ownerPacketId}, dedupedAgainstSource=${dedupedAgainstSource2}, droppedBeforeSync=${droppedBeforeSync2})`
|
|
13795
14492
|
);
|
|
@@ -13806,7 +14503,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
|
|
|
13806
14503
|
let droppedStale = 0;
|
|
13807
14504
|
let dedupedAgainstSource = 0;
|
|
13808
14505
|
for (const sym of result.symbols) {
|
|
13809
|
-
if (sym.fragmentType === 0) continue;
|
|
14506
|
+
if (sym.fragmentType === 0 || sym.fragmentType === 1) continue;
|
|
13810
14507
|
if (isVideo) {
|
|
13811
14508
|
const fi = sym.fragment.fragmentationIndicator;
|
|
13812
14509
|
if (fi === 0) {
|
|
@@ -14135,7 +14832,7 @@ refreshStats_fn = function() {
|
|
|
14135
14832
|
this.stats.reassembledMFUs = asNumber(stats.reassembled_mfus);
|
|
14136
14833
|
this.stats.rejectedFragments = __privateGet(this, _rejectedFragments);
|
|
14137
14834
|
this.stats.timedOutMFUs = asNumber(stats.timed_out_mfus);
|
|
14138
|
-
this.stats.evictedMemoryLimit = asNumber(stats.evicted_memory_limit);
|
|
14835
|
+
this.stats.evictedMemoryLimit = asNumber(stats.evicted_memory_limit) + asNumber(stats.evicted_data_units ?? 0);
|
|
14139
14836
|
this.stats.outOfOrderFragments = asNumber(stats.out_of_order_fragments);
|
|
14140
14837
|
stats.free?.();
|
|
14141
14838
|
};
|
|
@@ -14245,6 +14942,1333 @@ var ReedSolomonCodec = class {
|
|
|
14245
14942
|
}
|
|
14246
14943
|
};
|
|
14247
14944
|
|
|
14945
|
+
// node_modules/@blockcast/mmt-transport/dist/types.js
|
|
14946
|
+
var MoqFecAlgorithm;
|
|
14947
|
+
(function(MoqFecAlgorithm2) {
|
|
14948
|
+
MoqFecAlgorithm2[MoqFecAlgorithm2["None"] = 0] = "None";
|
|
14949
|
+
MoqFecAlgorithm2[MoqFecAlgorithm2["Xor"] = 1] = "Xor";
|
|
14950
|
+
MoqFecAlgorithm2[MoqFecAlgorithm2["RaptorQ"] = 2] = "RaptorQ";
|
|
14951
|
+
})(MoqFecAlgorithm || (MoqFecAlgorithm = {}));
|
|
14952
|
+
|
|
14953
|
+
// node_modules/@blockcast/mmt-transport/dist/driad-discovery.js
|
|
14954
|
+
var DRIADDiscovery = class {
|
|
14955
|
+
constructor() {
|
|
14956
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
14957
|
+
__publicField(this, "cacheExpiry", 3e5);
|
|
14958
|
+
}
|
|
14959
|
+
// 5 minutes
|
|
14960
|
+
/**
|
|
14961
|
+
* Discover AMT relays for a multicast source address (RFC 8777)
|
|
14962
|
+
*
|
|
14963
|
+
* @param source - The multicast source IP address (NOT the group!)
|
|
14964
|
+
* @returns Array of discovered AMT relays that can tunnel traffic from this source
|
|
14965
|
+
*/
|
|
14966
|
+
async discoverRelays(source) {
|
|
14967
|
+
const cached = this.cache.get(source);
|
|
14968
|
+
if (cached) {
|
|
14969
|
+
return cached;
|
|
14970
|
+
}
|
|
14971
|
+
try {
|
|
14972
|
+
const reversedSource = source.split(".").reverse().join(".");
|
|
14973
|
+
const queryDomain = `${reversedSource}.amt.in-addr.arpa`;
|
|
14974
|
+
console.log(`[DRIAD] Discovering relays for source ${source} via ${queryDomain}`);
|
|
14975
|
+
const relays = await this.queryDRIADProxy(source);
|
|
14976
|
+
if (relays.length > 0) {
|
|
14977
|
+
this.cache.set(source, relays);
|
|
14978
|
+
setTimeout(() => this.cache.delete(source), this.cacheExpiry);
|
|
14979
|
+
}
|
|
14980
|
+
return relays;
|
|
14981
|
+
} catch (error) {
|
|
14982
|
+
console.warn(`[DRIAD] Discovery failed for source ${source}:`, error);
|
|
14983
|
+
return [];
|
|
14984
|
+
}
|
|
14985
|
+
}
|
|
14986
|
+
/**
|
|
14987
|
+
* Query DRIAD proxy service (browser-compatible)
|
|
14988
|
+
*/
|
|
14989
|
+
async queryDRIADProxy(source) {
|
|
14990
|
+
const proxyEndpoints = [
|
|
14991
|
+
"https://driad.amt.net/api/v1/relays",
|
|
14992
|
+
"https://multicast-gateway.cloudflare.com/driad"
|
|
14993
|
+
];
|
|
14994
|
+
for (const endpoint of proxyEndpoints) {
|
|
14995
|
+
try {
|
|
14996
|
+
const response = await fetch(`${endpoint}?source=${encodeURIComponent(source)}`, {
|
|
14997
|
+
method: "GET",
|
|
14998
|
+
headers: { Accept: "application/json" },
|
|
14999
|
+
signal: AbortSignal.timeout(5e3)
|
|
15000
|
+
});
|
|
15001
|
+
if (response.ok) {
|
|
15002
|
+
const data = await response.json();
|
|
15003
|
+
return data.relays || [];
|
|
15004
|
+
}
|
|
15005
|
+
} catch {
|
|
15006
|
+
}
|
|
15007
|
+
}
|
|
15008
|
+
const dohRelay = await this.queryDNSOverHTTPS(source);
|
|
15009
|
+
if (dohRelay) {
|
|
15010
|
+
return [dohRelay];
|
|
15011
|
+
}
|
|
15012
|
+
return [];
|
|
15013
|
+
}
|
|
15014
|
+
/**
|
|
15015
|
+
* Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)
|
|
15016
|
+
*/
|
|
15017
|
+
async queryDNSOverHTTPS(source) {
|
|
15018
|
+
try {
|
|
15019
|
+
const reversedSource = source.split(".").reverse().join(".");
|
|
15020
|
+
const queryName = `${reversedSource}.amt.in-addr.arpa`;
|
|
15021
|
+
const dohUrl = `https://cloudflare-dns.com/dns-query?name=${queryName}&type=A`;
|
|
15022
|
+
const response = await fetch(dohUrl, {
|
|
15023
|
+
headers: { Accept: "application/dns-json" },
|
|
15024
|
+
signal: AbortSignal.timeout(3e3)
|
|
15025
|
+
});
|
|
15026
|
+
if (!response.ok) {
|
|
15027
|
+
return null;
|
|
15028
|
+
}
|
|
15029
|
+
const result = await response.json();
|
|
15030
|
+
if (result.Answer && result.Answer.length > 0) {
|
|
15031
|
+
return {
|
|
15032
|
+
host: result.Answer[0].data,
|
|
15033
|
+
port: 2268,
|
|
15034
|
+
priority: 10,
|
|
15035
|
+
weight: 100,
|
|
15036
|
+
asn: 0
|
|
15037
|
+
};
|
|
15038
|
+
}
|
|
15039
|
+
return null;
|
|
15040
|
+
} catch (error) {
|
|
15041
|
+
console.warn("[DRIAD] DNS-over-HTTPS query failed:", error);
|
|
15042
|
+
return null;
|
|
15043
|
+
}
|
|
15044
|
+
}
|
|
15045
|
+
/**
|
|
15046
|
+
* Select best relay based on RTT measurements
|
|
15047
|
+
*/
|
|
15048
|
+
async selectBestRelay(relays) {
|
|
15049
|
+
if (relays.length === 0)
|
|
15050
|
+
return null;
|
|
15051
|
+
const measured = await Promise.all(relays.map(async (relay) => {
|
|
15052
|
+
const rtt = await this.measureRTT(relay.host, relay.port);
|
|
15053
|
+
return { ...relay, rtt };
|
|
15054
|
+
}));
|
|
15055
|
+
measured.sort((a, b) => {
|
|
15056
|
+
if (a.priority !== b.priority)
|
|
15057
|
+
return a.priority - b.priority;
|
|
15058
|
+
return (a.rtt || Infinity) - (b.rtt || Infinity);
|
|
15059
|
+
});
|
|
15060
|
+
console.log(`[DRIAD] Selected relay: ${measured[0].host} (RTT: ${measured[0].rtt}ms)`);
|
|
15061
|
+
return measured[0];
|
|
15062
|
+
}
|
|
15063
|
+
async measureRTT(host, _port) {
|
|
15064
|
+
const start = performance.now();
|
|
15065
|
+
try {
|
|
15066
|
+
await fetch(`https://${host}/health`, {
|
|
15067
|
+
method: "HEAD",
|
|
15068
|
+
signal: AbortSignal.timeout(2e3),
|
|
15069
|
+
mode: "no-cors"
|
|
15070
|
+
});
|
|
15071
|
+
return performance.now() - start;
|
|
15072
|
+
} catch {
|
|
15073
|
+
return Infinity;
|
|
15074
|
+
}
|
|
15075
|
+
}
|
|
15076
|
+
/**
|
|
15077
|
+
* Clear the relay cache
|
|
15078
|
+
*/
|
|
15079
|
+
clearCache() {
|
|
15080
|
+
this.cache.clear();
|
|
15081
|
+
}
|
|
15082
|
+
/**
|
|
15083
|
+
* Get cached relays for a source address
|
|
15084
|
+
*/
|
|
15085
|
+
getCached(source) {
|
|
15086
|
+
return this.cache.get(source);
|
|
15087
|
+
}
|
|
15088
|
+
};
|
|
15089
|
+
var driadDiscovery = new DRIADDiscovery();
|
|
15090
|
+
|
|
15091
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-tier.js
|
|
15092
|
+
function endpointKey(endpoint) {
|
|
15093
|
+
return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
|
|
15094
|
+
}
|
|
15095
|
+
function selectMulticastTier(endpoints, estimate) {
|
|
15096
|
+
if (endpoints.length <= 1)
|
|
15097
|
+
return endpoints[0];
|
|
15098
|
+
const ranked = endpoints.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
|
|
15099
|
+
if (ranked.length < 2)
|
|
15100
|
+
return endpoints[0];
|
|
15101
|
+
if (estimate === void 0)
|
|
15102
|
+
return ranked[0];
|
|
15103
|
+
const budget = estimate * 0.8;
|
|
15104
|
+
let selected;
|
|
15105
|
+
for (const endpoint of ranked) {
|
|
15106
|
+
if (endpoint.bandwidth <= budget)
|
|
15107
|
+
selected = endpoint;
|
|
15108
|
+
}
|
|
15109
|
+
return selected ?? ranked[0];
|
|
15110
|
+
}
|
|
15111
|
+
|
|
15112
|
+
// node_modules/@blockcast/mmt-transport/dist/abr-controller.js
|
|
15113
|
+
var QUALITY_LEVELS = [
|
|
15114
|
+
{ name: "240p", bitrate: 3e5, width: 426, height: 240, fps: 15 },
|
|
15115
|
+
{ name: "360p", bitrate: 5e5, width: 640, height: 360, fps: 24 },
|
|
15116
|
+
{ name: "480p", bitrate: 1e6, width: 854, height: 480, fps: 30 },
|
|
15117
|
+
{ name: "720p", bitrate: 25e5, width: 1280, height: 720, fps: 30 },
|
|
15118
|
+
{ name: "1080p", bitrate: 5e6, width: 1920, height: 1080, fps: 30 },
|
|
15119
|
+
{ name: "1440p", bitrate: 8e6, width: 2560, height: 1440, fps: 30 },
|
|
15120
|
+
{ name: "4K", bitrate: 15e6, width: 3840, height: 2160, fps: 30 }
|
|
15121
|
+
];
|
|
15122
|
+
var AdaptiveBitrateController = class {
|
|
15123
|
+
constructor(initialQuality) {
|
|
15124
|
+
__publicField(this, "currentQualityIndex");
|
|
15125
|
+
__publicField(this, "history", []);
|
|
15126
|
+
__publicField(this, "historyWindow", 1e4);
|
|
15127
|
+
// 10 second window
|
|
15128
|
+
__publicField(this, "upgradeThreshold", 0.8);
|
|
15129
|
+
// 80% of target bitrate sustained
|
|
15130
|
+
__publicField(this, "downgradeThreshold", 0.5);
|
|
15131
|
+
// 50% of target bitrate triggers downgrade
|
|
15132
|
+
__publicField(this, "stabilityPeriod", 5e3);
|
|
15133
|
+
// Wait 5s before upgrading
|
|
15134
|
+
__publicField(this, "lastQualityChange", 0);
|
|
15135
|
+
__publicField(this, "onQualityChange");
|
|
15136
|
+
// FEC stats for effective loss rate calculation
|
|
15137
|
+
__publicField(this, "fecStats", {
|
|
15138
|
+
rawLossRate: 0,
|
|
15139
|
+
effectiveLossRate: 0,
|
|
15140
|
+
recoveryRate: 1,
|
|
15141
|
+
fecEnabled: false
|
|
15142
|
+
});
|
|
15143
|
+
this.currentQualityIndex = initialQuality ? QUALITY_LEVELS.findIndex((q) => q.name === initialQuality.name) : 0;
|
|
15144
|
+
if (this.currentQualityIndex < 0)
|
|
15145
|
+
this.currentQualityIndex = 0;
|
|
15146
|
+
}
|
|
15147
|
+
setCallback(callback) {
|
|
15148
|
+
this.onQualityChange = callback;
|
|
15149
|
+
}
|
|
15150
|
+
getCurrentQuality() {
|
|
15151
|
+
return QUALITY_LEVELS[this.currentQualityIndex];
|
|
15152
|
+
}
|
|
15153
|
+
/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */
|
|
15154
|
+
selectMulticastTier(endpoints, estimate = this.getMetrics().measuredBitrate || void 0) {
|
|
15155
|
+
return selectMulticastTier(endpoints, estimate);
|
|
15156
|
+
}
|
|
15157
|
+
/**
|
|
15158
|
+
* Record network sample for bitrate adaptation
|
|
15159
|
+
*
|
|
15160
|
+
* @param bytesReceived - Bytes received in this sample
|
|
15161
|
+
* @param packetsLost - Packets lost (raw, before FEC recovery)
|
|
15162
|
+
*/
|
|
15163
|
+
recordSample(bytesReceived, packetsLost) {
|
|
15164
|
+
const now = Date.now();
|
|
15165
|
+
this.history.push({ timestamp: now, bytesReceived, packetsLost });
|
|
15166
|
+
this.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);
|
|
15167
|
+
const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
|
|
15168
|
+
const currentQuality = this.getCurrentQuality();
|
|
15169
|
+
const effectiveLossRate = this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate;
|
|
15170
|
+
if (now - this.lastQualityChange < this.stabilityPeriod) {
|
|
15171
|
+
return;
|
|
15172
|
+
}
|
|
15173
|
+
if (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {
|
|
15174
|
+
this.downgrade();
|
|
15175
|
+
return;
|
|
15176
|
+
}
|
|
15177
|
+
if (measuredBitrate > currentQuality.bitrate * this.upgradeThreshold && effectiveLossRate < 0.01 && this.currentQualityIndex < QUALITY_LEVELS.length - 1) {
|
|
15178
|
+
this.upgrade();
|
|
15179
|
+
}
|
|
15180
|
+
}
|
|
15181
|
+
/**
|
|
15182
|
+
* Update FEC statistics for ABR decisions
|
|
15183
|
+
*
|
|
15184
|
+
* Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate
|
|
15185
|
+
* (after FEC recovery) rather than raw transport loss rate.
|
|
15186
|
+
*
|
|
15187
|
+
* @param stats - FEC statistics from the FEC decoder
|
|
15188
|
+
*/
|
|
15189
|
+
setFecStats(stats) {
|
|
15190
|
+
this.fecStats = stats;
|
|
15191
|
+
}
|
|
15192
|
+
/**
|
|
15193
|
+
* Get current FEC statistics
|
|
15194
|
+
*/
|
|
15195
|
+
getFecStats() {
|
|
15196
|
+
return { ...this.fecStats };
|
|
15197
|
+
}
|
|
15198
|
+
calculateMetrics() {
|
|
15199
|
+
if (this.history.length < 2) {
|
|
15200
|
+
return { measuredBitrate: 0, lossRate: 0 };
|
|
15201
|
+
}
|
|
15202
|
+
const totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);
|
|
15203
|
+
const totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);
|
|
15204
|
+
const duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1e3;
|
|
15205
|
+
const measuredBitrate = duration > 0 ? totalBytes * 8 / duration : 0;
|
|
15206
|
+
const lossRate = totalLost / (this.history.length + totalLost);
|
|
15207
|
+
return { measuredBitrate, lossRate };
|
|
15208
|
+
}
|
|
15209
|
+
upgrade() {
|
|
15210
|
+
if (this.currentQualityIndex >= QUALITY_LEVELS.length - 1)
|
|
15211
|
+
return;
|
|
15212
|
+
this.currentQualityIndex++;
|
|
15213
|
+
this.lastQualityChange = Date.now();
|
|
15214
|
+
const newQuality = this.getCurrentQuality();
|
|
15215
|
+
console.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
|
|
15216
|
+
this.onQualityChange?.(newQuality);
|
|
15217
|
+
}
|
|
15218
|
+
downgrade() {
|
|
15219
|
+
if (this.currentQualityIndex <= 0)
|
|
15220
|
+
return;
|
|
15221
|
+
this.currentQualityIndex--;
|
|
15222
|
+
this.lastQualityChange = Date.now();
|
|
15223
|
+
const newQuality = this.getCurrentQuality();
|
|
15224
|
+
console.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
|
|
15225
|
+
this.onQualityChange?.(newQuality);
|
|
15226
|
+
}
|
|
15227
|
+
/**
|
|
15228
|
+
* Force quality level (for testing)
|
|
15229
|
+
*/
|
|
15230
|
+
setQuality(name) {
|
|
15231
|
+
const index = QUALITY_LEVELS.findIndex((q) => q.name === name);
|
|
15232
|
+
if (index >= 0) {
|
|
15233
|
+
this.currentQualityIndex = index;
|
|
15234
|
+
this.lastQualityChange = Date.now();
|
|
15235
|
+
this.onQualityChange?.(this.getCurrentQuality());
|
|
15236
|
+
}
|
|
15237
|
+
}
|
|
15238
|
+
/**
|
|
15239
|
+
* Get current metrics including both raw and effective loss rates
|
|
15240
|
+
*/
|
|
15241
|
+
getMetrics() {
|
|
15242
|
+
const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
|
|
15243
|
+
return {
|
|
15244
|
+
measuredBitrate,
|
|
15245
|
+
rawLossRate,
|
|
15246
|
+
effectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,
|
|
15247
|
+
fecEnabled: this.fecStats.fecEnabled,
|
|
15248
|
+
sampleCount: this.history.length
|
|
15249
|
+
};
|
|
15250
|
+
}
|
|
15251
|
+
/**
|
|
15252
|
+
* Reset history and start fresh
|
|
15253
|
+
*/
|
|
15254
|
+
reset() {
|
|
15255
|
+
this.history = [];
|
|
15256
|
+
this.lastQualityChange = 0;
|
|
15257
|
+
}
|
|
15258
|
+
};
|
|
15259
|
+
|
|
15260
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-orchestrator.js
|
|
15261
|
+
var DEFAULT_DWELL_MS = 3e3;
|
|
15262
|
+
var DEFAULT_JOIN_DEADLINE_MS = 5e3;
|
|
15263
|
+
var _state2, _current, _target, _estimate, _favored, _joinStartedAt, _lastCommittedTimestamp, _dwellMs, _joinDeadlineMs, _MulticastOrchestrator_instances, now_fn;
|
|
15264
|
+
var MulticastOrchestrator = class {
|
|
15265
|
+
constructor(options) {
|
|
15266
|
+
__privateAdd(this, _MulticastOrchestrator_instances);
|
|
15267
|
+
__publicField(this, "options");
|
|
15268
|
+
__privateAdd(this, _state2, "stable");
|
|
15269
|
+
__privateAdd(this, _current);
|
|
15270
|
+
__privateAdd(this, _target);
|
|
15271
|
+
__privateAdd(this, _estimate);
|
|
15272
|
+
__privateAdd(this, _favored);
|
|
15273
|
+
__privateAdd(this, _joinStartedAt, 0);
|
|
15274
|
+
__privateAdd(this, _lastCommittedTimestamp, Number.NEGATIVE_INFINITY);
|
|
15275
|
+
__privateAdd(this, _dwellMs);
|
|
15276
|
+
__privateAdd(this, _joinDeadlineMs);
|
|
15277
|
+
this.options = options;
|
|
15278
|
+
__privateSet(this, _dwellMs, options.dwellMs ?? DEFAULT_DWELL_MS);
|
|
15279
|
+
__privateSet(this, _joinDeadlineMs, options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS);
|
|
15280
|
+
if (!Number.isFinite(__privateGet(this, _dwellMs)) || !Number.isFinite(__privateGet(this, _joinDeadlineMs)) || __privateGet(this, _dwellMs) <= 0 || __privateGet(this, _joinDeadlineMs) <= 0) {
|
|
15281
|
+
throw new Error("Multicast dwell and join deadline must be positive");
|
|
15282
|
+
}
|
|
15283
|
+
}
|
|
15284
|
+
get state() {
|
|
15285
|
+
return __privateGet(this, _state2);
|
|
15286
|
+
}
|
|
15287
|
+
get current() {
|
|
15288
|
+
return __privateGet(this, _current);
|
|
15289
|
+
}
|
|
15290
|
+
get target() {
|
|
15291
|
+
return __privateGet(this, _target);
|
|
15292
|
+
}
|
|
15293
|
+
get estimate() {
|
|
15294
|
+
return __privateGet(this, _estimate);
|
|
15295
|
+
}
|
|
15296
|
+
start() {
|
|
15297
|
+
__privateGet(this, _current) ?? __privateSet(this, _current, this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, void 0));
|
|
15298
|
+
return __privateGet(this, _current);
|
|
15299
|
+
}
|
|
15300
|
+
onGoodput(sample) {
|
|
15301
|
+
const bitrate = sample.bitrate;
|
|
15302
|
+
if (!__privateGet(this, _current) || this.options.adaptive === false)
|
|
15303
|
+
return void 0;
|
|
15304
|
+
if (__privateGet(this, _state2) === "joining")
|
|
15305
|
+
return this.poll();
|
|
15306
|
+
if (bitrate === void 0 || !Number.isFinite(bitrate))
|
|
15307
|
+
return void 0;
|
|
15308
|
+
__privateSet(this, _estimate, bitrate);
|
|
15309
|
+
const desired = selectMulticastTier(this.options.endpoints, bitrate);
|
|
15310
|
+
if (!desired)
|
|
15311
|
+
return void 0;
|
|
15312
|
+
const desiredKey = endpointKey(desired);
|
|
15313
|
+
if (desiredKey === endpointKey(__privateGet(this, _current))) {
|
|
15314
|
+
if (__privateGet(this, _favored)?.source === "goodput")
|
|
15315
|
+
__privateSet(this, _favored, void 0);
|
|
15316
|
+
return void 0;
|
|
15317
|
+
}
|
|
15318
|
+
if (typeof desired.bandwidth !== "number" || typeof __privateGet(this, _current).bandwidth !== "number" || desired.bandwidth >= __privateGet(this, _current).bandwidth) {
|
|
15319
|
+
if (__privateGet(this, _favored)?.source === "goodput")
|
|
15320
|
+
__privateSet(this, _favored, void 0);
|
|
15321
|
+
return void 0;
|
|
15322
|
+
}
|
|
15323
|
+
const now = __privateMethod(this, _MulticastOrchestrator_instances, now_fn).call(this);
|
|
15324
|
+
if (!__privateGet(this, _favored) || __privateGet(this, _favored).key !== desiredKey || __privateGet(this, _favored).source !== "goodput") {
|
|
15325
|
+
__privateSet(this, _favored, { key: desiredKey, since: now, source: "goodput" });
|
|
15326
|
+
return void 0;
|
|
15327
|
+
}
|
|
15328
|
+
if (now - __privateGet(this, _favored).since < __privateGet(this, _dwellMs))
|
|
15329
|
+
return void 0;
|
|
15330
|
+
__privateSet(this, _state2, "joining");
|
|
15331
|
+
__privateSet(this, _target, desired);
|
|
15332
|
+
__privateSet(this, _joinStartedAt, now);
|
|
15333
|
+
__privateSet(this, _favored, void 0);
|
|
15334
|
+
return { kind: "join-target", current: __privateGet(this, _current), target: desired };
|
|
15335
|
+
}
|
|
15336
|
+
onHeadroom(sample) {
|
|
15337
|
+
if (!__privateGet(this, _current) || this.options.adaptive === false || __privateGet(this, _state2) === "joining")
|
|
15338
|
+
return void 0;
|
|
15339
|
+
if (!sample.healthy) {
|
|
15340
|
+
if (__privateGet(this, _favored)?.source === "headroom")
|
|
15341
|
+
__privateSet(this, _favored, void 0);
|
|
15342
|
+
return void 0;
|
|
15343
|
+
}
|
|
15344
|
+
if (__privateGet(this, _favored)?.source === "goodput")
|
|
15345
|
+
return void 0;
|
|
15346
|
+
const currentBandwidth = __privateGet(this, _current).bandwidth;
|
|
15347
|
+
if (typeof currentBandwidth !== "number")
|
|
15348
|
+
return void 0;
|
|
15349
|
+
const desired = this.options.endpoints.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth).sort((a, b) => a.bandwidth - b.bandwidth)[0];
|
|
15350
|
+
if (!desired) {
|
|
15351
|
+
__privateSet(this, _favored, void 0);
|
|
15352
|
+
return void 0;
|
|
15353
|
+
}
|
|
15354
|
+
const desiredKey = endpointKey(desired);
|
|
15355
|
+
if (desiredKey === endpointKey(__privateGet(this, _current))) {
|
|
15356
|
+
__privateSet(this, _favored, void 0);
|
|
15357
|
+
return void 0;
|
|
15358
|
+
}
|
|
15359
|
+
const now = __privateMethod(this, _MulticastOrchestrator_instances, now_fn).call(this);
|
|
15360
|
+
if (!__privateGet(this, _favored) || __privateGet(this, _favored).key !== desiredKey) {
|
|
15361
|
+
__privateSet(this, _favored, { key: desiredKey, since: now, source: "headroom" });
|
|
15362
|
+
return void 0;
|
|
15363
|
+
}
|
|
15364
|
+
if (now - __privateGet(this, _favored).since < __privateGet(this, _dwellMs))
|
|
15365
|
+
return void 0;
|
|
15366
|
+
__privateSet(this, _state2, "joining");
|
|
15367
|
+
__privateSet(this, _target, desired);
|
|
15368
|
+
__privateSet(this, _joinStartedAt, now);
|
|
15369
|
+
__privateSet(this, _favored, void 0);
|
|
15370
|
+
return { kind: "join-target", current: __privateGet(this, _current), target: desired };
|
|
15371
|
+
}
|
|
15372
|
+
onTierPacket(packet) {
|
|
15373
|
+
if (!__privateGet(this, _current))
|
|
15374
|
+
return void 0;
|
|
15375
|
+
if (packet.endpointKey === endpointKey(__privateGet(this, _current)) && Number.isFinite(packet.timestamp)) {
|
|
15376
|
+
__privateSet(this, _lastCommittedTimestamp, Math.max(__privateGet(this, _lastCommittedTimestamp), packet.timestamp));
|
|
15377
|
+
}
|
|
15378
|
+
if (__privateGet(this, _state2) !== "joining" || !__privateGet(this, _target))
|
|
15379
|
+
return void 0;
|
|
15380
|
+
const expired = this.poll();
|
|
15381
|
+
if (expired)
|
|
15382
|
+
return expired;
|
|
15383
|
+
if (packet.endpointKey !== endpointKey(__privateGet(this, _target)) || !packet.keyframe || !packet.hasValidCodecConfig || !Number.isFinite(packet.timestamp) || packet.timestamp < __privateGet(this, _lastCommittedTimestamp))
|
|
15384
|
+
return void 0;
|
|
15385
|
+
const command = {
|
|
15386
|
+
kind: "commit-swap",
|
|
15387
|
+
target: __privateGet(this, _target),
|
|
15388
|
+
old: __privateGet(this, _current)
|
|
15389
|
+
};
|
|
15390
|
+
__privateSet(this, _lastCommittedTimestamp, packet.timestamp);
|
|
15391
|
+
__privateSet(this, _current, __privateGet(this, _target));
|
|
15392
|
+
__privateSet(this, _target, void 0);
|
|
15393
|
+
__privateSet(this, _state2, "stable");
|
|
15394
|
+
return command;
|
|
15395
|
+
}
|
|
15396
|
+
poll() {
|
|
15397
|
+
if (__privateGet(this, _state2) !== "joining" || !__privateGet(this, _target) || !__privateGet(this, _current))
|
|
15398
|
+
return void 0;
|
|
15399
|
+
if (__privateMethod(this, _MulticastOrchestrator_instances, now_fn).call(this) - __privateGet(this, _joinStartedAt) < __privateGet(this, _joinDeadlineMs))
|
|
15400
|
+
return void 0;
|
|
15401
|
+
return this.cancel();
|
|
15402
|
+
}
|
|
15403
|
+
cancel() {
|
|
15404
|
+
if (__privateGet(this, _state2) !== "joining" || !__privateGet(this, _target) || !__privateGet(this, _current))
|
|
15405
|
+
return void 0;
|
|
15406
|
+
const command = {
|
|
15407
|
+
kind: "abandon-join",
|
|
15408
|
+
current: __privateGet(this, _current),
|
|
15409
|
+
target: __privateGet(this, _target)
|
|
15410
|
+
};
|
|
15411
|
+
__privateSet(this, _target, void 0);
|
|
15412
|
+
__privateSet(this, _state2, "stable");
|
|
15413
|
+
return command;
|
|
15414
|
+
}
|
|
15415
|
+
};
|
|
15416
|
+
_state2 = new WeakMap();
|
|
15417
|
+
_current = new WeakMap();
|
|
15418
|
+
_target = new WeakMap();
|
|
15419
|
+
_estimate = new WeakMap();
|
|
15420
|
+
_favored = new WeakMap();
|
|
15421
|
+
_joinStartedAt = new WeakMap();
|
|
15422
|
+
_lastCommittedTimestamp = new WeakMap();
|
|
15423
|
+
_dwellMs = new WeakMap();
|
|
15424
|
+
_joinDeadlineMs = new WeakMap();
|
|
15425
|
+
_MulticastOrchestrator_instances = new WeakSet();
|
|
15426
|
+
now_fn = function() {
|
|
15427
|
+
const now = this.options.now();
|
|
15428
|
+
if (!Number.isFinite(now))
|
|
15429
|
+
throw new Error("Multicast orchestrator clock must be finite");
|
|
15430
|
+
return now;
|
|
15431
|
+
};
|
|
15432
|
+
|
|
15433
|
+
// node_modules/@blockcast/mmt-transport/dist/endpoint-config.js
|
|
15434
|
+
function toWindowMulticastSubscribeConfig(input) {
|
|
15435
|
+
if (input.mode !== void 0 && !["auto", "native", "tunnel"].includes(input.mode)) {
|
|
15436
|
+
throw new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);
|
|
15437
|
+
}
|
|
15438
|
+
const base = {
|
|
15439
|
+
group: input.group,
|
|
15440
|
+
port: input.port,
|
|
15441
|
+
...input.source?.trim() ? { source: input.source.trim() } : {},
|
|
15442
|
+
consume: "bytes",
|
|
15443
|
+
...input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}
|
|
15444
|
+
};
|
|
15445
|
+
const relay = input.amtRelay?.trim();
|
|
15446
|
+
const wantsAmt = input.mode === "tunnel" || input.mode !== "native" && relay !== void 0 && relay !== "";
|
|
15447
|
+
if (!wantsAmt) {
|
|
15448
|
+
const native = {
|
|
15449
|
+
...base,
|
|
15450
|
+
origin: "ip-multicast",
|
|
15451
|
+
...input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}
|
|
15452
|
+
};
|
|
15453
|
+
return native;
|
|
15454
|
+
}
|
|
15455
|
+
const relayPort = input.amtRelayPort;
|
|
15456
|
+
if (!relay || relayPort === void 0 || !isPositivePort(relayPort)) {
|
|
15457
|
+
throw new TypeError("AMT requires a resolved relay and a valid relay port");
|
|
15458
|
+
}
|
|
15459
|
+
return {
|
|
15460
|
+
...base,
|
|
15461
|
+
origin: "amt",
|
|
15462
|
+
relay,
|
|
15463
|
+
relayPort
|
|
15464
|
+
};
|
|
15465
|
+
}
|
|
15466
|
+
function isValidMulticastGroupAddress(address) {
|
|
15467
|
+
const value = address?.trim().toLowerCase();
|
|
15468
|
+
if (!value)
|
|
15469
|
+
return false;
|
|
15470
|
+
if (value.startsWith("ff"))
|
|
15471
|
+
return isValidIpv6MulticastAddress(value);
|
|
15472
|
+
const parts = value.split(".");
|
|
15473
|
+
if (parts.length !== 4)
|
|
15474
|
+
return false;
|
|
15475
|
+
const octets = parts.map((part) => {
|
|
15476
|
+
if (!/^\d+$/.test(part))
|
|
15477
|
+
return Number.NaN;
|
|
15478
|
+
const octet = Number(part);
|
|
15479
|
+
return Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;
|
|
15480
|
+
});
|
|
15481
|
+
return octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;
|
|
15482
|
+
}
|
|
15483
|
+
function normalizeMulticastEndpoint(endpoint) {
|
|
15484
|
+
const groupAddress = endpoint.groupAddress.trim().toLowerCase();
|
|
15485
|
+
const sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || void 0;
|
|
15486
|
+
const amtRelay = endpoint.amtRelay?.trim().toLowerCase() || void 0;
|
|
15487
|
+
if (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port))
|
|
15488
|
+
return void 0;
|
|
15489
|
+
if (endpoint.mode && !["auto", "native", "tunnel"].includes(endpoint.mode))
|
|
15490
|
+
return void 0;
|
|
15491
|
+
if (endpoint.amtRelayPort !== void 0 && !isPositivePort(endpoint.amtRelayPort))
|
|
15492
|
+
return void 0;
|
|
15493
|
+
return {
|
|
15494
|
+
groupAddress,
|
|
15495
|
+
...sourceAddress ? { sourceAddress } : {},
|
|
15496
|
+
port: endpoint.port,
|
|
15497
|
+
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
15498
|
+
...amtRelay ? { amtRelay } : {},
|
|
15499
|
+
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}
|
|
15500
|
+
};
|
|
15501
|
+
}
|
|
15502
|
+
function multicastEndpointKey(endpoint) {
|
|
15503
|
+
const normalized = normalizeMulticastEndpoint(endpoint);
|
|
15504
|
+
if (!normalized)
|
|
15505
|
+
throw new Error("Invalid multicast endpoint");
|
|
15506
|
+
return JSON.stringify([
|
|
15507
|
+
normalized.sourceAddress ?? "",
|
|
15508
|
+
normalized.groupAddress,
|
|
15509
|
+
normalized.port,
|
|
15510
|
+
normalized.mode ?? "auto",
|
|
15511
|
+
normalized.amtRelay ?? "",
|
|
15512
|
+
normalized.amtRelayPort ?? ""
|
|
15513
|
+
]);
|
|
15514
|
+
}
|
|
15515
|
+
function isValidIpv6MulticastAddress(value) {
|
|
15516
|
+
if (!/^[0-9a-f:]+$/.test(value))
|
|
15517
|
+
return false;
|
|
15518
|
+
const compressed = value.includes("::");
|
|
15519
|
+
if (value.indexOf("::") !== value.lastIndexOf("::"))
|
|
15520
|
+
return false;
|
|
15521
|
+
const groups = value.split(":").filter(Boolean);
|
|
15522
|
+
if (groups.length === 0 || groups.length > 8)
|
|
15523
|
+
return false;
|
|
15524
|
+
if (!compressed && groups.length !== 8)
|
|
15525
|
+
return false;
|
|
15526
|
+
return groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));
|
|
15527
|
+
}
|
|
15528
|
+
function isPositivePort(port) {
|
|
15529
|
+
return Number.isInteger(port) && port > 0 && port <= 65535;
|
|
15530
|
+
}
|
|
15531
|
+
|
|
15532
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-subscription-pool.js
|
|
15533
|
+
var _entries, _gatewayIds, _nextGatewayId, _MulticastSubscriptionPool_instances, createEntry_fn, gatewayId_fn, detach_fn, dispatch_fn;
|
|
15534
|
+
var MulticastSubscriptionPool = class {
|
|
15535
|
+
constructor() {
|
|
15536
|
+
__privateAdd(this, _MulticastSubscriptionPool_instances);
|
|
15537
|
+
__privateAdd(this, _entries, /* @__PURE__ */ new Map());
|
|
15538
|
+
__privateAdd(this, _gatewayIds, /* @__PURE__ */ new WeakMap());
|
|
15539
|
+
__privateAdd(this, _nextGatewayId, 1);
|
|
15540
|
+
}
|
|
15541
|
+
async acquire(gateway, config, consumer) {
|
|
15542
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
15543
|
+
groupAddress: config.group,
|
|
15544
|
+
sourceAddress: config.source,
|
|
15545
|
+
port: config.port,
|
|
15546
|
+
mode: config.mode,
|
|
15547
|
+
amtRelay: config.amtRelay,
|
|
15548
|
+
amtRelayPort: config.amtRelayPort
|
|
15549
|
+
});
|
|
15550
|
+
if (!endpoint)
|
|
15551
|
+
throw new Error("Invalid multicast endpoint");
|
|
15552
|
+
if (typeof gateway.subscribe !== "function")
|
|
15553
|
+
throw new Error("window.multicast.subscribe unavailable");
|
|
15554
|
+
const key = multicastEndpointKey(endpoint);
|
|
15555
|
+
const internalKey = `${__privateMethod(this, _MulticastSubscriptionPool_instances, gatewayId_fn).call(this, gateway)}|${key}`;
|
|
15556
|
+
let record = __privateGet(this, _entries).get(internalKey);
|
|
15557
|
+
if (!record) {
|
|
15558
|
+
record = { claims: 0, entry: Promise.resolve(null) };
|
|
15559
|
+
const createdRecord = record;
|
|
15560
|
+
record.entry = __privateMethod(this, _MulticastSubscriptionPool_instances, createEntry_fn).call(this, gateway, endpoint, (entry2) => {
|
|
15561
|
+
if (__privateGet(this, _entries).get(internalKey) === createdRecord)
|
|
15562
|
+
__privateGet(this, _entries).delete(internalKey);
|
|
15563
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, detach_fn).call(this, entry2);
|
|
15564
|
+
}).catch((error) => {
|
|
15565
|
+
if (__privateGet(this, _entries).get(internalKey) === createdRecord)
|
|
15566
|
+
__privateGet(this, _entries).delete(internalKey);
|
|
15567
|
+
throw error;
|
|
15568
|
+
});
|
|
15569
|
+
__privateGet(this, _entries).set(internalKey, record);
|
|
15570
|
+
}
|
|
15571
|
+
record.claims++;
|
|
15572
|
+
let entry;
|
|
15573
|
+
try {
|
|
15574
|
+
entry = await record.entry;
|
|
15575
|
+
} catch (error) {
|
|
15576
|
+
record.claims--;
|
|
15577
|
+
throw error;
|
|
15578
|
+
}
|
|
15579
|
+
if (entry.closed) {
|
|
15580
|
+
record.claims--;
|
|
15581
|
+
return this.acquire(gateway, config, consumer);
|
|
15582
|
+
}
|
|
15583
|
+
const token = Symbol(key);
|
|
15584
|
+
entry.consumers.set(token, consumer);
|
|
15585
|
+
let released = false;
|
|
15586
|
+
return {
|
|
15587
|
+
key,
|
|
15588
|
+
release: async () => {
|
|
15589
|
+
if (released)
|
|
15590
|
+
return;
|
|
15591
|
+
released = true;
|
|
15592
|
+
entry.consumers.delete(token);
|
|
15593
|
+
record.claims--;
|
|
15594
|
+
if (record.claims > 0)
|
|
15595
|
+
return;
|
|
15596
|
+
if (__privateGet(this, _entries).get(internalKey) === record)
|
|
15597
|
+
__privateGet(this, _entries).delete(internalKey);
|
|
15598
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, detach_fn).call(this, entry);
|
|
15599
|
+
if (!entry.closed)
|
|
15600
|
+
await entry.subscription.unsubscribe();
|
|
15601
|
+
}
|
|
15602
|
+
};
|
|
15603
|
+
}
|
|
15604
|
+
get size() {
|
|
15605
|
+
return __privateGet(this, _entries).size;
|
|
15606
|
+
}
|
|
15607
|
+
};
|
|
15608
|
+
_entries = new WeakMap();
|
|
15609
|
+
_gatewayIds = new WeakMap();
|
|
15610
|
+
_nextGatewayId = new WeakMap();
|
|
15611
|
+
_MulticastSubscriptionPool_instances = new WeakSet();
|
|
15612
|
+
createEntry_fn = async function(gateway, endpoint, onClosed) {
|
|
15613
|
+
const subscription = await gateway.subscribe(toWindowMulticastSubscribeConfig({
|
|
15614
|
+
group: endpoint.groupAddress,
|
|
15615
|
+
...endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {},
|
|
15616
|
+
port: endpoint.port,
|
|
15617
|
+
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
15618
|
+
...endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {},
|
|
15619
|
+
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}
|
|
15620
|
+
}));
|
|
15621
|
+
const consumers = /* @__PURE__ */ new Map();
|
|
15622
|
+
let entry;
|
|
15623
|
+
const bytesListener = (event) => {
|
|
15624
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, dispatch_fn).call(this, consumers, (consumer) => consumer.onBytes(event.detail));
|
|
15625
|
+
};
|
|
15626
|
+
const stateListener = (event) => {
|
|
15627
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, dispatch_fn).call(this, consumers, (consumer) => consumer.onState?.(event.detail));
|
|
15628
|
+
if (event.detail === "closed" && !entry.closed) {
|
|
15629
|
+
entry.closed = true;
|
|
15630
|
+
onClosed(entry);
|
|
15631
|
+
}
|
|
15632
|
+
};
|
|
15633
|
+
const errorListener = (event) => {
|
|
15634
|
+
const error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));
|
|
15635
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, dispatch_fn).call(this, consumers, (consumer) => consumer.onError?.(error));
|
|
15636
|
+
};
|
|
15637
|
+
entry = { subscription, consumers, bytesListener, stateListener, errorListener, closed: false };
|
|
15638
|
+
subscription.addEventListener("bytes", bytesListener);
|
|
15639
|
+
subscription.addEventListener("state", stateListener);
|
|
15640
|
+
subscription.addEventListener("error", errorListener);
|
|
15641
|
+
return entry;
|
|
15642
|
+
};
|
|
15643
|
+
gatewayId_fn = function(gateway) {
|
|
15644
|
+
const object = gateway;
|
|
15645
|
+
let id = __privateGet(this, _gatewayIds).get(object);
|
|
15646
|
+
if (!id) {
|
|
15647
|
+
id = __privateWrapper(this, _nextGatewayId)._++;
|
|
15648
|
+
__privateGet(this, _gatewayIds).set(object, id);
|
|
15649
|
+
}
|
|
15650
|
+
return id;
|
|
15651
|
+
};
|
|
15652
|
+
detach_fn = function(entry) {
|
|
15653
|
+
entry.subscription.removeEventListener("bytes", entry.bytesListener);
|
|
15654
|
+
entry.subscription.removeEventListener("state", entry.stateListener);
|
|
15655
|
+
entry.subscription.removeEventListener("error", entry.errorListener);
|
|
15656
|
+
};
|
|
15657
|
+
dispatch_fn = function(consumers, callback) {
|
|
15658
|
+
for (const consumer of consumers.values()) {
|
|
15659
|
+
try {
|
|
15660
|
+
callback(consumer);
|
|
15661
|
+
} catch (error) {
|
|
15662
|
+
console.error("[Transport] multicast consumer callback failed", error);
|
|
15663
|
+
}
|
|
15664
|
+
}
|
|
15665
|
+
};
|
|
15666
|
+
var multicastSubscriptionPool = new MulticastSubscriptionPool();
|
|
15667
|
+
|
|
15668
|
+
// node_modules/@blockcast/mmt-transport/dist/transport-manager.js
|
|
15669
|
+
var TransportManager = class {
|
|
15670
|
+
constructor() {
|
|
15671
|
+
__publicField(this, "state", {
|
|
15672
|
+
mode: "disconnected",
|
|
15673
|
+
connected: false,
|
|
15674
|
+
packetsReceived: 0,
|
|
15675
|
+
bytesReceived: 0,
|
|
15676
|
+
lastPacketTime: 0
|
|
15677
|
+
});
|
|
15678
|
+
__publicField(this, "capabilities", null);
|
|
15679
|
+
__publicField(this, "subscription", null);
|
|
15680
|
+
__publicField(this, "driad", new DRIADDiscovery());
|
|
15681
|
+
__publicField(this, "abr");
|
|
15682
|
+
__publicField(this, "multicastOrchestrator", null);
|
|
15683
|
+
__publicField(this, "onOrchestratorCommand");
|
|
15684
|
+
__publicField(this, "connectionGeneration", 0);
|
|
15685
|
+
__publicField(this, "onPacket");
|
|
15686
|
+
__publicField(this, "onError");
|
|
15687
|
+
__publicField(this, "onStateChange");
|
|
15688
|
+
this.abr = new AdaptiveBitrateController();
|
|
15689
|
+
}
|
|
15690
|
+
getWindow() {
|
|
15691
|
+
return typeof window === "undefined" ? void 0 : window;
|
|
15692
|
+
}
|
|
15693
|
+
getWindowMulticastGateway() {
|
|
15694
|
+
return this.getWindow()?.multicast;
|
|
15695
|
+
}
|
|
15696
|
+
async getGatewayCapabilities(gateway) {
|
|
15697
|
+
if (!gateway?.capabilities)
|
|
15698
|
+
return void 0;
|
|
15699
|
+
try {
|
|
15700
|
+
return typeof gateway.capabilities === "function" ? await gateway.capabilities() : gateway.capabilities;
|
|
15701
|
+
} catch (error) {
|
|
15702
|
+
console.warn("[Transport] window.multicast capabilities failed:", error);
|
|
15703
|
+
return void 0;
|
|
15704
|
+
}
|
|
15705
|
+
}
|
|
15706
|
+
async gatewaySupportsBytes(gateway) {
|
|
15707
|
+
if (typeof gateway?.subscribe !== "function")
|
|
15708
|
+
return false;
|
|
15709
|
+
if (!gateway.capabilities)
|
|
15710
|
+
return true;
|
|
15711
|
+
const capabilities = await this.getGatewayCapabilities(gateway);
|
|
15712
|
+
if (!capabilities)
|
|
15713
|
+
return false;
|
|
15714
|
+
const consumeModes = capabilities.consumeModes;
|
|
15715
|
+
return !consumeModes || consumeModes.includes("bytes");
|
|
15716
|
+
}
|
|
15717
|
+
/**
|
|
15718
|
+
* Detect available transport capabilities
|
|
15719
|
+
*/
|
|
15720
|
+
async detectCapabilities() {
|
|
15721
|
+
if (this.capabilities)
|
|
15722
|
+
return this.capabilities;
|
|
15723
|
+
const gateway = this.getWindowMulticastGateway();
|
|
15724
|
+
const hasGatewayBytes = await this.gatewaySupportsBytes(gateway);
|
|
15725
|
+
const win = this.getWindow();
|
|
15726
|
+
this.capabilities = {
|
|
15727
|
+
nativeSSM: hasGatewayBytes,
|
|
15728
|
+
nativeIGMP: hasGatewayBytes,
|
|
15729
|
+
directSockets: typeof win?.UDPSocket !== "undefined",
|
|
15730
|
+
amt: false,
|
|
15731
|
+
driad: true,
|
|
15732
|
+
// DRIAD uses HTTP proxy, always available
|
|
15733
|
+
fecDecode: false
|
|
15734
|
+
};
|
|
15735
|
+
console.log("[Transport] Capabilities:", this.capabilities);
|
|
15736
|
+
return this.capabilities;
|
|
15737
|
+
}
|
|
15738
|
+
/**
|
|
15739
|
+
* Connect using best available transport
|
|
15740
|
+
*
|
|
15741
|
+
* Promotion hierarchy:
|
|
15742
|
+
* 1. window.multicast gateway bytes
|
|
15743
|
+
* 2. AMT proxy (specified or discovered via DRIAD)
|
|
15744
|
+
* 3. MOQ WebTransport (final fallback)
|
|
15745
|
+
*/
|
|
15746
|
+
async connect(config) {
|
|
15747
|
+
const generation = ++this.connectionGeneration;
|
|
15748
|
+
this.subscription?.close();
|
|
15749
|
+
this.subscription = null;
|
|
15750
|
+
this.state = {
|
|
15751
|
+
mode: "disconnected",
|
|
15752
|
+
connected: false,
|
|
15753
|
+
packetsReceived: 0,
|
|
15754
|
+
bytesReceived: 0,
|
|
15755
|
+
lastPacketTime: 0
|
|
15756
|
+
};
|
|
15757
|
+
this.onStateChange?.(this.state);
|
|
15758
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
15759
|
+
groupAddress: config.group,
|
|
15760
|
+
sourceAddress: config.source,
|
|
15761
|
+
port: config.port,
|
|
15762
|
+
mode: config.mode,
|
|
15763
|
+
amtRelay: config.amtRelay,
|
|
15764
|
+
amtRelayPort: config.amtRelayPort
|
|
15765
|
+
});
|
|
15766
|
+
if (!endpoint) {
|
|
15767
|
+
const error = new Error("Invalid multicast endpoint configuration");
|
|
15768
|
+
this.onError?.(error);
|
|
15769
|
+
return false;
|
|
15770
|
+
}
|
|
15771
|
+
config = {
|
|
15772
|
+
...config,
|
|
15773
|
+
group: endpoint.groupAddress,
|
|
15774
|
+
source: endpoint.sourceAddress ?? "",
|
|
15775
|
+
mode: endpoint.mode,
|
|
15776
|
+
amtRelay: endpoint.amtRelay,
|
|
15777
|
+
amtRelayPort: endpoint.amtRelayPort
|
|
15778
|
+
};
|
|
15779
|
+
const caps = await this.detectCapabilities();
|
|
15780
|
+
if (generation !== this.connectionGeneration)
|
|
15781
|
+
return false;
|
|
15782
|
+
if (config.initialQuality) {
|
|
15783
|
+
this.abr = new AdaptiveBitrateController(config.initialQuality);
|
|
15784
|
+
}
|
|
15785
|
+
this.abr.setCallback((quality) => {
|
|
15786
|
+
this.state.quality = quality;
|
|
15787
|
+
this.subscription?.onqualitychange?.(quality);
|
|
15788
|
+
this.onStateChange?.(this.state);
|
|
15789
|
+
});
|
|
15790
|
+
let connected = false;
|
|
15791
|
+
if (caps.nativeSSM || caps.nativeIGMP) {
|
|
15792
|
+
console.log("[Transport] Attempting window.multicast...");
|
|
15793
|
+
connected = await this.connectWindowMulticast(config, generation);
|
|
15794
|
+
if (connected) {
|
|
15795
|
+
if (generation !== this.connectionGeneration)
|
|
15796
|
+
return false;
|
|
15797
|
+
this.state.mode = "multicast";
|
|
15798
|
+
return true;
|
|
15799
|
+
}
|
|
15800
|
+
}
|
|
15801
|
+
if (!connected) {
|
|
15802
|
+
console.log("[Transport] Attempting AMT tunnel...");
|
|
15803
|
+
connected = await this.connectAmt(config, generation);
|
|
15804
|
+
if (connected) {
|
|
15805
|
+
if (generation !== this.connectionGeneration)
|
|
15806
|
+
return false;
|
|
15807
|
+
this.state.mode = "amt";
|
|
15808
|
+
return true;
|
|
15809
|
+
}
|
|
15810
|
+
}
|
|
15811
|
+
console.log("[Transport] All multicast transports failed, falling back to MOQ");
|
|
15812
|
+
return false;
|
|
15813
|
+
}
|
|
15814
|
+
/**
|
|
15815
|
+
* Connect via the lowercase window.multicast bytes gateway.
|
|
15816
|
+
*/
|
|
15817
|
+
async connectWindowMulticast(config, generation) {
|
|
15818
|
+
try {
|
|
15819
|
+
const gateway = this.getWindowMulticastGateway();
|
|
15820
|
+
if (typeof gateway?.subscribe !== "function")
|
|
15821
|
+
return false;
|
|
15822
|
+
const lease = await multicastSubscriptionPool.acquire(gateway, config, {
|
|
15823
|
+
onBytes: (data) => {
|
|
15824
|
+
if (generation === this.connectionGeneration)
|
|
15825
|
+
this.handlePacket(data);
|
|
15826
|
+
},
|
|
15827
|
+
onState: (state) => {
|
|
15828
|
+
if (generation !== this.connectionGeneration)
|
|
15829
|
+
return;
|
|
15830
|
+
this.state.connected = state !== "closed";
|
|
15831
|
+
this.onStateChange?.(this.state);
|
|
15832
|
+
},
|
|
15833
|
+
onError: (error) => {
|
|
15834
|
+
if (generation !== this.connectionGeneration)
|
|
15835
|
+
return;
|
|
15836
|
+
console.error("[Transport] window.multicast error:", error);
|
|
15837
|
+
this.onError?.(error);
|
|
15838
|
+
}
|
|
15839
|
+
});
|
|
15840
|
+
if (generation !== this.connectionGeneration) {
|
|
15841
|
+
await lease.release();
|
|
15842
|
+
return false;
|
|
15843
|
+
}
|
|
15844
|
+
this.subscription = {
|
|
15845
|
+
id: lease.key,
|
|
15846
|
+
config,
|
|
15847
|
+
close: () => {
|
|
15848
|
+
void lease.release();
|
|
15849
|
+
}
|
|
15850
|
+
};
|
|
15851
|
+
this.state = {
|
|
15852
|
+
mode: "multicast",
|
|
15853
|
+
connected: true,
|
|
15854
|
+
group: config.group,
|
|
15855
|
+
source: config.source,
|
|
15856
|
+
quality: this.abr.getCurrentQuality(),
|
|
15857
|
+
packetsReceived: 0,
|
|
15858
|
+
bytesReceived: 0,
|
|
15859
|
+
lastPacketTime: 0
|
|
15860
|
+
};
|
|
15861
|
+
console.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);
|
|
15862
|
+
this.onStateChange?.(this.state);
|
|
15863
|
+
return true;
|
|
15864
|
+
} catch (error) {
|
|
15865
|
+
console.warn("[Transport] window.multicast failed:", error);
|
|
15866
|
+
return false;
|
|
15867
|
+
}
|
|
15868
|
+
}
|
|
15869
|
+
/**
|
|
15870
|
+
* Connect via AMT proxy (with optional DRIAD discovery)
|
|
15871
|
+
*/
|
|
15872
|
+
async connectAmt(config, generation) {
|
|
15873
|
+
try {
|
|
15874
|
+
let amtRelay = config.amtRelay;
|
|
15875
|
+
if (!amtRelay && config.driadEnabled !== false && config.source) {
|
|
15876
|
+
console.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);
|
|
15877
|
+
const relays = await this.driad.discoverRelays(config.source);
|
|
15878
|
+
if (generation !== this.connectionGeneration)
|
|
15879
|
+
return false;
|
|
15880
|
+
const bestRelay = await this.driad.selectBestRelay(relays);
|
|
15881
|
+
if (generation !== this.connectionGeneration)
|
|
15882
|
+
return false;
|
|
15883
|
+
if (bestRelay) {
|
|
15884
|
+
amtRelay = bestRelay.host;
|
|
15885
|
+
this.state.amtRelay = amtRelay;
|
|
15886
|
+
}
|
|
15887
|
+
}
|
|
15888
|
+
if (!amtRelay) {
|
|
15889
|
+
console.warn("[Transport] No AMT relay available (DRIAD requires source address for SSM)");
|
|
15890
|
+
return false;
|
|
15891
|
+
}
|
|
15892
|
+
return await this.connectAmtProxy(config, amtRelay, generation);
|
|
15893
|
+
} catch (error) {
|
|
15894
|
+
console.warn("[Transport] AMT connection failed:", error);
|
|
15895
|
+
return false;
|
|
15896
|
+
}
|
|
15897
|
+
}
|
|
15898
|
+
/**
|
|
15899
|
+
* Connect via WebSocket-based AMT proxy (browser fallback)
|
|
15900
|
+
*/
|
|
15901
|
+
async connectAmtProxy(config, amtRelay, generation) {
|
|
15902
|
+
try {
|
|
15903
|
+
const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
|
|
15904
|
+
const relayAuthority = config.amtRelayPort ? `${relayHost}:${config.amtRelayPort}` : relayHost;
|
|
15905
|
+
const wsUrl = new URL(`wss://${relayAuthority}/amt-proxy`);
|
|
15906
|
+
wsUrl.searchParams.set("group", config.group);
|
|
15907
|
+
wsUrl.searchParams.set("source", config.source || "");
|
|
15908
|
+
wsUrl.searchParams.set("port", String(config.port));
|
|
15909
|
+
const ws = new WebSocket(wsUrl);
|
|
15910
|
+
return new Promise((resolve, reject) => {
|
|
15911
|
+
ws.binaryType = "arraybuffer";
|
|
15912
|
+
const timeout = setTimeout(() => {
|
|
15913
|
+
ws.close();
|
|
15914
|
+
reject(new Error("AMT proxy connection timeout"));
|
|
15915
|
+
}, 1e4);
|
|
15916
|
+
ws.onopen = () => {
|
|
15917
|
+
clearTimeout(timeout);
|
|
15918
|
+
if (generation !== this.connectionGeneration) {
|
|
15919
|
+
ws.close();
|
|
15920
|
+
resolve(false);
|
|
15921
|
+
return;
|
|
15922
|
+
}
|
|
15923
|
+
console.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);
|
|
15924
|
+
this.subscription = {
|
|
15925
|
+
id: `amt-proxy-${Date.now()}`,
|
|
15926
|
+
config,
|
|
15927
|
+
close: () => ws.close()
|
|
15928
|
+
};
|
|
15929
|
+
this.state = {
|
|
15930
|
+
mode: "amt",
|
|
15931
|
+
connected: true,
|
|
15932
|
+
group: config.group,
|
|
15933
|
+
source: config.source,
|
|
15934
|
+
amtRelay,
|
|
15935
|
+
quality: this.abr.getCurrentQuality(),
|
|
15936
|
+
packetsReceived: 0,
|
|
15937
|
+
bytesReceived: 0,
|
|
15938
|
+
lastPacketTime: 0
|
|
15939
|
+
};
|
|
15940
|
+
this.onStateChange?.(this.state);
|
|
15941
|
+
resolve(true);
|
|
15942
|
+
};
|
|
15943
|
+
ws.onmessage = (event) => {
|
|
15944
|
+
if (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {
|
|
15945
|
+
this.handlePacket(event.data);
|
|
15946
|
+
}
|
|
15947
|
+
};
|
|
15948
|
+
ws.onerror = (error) => {
|
|
15949
|
+
clearTimeout(timeout);
|
|
15950
|
+
if (generation !== this.connectionGeneration) {
|
|
15951
|
+
resolve(false);
|
|
15952
|
+
return;
|
|
15953
|
+
}
|
|
15954
|
+
console.error("[Transport] AMT proxy error:", error);
|
|
15955
|
+
reject(error);
|
|
15956
|
+
};
|
|
15957
|
+
ws.onclose = () => {
|
|
15958
|
+
clearTimeout(timeout);
|
|
15959
|
+
if (generation !== this.connectionGeneration) {
|
|
15960
|
+
resolve(false);
|
|
15961
|
+
return;
|
|
15962
|
+
}
|
|
15963
|
+
this.state.connected = false;
|
|
15964
|
+
this.onStateChange?.(this.state);
|
|
15965
|
+
resolve(false);
|
|
15966
|
+
};
|
|
15967
|
+
});
|
|
15968
|
+
} catch (error) {
|
|
15969
|
+
console.warn("[Transport] AMT proxy failed:", error);
|
|
15970
|
+
return false;
|
|
15971
|
+
}
|
|
15972
|
+
}
|
|
15973
|
+
/**
|
|
15974
|
+
* Handle received packet
|
|
15975
|
+
*/
|
|
15976
|
+
handlePacket(data) {
|
|
15977
|
+
this.state.packetsReceived++;
|
|
15978
|
+
this.state.bytesReceived += data.byteLength;
|
|
15979
|
+
this.state.lastPacketTime = Date.now();
|
|
15980
|
+
this.abr.recordSample(data.byteLength, 0);
|
|
15981
|
+
this.onPacket?.(data);
|
|
15982
|
+
}
|
|
15983
|
+
/**
|
|
15984
|
+
* Get current transport state
|
|
15985
|
+
*/
|
|
15986
|
+
getState() {
|
|
15987
|
+
return { ...this.state };
|
|
15988
|
+
}
|
|
15989
|
+
/**
|
|
15990
|
+
* Get current quality level
|
|
15991
|
+
*/
|
|
15992
|
+
getQuality() {
|
|
15993
|
+
return this.abr.getCurrentQuality();
|
|
15994
|
+
}
|
|
15995
|
+
/**
|
|
15996
|
+
* Force quality level
|
|
15997
|
+
*/
|
|
15998
|
+
setQuality(name) {
|
|
15999
|
+
this.abr.setQuality(name);
|
|
16000
|
+
}
|
|
16001
|
+
/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
|
|
16002
|
+
configureMulticastTiers(options, onCommand) {
|
|
16003
|
+
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
16004
|
+
this.multicastOrchestrator = new MulticastOrchestrator(options);
|
|
16005
|
+
this.onOrchestratorCommand = onCommand;
|
|
16006
|
+
return this.multicastOrchestrator.start();
|
|
16007
|
+
}
|
|
16008
|
+
recordMulticastGoodput(bitrate) {
|
|
16009
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
|
|
16010
|
+
}
|
|
16011
|
+
recordMulticastHeadroom(healthy) {
|
|
16012
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));
|
|
16013
|
+
}
|
|
16014
|
+
recordMulticastTierPacket(packet) {
|
|
16015
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
|
|
16016
|
+
}
|
|
16017
|
+
pollMulticastTierSwitch() {
|
|
16018
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());
|
|
16019
|
+
}
|
|
16020
|
+
/**
|
|
16021
|
+
* Set packet callback
|
|
16022
|
+
*/
|
|
16023
|
+
setPacketCallback(callback) {
|
|
16024
|
+
this.onPacket = callback;
|
|
16025
|
+
}
|
|
16026
|
+
/**
|
|
16027
|
+
* Set error callback
|
|
16028
|
+
*/
|
|
16029
|
+
setErrorCallback(callback) {
|
|
16030
|
+
this.onError = callback;
|
|
16031
|
+
}
|
|
16032
|
+
/**
|
|
16033
|
+
* Set state change callback
|
|
16034
|
+
*/
|
|
16035
|
+
setStateCallback(callback) {
|
|
16036
|
+
this.onStateChange = callback;
|
|
16037
|
+
}
|
|
16038
|
+
/**
|
|
16039
|
+
* Disconnect
|
|
16040
|
+
*/
|
|
16041
|
+
disconnect() {
|
|
16042
|
+
this.connectionGeneration++;
|
|
16043
|
+
this.subscription?.close();
|
|
16044
|
+
this.subscription = null;
|
|
16045
|
+
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
16046
|
+
this.multicastOrchestrator = null;
|
|
16047
|
+
this.onOrchestratorCommand = void 0;
|
|
16048
|
+
this.state = {
|
|
16049
|
+
mode: "disconnected",
|
|
16050
|
+
connected: false,
|
|
16051
|
+
packetsReceived: 0,
|
|
16052
|
+
bytesReceived: 0,
|
|
16053
|
+
lastPacketTime: 0
|
|
16054
|
+
};
|
|
16055
|
+
this.onStateChange?.(this.state);
|
|
16056
|
+
}
|
|
16057
|
+
dispatchOrchestratorCommand(command) {
|
|
16058
|
+
if (command)
|
|
16059
|
+
this.onOrchestratorCommand?.(command);
|
|
16060
|
+
return command;
|
|
16061
|
+
}
|
|
16062
|
+
};
|
|
16063
|
+
var transportManager = new TransportManager();
|
|
16064
|
+
|
|
16065
|
+
// node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
|
|
16066
|
+
var MAX_MOQT_PRIORITY = 255;
|
|
16067
|
+
function moqtPriority(value, field) {
|
|
16068
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {
|
|
16069
|
+
throw new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);
|
|
16070
|
+
}
|
|
16071
|
+
return value;
|
|
16072
|
+
}
|
|
16073
|
+
function moqtSubscriberPriority(value) {
|
|
16074
|
+
return moqtPriority(value, "MOQT subscriber priority");
|
|
16075
|
+
}
|
|
16076
|
+
function moqtPublisherPriority(value) {
|
|
16077
|
+
return moqtPriority(value, "MOQT publisher priority");
|
|
16078
|
+
}
|
|
16079
|
+
var MOQT_SUBSCRIBER_PRIORITY = Object.freeze({
|
|
16080
|
+
catalog: moqtSubscriberPriority(0),
|
|
16081
|
+
deadlineRepair: moqtSubscriberPriority(0),
|
|
16082
|
+
audio: moqtSubscriberPriority(1),
|
|
16083
|
+
video: moqtSubscriberPriority(2),
|
|
16084
|
+
backgroundRepair: moqtSubscriberPriority(3)
|
|
16085
|
+
});
|
|
16086
|
+
var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
16087
|
+
catalog: moqtPublisherPriority(32),
|
|
16088
|
+
source: moqtPublisherPriority(128),
|
|
16089
|
+
repair: moqtPublisherPriority(240)
|
|
16090
|
+
});
|
|
16091
|
+
|
|
16092
|
+
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
16093
|
+
var DEFAULT_AMT_PORT = 2268;
|
|
16094
|
+
var AMTGatewayManager = class {
|
|
16095
|
+
constructor() {
|
|
16096
|
+
__publicField(this, "subscriptions", /* @__PURE__ */ new Map());
|
|
16097
|
+
__publicField(this, "messageHandler");
|
|
16098
|
+
__publicField(this, "pendingPackets", /* @__PURE__ */ new Map());
|
|
16099
|
+
__publicField(this, "driad", new DRIADDiscovery());
|
|
16100
|
+
this.messageHandler = (event) => {
|
|
16101
|
+
const data = event.data;
|
|
16102
|
+
console.log(`[AMT] SW message received:`, data.type);
|
|
16103
|
+
if (data.type === "UDP_PACKET") {
|
|
16104
|
+
this.handlePacket(data);
|
|
16105
|
+
} else if (data.type === "SOCKET_ERROR") {
|
|
16106
|
+
this.handleSocketError(data);
|
|
16107
|
+
} else if (data.type === "DEBUG_LOG") {
|
|
16108
|
+
console.log(`[AMT SW Debug] ${data.message}`);
|
|
16109
|
+
}
|
|
16110
|
+
};
|
|
16111
|
+
if (typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
16112
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
16113
|
+
console.log("[AMT] Registered service worker message handler");
|
|
16114
|
+
} else {
|
|
16115
|
+
console.warn("[AMT] Service worker not available");
|
|
16116
|
+
}
|
|
16117
|
+
}
|
|
16118
|
+
/**
|
|
16119
|
+
* Subscribe via AMT tunnel
|
|
16120
|
+
*/
|
|
16121
|
+
async subscribe(config) {
|
|
16122
|
+
const { key } = config;
|
|
16123
|
+
const keyStr = this.getKeyString(key);
|
|
16124
|
+
if (this.subscriptions.has(keyStr)) {
|
|
16125
|
+
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
16126
|
+
return keyStr;
|
|
16127
|
+
}
|
|
16128
|
+
try {
|
|
16129
|
+
let relayAddress;
|
|
16130
|
+
let discoveryMethod;
|
|
16131
|
+
const relayPort = config.relayPort || DEFAULT_AMT_PORT;
|
|
16132
|
+
if (config.relayAddress) {
|
|
16133
|
+
relayAddress = config.relayAddress;
|
|
16134
|
+
discoveryMethod = "manual";
|
|
16135
|
+
console.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);
|
|
16136
|
+
} else if (key.source) {
|
|
16137
|
+
console.log(`[AMT] No relay configured, discovering via DRIAD for source ${key.source}...`);
|
|
16138
|
+
const relays = await this.driad.discoverRelays(key.source);
|
|
16139
|
+
const bestRelay = await this.driad.selectBestRelay(relays);
|
|
16140
|
+
if (!bestRelay) {
|
|
16141
|
+
throw new Error(`AMT relay discovery failed for source ${key.source}. Please specify a relay address in the subscription configuration.`);
|
|
16142
|
+
}
|
|
16143
|
+
relayAddress = bestRelay.host;
|
|
16144
|
+
discoveryMethod = "driad";
|
|
16145
|
+
console.log(`[AMT] Discovered relay via DRIAD: ${relayAddress}:${relayPort}`);
|
|
16146
|
+
} else {
|
|
16147
|
+
throw new Error(`AMT relay discovery requires a source address for SSM (RFC 8777). Please specify either a relay address or source address.`);
|
|
16148
|
+
}
|
|
16149
|
+
const subscriptionId = keyStr;
|
|
16150
|
+
this.pendingPackets.set(subscriptionId, []);
|
|
16151
|
+
const subscription = {
|
|
16152
|
+
key,
|
|
16153
|
+
subscriptionId,
|
|
16154
|
+
config,
|
|
16155
|
+
handshakeComplete: false,
|
|
16156
|
+
relay: {
|
|
16157
|
+
address: relayAddress,
|
|
16158
|
+
port: relayPort,
|
|
16159
|
+
discoveryMethod,
|
|
16160
|
+
discoveredAt: Date.now()
|
|
16161
|
+
}
|
|
16162
|
+
};
|
|
16163
|
+
this.subscriptions.set(keyStr, subscription);
|
|
16164
|
+
console.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);
|
|
16165
|
+
return keyStr;
|
|
16166
|
+
} catch (error) {
|
|
16167
|
+
const subscription = this.subscriptions.get(keyStr);
|
|
16168
|
+
if (subscription) {
|
|
16169
|
+
this.subscriptions.delete(keyStr);
|
|
16170
|
+
this.pendingPackets.delete(subscription.subscriptionId);
|
|
16171
|
+
}
|
|
16172
|
+
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
16173
|
+
}
|
|
16174
|
+
}
|
|
16175
|
+
/**
|
|
16176
|
+
* Unsubscribe from AMT tunnel
|
|
16177
|
+
*/
|
|
16178
|
+
async unsubscribe(handle) {
|
|
16179
|
+
const subscription = this.subscriptions.get(handle);
|
|
16180
|
+
if (!subscription) {
|
|
16181
|
+
throw new Error(`Subscription ${handle} not found`);
|
|
16182
|
+
}
|
|
16183
|
+
try {
|
|
16184
|
+
if (subscription.keepAliveTimer) {
|
|
16185
|
+
clearInterval(subscription.keepAliveTimer);
|
|
16186
|
+
}
|
|
16187
|
+
console.log(`[AMT] Unsubscribed from ${handle}`);
|
|
16188
|
+
} catch (error) {
|
|
16189
|
+
console.error("[AMT] Error during unsubscribe:", error);
|
|
16190
|
+
} finally {
|
|
16191
|
+
this.subscriptions.delete(handle);
|
|
16192
|
+
this.pendingPackets.delete(subscription.subscriptionId);
|
|
16193
|
+
}
|
|
16194
|
+
}
|
|
16195
|
+
/**
|
|
16196
|
+
* Get list of active subscriptions
|
|
16197
|
+
*/
|
|
16198
|
+
getSubscriptions() {
|
|
16199
|
+
return Array.from(this.subscriptions.keys());
|
|
16200
|
+
}
|
|
16201
|
+
/**
|
|
16202
|
+
* Handle packet from service worker
|
|
16203
|
+
*/
|
|
16204
|
+
handlePacket(message) {
|
|
16205
|
+
const { subscriptionId, data } = message;
|
|
16206
|
+
if (!subscriptionId)
|
|
16207
|
+
return;
|
|
16208
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
16209
|
+
if (!subscription) {
|
|
16210
|
+
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
16211
|
+
return;
|
|
16212
|
+
}
|
|
16213
|
+
if (!subscription.handshakeComplete) {
|
|
16214
|
+
const queue = this.pendingPackets.get(subscriptionId);
|
|
16215
|
+
const packetData2 = data;
|
|
16216
|
+
if (queue) {
|
|
16217
|
+
queue.push({
|
|
16218
|
+
data: packetData2.data,
|
|
16219
|
+
remoteAddress: packetData2.remoteAddress,
|
|
16220
|
+
remotePort: packetData2.remotePort
|
|
16221
|
+
});
|
|
16222
|
+
}
|
|
16223
|
+
return;
|
|
16224
|
+
}
|
|
16225
|
+
const packetData = data;
|
|
16226
|
+
const metadata = {
|
|
16227
|
+
sourceAddress: subscription.relay?.address || "unknown",
|
|
16228
|
+
sourcePort: subscription.relay?.port || 0,
|
|
16229
|
+
transport: "tunnel",
|
|
16230
|
+
timestamp: performance.now()
|
|
16231
|
+
};
|
|
16232
|
+
subscription.config.onPacket(packetData.data, metadata);
|
|
16233
|
+
}
|
|
16234
|
+
/**
|
|
16235
|
+
* Handle socket error from service worker
|
|
16236
|
+
*/
|
|
16237
|
+
handleSocketError(message) {
|
|
16238
|
+
const { subscriptionId, error } = message;
|
|
16239
|
+
if (!subscriptionId)
|
|
16240
|
+
return;
|
|
16241
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
16242
|
+
if (!subscription) {
|
|
16243
|
+
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
16244
|
+
return;
|
|
16245
|
+
}
|
|
16246
|
+
subscription.config.onError(new Error(error || "Unknown error"));
|
|
16247
|
+
}
|
|
16248
|
+
/**
|
|
16249
|
+
* Cleanup on destroy
|
|
16250
|
+
*/
|
|
16251
|
+
destroy() {
|
|
16252
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
16253
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
16254
|
+
}
|
|
16255
|
+
const handles = Array.from(this.subscriptions.keys());
|
|
16256
|
+
for (const handle of handles) {
|
|
16257
|
+
this.unsubscribe(handle).catch((err) => {
|
|
16258
|
+
console.error(`[AMT] Error unsubscribing ${handle}:`, err);
|
|
16259
|
+
});
|
|
16260
|
+
}
|
|
16261
|
+
}
|
|
16262
|
+
/**
|
|
16263
|
+
* Get key string for HashMap
|
|
16264
|
+
*/
|
|
16265
|
+
getKeyString(key) {
|
|
16266
|
+
const source = key.source ? `@${key.source}` : "";
|
|
16267
|
+
return `${key.group}:${key.port}${source}`;
|
|
16268
|
+
}
|
|
16269
|
+
};
|
|
16270
|
+
var amtGatewayManager = new AMTGatewayManager();
|
|
16271
|
+
|
|
14248
16272
|
// src/multicast-transport.ts
|
|
14249
16273
|
var MAX_TRACKED_DROPPED_PIDS = 16;
|
|
14250
16274
|
function getMulticastGlobals() {
|
|
@@ -14253,19 +16277,13 @@ function getMulticastGlobals() {
|
|
|
14253
16277
|
function hasMulticastGateway() {
|
|
14254
16278
|
return typeof getMulticastGlobals().multicast?.subscribe === "function";
|
|
14255
16279
|
}
|
|
14256
|
-
|
|
14257
|
-
if (value === "native") return "native";
|
|
14258
|
-
if (value === "amt") return "amt";
|
|
14259
|
-
return void 0;
|
|
14260
|
-
}
|
|
14261
|
-
var _onPacket, _subscription2, _bytesListener, _transportListener, _resolvedTransport, _videoConfirmed, _audioConfirmed, _watchdog, _failbackWindowMs, _onTakeover, _onFailback, _disposed4, _stats3, _droppedPidCounts, _packetIds, _joinGeneration, _MulticastTransport_instances, releaseSubscription_fn, acceptPacket_fn, recordUnregisteredPidDrop_fn, checkTakeover_fn, resetWatchdog_fn;
|
|
16280
|
+
var _onPacket, _subscription2, _bytesListener, _resolvedTransport, _videoConfirmed, _audioConfirmed, _watchdog, _failbackWindowMs, _onTakeover, _onFailback, _disposed4, _stats3, _droppedPidCounts, _packetIds, _joinGeneration, _MulticastTransport_instances, releaseSubscription_fn, acceptPacket_fn, recordUnregisteredPidDrop_fn, checkTakeover_fn, resetWatchdog_fn;
|
|
14262
16281
|
var _MulticastTransport = class _MulticastTransport {
|
|
14263
16282
|
constructor(onPacket) {
|
|
14264
16283
|
__privateAdd(this, _MulticastTransport_instances);
|
|
14265
16284
|
__privateAdd(this, _onPacket);
|
|
14266
16285
|
__privateAdd(this, _subscription2, null);
|
|
14267
16286
|
__privateAdd(this, _bytesListener, null);
|
|
14268
|
-
__privateAdd(this, _transportListener, null);
|
|
14269
16287
|
__privateAdd(this, _resolvedTransport);
|
|
14270
16288
|
__privateAdd(this, _videoConfirmed, false);
|
|
14271
16289
|
__privateAdd(this, _audioConfirmed, false);
|
|
@@ -14301,17 +16319,17 @@ var _MulticastTransport = class _MulticastTransport {
|
|
|
14301
16319
|
console.log(
|
|
14302
16320
|
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress}, ${config.groupAddress}, ${config.port})`
|
|
14303
16321
|
);
|
|
14304
|
-
const
|
|
16322
|
+
const subscribeConfig = toWindowMulticastSubscribeConfig({
|
|
14305
16323
|
source: config.sourceAddress,
|
|
14306
16324
|
group: config.groupAddress,
|
|
14307
16325
|
port: config.port,
|
|
14308
|
-
consume: "bytes",
|
|
14309
16326
|
mode: config.mode,
|
|
14310
16327
|
amtRelay: config.amtRelay,
|
|
14311
16328
|
amtRelayPort: config.amtRelayPort,
|
|
14312
16329
|
packetIds: config.packetIds,
|
|
14313
|
-
tracks: config.tracks
|
|
16330
|
+
tracks: config.tracks
|
|
14314
16331
|
});
|
|
16332
|
+
const subscription = await getMulticastGlobals().multicast.subscribe(subscribeConfig);
|
|
14315
16333
|
console.log(`[MulticastTransport] window.multicast.subscribe returned:`, !!subscription);
|
|
14316
16334
|
if (__privateGet(this, _disposed4) || generation !== __privateGet(this, _joinGeneration)) {
|
|
14317
16335
|
void subscription.unsubscribe();
|
|
@@ -14333,18 +16351,11 @@ var _MulticastTransport = class _MulticastTransport {
|
|
|
14333
16351
|
__privateGet(this, _onPacket).call(this, packet, context);
|
|
14334
16352
|
__privateMethod(this, _MulticastTransport_instances, resetWatchdog_fn).call(this);
|
|
14335
16353
|
};
|
|
14336
|
-
const onTransportChange = (event) => {
|
|
14337
|
-
if (__privateGet(this, _disposed4) || generation !== __privateGet(this, _joinGeneration)) return;
|
|
14338
|
-
const transport = normalizeResolvedTransport(event.detail);
|
|
14339
|
-
if (transport) __privateSet(this, _resolvedTransport, transport);
|
|
14340
|
-
};
|
|
14341
16354
|
__privateSet(this, _subscription2, subscription);
|
|
14342
16355
|
__privateSet(this, _bytesListener, onBytes);
|
|
14343
|
-
__privateSet(this, _transportListener, onTransportChange);
|
|
14344
16356
|
__privateSet(this, _packetIds, packetIds);
|
|
14345
|
-
__privateSet(this, _resolvedTransport,
|
|
16357
|
+
__privateSet(this, _resolvedTransport, subscribeConfig.origin === "amt" ? "amt" : "native");
|
|
14346
16358
|
subscription.addEventListener("bytes", onBytes);
|
|
14347
|
-
subscription.addEventListener("transportchange", onTransportChange);
|
|
14348
16359
|
__privateMethod(this, _MulticastTransport_instances, resetWatchdog_fn).call(this);
|
|
14349
16360
|
console.log(
|
|
14350
16361
|
`[MulticastTransport] Joined ${config.groupAddress}:${config.port}`
|
|
@@ -14412,7 +16423,6 @@ var _MulticastTransport = class _MulticastTransport {
|
|
|
14412
16423
|
_onPacket = new WeakMap();
|
|
14413
16424
|
_subscription2 = new WeakMap();
|
|
14414
16425
|
_bytesListener = new WeakMap();
|
|
14415
|
-
_transportListener = new WeakMap();
|
|
14416
16426
|
_resolvedTransport = new WeakMap();
|
|
14417
16427
|
_videoConfirmed = new WeakMap();
|
|
14418
16428
|
_audioConfirmed = new WeakMap();
|
|
@@ -14433,14 +16443,10 @@ releaseSubscription_fn = function() {
|
|
|
14433
16443
|
if (__privateGet(this, _bytesListener)) {
|
|
14434
16444
|
__privateGet(this, _subscription2).removeEventListener("bytes", __privateGet(this, _bytesListener));
|
|
14435
16445
|
}
|
|
14436
|
-
if (__privateGet(this, _transportListener)) {
|
|
14437
|
-
__privateGet(this, _subscription2).removeEventListener("transportchange", __privateGet(this, _transportListener));
|
|
14438
|
-
}
|
|
14439
16446
|
void __privateGet(this, _subscription2).unsubscribe();
|
|
14440
16447
|
}
|
|
14441
16448
|
__privateSet(this, _subscription2, null);
|
|
14442
16449
|
__privateSet(this, _bytesListener, null);
|
|
14443
|
-
__privateSet(this, _transportListener, null);
|
|
14444
16450
|
__privateSet(this, _resolvedTransport, void 0);
|
|
14445
16451
|
__privateSet(this, _packetIds, null);
|
|
14446
16452
|
};
|
|
@@ -14516,12 +16522,22 @@ function createDefaultRepairLifecycleEvent(detail, options) {
|
|
|
14516
16522
|
function isRecord(value) {
|
|
14517
16523
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14518
16524
|
}
|
|
14519
|
-
function
|
|
16525
|
+
function isRepairStreamIdentity(value) {
|
|
14520
16526
|
if (!isRecord(value)) return false;
|
|
14521
16527
|
const packetId = value["packetId"];
|
|
14522
16528
|
const catalogTrackName = value["catalogTrackName"];
|
|
16529
|
+
return Number.isSafeInteger(packetId) && packetId >= 0 && (catalogTrackName === null || typeof catalogTrackName === "string" && catalogTrackName.length > 0);
|
|
16530
|
+
}
|
|
16531
|
+
function isRepairBlockIdentity(value) {
|
|
16532
|
+
if (!isRecord(value)) return false;
|
|
14523
16533
|
const sbn = value["sbn"];
|
|
14524
|
-
return
|
|
16534
|
+
return isRepairStreamIdentity(value) && Number.isSafeInteger(sbn) && sbn >= 0;
|
|
16535
|
+
}
|
|
16536
|
+
function isDecoderUnavailableReason(value) {
|
|
16537
|
+
return value === "cleared" || value === "disposed";
|
|
16538
|
+
}
|
|
16539
|
+
function isFecFallbackDecoder(value) {
|
|
16540
|
+
return value === "sync" || value === "none";
|
|
14525
16541
|
}
|
|
14526
16542
|
function isRepairLifecycleEvent(value) {
|
|
14527
16543
|
if (!isRecord(value)) return false;
|
|
@@ -14529,7 +16545,11 @@ function isRepairLifecycleEvent(value) {
|
|
|
14529
16545
|
const type = value["type"];
|
|
14530
16546
|
const block = value["block"];
|
|
14531
16547
|
const symbols = value["symbols"];
|
|
14532
|
-
if (version !== 1
|
|
16548
|
+
if (version !== 1) return false;
|
|
16549
|
+
if (type === "decoder-unavailable") {
|
|
16550
|
+
return isRepairStreamIdentity(value["stream"]) && isDecoderUnavailableReason(value["reason"]) && isFecFallbackDecoder(value["fallbackDecoder"]) && block === void 0 && symbols === void 0;
|
|
16551
|
+
}
|
|
16552
|
+
if (type !== "requested" && type !== "recovered" && type !== "failed" || !isRepairBlockIdentity(block) || value["stream"] !== void 0 || value["reason"] !== void 0 || value["fallbackDecoder"] !== void 0) {
|
|
14533
16553
|
return false;
|
|
14534
16554
|
}
|
|
14535
16555
|
if (type === "requested") return symbols === void 0;
|
|
@@ -14548,22 +16568,25 @@ function forwardRepairLifecycleEvents(source, target, options = {}) {
|
|
|
14548
16568
|
target.dispatchEvent(forwardedEvent);
|
|
14549
16569
|
});
|
|
14550
16570
|
}
|
|
14551
|
-
function
|
|
16571
|
+
function repairStreamIdentity(packetId, catalogTrackName) {
|
|
14552
16572
|
if (!Number.isSafeInteger(packetId) || packetId < 0) {
|
|
14553
16573
|
throw new TypeError("packetId must be a non-negative safe integer");
|
|
14554
16574
|
}
|
|
14555
|
-
if (!Number.isSafeInteger(sbn) || sbn < 0) {
|
|
14556
|
-
throw new TypeError("sbn must be a non-negative safe integer");
|
|
14557
|
-
}
|
|
14558
16575
|
if (catalogTrackName !== void 0 && (typeof catalogTrackName !== "string" || catalogTrackName.length === 0)) {
|
|
14559
16576
|
throw new TypeError("catalogTrackName must be a non-empty string when provided");
|
|
14560
16577
|
}
|
|
14561
16578
|
return {
|
|
14562
16579
|
packetId,
|
|
14563
|
-
catalogTrackName: catalogTrackName ?? null
|
|
14564
|
-
sbn
|
|
16580
|
+
catalogTrackName: catalogTrackName ?? null
|
|
14565
16581
|
};
|
|
14566
16582
|
}
|
|
16583
|
+
function repairBlockIdentity(packetId, sbn, catalogTrackName) {
|
|
16584
|
+
const stream = repairStreamIdentity(packetId, catalogTrackName);
|
|
16585
|
+
if (!Number.isSafeInteger(sbn) || sbn < 0) {
|
|
16586
|
+
throw new TypeError("sbn must be a non-negative safe integer");
|
|
16587
|
+
}
|
|
16588
|
+
return { ...stream, sbn };
|
|
16589
|
+
}
|
|
14567
16590
|
function assertPositiveGeometry(label, value) {
|
|
14568
16591
|
if (value === void 0) return;
|
|
14569
16592
|
if (!Number.isFinite(value) || value <= 0) {
|
|
@@ -14733,6 +16756,15 @@ var MulticastManager = class {
|
|
|
14733
16756
|
}
|
|
14734
16757
|
});
|
|
14735
16758
|
};
|
|
16759
|
+
__privateGet(this, _fec).onDecoderUnavailable = (event) => {
|
|
16760
|
+
__privateMethod(this, _MulticastManager_instances, emitRepairLifecycle_fn).call(this, {
|
|
16761
|
+
version: 1,
|
|
16762
|
+
type: "decoder-unavailable",
|
|
16763
|
+
stream: repairStreamIdentity(event.packetId, event.catalogTrackName),
|
|
16764
|
+
reason: event.reason,
|
|
16765
|
+
fallbackDecoder: event.fallbackDecoder
|
|
16766
|
+
});
|
|
16767
|
+
};
|
|
14736
16768
|
}
|
|
14737
16769
|
// ---------------------------------------------------------------------------
|
|
14738
16770
|
// Decomposed component accessors (for new code)
|
|
@@ -14787,6 +16819,17 @@ var MulticastManager = class {
|
|
|
14787
16819
|
__privateMethod(this, _MulticastManager_instances, configureTrackMapper_fn).call(this, packetId, config);
|
|
14788
16820
|
__privateGet(this, _router).setPacketIdType(packetId, trackType);
|
|
14789
16821
|
}
|
|
16822
|
+
/**
|
|
16823
|
+
* Stop routing a stream's symbols to its Worker decoder. Returns false when
|
|
16824
|
+
* none is registered; on success one `decoder-unavailable` repair lifecycle
|
|
16825
|
+
* event with reason 'cleared' reaches every observer, carrying whether a
|
|
16826
|
+
* synchronous WASM decoder still serves the stream. The packet mapping and
|
|
16827
|
+
* track type are untouched: the track still exists, it just has no Worker
|
|
16828
|
+
* decoder. Call this before disposing the client.
|
|
16829
|
+
*/
|
|
16830
|
+
clearFecWorker(packetId, catalogTrackName) {
|
|
16831
|
+
return __privateGet(this, _fec).clearFecWorker(packetId, catalogTrackName);
|
|
16832
|
+
}
|
|
14790
16833
|
/** Register callback for Worker-level FecTrackStats updates. */
|
|
14791
16834
|
setOnFecStatsUpdate(cb) {
|
|
14792
16835
|
__privateGet(this, _fec).onFecStatsUpdate = cb;
|
|
@@ -14832,8 +16875,9 @@ var MulticastManager = class {
|
|
|
14832
16875
|
repairSymbols: config.fecRepairSymbols,
|
|
14833
16876
|
symbolSize: config.fecSymbolSize,
|
|
14834
16877
|
mediaUnitsPerSecond: config.fps,
|
|
14835
|
-
bandwidthBps: config.bandwidth
|
|
14836
|
-
|
|
16878
|
+
bandwidthBps: config.bandwidth,
|
|
16879
|
+
jitterBufferMs: config.fecReorderToleranceMs
|
|
16880
|
+
}) : void 0;
|
|
14837
16881
|
const fecConfig = {
|
|
14838
16882
|
k: config.fecK,
|
|
14839
16883
|
symbolSize: config.fecSymbolSize,
|
|
@@ -14870,8 +16914,9 @@ var MulticastManager = class {
|
|
|
14870
16914
|
repairSymbols: config.fecRepairSymbols,
|
|
14871
16915
|
symbolSize: config.fecSymbolSize,
|
|
14872
16916
|
mediaUnitsPerSecond: config.fps,
|
|
14873
|
-
bandwidthBps: config.bandwidth
|
|
14874
|
-
|
|
16917
|
+
bandwidthBps: config.bandwidth,
|
|
16918
|
+
jitterBufferMs: config.fecReorderToleranceMs
|
|
16919
|
+
}) : void 0;
|
|
14875
16920
|
__privateGet(this, _fec).configure({
|
|
14876
16921
|
k: config.fecK,
|
|
14877
16922
|
symbolSize: config.fecSymbolSize,
|
|
@@ -15388,11 +17433,13 @@ applyStreamGlobalFecTiming_fn = function() {
|
|
|
15388
17433
|
};
|
|
15389
17434
|
_onTrackTypeDiscovered = new WeakMap();
|
|
15390
17435
|
emitRepairLifecycle_fn = function(event) {
|
|
15391
|
-
const immutableEvent = Object.freeze(
|
|
15392
|
-
...event,
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
17436
|
+
const immutableEvent = Object.freeze(
|
|
17437
|
+
event.type === "decoder-unavailable" ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
17438
|
+
...event,
|
|
17439
|
+
block: Object.freeze({ ...event.block }),
|
|
17440
|
+
...event.type === "requested" ? {} : { symbols: Object.freeze({ ...event.symbols }) }
|
|
17441
|
+
}
|
|
17442
|
+
);
|
|
15396
17443
|
console.info(
|
|
15397
17444
|
`${REPAIR_LIFECYCLE_LOG_PREFIX}${JSON.stringify(immutableEvent)}`
|
|
15398
17445
|
);
|
|
@@ -15416,7 +17463,8 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
15416
17463
|
sourceSymbols: opts.fecK,
|
|
15417
17464
|
repairSymbols: opts.fecRepairCount,
|
|
15418
17465
|
symbolSize: opts.fecSymbolSize,
|
|
15419
|
-
mediaUnitsPerSecond: 1e3 / opts.mediaUnitDurationMs
|
|
17466
|
+
mediaUnitsPerSecond: 1e3 / opts.mediaUnitDurationMs,
|
|
17467
|
+
jitterBufferMs: opts.fecReorderToleranceMs
|
|
15420
17468
|
});
|
|
15421
17469
|
const origin = typeof location !== "undefined" && location.origin ? location.origin : "http://localhost";
|
|
15422
17470
|
const workerUrl = new URL(opts.assetUrls.fecWorker, origin);
|
|
@@ -15462,15 +17510,23 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
15462
17510
|
relayBlockSigEnabled: false
|
|
15463
17511
|
};
|
|
15464
17512
|
client.configure(fecTrackConfig, rqWasm, altaKeyBuffer, altaWasmModule);
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
17513
|
+
try {
|
|
17514
|
+
opts.multicastManager.setFecWorker(opts.packetId, client, opts.trackType, {
|
|
17515
|
+
k: opts.fecK,
|
|
17516
|
+
symbolSize: opts.fecSymbolSize,
|
|
17517
|
+
interleaveDepthMs: opts.fecInterleaveMs,
|
|
17518
|
+
// FEC timing is expressed in media units. For audio this is the access-unit
|
|
17519
|
+
// cadence (for example 1024/48kHz), not a video-only framerate field.
|
|
17520
|
+
fps: 1e3 / opts.mediaUnitDurationMs,
|
|
17521
|
+
timeoutMs: deliveryWindowMs
|
|
17522
|
+
}, opts.trackName);
|
|
17523
|
+
} catch (e) {
|
|
17524
|
+
try {
|
|
17525
|
+
client.dispose();
|
|
17526
|
+
} catch {
|
|
17527
|
+
}
|
|
17528
|
+
throw e;
|
|
17529
|
+
}
|
|
15474
17530
|
let disposed = false;
|
|
15475
17531
|
return {
|
|
15476
17532
|
client,
|
|
@@ -15478,6 +17534,11 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
15478
17534
|
dispose: () => {
|
|
15479
17535
|
if (disposed) return;
|
|
15480
17536
|
disposed = true;
|
|
17537
|
+
if (!opts.multicastManager.clearFecWorker(opts.packetId, opts.trackName)) {
|
|
17538
|
+
console.warn(
|
|
17539
|
+
`[fec-worker-setup] packetId=${opts.packetId} track=${opts.trackName} Worker was not registered when disposed`
|
|
17540
|
+
);
|
|
17541
|
+
}
|
|
15481
17542
|
try {
|
|
15482
17543
|
client.dispose();
|
|
15483
17544
|
} catch {
|
|
@@ -16361,7 +18422,7 @@ _readIndex = new WeakMap();
|
|
|
16361
18422
|
_stalled = new WeakMap();
|
|
16362
18423
|
|
|
16363
18424
|
// src/mmtp-decode-pipeline.ts
|
|
16364
|
-
var _parser2, _fecClient, _frameCb2, _initCb2, _needsKeyframe, _codec, _gateDropped, _gateEmitted, _fedObjects, _warnedNoParser, _feedOutcomes, _MmtpDecodePipeline_instances, gatedEmit_fn;
|
|
18425
|
+
var _parser2, _fecClient, _frameCb2, _initCb2, _needsKeyframe, _codec, _trackType, _gateDropped, _gateEmitted, _fedObjects, _warnedNoParser, _feedOutcomes, _MmtpDecodePipeline_instances, tag_fn, gatedEmit_fn;
|
|
16365
18426
|
var MmtpDecodePipeline = class {
|
|
16366
18427
|
constructor() {
|
|
16367
18428
|
__privateAdd(this, _MmtpDecodePipeline_instances);
|
|
@@ -16370,7 +18431,11 @@ var MmtpDecodePipeline = class {
|
|
|
16370
18431
|
__privateAdd(this, _frameCb2, null);
|
|
16371
18432
|
__privateAdd(this, _initCb2, null);
|
|
16372
18433
|
__privateAdd(this, _needsKeyframe, false);
|
|
16373
|
-
__privateAdd(this, _codec,
|
|
18434
|
+
__privateAdd(this, _codec, null);
|
|
18435
|
+
/** Set together with #codec at configure(). Also the diagnostic label: a
|
|
18436
|
+
* player builds one pipeline per track, and codec alone does not say which
|
|
18437
|
+
* role a line belongs to at a glance. */
|
|
18438
|
+
__privateAdd(this, _trackType, null);
|
|
16374
18439
|
/** Diagnostic counters. The keyframe gate drops silently, so a parser that
|
|
16375
18440
|
* never marks a frame as a keyframe stalls the whole pipeline with no
|
|
16376
18441
|
* output anywhere: frames are fed, frames are parsed, and nothing is
|
|
@@ -16399,6 +18464,7 @@ var MmtpDecodePipeline = class {
|
|
|
16399
18464
|
__privateSet(this, _warnedNoParser, false);
|
|
16400
18465
|
__privateSet(this, _codec, config.codec);
|
|
16401
18466
|
const trackType = isVideoCodec(config.codec) ? "video" : "audio";
|
|
18467
|
+
__privateSet(this, _trackType, trackType);
|
|
16402
18468
|
const trackInfo = {
|
|
16403
18469
|
type: trackType,
|
|
16404
18470
|
codec: config.codec,
|
|
@@ -16442,6 +18508,24 @@ var MmtpDecodePipeline = class {
|
|
|
16442
18508
|
get reorderStats() {
|
|
16443
18509
|
return __privateGet(this, _parser2)?.reorderStats ?? null;
|
|
16444
18510
|
}
|
|
18511
|
+
/**
|
|
18512
|
+
* Feed tallies for this track. A fresh snapshot per call, not a live view.
|
|
18513
|
+
*
|
|
18514
|
+
* Unlike reorderStats this never returns null: the counters belong to the
|
|
18515
|
+
* pipeline rather than the parser, so they survive dispose(). That is
|
|
18516
|
+
* deliberate — a wedged pipeline is usually read after teardown, which is
|
|
18517
|
+
* exactly when the parser is already gone.
|
|
18518
|
+
*/
|
|
18519
|
+
get feedStats() {
|
|
18520
|
+
return {
|
|
18521
|
+
trackType: __privateGet(this, _trackType),
|
|
18522
|
+
codec: __privateGet(this, _codec),
|
|
18523
|
+
fedObjects: __privateGet(this, _fedObjects),
|
|
18524
|
+
emitted: __privateGet(this, _gateEmitted),
|
|
18525
|
+
gateDropped: __privateGet(this, _gateDropped),
|
|
18526
|
+
outcomes: Object.fromEntries(__privateGet(this, _feedOutcomes))
|
|
18527
|
+
};
|
|
18528
|
+
}
|
|
16445
18529
|
/**
|
|
16446
18530
|
* Feed a raw MoQ object payload (one MMTP packet per MoQ object).
|
|
16447
18531
|
* Optionally feeds source symbols to FEC client before parsing.
|
|
@@ -16450,33 +18534,37 @@ var MmtpDecodePipeline = class {
|
|
|
16450
18534
|
if (!__privateGet(this, _parser2)) {
|
|
16451
18535
|
if (!__privateGet(this, _warnedNoParser)) {
|
|
16452
18536
|
__privateSet(this, _warnedNoParser, true);
|
|
16453
|
-
console.warn(
|
|
18537
|
+
console.warn(`[mmtp-pipeline] feedMoqObject called with NO PARSER configured ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} \u2014 packet dropped`);
|
|
16454
18538
|
}
|
|
16455
18539
|
return;
|
|
16456
18540
|
}
|
|
16457
18541
|
__privateSet(this, _fedObjects, __privateGet(this, _fedObjects) + 1);
|
|
16458
18542
|
if (__privateGet(this, _fedObjects) === 1) {
|
|
16459
|
-
console.log(`[mmtp-pipeline] first object fed
|
|
18543
|
+
console.log(`[mmtp-pipeline] first object fed ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${packet.length}`);
|
|
16460
18544
|
}
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
if (
|
|
16464
|
-
__privateGet(this,
|
|
18545
|
+
let outcomeKind = "threw";
|
|
18546
|
+
try {
|
|
18547
|
+
if (__privateGet(this, _fecClient)?.addSourceSymbol) {
|
|
18548
|
+
const info = __privateGet(this, _parser2).parseHeader(packet);
|
|
18549
|
+
if (info?.fecPayloadId) {
|
|
18550
|
+
__privateGet(this, _fecClient).addSourceSymbol(info.fecPayloadId.ssId, packet);
|
|
18551
|
+
}
|
|
18552
|
+
}
|
|
18553
|
+
const record = __privateGet(this, _parser2).feed(packet);
|
|
18554
|
+
outcomeKind = record?.kind ?? "null(parse-or-extract-failed)";
|
|
18555
|
+
} finally {
|
|
18556
|
+
const seen = (__privateGet(this, _feedOutcomes).get(outcomeKind) ?? 0) + 1;
|
|
18557
|
+
__privateGet(this, _feedOutcomes).set(outcomeKind, seen);
|
|
18558
|
+
if (seen === 1) {
|
|
18559
|
+
console.log(
|
|
18560
|
+
`[mmtp-pipeline] feed outcome FIRST '${outcomeKind}' ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} at object #${__privateGet(this, _fedObjects)} bytes=${packet.length}`
|
|
18561
|
+
);
|
|
16465
18562
|
}
|
|
16466
|
-
}
|
|
16467
|
-
const record = __privateGet(this, _parser2).feed(packet);
|
|
16468
|
-
const kind = record?.kind ?? "null(parse-or-extract-failed)";
|
|
16469
|
-
const seen = (__privateGet(this, _feedOutcomes).get(kind) ?? 0) + 1;
|
|
16470
|
-
__privateGet(this, _feedOutcomes).set(kind, seen);
|
|
16471
|
-
if (seen === 1) {
|
|
16472
|
-
console.log(
|
|
16473
|
-
`[mmtp-pipeline] feed outcome FIRST '${kind}' at object #${__privateGet(this, _fedObjects)} bytes=${packet.length}`
|
|
16474
|
-
);
|
|
16475
18563
|
}
|
|
16476
18564
|
if (__privateGet(this, _fedObjects) % 200 === 0) {
|
|
16477
18565
|
const hist = [...__privateGet(this, _feedOutcomes)].map(([k, v]) => `${k}=${v}`).join(" ");
|
|
16478
18566
|
console.log(
|
|
16479
|
-
`[mmtp-pipeline] feed histogram @${__privateGet(this, _fedObjects)}: ${hist} | emitted=${__privateGet(this, _gateEmitted)} gateDropped=${__privateGet(this, _gateDropped)}`
|
|
18567
|
+
`[mmtp-pipeline] feed histogram ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} @${__privateGet(this, _fedObjects)}: ${hist} | emitted=${__privateGet(this, _gateEmitted)} gateDropped=${__privateGet(this, _gateDropped)}`
|
|
16480
18568
|
);
|
|
16481
18569
|
}
|
|
16482
18570
|
}
|
|
@@ -16496,7 +18584,10 @@ var MmtpDecodePipeline = class {
|
|
|
16496
18584
|
...meta.mmtpTimestamp === void 0 ? {} : { mmtpTimestamp: meta.mmtpTimestamp },
|
|
16497
18585
|
keyframe: meta.keyframe,
|
|
16498
18586
|
packetId: meta.packetId,
|
|
16499
|
-
|
|
18587
|
+
// Same derivation as configure(), read from the field it set rather than
|
|
18588
|
+
// recomputed. Pre-configure the parser is null and routeFrame below is a
|
|
18589
|
+
// no-op, so the fallback is unreachable in practice.
|
|
18590
|
+
trackType: __privateGet(this, _trackType) ?? "audio",
|
|
16500
18591
|
packetSequenceNumber: meta.packetSequenceNumber
|
|
16501
18592
|
};
|
|
16502
18593
|
if (meta.sourceDiscontinuity) frameMeta.sourceDiscontinuity = true;
|
|
@@ -16522,7 +18613,7 @@ var MmtpDecodePipeline = class {
|
|
|
16522
18613
|
const droppedFrames = (__privateGet(this, _parser2)?.reorderStats.droppedOnLiveDispose ?? droppedBefore) - droppedBefore;
|
|
16523
18614
|
if (droppedFrames > 0) {
|
|
16524
18615
|
console.warn(
|
|
16525
|
-
`[MmtpDecodePipeline] live dispose dropped ${droppedFrames} buffered frame(s); finite inputs must use dispose({ flush: true })`
|
|
18616
|
+
`[MmtpDecodePipeline] live dispose ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} dropped ${droppedFrames} buffered frame(s); finite inputs must use dispose({ flush: true })`
|
|
16526
18617
|
);
|
|
16527
18618
|
}
|
|
16528
18619
|
__privateSet(this, _parser2, null);
|
|
@@ -16537,12 +18628,19 @@ _frameCb2 = new WeakMap();
|
|
|
16537
18628
|
_initCb2 = new WeakMap();
|
|
16538
18629
|
_needsKeyframe = new WeakMap();
|
|
16539
18630
|
_codec = new WeakMap();
|
|
18631
|
+
_trackType = new WeakMap();
|
|
16540
18632
|
_gateDropped = new WeakMap();
|
|
16541
18633
|
_gateEmitted = new WeakMap();
|
|
16542
18634
|
_fedObjects = new WeakMap();
|
|
16543
18635
|
_warnedNoParser = new WeakMap();
|
|
16544
18636
|
_feedOutcomes = new WeakMap();
|
|
16545
18637
|
_MmtpDecodePipeline_instances = new WeakSet();
|
|
18638
|
+
/** Identity prefix for diagnostics: one pipeline per track means two
|
|
18639
|
+
* instances write to the same console, and unlabelled lines interleave into
|
|
18640
|
+
* a single unreadable stream. */
|
|
18641
|
+
tag_fn = function() {
|
|
18642
|
+
return `track=${__privateGet(this, _trackType) ?? "unconfigured"} codec=${__privateGet(this, _codec) ?? "none"}`;
|
|
18643
|
+
};
|
|
16546
18644
|
gatedEmit_fn = function(data, meta) {
|
|
16547
18645
|
var _a;
|
|
16548
18646
|
let sourceDiscontinuity = false;
|
|
@@ -16551,7 +18649,7 @@ gatedEmit_fn = function(data, meta) {
|
|
|
16551
18649
|
__privateSet(this, _gateDropped, __privateGet(this, _gateDropped) + 1);
|
|
16552
18650
|
if (__privateGet(this, _gateDropped) <= 3 || __privateGet(this, _gateDropped) % 200 === 0) {
|
|
16553
18651
|
console.warn(
|
|
16554
|
-
`[mmtp-pipeline] keyframe gate DROP #${__privateGet(this, _gateDropped)}
|
|
18652
|
+
`[mmtp-pipeline] keyframe gate DROP #${__privateGet(this, _gateDropped)} ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${data.length} fed=${__privateGet(this, _fedObjects)} emitted=${__privateGet(this, _gateEmitted)}`
|
|
16555
18653
|
);
|
|
16556
18654
|
}
|
|
16557
18655
|
return;
|
|
@@ -16559,12 +18657,12 @@ gatedEmit_fn = function(data, meta) {
|
|
|
16559
18657
|
__privateSet(this, _needsKeyframe, false);
|
|
16560
18658
|
sourceDiscontinuity = true;
|
|
16561
18659
|
console.log(
|
|
16562
|
-
`[mmtp-pipeline] keyframe gate OPENED
|
|
18660
|
+
`[mmtp-pipeline] keyframe gate OPENED ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} after ${__privateGet(this, _gateDropped)} drops, fed=${__privateGet(this, _fedObjects)}`
|
|
16563
18661
|
);
|
|
16564
18662
|
}
|
|
16565
18663
|
__privateSet(this, _gateEmitted, __privateGet(this, _gateEmitted) + 1);
|
|
16566
18664
|
if (__privateGet(this, _gateEmitted) === 1) {
|
|
16567
|
-
console.log(`[mmtp-pipeline] first frame emitted
|
|
18665
|
+
console.log(`[mmtp-pipeline] first frame emitted ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${data.length}`);
|
|
16568
18666
|
}
|
|
16569
18667
|
const outMeta = sourceDiscontinuity ? { ...meta, sourceDiscontinuity: true } : meta;
|
|
16570
18668
|
(_a = __privateGet(this, _frameCb2)) == null ? void 0 : _a.call(this, data, outMeta);
|
|
@@ -17238,12 +19336,140 @@ emit_fn = function(params) {
|
|
|
17238
19336
|
};
|
|
17239
19337
|
|
|
17240
19338
|
// src/catalog-fec-params.ts
|
|
19339
|
+
var MAX_SAFE_TICKS_PER_HZ = Number.MAX_SAFE_INTEGER / 1e3;
|
|
19340
|
+
function timescaleHz(value) {
|
|
19341
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
19342
|
+
}
|
|
19343
|
+
function catalogGroupDurationMs(rendition) {
|
|
19344
|
+
if (!rendition) return void 0;
|
|
19345
|
+
const ticks = numericField(rendition.groupDurationTicks);
|
|
19346
|
+
const ms = numericField(rendition.groupDurationMs);
|
|
19347
|
+
const timescale = timescaleHz(rendition.container?.kind === "mmtp" ? rendition.container.timescale : void 0) ?? timescaleHz(rendition.timescale);
|
|
19348
|
+
if (ticks !== void 0 && timescale !== void 0 && Number.isSafeInteger(ticks) && ticks <= timescale * MAX_SAFE_TICKS_PER_HZ) {
|
|
19349
|
+
return resolveGroupDurationMs({ groupDurationTicks: ticks }, timescale);
|
|
19350
|
+
}
|
|
19351
|
+
return ms;
|
|
19352
|
+
}
|
|
19353
|
+
function resolveCatalogTrackGroupDurationMs(catalog, track) {
|
|
19354
|
+
const renditions = track.type === "audio" ? catalog?.audio?.renditions : catalog?.video?.renditions;
|
|
19355
|
+
return catalogGroupDurationMs(renditions?.[track.name]);
|
|
19356
|
+
}
|
|
17241
19357
|
var NO_FEC = {
|
|
17242
19358
|
fecK: 0,
|
|
17243
19359
|
fecRepairCount: 0,
|
|
19360
|
+
fecTotalRepairCount: 0,
|
|
17244
19361
|
fecSymbolSize: 0,
|
|
17245
19362
|
fecInterleaveMs: 0
|
|
17246
19363
|
};
|
|
19364
|
+
function recordOf(value) {
|
|
19365
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
19366
|
+
}
|
|
19367
|
+
function msfTracks(catalog) {
|
|
19368
|
+
const raw = catalog.tracks;
|
|
19369
|
+
if (!Array.isArray(raw)) return void 0;
|
|
19370
|
+
const common = recordOf(catalog.commonTrackFields) ?? {};
|
|
19371
|
+
return { tracks: raw.map((t) => recordOf(t) ?? {}), common };
|
|
19372
|
+
}
|
|
19373
|
+
function resolveRepairLayers(catalog, source) {
|
|
19374
|
+
if (!catalog) return void 0;
|
|
19375
|
+
const sourceName = typeof source === "string" ? source : source.name;
|
|
19376
|
+
const hang = catalog;
|
|
19377
|
+
const roles = typeof source === "string" ? ["video", "audio"] : source.type === "audio" ? ["audio"] : source.type === "video" ? ["video"] : ["video", "audio"];
|
|
19378
|
+
const role = roles.find((r) => recordOf(hang[r]?.renditions?.[sourceName]) !== void 0);
|
|
19379
|
+
if (role !== void 0) {
|
|
19380
|
+
const renditionPath = `$.${role}.renditions[${JSON.stringify(sourceName)}]`;
|
|
19381
|
+
const rendition = recordOf(hang[role].renditions[sourceName]);
|
|
19382
|
+
rejectRepairFieldsOnMediaTrack(renditionPath, rendition, throwRepairContractError);
|
|
19383
|
+
const repairRenditions = recordOf(hang.repair?.renditions) ?? {};
|
|
19384
|
+
const descriptors2 = Object.entries(repairRenditions).map(([name, entry]) => {
|
|
19385
|
+
const path = `$.repair.renditions[${JSON.stringify(name)}]`;
|
|
19386
|
+
const fields = recordOf(entry);
|
|
19387
|
+
if (fields === void 0) {
|
|
19388
|
+
throwRepairContractError("track_repair_layer", path, "a repair rendition must be an object");
|
|
19389
|
+
}
|
|
19390
|
+
rejectRepairTrackGeometry(path, fields, throwRepairContractError);
|
|
19391
|
+
return describeRepairTrack(name, path, fields);
|
|
19392
|
+
});
|
|
19393
|
+
const fecOfRendition = (entry) => {
|
|
19394
|
+
const fec2 = recordOf(recordOf(entry)?.["fec"]);
|
|
19395
|
+
return fec2 !== void 0 && fec2["algorithm"] !== "none" ? fec2 : void 0;
|
|
19396
|
+
};
|
|
19397
|
+
const mediaRenditions = [
|
|
19398
|
+
...Object.values(recordOf(hang.video?.renditions) ?? {}),
|
|
19399
|
+
...Object.values(recordOf(hang.audio?.renditions) ?? {})
|
|
19400
|
+
];
|
|
19401
|
+
const reachability2 = repairReachability(mediaRenditions.map((entry) => recordOf(entry)?.["fec"]));
|
|
19402
|
+
for (const d of descriptors2) {
|
|
19403
|
+
validateRepairTrackFields(d, throwRepairContractError);
|
|
19404
|
+
assertRepairTrackReachable(d, reachability2, throwRepairContractError);
|
|
19405
|
+
const target = dependsTarget(d);
|
|
19406
|
+
if (target === void 0) continue;
|
|
19407
|
+
const targetFec = fecOfRendition(hang.video?.renditions?.[target]) ?? fecOfRendition(hang.audio?.renditions?.[target]);
|
|
19408
|
+
if (targetFec === void 0) {
|
|
19409
|
+
throwRepairContractError(
|
|
19410
|
+
"track_repair_depends",
|
|
19411
|
+
`${d.path}.depends`,
|
|
19412
|
+
`${JSON.stringify(target)} is not a video or audio rendition with FEC`
|
|
19413
|
+
);
|
|
19414
|
+
}
|
|
19415
|
+
}
|
|
19416
|
+
return assembleRepairLayers(
|
|
19417
|
+
geometryOf(sourceName, `${renditionPath}.fec`, recordOf(rendition["fec"])),
|
|
19418
|
+
descriptors2,
|
|
19419
|
+
throwRepairContractError
|
|
19420
|
+
);
|
|
19421
|
+
}
|
|
19422
|
+
const msf = msfTracks(catalog);
|
|
19423
|
+
const msfIndex = msf ? msf.tracks.findIndex((t) => t["name"] === sourceName) : -1;
|
|
19424
|
+
if (!msf || msfIndex < 0) return void 0;
|
|
19425
|
+
const track = msf.tracks[msfIndex];
|
|
19426
|
+
const fec = recordOf(track["fec"]);
|
|
19427
|
+
const packagingOf = (t) => t["packaging"] ?? msf.common["packaging"];
|
|
19428
|
+
const fecOfTrack = (t) => {
|
|
19429
|
+
const trackFec = recordOf(t?.["fec"]);
|
|
19430
|
+
return trackFec !== void 0 && trackFec["algorithm"] !== "none" ? trackFec : void 0;
|
|
19431
|
+
};
|
|
19432
|
+
const descriptors = [];
|
|
19433
|
+
const mediaTracks = [];
|
|
19434
|
+
msf.tracks.forEach((t, i) => {
|
|
19435
|
+
const path = `$.tracks[${i}]`;
|
|
19436
|
+
if (packagingOf(t) === "fec-repair") {
|
|
19437
|
+
rejectRepairTrackGeometry(path, t, throwRepairContractError);
|
|
19438
|
+
if (typeof t["name"] === "string") descriptors.push(describeRepairTrack(t["name"], path, t));
|
|
19439
|
+
return;
|
|
19440
|
+
}
|
|
19441
|
+
rejectRepairFieldsOnMediaTrack(path, t, throwRepairContractError);
|
|
19442
|
+
mediaTracks.push(t);
|
|
19443
|
+
});
|
|
19444
|
+
const reachability = repairReachability(mediaTracks.map((t) => t["fec"]));
|
|
19445
|
+
for (const d of descriptors) {
|
|
19446
|
+
validateRepairTrackFields(d, throwRepairContractError);
|
|
19447
|
+
assertRepairTrackReachable(d, reachability, throwRepairContractError);
|
|
19448
|
+
const target = dependsTarget(d);
|
|
19449
|
+
if (target === void 0) continue;
|
|
19450
|
+
const targetTrack = msf.tracks.find((t) => t["name"] === target);
|
|
19451
|
+
if (targetTrack === void 0 || packagingOf(targetTrack) !== "mmtp" || fecOfTrack(targetTrack) === void 0) {
|
|
19452
|
+
throwRepairContractError(
|
|
19453
|
+
"track_repair_depends",
|
|
19454
|
+
`${d.path}.depends`,
|
|
19455
|
+
`${JSON.stringify(target)} is not an mmtp track with FEC`
|
|
19456
|
+
);
|
|
19457
|
+
}
|
|
19458
|
+
}
|
|
19459
|
+
return assembleRepairLayers(geometryOf(sourceName, `$.tracks[${msfIndex}].fec`, fec), descriptors, throwRepairContractError, {
|
|
19460
|
+
layerZeroMustBeDeclared: true
|
|
19461
|
+
});
|
|
19462
|
+
}
|
|
19463
|
+
function geometryOf(name, path, fec) {
|
|
19464
|
+
return {
|
|
19465
|
+
name,
|
|
19466
|
+
path,
|
|
19467
|
+
algorithm: fec?.["algorithm"],
|
|
19468
|
+
sourceSymbols: fec?.["sourceSymbols"],
|
|
19469
|
+
repairSymbols: fec?.["repairSymbols"],
|
|
19470
|
+
repairTrack: fec?.["repairTrack"]
|
|
19471
|
+
};
|
|
19472
|
+
}
|
|
17247
19473
|
function numericField(value) {
|
|
17248
19474
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
17249
19475
|
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
@@ -17252,13 +19478,16 @@ function nonNegativeField(value) {
|
|
|
17252
19478
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
17253
19479
|
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
17254
19480
|
}
|
|
17255
|
-
function fecParams(rendition, fps, sampleRate) {
|
|
19481
|
+
function fecParams(catalog, track, rendition, fps, sampleRate) {
|
|
17256
19482
|
const mediaRate = sampleRate === void 0 ? { fps } : { fps, sampleRate };
|
|
17257
19483
|
const fec = rendition?.fec;
|
|
17258
19484
|
if (!fec) return { ...NO_FEC, ...mediaRate };
|
|
19485
|
+
const repair = track ? resolveRepairLayers(catalog, track) : void 0;
|
|
17259
19486
|
return {
|
|
17260
19487
|
fecK: fec.sourceSymbols ?? 0,
|
|
17261
19488
|
fecRepairCount: fec.repairSymbols ?? 0,
|
|
19489
|
+
// The wire RSB_length: every base layer's packets carry the base total (§7.1).
|
|
19490
|
+
fecTotalRepairCount: repair?.totalBaseRepairSymbols ?? fec.repairSymbols ?? 0,
|
|
17262
19491
|
fecSymbolSize: fec.symbolSize ?? 0,
|
|
17263
19492
|
// interleaveDepthMs is the FEC block span in MILLISECONDS (§5.1) — the
|
|
17264
19493
|
// only interleave key the catalog carries post-BLO-14526. Receivers
|
|
@@ -17270,6 +19499,8 @@ function fecParams(rendition, fps, sampleRate) {
|
|
|
17270
19499
|
// so it cannot be reused here — a declared 0 is a legitimate value.
|
|
17271
19500
|
fecReorderToleranceMs: nonNegativeField(fec.reorderToleranceMs),
|
|
17272
19501
|
repairTrackName: fec.repairTrack,
|
|
19502
|
+
repairLayers: repair?.baseLayers,
|
|
19503
|
+
repairOverlays: repair?.overlays,
|
|
17273
19504
|
...mediaRate
|
|
17274
19505
|
};
|
|
17275
19506
|
}
|
|
@@ -17283,8 +19514,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
|
|
|
17283
19514
|
const catalogFps = numericField(video?.framerate);
|
|
17284
19515
|
const sampleRate = numericField(audio?.sampleRate ?? audio?.samplerate);
|
|
17285
19516
|
const audioFps = sampleRate ? sampleRate / 1024 : void 0;
|
|
17286
|
-
const videoParams = fecParams(video, catalogFps);
|
|
17287
|
-
const audioParams = fecParams(
|
|
19517
|
+
const videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: "video" } : void 0, video, catalogFps);
|
|
19518
|
+
const audioParams = fecParams(
|
|
19519
|
+
catalog,
|
|
19520
|
+
audioTrackName ? { name: audioTrackName, type: "audio" } : void 0,
|
|
19521
|
+
audio,
|
|
19522
|
+
audioFps,
|
|
19523
|
+
sampleRate
|
|
19524
|
+
);
|
|
17288
19525
|
return {
|
|
17289
19526
|
videoTrackName,
|
|
17290
19527
|
audioTrackName,
|
|
@@ -17320,8 +19557,10 @@ function catalogAmtNetworkSource(networkSource) {
|
|
|
17320
19557
|
// src/repair-fetch.ts
|
|
17321
19558
|
function wireRepairFetch(mc, opts) {
|
|
17322
19559
|
const inFlight = /* @__PURE__ */ new Map();
|
|
19560
|
+
const warnedNoTarget = /* @__PURE__ */ new Set();
|
|
17323
19561
|
let disposed = false;
|
|
17324
19562
|
const requestKey = (sbn, packetId, catalogTrackName) => JSON.stringify([packetId, catalogTrackName ?? null, sbn]);
|
|
19563
|
+
const noTargetKey = (packetId, catalogTrackName) => JSON.stringify([packetId, catalogTrackName ?? null]);
|
|
17325
19564
|
const removeResetObserver = mc.addTrackResetObserver(
|
|
17326
19565
|
(packetId, catalogTrackName) => {
|
|
17327
19566
|
for (const [key, request] of inFlight) {
|
|
@@ -17330,6 +19569,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
17330
19569
|
inFlight.delete(key);
|
|
17331
19570
|
}
|
|
17332
19571
|
}
|
|
19572
|
+
warnedNoTarget.delete(noTargetKey(packetId, catalogTrackName));
|
|
17333
19573
|
}
|
|
17334
19574
|
);
|
|
17335
19575
|
const timeoutFor = (sbn, packetId, catalogTrackName) => opts.timeoutMsForPacket?.(packetId) ?? mc.fecManager.repairFetchTimeoutMsForBlock(
|
|
@@ -17368,6 +19608,13 @@ function wireRepairFetch(mc, opts) {
|
|
|
17368
19608
|
}
|
|
17369
19609
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
17370
19610
|
if (!target) {
|
|
19611
|
+
const warnKey = noTargetKey(packetId, catalogTrackName);
|
|
19612
|
+
if (!warnedNoTarget.has(warnKey)) {
|
|
19613
|
+
warnedNoTarget.add(warnKey);
|
|
19614
|
+
console.warn(
|
|
19615
|
+
`[RepairFetch] no repair target for pid=${packetId} sbn=${sbn}${catalogTrackName === void 0 ? "" : ` track=${catalogTrackName}`}; releasing without a fetch`
|
|
19616
|
+
);
|
|
19617
|
+
}
|
|
17371
19618
|
clearRepair(sbn, packetId, catalogTrackName);
|
|
17372
19619
|
return;
|
|
17373
19620
|
}
|
|
@@ -17437,6 +19684,7 @@ export {
|
|
|
17437
19684
|
CmafGroupBatcher,
|
|
17438
19685
|
CmcdProducer,
|
|
17439
19686
|
CmsfCatalogValidationError,
|
|
19687
|
+
DEFAULT_REPAIR_PRIORITY,
|
|
17440
19688
|
FECManager,
|
|
17441
19689
|
FEC_ESI_MAX,
|
|
17442
19690
|
FEC_SBN_MAX,
|
|
@@ -17444,6 +19692,10 @@ export {
|
|
|
17444
19692
|
FecStarvationDetector,
|
|
17445
19693
|
KEYFRAME_LOST_TIMEOUT_SLACK,
|
|
17446
19694
|
LOCHeaderExtensionId,
|
|
19695
|
+
MAX_ENCODING_SYMBOL_ID,
|
|
19696
|
+
MAX_RAPTORQ_SOURCE_SYMBOLS,
|
|
19697
|
+
MAX_REPAIR_SYMBOLS,
|
|
19698
|
+
MAX_TAKEOVER_DEADLINE_MS,
|
|
17447
19699
|
MFUReassembler,
|
|
17448
19700
|
MediaRouter,
|
|
17449
19701
|
MfuReassemblyError,
|
|
@@ -17458,7 +19710,12 @@ export {
|
|
|
17458
19710
|
NoneCodec,
|
|
17459
19711
|
REPAIR_LIFECYCLE_EVENT_TYPE,
|
|
17460
19712
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
19713
|
+
REPAIR_PRIORITY_MAX,
|
|
19714
|
+
REPAIR_PRIORITY_MIN,
|
|
19715
|
+
REPAIR_TRACK_GEOMETRY_KEYS,
|
|
19716
|
+
REPAIR_TRACK_ONLY_KEYS,
|
|
17461
19717
|
ReedSolomonCodec,
|
|
19718
|
+
RepairContractError,
|
|
17462
19719
|
SampleCombineError,
|
|
17463
19720
|
SampleCombiner,
|
|
17464
19721
|
SessionManager,
|
|
@@ -17469,11 +19726,14 @@ export {
|
|
|
17469
19726
|
WasmMfuReassembler,
|
|
17470
19727
|
annexBToAvcc,
|
|
17471
19728
|
asSeconds,
|
|
19729
|
+
assembleRepairLayers,
|
|
19730
|
+
assertRepairTrackReachable,
|
|
17472
19731
|
avccToAnnexB,
|
|
17473
19732
|
buildAudioInitSegment,
|
|
17474
19733
|
buildVideoInitSegment,
|
|
17475
19734
|
catalogAmtNetworkSource,
|
|
17476
19735
|
catalogFecInterleaveMs,
|
|
19736
|
+
catalogGroupDurationMs,
|
|
17477
19737
|
classifyH264Keyframe,
|
|
17478
19738
|
classifyPacket,
|
|
17479
19739
|
codecStringFromAvcC,
|
|
@@ -17488,6 +19748,8 @@ export {
|
|
|
17488
19748
|
decodeLocHeader,
|
|
17489
19749
|
decodeLocTimestamp,
|
|
17490
19750
|
decodeTimestamp,
|
|
19751
|
+
dependsTarget,
|
|
19752
|
+
describeRepairTrack,
|
|
17491
19753
|
detectH264Keyframe,
|
|
17492
19754
|
driftCheck,
|
|
17493
19755
|
encodeDataSegment,
|
|
@@ -17541,12 +19803,17 @@ export {
|
|
|
17541
19803
|
parseAacSampleRate,
|
|
17542
19804
|
parseOpusFrameSamples,
|
|
17543
19805
|
processMMTPWithReassembly,
|
|
19806
|
+
rejectRepairFieldsOnMediaTrack,
|
|
19807
|
+
rejectRepairTrackGeometry,
|
|
17544
19808
|
reliableMoqMfuReassembly,
|
|
17545
19809
|
removeEmulationPrevention,
|
|
19810
|
+
repairReachability,
|
|
17546
19811
|
resolveCatalogTrackFec,
|
|
19812
|
+
resolveCatalogTrackGroupDurationMs,
|
|
17547
19813
|
resolveGlobalMmtWasmBindings,
|
|
17548
19814
|
resolveGroupDurationMs,
|
|
17549
19815
|
resolveGroupDurationTicks,
|
|
19816
|
+
resolveRepairLayers,
|
|
17550
19817
|
secondsFromTicks,
|
|
17551
19818
|
setupFecWorkerForTrack,
|
|
17552
19819
|
setupTrackPipeline,
|
|
@@ -17556,6 +19823,7 @@ export {
|
|
|
17556
19823
|
unwrapMulticastPacket,
|
|
17557
19824
|
validateCmsfMmtpCatalog,
|
|
17558
19825
|
validateMulticastContainerContract,
|
|
19826
|
+
validateRepairTrackFields,
|
|
17559
19827
|
validateSwitchingSetTiming,
|
|
17560
19828
|
wireRepairFetch,
|
|
17561
19829
|
wrapAdtsFrame
|