@blockcast/mmt-container 0.1.0-main.30d2c7ed30b2 → 0.1.0-main.342ee38b60e1
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 +2390 -246
- 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.js.map +1 -1
- 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 +99 -1
- package/dist/mfu-reassembler.d.ts.map +1 -1
- package/dist/mfu-reassembler.js +95 -35
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.js +94 -35
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +432 -48
- 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 +155 -59
- package/dist/mmtp-decode-pipeline.js.map +3 -3
- package/dist/mmtp-router.js +94 -35
- package/dist/mmtp-router.js.map +2 -2
- 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 +1619 -112
- 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 +432 -48
- package/dist/no-modules.js.map +4 -4
- 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.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +94 -35
- 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/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 +252 -38
- package/src/mmtp-decode-pipeline.ts +93 -24
- package/src/mmtp-to-cmaf.ts +25 -1
- package/src/multicast-manager.ts +106 -22
- package/src/multicast-transport.ts +13 -27
- package/src/repair-layers.ts +490 -0
- package/src/track-session.ts +201 -119
- 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);
|
|
@@ -1086,12 +1098,9 @@ var MFUReassembler = class {
|
|
|
1086
1098
|
state.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onTimeout_fn).call(this, bufferKey), Math.max(0, state.startedAt + ms - now));
|
|
1087
1099
|
}
|
|
1088
1100
|
}
|
|
1089
|
-
for (const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (ms !== null && __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) === void 0) {
|
|
1093
|
-
pending.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address), Math.max(0, pending.startedAt + ms - now));
|
|
1094
|
-
}
|
|
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);
|
|
1095
1104
|
}
|
|
1096
1105
|
}
|
|
1097
1106
|
get timeoutMs() {
|
|
@@ -1255,14 +1264,26 @@ var MFUReassembler = class {
|
|
|
1255
1264
|
this.stats.outOfOrderFragments = 0;
|
|
1256
1265
|
}
|
|
1257
1266
|
/**
|
|
1258
|
-
* 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.
|
|
1259
1284
|
*/
|
|
1260
1285
|
get pendingCount() {
|
|
1261
|
-
|
|
1262
|
-
for (const address of __privateGet(this, _pendingContinuations).keys()) {
|
|
1263
|
-
if (__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) === void 0) count++;
|
|
1264
|
-
}
|
|
1265
|
-
return count;
|
|
1286
|
+
return __privateGet(this, _buffers).size + __privateMethod(this, _MFUReassembler_instances, unopenedParkedRunCount_fn).call(this);
|
|
1266
1287
|
}
|
|
1267
1288
|
/**
|
|
1268
1289
|
* Get current buffer status
|
|
@@ -1323,29 +1344,33 @@ addAssociatedFragment_fn = function(fragment, runKey, address, startedAt) {
|
|
|
1323
1344
|
};
|
|
1324
1345
|
parkContinuation_fn = function(address, fragment) {
|
|
1325
1346
|
let pending = __privateGet(this, _pendingContinuations).get(address);
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (
|
|
1330
|
-
|
|
1331
|
-
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
|
|
1332
|
-
}
|
|
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)) {
|
|
1351
|
+
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
|
|
1333
1352
|
}
|
|
1334
|
-
|
|
1353
|
+
}
|
|
1354
|
+
let armAfterInsert = false;
|
|
1355
|
+
if (pending === void 0) {
|
|
1335
1356
|
pending = {
|
|
1336
1357
|
fragments: /* @__PURE__ */ new Map(),
|
|
1337
|
-
startedAt,
|
|
1338
|
-
admissionSeq: open
|
|
1358
|
+
startedAt: Date.now(),
|
|
1359
|
+
admissionSeq: open !== void 0 ? open[1].admissionSeq : __privateWrapper(this, _nextAdmissionSeq)._++
|
|
1339
1360
|
};
|
|
1340
|
-
if (awaitingFirst && __privateGet(this, _timeoutMs) !== null) {
|
|
1341
|
-
pending.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address), __privateGet(this, _timeoutMs));
|
|
1342
|
-
}
|
|
1343
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;
|
|
1344
1368
|
}
|
|
1345
1369
|
if (pending.fragments.has(fragment.fragmentCounter)) {
|
|
1346
1370
|
__privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "duplicate queued continuation counter would replace retained media");
|
|
1347
1371
|
}
|
|
1348
1372
|
pending.fragments.set(fragment.fragmentCounter, fragment);
|
|
1373
|
+
if (armAfterInsert) __privateMethod(this, _MFUReassembler_instances, armHeadlessPendingTimeout_fn).call(this, address);
|
|
1349
1374
|
};
|
|
1350
1375
|
drainPendingContinuations_fn = function(address, runKey) {
|
|
1351
1376
|
while (true) {
|
|
@@ -1368,15 +1393,35 @@ drainPendingContinuations_fn = function(address, runKey) {
|
|
|
1368
1393
|
}
|
|
1369
1394
|
}
|
|
1370
1395
|
};
|
|
1371
|
-
/**
|
|
1372
|
-
*
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
+
}
|
|
1377
1414
|
}
|
|
1378
1415
|
return count;
|
|
1379
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
|
+
};
|
|
1380
1425
|
/**
|
|
1381
1426
|
* Drop the incomplete data unit admitted longest ago so a newly arriving one
|
|
1382
1427
|
* can take its slot. Selection is by monotonic admission ordinal, never by
|
|
@@ -1388,6 +1433,12 @@ incompleteRunCount_fn = function() {
|
|
|
1388
1433
|
* Returns false only if no slot is charged, which cannot happen at the bound
|
|
1389
1434
|
* (`maxBuffered` is a positive integer); admission then falls back to the
|
|
1390
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.
|
|
1391
1442
|
*/
|
|
1392
1443
|
evictLeastRecentlyAdmitted_fn = function() {
|
|
1393
1444
|
let victimAddress;
|
|
@@ -1399,7 +1450,9 @@ evictLeastRecentlyAdmitted_fn = function() {
|
|
|
1399
1450
|
}
|
|
1400
1451
|
}
|
|
1401
1452
|
for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
|
|
1402
|
-
if (__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0)
|
|
1453
|
+
if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1403
1456
|
if (pending.admissionSeq < victimSeq) {
|
|
1404
1457
|
victimSeq = pending.admissionSeq;
|
|
1405
1458
|
victimAddress = address;
|
|
@@ -1434,7 +1487,10 @@ associateDataUnit_fn = function(fragment, runKey, address) {
|
|
|
1434
1487
|
const fi = fragment.fragmentationIndicator;
|
|
1435
1488
|
if (fi === 1) {
|
|
1436
1489
|
const parked = __privateGet(this, _pendingContinuations).get(address);
|
|
1437
|
-
const inheritsParkedSlot = (
|
|
1490
|
+
const inheritsParkedSlot = parkedRunHoldsASlot(
|
|
1491
|
+
__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0,
|
|
1492
|
+
parked?.fragments.size ?? 0
|
|
1493
|
+
);
|
|
1438
1494
|
const sampleIdentity = hasSampleIdentity(fragment);
|
|
1439
1495
|
const hasSampleFields = hasAnySampleIdentityField(fragment);
|
|
1440
1496
|
const hasItemIdentity = fragment.itemId !== void 0;
|
|
@@ -1605,11 +1661,19 @@ clearPendingTimeout_fn = function(address) {
|
|
|
1605
1661
|
/**
|
|
1606
1662
|
* A queue whose run closed without completing is headless again and expires
|
|
1607
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).
|
|
1608
1672
|
*/
|
|
1609
1673
|
armHeadlessPendingTimeout_fn = function(address) {
|
|
1610
1674
|
const pending = __privateGet(this, _pendingContinuations).get(address);
|
|
1611
1675
|
if (pending === void 0 || pending.timeoutId !== void 0 || __privateGet(this, _timeoutMs) === null) return;
|
|
1612
|
-
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;
|
|
1613
1677
|
pending.timeoutId = setTimeout(
|
|
1614
1678
|
() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address),
|
|
1615
1679
|
Math.max(0, pending.startedAt + __privateGet(this, _timeoutMs) - Date.now())
|
|
@@ -1622,6 +1686,10 @@ deletePendingRun_fn = function(address) {
|
|
|
1622
1686
|
onPendingTimeout_fn = function(address) {
|
|
1623
1687
|
const pending = __privateGet(this, _pendingContinuations).get(address);
|
|
1624
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
|
+
}
|
|
1625
1693
|
this.stats.timedOutMFUs++;
|
|
1626
1694
|
console.warn(
|
|
1627
1695
|
`[MfuReassembler] timeout: run=${address} fragments=${pending.fragments.size} (continuations awaiting first fragment, discarded)`
|
|
@@ -5735,6 +5803,289 @@ function resolveMulticastAuthCatalog(catalog, playerMode) {
|
|
|
5735
5803
|
};
|
|
5736
5804
|
}
|
|
5737
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
|
+
|
|
5738
6089
|
// src/carp-catalog.ts
|
|
5739
6090
|
var VALID_ROLE_SUFFIXES = /* @__PURE__ */ new Set(["base", "delta", "repair"]);
|
|
5740
6091
|
function parseTrackRole(trackName) {
|
|
@@ -5861,7 +6212,7 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5861
6212
|
const tracks = catalog["tracks"];
|
|
5862
6213
|
const trackRecords = [];
|
|
5863
6214
|
const effective = (track, field) => track[field] ?? common[field];
|
|
5864
|
-
const
|
|
6215
|
+
const positiveInteger2 = (input) => Number.isSafeInteger(input) && input > 0;
|
|
5865
6216
|
const positiveNumber = (input) => typeof input === "number" && Number.isFinite(input) && input > 0;
|
|
5866
6217
|
for (let index = 0; index < tracks.length; index++) {
|
|
5867
6218
|
const raw = tracks[index];
|
|
@@ -5897,21 +6248,27 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5897
6248
|
const groupDurationTicks = effective(track, "groupDurationTicks");
|
|
5898
6249
|
const keyframeIntervalMs = effective(track, "keyframeIntervalMs");
|
|
5899
6250
|
const keyframeIntervalTicks = effective(track, "keyframeIntervalTicks");
|
|
5900
|
-
if (groupDurationMs !== void 0 && !
|
|
6251
|
+
if (groupDurationMs !== void 0 && !positiveInteger2(groupDurationMs)) {
|
|
5901
6252
|
catalogError("track_group_duration_ms", `${path}.groupDurationMs`, "must be a positive integer");
|
|
5902
6253
|
}
|
|
5903
|
-
if (groupDurationTicks !== void 0 && !
|
|
6254
|
+
if (groupDurationTicks !== void 0 && !positiveInteger2(groupDurationTicks)) {
|
|
5904
6255
|
catalogError("track_group_duration_ticks", `${path}.groupDurationTicks`, "must be a positive integer");
|
|
5905
6256
|
}
|
|
5906
|
-
if (keyframeIntervalMs !== void 0 && !positiveNumber(keyframeIntervalMs) || keyframeIntervalTicks !== void 0 && !
|
|
6257
|
+
if (keyframeIntervalMs !== void 0 && !positiveNumber(keyframeIntervalMs) || keyframeIntervalTicks !== void 0 && !positiveInteger2(keyframeIntervalTicks)) {
|
|
5907
6258
|
catalogError("track_keyframe_interval", path, "keyframe interval fields must be positive numbers");
|
|
5908
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
|
+
}
|
|
5909
6266
|
if (packaging === "mmtp") {
|
|
5910
6267
|
const mmtpMode = effective(track, "mmtpMode");
|
|
5911
6268
|
if (mmtpMode !== "mpu" && mmtpMode !== "mfu") {
|
|
5912
6269
|
catalogError("track_mmtp_mode", `${path}.mmtpMode`, "must be mpu or mfu");
|
|
5913
6270
|
}
|
|
5914
|
-
if (!
|
|
6271
|
+
if (!positiveInteger2(timescale)) {
|
|
5915
6272
|
catalogError("track_timescale", `${path}.timescale`, "must be a positive integer");
|
|
5916
6273
|
}
|
|
5917
6274
|
if (groupDurationMs === void 0 && groupDurationTicks === void 0) {
|
|
@@ -5938,12 +6295,19 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5938
6295
|
if ("interleaveDepth" in config || "interleaveMs" in config) {
|
|
5939
6296
|
catalogError("track_fec", `${path}.fec`, "interleaveDepthMs is the only supported interleave field");
|
|
5940
6297
|
}
|
|
5941
|
-
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"])) {
|
|
5942
6306
|
catalogError("track_fec", `${path}.fec.interleaveDepthMs`, "must be a positive integer");
|
|
5943
6307
|
}
|
|
5944
6308
|
if (algorithm !== "none") {
|
|
5945
6309
|
for (const field of ["sourceSymbols", "repairSymbols", "symbolSize"]) {
|
|
5946
|
-
if (!
|
|
6310
|
+
if (!positiveInteger2(config[field])) {
|
|
5947
6311
|
catalogError("track_fec", `${path}.fec.${field}`, "must be a positive integer");
|
|
5948
6312
|
}
|
|
5949
6313
|
}
|
|
@@ -5953,15 +6317,43 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
5953
6317
|
}
|
|
5954
6318
|
}
|
|
5955
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
|
+
}
|
|
5956
6340
|
for (let index = 0; index < trackRecords.length; index++) {
|
|
5957
6341
|
const track = trackRecords[index];
|
|
5958
6342
|
const fec = track["fec"];
|
|
5959
6343
|
if (!fec || fec["algorithm"] === "none") continue;
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
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
|
+
);
|
|
5965
6357
|
}
|
|
5966
6358
|
const switchingSets = /* @__PURE__ */ new Map();
|
|
5967
6359
|
for (let index = 0; index < trackRecords.length; index++) {
|
|
@@ -7385,6 +7777,7 @@ var MmtpToCmaf = class {
|
|
|
7385
7777
|
const samplesPerFrame = __privateGet(this, _audioTimescale) !== 1e6 ? __privateGet(this, _aacFrameSamples) : sampleDuration;
|
|
7386
7778
|
const scaledTs = __privateGet(this, _audioTimescale) !== 1e6 ? __privateGet(this, _audioSampleCount) * samplesPerFrame : timestamp - __privateGet(this, _audioBaseTs);
|
|
7387
7779
|
__privateWrapper(this, _audioSampleCount)._++;
|
|
7780
|
+
const scaledCompositionTimeOffset = compositionTimeOffset === void 0 || __privateGet(this, _audioTimescale) === 1e6 ? compositionTimeOffset : Math.round(compositionTimeOffset * __privateGet(this, _audioTimescale) / 1e6);
|
|
7388
7781
|
return encodeDataSegment(
|
|
7389
7782
|
sampleData,
|
|
7390
7783
|
scaledTs,
|
|
@@ -7392,7 +7785,7 @@ var MmtpToCmaf = class {
|
|
|
7392
7785
|
true,
|
|
7393
7786
|
__privateWrapper(this, _audioSeq)._++,
|
|
7394
7787
|
1,
|
|
7395
|
-
|
|
7788
|
+
scaledCompositionTimeOffset
|
|
7396
7789
|
);
|
|
7397
7790
|
}
|
|
7398
7791
|
/** Convert an ordered audio batch using the caller-owned timeline. */
|
|
@@ -8501,6 +8894,15 @@ function deriveFecInterleaveDepth(interleaveDepthMs, mediaUnitDurationMs) {
|
|
|
8501
8894
|
}
|
|
8502
8895
|
return Math.max(1, Math.floor(interleaveDepthMs / mediaUnitDurationMs + 0.5));
|
|
8503
8896
|
}
|
|
8897
|
+
function fecBlockBudgetMs(interleaveDepthMs, reorderToleranceMs) {
|
|
8898
|
+
if (!Number.isFinite(interleaveDepthMs) || !(interleaveDepthMs > 0)) {
|
|
8899
|
+
throw new Error(`[FEC] interleaveDepthMs must be a finite value > 0 (got ${interleaveDepthMs}ms). Fix encoder config.`);
|
|
8900
|
+
}
|
|
8901
|
+
if (!(reorderToleranceMs >= 0) || !Number.isFinite(reorderToleranceMs)) {
|
|
8902
|
+
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.`);
|
|
8903
|
+
}
|
|
8904
|
+
return Math.ceil(interleaveDepthMs + reorderToleranceMs);
|
|
8905
|
+
}
|
|
8504
8906
|
function parseRepairPacket(packet) {
|
|
8505
8907
|
const parsed = parseMoqRepairFrame(packet);
|
|
8506
8908
|
if (!parsed)
|
|
@@ -8602,32 +9004,18 @@ function computeFecLossWindowMs(fecInterleaveMs, fecK, jitterBufferMs) {
|
|
|
8602
9004
|
function computeFecDeliveryWindowMs(config) {
|
|
8603
9005
|
const fecInterleaveMs = config.interleaveDepthMs;
|
|
8604
9006
|
const fecK = config.sourceSymbols;
|
|
8605
|
-
if (fecInterleaveMs <= 0) {
|
|
9007
|
+
if (!Number.isFinite(fecInterleaveMs) || fecInterleaveMs <= 0) {
|
|
8606
9008
|
throw new Error(
|
|
8607
9009
|
`[TrackSession] fecInterleaveMs=${fecInterleaveMs}. Derive a positive FEC interleave window from the stream catalog.`
|
|
8608
9010
|
);
|
|
8609
9011
|
}
|
|
8610
|
-
if (fecK <= 0) {
|
|
9012
|
+
if (!Number.isFinite(fecK) || fecK <= 0) {
|
|
8611
9013
|
throw new Error(
|
|
8612
9014
|
`[TrackSession] fecK=${fecK}. Derive a positive FEC source-symbol count from the stream catalog.`
|
|
8613
9015
|
);
|
|
8614
9016
|
}
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
const mediaUnitMs = 1e3 / config.mediaUnitsPerSecond;
|
|
8618
|
-
const interleaveUnits = deriveFecInterleaveDepth(fecInterleaveMs, mediaUnitMs);
|
|
8619
|
-
const deliverySymbols = fecK + Math.max(0, config.repairSymbols ?? 0);
|
|
8620
|
-
spansMs.push(deliverySymbols * interleaveUnits * mediaUnitMs);
|
|
8621
|
-
}
|
|
8622
|
-
if (config.symbolSize && config.symbolSize > 0 && config.bandwidthBps && config.bandwidthBps > 0) {
|
|
8623
|
-
const repairSymbols = Math.max(0, config.repairSymbols ?? 0);
|
|
8624
|
-
spansMs.push(
|
|
8625
|
-
(fecK + repairSymbols) * config.symbolSize * 8 * 1e3 / config.bandwidthBps
|
|
8626
|
-
);
|
|
8627
|
-
}
|
|
8628
|
-
const coreMs = Math.max(...spansMs);
|
|
8629
|
-
const marginMs = config.jitterBufferMs !== void 0 && config.jitterBufferMs >= 0 ? config.jitterBufferMs : fecInterleaveMs;
|
|
8630
|
-
return Math.ceil(coreMs) + Math.ceil(marginMs);
|
|
9017
|
+
if (config.jitterBufferMs === void 0) return Math.ceil(fecInterleaveMs);
|
|
9018
|
+
return fecBlockBudgetMs(fecInterleaveMs, config.jitterBufferMs);
|
|
8631
9019
|
}
|
|
8632
9020
|
function computeFecDeliveryWindowMediaUnits(config, timestampTimescale) {
|
|
8633
9021
|
if (!Number.isFinite(timestampTimescale) || timestampTimescale <= 0) {
|
|
@@ -8643,7 +9031,7 @@ function computeFecDeliveryWindowMediaUnits(config, timestampTimescale) {
|
|
|
8643
9031
|
}
|
|
8644
9032
|
return deliveryWindow;
|
|
8645
9033
|
}
|
|
8646
|
-
var _state, _subscription, _hysteresisTimer, _joinDeadlineTimer, _previousPrimary, _manager, _suppressFired, _TrackSession_instances, transition_fn, clearHysteresis_fn, armJoinDeadline_fn, clearJoinDeadline_fn, suppress_fn, unsuppress_fn;
|
|
9034
|
+
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;
|
|
8647
9035
|
var TrackSession = class {
|
|
8648
9036
|
constructor(config, manager) {
|
|
8649
9037
|
__privateAdd(this, _TrackSession_instances);
|
|
@@ -8660,6 +9048,12 @@ var TrackSession = class {
|
|
|
8660
9048
|
// from `failing` (exactly where a starving track sits) was re-subscribed to
|
|
8661
9049
|
// unicast with the host's media still muted: MoQ playing into a dead sink.
|
|
8662
9050
|
__privateAdd(this, _suppressFired, false);
|
|
9051
|
+
// Single FEC authority flag: set by the fec-setup edges (joinStarted,
|
|
9052
|
+
// enterUnicastFallback), cleared by the fec-teardown edge in #transition.
|
|
9053
|
+
// Setup and teardown are paired by construction, so a takeover timeout that
|
|
9054
|
+
// parks the session in `unicast` keeps the decoder the still-joined
|
|
9055
|
+
// transport is feeding, and the later unsubscribe still tears it down.
|
|
9056
|
+
__privateAdd(this, _fecArmed, false);
|
|
8663
9057
|
this.config = config;
|
|
8664
9058
|
__privateSet(this, _manager, manager);
|
|
8665
9059
|
}
|
|
@@ -8734,7 +9128,7 @@ var TrackSession = class {
|
|
|
8734
9128
|
joinStarted() {
|
|
8735
9129
|
if (__privateGet(this, _state) !== "unicast") return;
|
|
8736
9130
|
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
|
|
8737
|
-
|
|
9131
|
+
__privateMethod(this, _TrackSession_instances, armFec_fn).call(this);
|
|
8738
9132
|
}
|
|
8739
9133
|
/**
|
|
8740
9134
|
* any non-fallback state → unicast-fallback. Used when the SSM bridge is
|
|
@@ -8755,7 +9149,7 @@ var TrackSession = class {
|
|
|
8755
9149
|
__privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
|
|
8756
9150
|
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast-fallback");
|
|
8757
9151
|
__privateMethod(this, _TrackSession_instances, suppress_fn).call(this);
|
|
8758
|
-
|
|
9152
|
+
__privateMethod(this, _TrackSession_instances, armFec_fn).call(this);
|
|
8759
9153
|
}
|
|
8760
9154
|
/**
|
|
8761
9155
|
* unicast-fallback → unicast. Called when the SSM bridge appears mid-session
|
|
@@ -8786,7 +9180,11 @@ var TrackSession = class {
|
|
|
8786
9180
|
if (__privateGet(this, _state) !== "multicast" && __privateGet(this, _state) !== "amt") return;
|
|
8787
9181
|
__privateSet(this, _previousPrimary, __privateGet(this, _state));
|
|
8788
9182
|
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "failing");
|
|
8789
|
-
const hysteresisMs = computeFecLossWindowMs(
|
|
9183
|
+
const hysteresisMs = computeFecLossWindowMs(
|
|
9184
|
+
this.config.fecInterleaveMs,
|
|
9185
|
+
this.config.fecK,
|
|
9186
|
+
this.config.fecReorderToleranceMs
|
|
9187
|
+
);
|
|
8790
9188
|
__privateSet(this, _hysteresisTimer, setTimeout(() => {
|
|
8791
9189
|
this.hysteresisExpired();
|
|
8792
9190
|
}, hysteresisMs));
|
|
@@ -8810,23 +9208,19 @@ var TrackSession = class {
|
|
|
8810
9208
|
const prefs = this.config.preferredTransports;
|
|
8811
9209
|
const currentIdx = current ? prefs.indexOf(current) : -1;
|
|
8812
9210
|
const nextMode = prefs[currentIdx + 1];
|
|
8813
|
-
if (nextMode) {
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
8817
|
-
if (this.config.trackType === "audio") {
|
|
8818
|
-
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
|
|
8819
|
-
}
|
|
8820
|
-
} else {
|
|
8821
|
-
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
|
|
8822
|
-
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
8823
|
-
}
|
|
8824
|
-
} else {
|
|
8825
|
-
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
|
|
9211
|
+
if (nextMode !== void 0 && nextMode !== "moq") {
|
|
9212
|
+
__privateSet(this, _previousPrimary, nextMode);
|
|
9213
|
+
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
|
|
8826
9214
|
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
9215
|
+
return;
|
|
9216
|
+
}
|
|
9217
|
+
const from = __privateGet(this, _state);
|
|
9218
|
+
__privateSet(this, _previousPrimary, null);
|
|
9219
|
+
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
|
|
9220
|
+
__privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
|
|
9221
|
+
__privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
|
|
9222
|
+
if (from !== "unicast" && this.config.trackType === "audio") {
|
|
9223
|
+
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
|
|
8830
9224
|
}
|
|
8831
9225
|
}
|
|
8832
9226
|
/** Hysteresis expired → failback to next transport (Claim 9g). */
|
|
@@ -8850,11 +9244,20 @@ var TrackSession = class {
|
|
|
8850
9244
|
get isUnicastFallback() {
|
|
8851
9245
|
return __privateGet(this, _state) === "unicast-fallback";
|
|
8852
9246
|
}
|
|
9247
|
+
/**
|
|
9248
|
+
* True while this track owns a FEC decoder (fec-setup fired, fec-teardown
|
|
9249
|
+
* not yet). Stays true across a takeover timeout that parked the session in
|
|
9250
|
+
* `unicast`; SessionManager.failbackAll uses it to release such sessions.
|
|
9251
|
+
*/
|
|
9252
|
+
get isFecArmed() {
|
|
9253
|
+
return __privateGet(this, _fecArmed);
|
|
9254
|
+
}
|
|
8853
9255
|
dispose() {
|
|
8854
9256
|
__privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
|
|
8855
9257
|
__privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
|
|
8856
9258
|
__privateSet(this, _state, "idle");
|
|
8857
9259
|
__privateSet(this, _subscription, null);
|
|
9260
|
+
__privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
|
|
8858
9261
|
}
|
|
8859
9262
|
};
|
|
8860
9263
|
_state = new WeakMap();
|
|
@@ -8864,20 +9267,20 @@ _joinDeadlineTimer = new WeakMap();
|
|
|
8864
9267
|
_previousPrimary = new WeakMap();
|
|
8865
9268
|
_manager = new WeakMap();
|
|
8866
9269
|
_suppressFired = new WeakMap();
|
|
9270
|
+
_fecArmed = new WeakMap();
|
|
8867
9271
|
_TrackSession_instances = new WeakSet();
|
|
8868
9272
|
// -----------------------------------------------------------------------
|
|
8869
9273
|
// Internal
|
|
8870
9274
|
// -----------------------------------------------------------------------
|
|
8871
|
-
transition_fn = function(to) {
|
|
9275
|
+
transition_fn = function(to, opts) {
|
|
8872
9276
|
const from = __privateGet(this, _state);
|
|
8873
9277
|
if (from === to) return;
|
|
9278
|
+
const joinDeadlineMs = to === "joining" ? __privateGet(this, _manager).takeoverDeadlineMs : void 0;
|
|
8874
9279
|
__privateSet(this, _state, to);
|
|
8875
9280
|
if (from === "joining") __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
|
|
8876
|
-
if (
|
|
9281
|
+
if (joinDeadlineMs !== void 0) __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this, joinDeadlineMs);
|
|
8877
9282
|
const fecActive = (s) => s === "joining" || s === "multicast" || s === "amt" || s === "failing" || s === "unicast-fallback";
|
|
8878
|
-
if (fecActive(
|
|
8879
|
-
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-teardown" }, this.config);
|
|
8880
|
-
}
|
|
9283
|
+
if (!fecActive(to) && !opts?.retainFec) __privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
|
|
8881
9284
|
__privateGet(this, _manager).callbacks?.onStateChange?.(this.config.trackName, from, to);
|
|
8882
9285
|
};
|
|
8883
9286
|
clearHysteresis_fn = function() {
|
|
@@ -8886,14 +9289,13 @@ clearHysteresis_fn = function() {
|
|
|
8886
9289
|
__privateSet(this, _hysteresisTimer, null);
|
|
8887
9290
|
}
|
|
8888
9291
|
};
|
|
8889
|
-
armJoinDeadline_fn = function() {
|
|
9292
|
+
armJoinDeadline_fn = function(deadlineMs) {
|
|
8890
9293
|
__privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
|
|
8891
|
-
|
|
8892
|
-
if (deadlineMs <= 0) return;
|
|
9294
|
+
if (deadlineMs === 0) return;
|
|
8893
9295
|
__privateSet(this, _joinDeadlineTimer, setTimeout(() => {
|
|
8894
9296
|
__privateSet(this, _joinDeadlineTimer, null);
|
|
8895
9297
|
if (__privateGet(this, _state) !== "joining") return;
|
|
8896
|
-
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
|
|
9298
|
+
__privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast", { retainFec: true });
|
|
8897
9299
|
__privateGet(this, _manager).callbacks?.onTakeoverTimeout?.(this.config.trackName);
|
|
8898
9300
|
}, deadlineMs));
|
|
8899
9301
|
};
|
|
@@ -8903,6 +9305,18 @@ clearJoinDeadline_fn = function() {
|
|
|
8903
9305
|
__privateSet(this, _joinDeadlineTimer, null);
|
|
8904
9306
|
}
|
|
8905
9307
|
};
|
|
9308
|
+
/** Fire fec-setup exactly once per FEC epoch. */
|
|
9309
|
+
armFec_fn = function() {
|
|
9310
|
+
if (__privateGet(this, _fecArmed)) return;
|
|
9311
|
+
__privateSet(this, _fecArmed, true);
|
|
9312
|
+
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
|
|
9313
|
+
};
|
|
9314
|
+
/** Fire fec-teardown for an armed decoder; no-op when none is armed. */
|
|
9315
|
+
disarmFec_fn = function() {
|
|
9316
|
+
if (!__privateGet(this, _fecArmed)) return;
|
|
9317
|
+
__privateSet(this, _fecArmed, false);
|
|
9318
|
+
__privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-teardown" }, this.config);
|
|
9319
|
+
};
|
|
8906
9320
|
/** Fire onSuppress(true) exactly once per suppression epoch. */
|
|
8907
9321
|
suppress_fn = function() {
|
|
8908
9322
|
if (__privateGet(this, _suppressFired)) return;
|
|
@@ -8915,18 +9329,14 @@ unsuppress_fn = function() {
|
|
|
8915
9329
|
__privateSet(this, _suppressFired, false);
|
|
8916
9330
|
__privateGet(this, _manager).callbacks?.onSuppress?.(this.config.trackName, false);
|
|
8917
9331
|
};
|
|
9332
|
+
var MAX_TAKEOVER_DEADLINE_MS = 2147483647;
|
|
9333
|
+
var _takeoverDeadlineMs, _SessionManager_instances, requireTakeoverDeadlineMs_fn;
|
|
8918
9334
|
var SessionManager = class {
|
|
8919
9335
|
constructor(poolOptions) {
|
|
9336
|
+
__privateAdd(this, _SessionManager_instances);
|
|
8920
9337
|
this.sessions = /* @__PURE__ */ new Map();
|
|
8921
9338
|
this.callbacks = null;
|
|
8922
|
-
|
|
8923
|
-
* Takeover deadline (BLO-9413/D9): max time a session may sit in `joining`
|
|
8924
|
-
* without a RAP before returning to `unicast` and firing onTakeoverTimeout.
|
|
8925
|
-
* MoQ is never suppressed during `joining`, so expiry is a silent abandon —
|
|
8926
|
-
* playback never glitches. Hosts tune per stream (e.g. GOP length); 0
|
|
8927
|
-
* disables.
|
|
8928
|
-
*/
|
|
8929
|
-
this.takeoverDeadlineMs = 5e3;
|
|
9339
|
+
__privateAdd(this, _takeoverDeadlineMs);
|
|
8930
9340
|
this.pool = new TransportPool(poolOptions);
|
|
8931
9341
|
this.pool.onSubscriptionFailed((sub) => {
|
|
8932
9342
|
for (const trackName of sub.consumers) {
|
|
@@ -8935,18 +9345,56 @@ var SessionManager = class {
|
|
|
8935
9345
|
}
|
|
8936
9346
|
});
|
|
8937
9347
|
}
|
|
9348
|
+
/**
|
|
9349
|
+
* Takeover deadline (BLO-9413/D9): the time a session may sit in `joining`
|
|
9350
|
+
* without a RAP before returning to `unicast` and firing onTakeoverTimeout.
|
|
9351
|
+
* MoQ is never suppressed during `joining`, so expiry abandons the takeover
|
|
9352
|
+
* without a playback glitch. The host derives it from the catalog (RAP
|
|
9353
|
+
* cadence from groupDurationTicks/timescale plus the signaled FEC delivery
|
|
9354
|
+
* window or reorder tolerance) and sets it before addTrack(). There is no
|
|
9355
|
+
* default: an unset value fails addTrack() closed, and reading the getter
|
|
9356
|
+
* before assignment throws the same way, so no host ever holds a
|
|
9357
|
+
* `number | undefined` it could coalesce back into a default.
|
|
9358
|
+
* `hasTakeoverDeadline` answers whether one was admitted. A declared 0
|
|
9359
|
+
* disables the deadline. The value is an integer number of milliseconds:
|
|
9360
|
+
* setTimeout truncates fractions, so a host that divides ticks by a
|
|
9361
|
+
* timescale must round before assigning.
|
|
9362
|
+
*
|
|
9363
|
+
* Assignment validates, so the diagnostic lands at the host's own call site.
|
|
9364
|
+
* That is what bounds the blast radius: `joining` is entered from
|
|
9365
|
+
* joinStarted(), from the transportFailed() failover branch -- itself
|
|
9366
|
+
* reachable from a setTimeout via hysteresisExpired(), where no caller could
|
|
9367
|
+
* catch a throw -- and from the activateMulticast()/activateAmt()/pool-failure
|
|
9368
|
+
* loops, where a throw on one session would abandon its siblings mid-loop
|
|
9369
|
+
* (the split-brain those loops exist to prevent). Refusing a bad value at
|
|
9370
|
+
* assignment means no host mutation can make an already-admitted session
|
|
9371
|
+
* throw out of a transport edge.
|
|
9372
|
+
*/
|
|
9373
|
+
get takeoverDeadlineMs() {
|
|
9374
|
+
return __privateMethod(this, _SessionManager_instances, requireTakeoverDeadlineMs_fn).call(this);
|
|
9375
|
+
}
|
|
9376
|
+
set takeoverDeadlineMs(value) {
|
|
9377
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_TAKEOVER_DEADLINE_MS) {
|
|
9378
|
+
throw new Error(
|
|
9379
|
+
`[SessionManager] takeoverDeadlineMs must be an integer from 0 to ${MAX_TAKEOVER_DEADLINE_MS} (0 disables it, larger values overflow setTimeout); got ${String(value)}`
|
|
9380
|
+
);
|
|
9381
|
+
}
|
|
9382
|
+
__privateSet(this, _takeoverDeadlineMs, value);
|
|
9383
|
+
}
|
|
9384
|
+
/** True once the host has admitted a deadline; a declared 0 counts. */
|
|
9385
|
+
get hasTakeoverDeadline() {
|
|
9386
|
+
return __privateGet(this, _takeoverDeadlineMs) !== void 0;
|
|
9387
|
+
}
|
|
8938
9388
|
/** Narrow to MMTP callbacks (onTrackEvent). Safe — optional chaining handles PIM-only consumers. */
|
|
8939
9389
|
get mmtp() {
|
|
8940
9390
|
return this.callbacks;
|
|
8941
9391
|
}
|
|
8942
|
-
// -----------------------------------------------------------------------
|
|
8943
|
-
// Track management
|
|
8944
|
-
// -----------------------------------------------------------------------
|
|
8945
9392
|
/** Add a track session from catalog config. */
|
|
8946
9393
|
addTrack(config) {
|
|
8947
9394
|
if (this.sessions.has(config.trackName)) {
|
|
8948
9395
|
throw new Error(`[SessionManager] Track "${config.trackName}" already exists`);
|
|
8949
9396
|
}
|
|
9397
|
+
__privateMethod(this, _SessionManager_instances, requireTakeoverDeadlineMs_fn).call(this);
|
|
8950
9398
|
const session = new TrackSession(config, this);
|
|
8951
9399
|
this.sessions.set(config.trackName, session);
|
|
8952
9400
|
return session;
|
|
@@ -8970,8 +9418,9 @@ var SessionManager = class {
|
|
|
8970
9418
|
* Prevents split-brain between video and audio.
|
|
8971
9419
|
*
|
|
8972
9420
|
* A takeover deadline can return a track from `joining` to `unicast` while
|
|
8973
|
-
* the multicast socket keeps delivering packets
|
|
8974
|
-
* later,
|
|
9421
|
+
* the multicast socket keeps delivering packets and the track keeps its FEC
|
|
9422
|
+
* decoder armed. If a valid RAP arrives later, that late-RAP edge suppresses
|
|
9423
|
+
* unicast and promotes multicast without a second fec-setup.
|
|
8975
9424
|
*/
|
|
8976
9425
|
activateMulticast() {
|
|
8977
9426
|
for (const [, session] of this.sessions) {
|
|
@@ -9003,13 +9452,17 @@ var SessionManager = class {
|
|
|
9003
9452
|
* and they must drain uninterrupted across transport-state events.
|
|
9004
9453
|
*
|
|
9005
9454
|
* Coherence invariant (BLO-9413): after this returns, every non-fallback
|
|
9006
|
-
* session is `unicast` (or `idle`)
|
|
9007
|
-
* any outstanding suppression regardless of the state
|
|
9008
|
-
* (`failing` included; see TrackSession.#suppressFired)
|
|
9455
|
+
* session is `unicast` (or `idle`), unsuppressed, and without a FEC decoder.
|
|
9456
|
+
* unsubscribe() unwinds any outstanding suppression regardless of the state
|
|
9457
|
+
* it tears down from (`failing` included; see TrackSession.#suppressFired),
|
|
9458
|
+
* and a session that a takeover timeout parked in `unicast` with its decoder
|
|
9459
|
+
* still armed is cycled too, because this transport edge is what releases
|
|
9460
|
+
* that decoder (unsubscribe fires fec-teardown).
|
|
9009
9461
|
*/
|
|
9010
9462
|
failbackAll() {
|
|
9011
9463
|
for (const [, session] of this.sessions) {
|
|
9012
|
-
|
|
9464
|
+
const parkedWithFec = session.state === "unicast" && session.isFecArmed;
|
|
9465
|
+
if (parkedWithFec || session.state !== "idle" && session.state !== "unicast" && session.state !== "unicast-fallback") {
|
|
9013
9466
|
session.unsubscribe();
|
|
9014
9467
|
session.subscribe();
|
|
9015
9468
|
}
|
|
@@ -9093,6 +9546,27 @@ var SessionManager = class {
|
|
|
9093
9546
|
this.callbacks = null;
|
|
9094
9547
|
}
|
|
9095
9548
|
};
|
|
9549
|
+
_takeoverDeadlineMs = new WeakMap();
|
|
9550
|
+
_SessionManager_instances = new WeakSet();
|
|
9551
|
+
// -----------------------------------------------------------------------
|
|
9552
|
+
// Track management
|
|
9553
|
+
// -----------------------------------------------------------------------
|
|
9554
|
+
/**
|
|
9555
|
+
* The admitted takeover deadline. Throws only when the host never set one,
|
|
9556
|
+
* every other invalid value being refused by the setter, and returns a
|
|
9557
|
+
* declared 0 as 0. The getter and addTrack() both route through it, so a
|
|
9558
|
+
* session cannot exist without an admitted deadline, which is what makes
|
|
9559
|
+
* the #transition() read unable to throw on an already-admitted session.
|
|
9560
|
+
*/
|
|
9561
|
+
requireTakeoverDeadlineMs_fn = function() {
|
|
9562
|
+
const value = __privateGet(this, _takeoverDeadlineMs);
|
|
9563
|
+
if (value === void 0) {
|
|
9564
|
+
throw new Error(
|
|
9565
|
+
"[SessionManager] takeoverDeadlineMs is unset; set a catalog-derived takeover deadline (0 disables it) before adding tracks"
|
|
9566
|
+
);
|
|
9567
|
+
}
|
|
9568
|
+
return value;
|
|
9569
|
+
};
|
|
9096
9570
|
|
|
9097
9571
|
// src/fec-manager.ts
|
|
9098
9572
|
var MIN_INTERLEAVE_DEPTH_MS = 33;
|
|
@@ -9111,7 +9585,7 @@ function newFecCounters() {
|
|
|
9111
9585
|
greenFillFrames: 0
|
|
9112
9586
|
};
|
|
9113
9587
|
}
|
|
9114
|
-
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;
|
|
9588
|
+
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;
|
|
9115
9589
|
var FECManager = class {
|
|
9116
9590
|
constructor() {
|
|
9117
9591
|
__privateAdd(this, _FECManager_instances);
|
|
@@ -9215,6 +9689,7 @@ var FECManager = class {
|
|
|
9215
9689
|
// validation harness — which caps headless warnings at 500 — fails even
|
|
9216
9690
|
// though the errors are duplicates of a single underlying event.
|
|
9217
9691
|
__privateAdd(this, _workerErrorCounts, /* @__PURE__ */ new Map());
|
|
9692
|
+
__privateAdd(this, _decoderUnavailableEvents, 0);
|
|
9218
9693
|
__privateAdd(this, _repairSymbolSizeMismatchCount, 0);
|
|
9219
9694
|
// Event callbacks — block lifecycle notifications for MmtpRouter wiring.
|
|
9220
9695
|
// NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
|
|
@@ -9223,6 +9698,17 @@ var FECManager = class {
|
|
|
9223
9698
|
this.onBlockStarted = null;
|
|
9224
9699
|
this.onBlockComplete = null;
|
|
9225
9700
|
this.onBlockFailed = null;
|
|
9701
|
+
/**
|
|
9702
|
+
* Fired once per stream when its Worker decoder stops being routable.
|
|
9703
|
+
* `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when
|
|
9704
|
+
* a symbol or reset found the registered client already disposed and
|
|
9705
|
+
* evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder
|
|
9706
|
+
* registered for the same stream keeps decoding its symbols, and 'none'
|
|
9707
|
+
* when nothing does. Only 'none' leaves the stream unprotected: the host
|
|
9708
|
+
* must then register a decoder again or leave the stream, because counting
|
|
9709
|
+
* its symbols as handled would hide the loss.
|
|
9710
|
+
*/
|
|
9711
|
+
this.onDecoderUnavailable = null;
|
|
9226
9712
|
// D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
|
|
9227
9713
|
// block arms a one-shot repair deadline after the derived K-symbol source
|
|
9228
9714
|
// horizon; a block still short of K when it fires requests a per-SBN MoQ
|
|
@@ -9256,6 +9742,7 @@ var FECManager = class {
|
|
|
9256
9742
|
startupRepairReadyPackets: __privateMethod(this, _FECManager_instances, packetIdsForStreamKeys_fn).call(this, __privateGet(this, _startupRepairReadyPackets)),
|
|
9257
9743
|
startupRepairFailOpenPackets: __privateMethod(this, _FECManager_instances, packetIdsForStreamKeys_fn).call(this, __privateGet(this, _startupRepairFailOpenPackets)),
|
|
9258
9744
|
pendingWorkerCleanups: __privateGet(this, _pendingWorkerCleanups).size,
|
|
9745
|
+
decoderUnavailableEvents: __privateGet(this, _decoderUnavailableEvents),
|
|
9259
9746
|
repairTrackedBlocks: __privateGet(this, _blockRepairRsIds).size,
|
|
9260
9747
|
lifecycleTrace: __privateGet(this, _lifecycleTrace).map((entry) => ({ ...entry }))
|
|
9261
9748
|
};
|
|
@@ -9390,8 +9877,12 @@ var FECManager = class {
|
|
|
9390
9877
|
__privateMethod(this, _FECManager_instances, discardFecDecoderClient_fn).call(this, client);
|
|
9391
9878
|
}
|
|
9392
9879
|
}
|
|
9393
|
-
for (const [key, client] of __privateGet(this, _workerClients)) {
|
|
9880
|
+
for (const [key, client] of [...__privateGet(this, _workerClients)]) {
|
|
9394
9881
|
if (!__privateMethod(this, _FECManager_instances, belongsToStream_fn).call(this, key, packetId, catalogTrackName)) continue;
|
|
9882
|
+
if (client.disposed === true) {
|
|
9883
|
+
__privateMethod(this, _FECManager_instances, evictDisposedWorker_fn).call(this, client);
|
|
9884
|
+
continue;
|
|
9885
|
+
}
|
|
9395
9886
|
client.reset();
|
|
9396
9887
|
clients.add(client);
|
|
9397
9888
|
}
|
|
@@ -9611,6 +10102,12 @@ var FECManager = class {
|
|
|
9611
10102
|
}
|
|
9612
10103
|
const streamKey = __privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName);
|
|
9613
10104
|
__privateMethod(this, _FECManager_instances, assertExclusiveDecoderClient_fn).call(this, streamKey, client, __privateGet(this, _workerClients), "worker");
|
|
10105
|
+
const incumbent = __privateGet(this, _workerClients).get(streamKey);
|
|
10106
|
+
if (incumbent !== void 0 && incumbent !== client) {
|
|
10107
|
+
throw new Error(
|
|
10108
|
+
`[FECManager] stream ${streamKey} already routes to a Worker; call clearFecWorker before registering another`
|
|
10109
|
+
);
|
|
10110
|
+
}
|
|
9614
10111
|
if (config) __privateMethod(this, _FECManager_instances, upsertTrackConfig_fn).call(this, packetId, config);
|
|
9615
10112
|
const workerK = __privateMethod(this, _FECManager_instances, resolveTrackConfig_fn).call(this, packetId).k;
|
|
9616
10113
|
__privateGet(this, _workerClients).set(streamKey, client);
|
|
@@ -9697,14 +10194,7 @@ var FECManager = class {
|
|
|
9697
10194
|
const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
|
|
9698
10195
|
return block.packetId === packetId && block.sbn === sbn && __privateGet(this, _pendingWorkerCleanupClients).get(key) === client;
|
|
9699
10196
|
});
|
|
9700
|
-
for (const key of cleanupKeys)
|
|
9701
|
-
__privateGet(this, _pendingWorkerCleanups).delete(key);
|
|
9702
|
-
__privateGet(this, _pendingWorkerCleanupClients).delete(key);
|
|
9703
|
-
if (__privateGet(this, _completedBlocks).has(key)) continue;
|
|
9704
|
-
const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
|
|
9705
|
-
__privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, "cleanup-ack", sbn, packetId, block.catalogTrackName);
|
|
9706
|
-
__privateMethod(this, _FECManager_instances, failBlock_fn).call(this, sbn, packetId, false, block.catalogTrackName);
|
|
9707
|
-
}
|
|
10197
|
+
for (const key of cleanupKeys) __privateMethod(this, _FECManager_instances, settleOwedCleanup_fn).call(this, key, "cleanup-ack");
|
|
9708
10198
|
});
|
|
9709
10199
|
client.onError((msg, code) => {
|
|
9710
10200
|
const requestedKind = code ?? msg;
|
|
@@ -9720,6 +10210,25 @@ var FECManager = class {
|
|
|
9720
10210
|
}
|
|
9721
10211
|
});
|
|
9722
10212
|
}
|
|
10213
|
+
/**
|
|
10214
|
+
* Remove a stream's Worker decoder from routing.
|
|
10215
|
+
*
|
|
10216
|
+
* Returns false when no Worker is registered under that exact stream key.
|
|
10217
|
+
* On success every cleanup acknowledgement still owed by the client is
|
|
10218
|
+
* resolved by failing its block now (a Worker that is going away cannot
|
|
10219
|
+
* post it), the client's block ownership is released, and
|
|
10220
|
+
* onDecoderUnavailable fires once with reason 'cleared' and the decoder
|
|
10221
|
+
* that remains for the stream ('sync' or 'none'). Call this before
|
|
10222
|
+
* disposing the client; a client disposed while still registered is
|
|
10223
|
+
* evicted on its next symbol or reset with reason 'disposed'.
|
|
10224
|
+
*/
|
|
10225
|
+
clearFecWorker(packetId, catalogTrackName) {
|
|
10226
|
+
const streamKey = __privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName);
|
|
10227
|
+
const client = __privateGet(this, _workerClients).get(streamKey);
|
|
10228
|
+
if (!client) return false;
|
|
10229
|
+
__privateMethod(this, _FECManager_instances, retireWorker_fn).call(this, streamKey, client, "cleared");
|
|
10230
|
+
return true;
|
|
10231
|
+
}
|
|
9723
10232
|
/** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
|
|
9724
10233
|
setFramerate(fps) {
|
|
9725
10234
|
if (__privateGet(this, _fpsDetected) || fps <= 0) return;
|
|
@@ -9770,7 +10279,7 @@ var FECManager = class {
|
|
|
9770
10279
|
const sbn = Math.floor(ssId / this.kForPacket(packetId));
|
|
9771
10280
|
const blockKey = __privateMethod(this, _FECManager_instances, blockKey_fn).call(this, sbn, packetId, catalogTrackName);
|
|
9772
10281
|
const syncBlockOwner = __privateGet(this, _syncDecoderBlockOwners).get(blockKey);
|
|
9773
|
-
const workerClient = __privateMethod(this, _FECManager_instances,
|
|
10282
|
+
const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
9774
10283
|
if (workerClient && !syncBlockOwner) {
|
|
9775
10284
|
if (!__privateMethod(this, _FECManager_instances, claimDecoderBlock_fn).call(this, workerClient, sbn, catalogTrackName)) return null;
|
|
9776
10285
|
const buf = fecData.buffer.slice(
|
|
@@ -9847,7 +10356,7 @@ var FECManager = class {
|
|
|
9847
10356
|
if (__privateGet(this, _repairRequestedBlocks).has(blockKey)) {
|
|
9848
10357
|
__privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, "repair-arrived", sbn, packetId, catalogTrackName, rsId);
|
|
9849
10358
|
}
|
|
9850
|
-
const workerClient = __privateMethod(this, _FECManager_instances,
|
|
10359
|
+
const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
9851
10360
|
if (workerClient && !syncBlockOwner) {
|
|
9852
10361
|
if (!__privateMethod(this, _FECManager_instances, claimDecoderBlock_fn).call(this, workerClient, sbn, catalogTrackName)) return null;
|
|
9853
10362
|
const buf = fecData.buffer.slice(
|
|
@@ -10132,6 +10641,7 @@ _fpsDetected = new WeakMap();
|
|
|
10132
10641
|
_fecFps = new WeakMap();
|
|
10133
10642
|
_fecInterleaveDepthMs = new WeakMap();
|
|
10134
10643
|
_workerErrorCounts = new WeakMap();
|
|
10644
|
+
_decoderUnavailableEvents = new WeakMap();
|
|
10135
10645
|
_repairSymbolSizeMismatchCount = new WeakMap();
|
|
10136
10646
|
_FECManager_instances = new WeakSet();
|
|
10137
10647
|
streamKey_fn = function(packetId, catalogTrackName) {
|
|
@@ -10151,6 +10661,14 @@ belongsToStream_fn = function(key, packetId, catalogTrackName) {
|
|
|
10151
10661
|
packetIdsForStreamKeys_fn = function(keys) {
|
|
10152
10662
|
return [...new Set([...keys].map((key) => Number(key.split(":", 1)[0])))];
|
|
10153
10663
|
};
|
|
10664
|
+
parseStreamKey_fn = function(key) {
|
|
10665
|
+
const separator = key.indexOf(":");
|
|
10666
|
+
if (separator < 0) return { packetId: Number(key) || 0 };
|
|
10667
|
+
return {
|
|
10668
|
+
packetId: Number(key.slice(0, separator)) || 0,
|
|
10669
|
+
catalogTrackName: decodeURIComponent(key.slice(separator + 1))
|
|
10670
|
+
};
|
|
10671
|
+
};
|
|
10154
10672
|
blockKey_fn = function(sbn, packetId, catalogTrackName) {
|
|
10155
10673
|
const base = `${packetId}:${sbn}`;
|
|
10156
10674
|
return catalogTrackName === void 0 ? base : `${base}:${encodeURIComponent(catalogTrackName)}`;
|
|
@@ -10170,6 +10688,18 @@ decoderForPacket_fn = function(packetId, catalogTrackName) {
|
|
|
10170
10688
|
workerForPacket_fn = function(packetId, catalogTrackName) {
|
|
10171
10689
|
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));
|
|
10172
10690
|
};
|
|
10691
|
+
/**
|
|
10692
|
+
* The stream's Worker client for routing, or undefined after evicting a
|
|
10693
|
+
* registered client that was already disposed. A disposed client throws on
|
|
10694
|
+
* every command, so it leaves routing here before a symbol reaches it and
|
|
10695
|
+
* the stream reports its decoder unavailable instead.
|
|
10696
|
+
*/
|
|
10697
|
+
routableWorkerForPacket_fn = function(packetId, catalogTrackName) {
|
|
10698
|
+
const client = __privateMethod(this, _FECManager_instances, workerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
10699
|
+
if (client === void 0 || client.disposed !== true) return client;
|
|
10700
|
+
__privateMethod(this, _FECManager_instances, evictDisposedWorker_fn).call(this, client);
|
|
10701
|
+
return void 0;
|
|
10702
|
+
};
|
|
10173
10703
|
claimDecoderBlock_fn = function(client, sbn, catalogTrackName) {
|
|
10174
10704
|
let blocks = __privateGet(this, _decoderBlockOwners).get(client);
|
|
10175
10705
|
if (!blocks) {
|
|
@@ -10337,7 +10867,7 @@ armBlockTimeout_fn = function(sbn, packetId, catalogTrackName, delayMs) {
|
|
|
10337
10867
|
__privateMethod(this, _FECManager_instances, failBlock_fn).call(this, sbn, packetId, false, catalogTrackName);
|
|
10338
10868
|
return;
|
|
10339
10869
|
}
|
|
10340
|
-
const workerClient = __privateMethod(this, _FECManager_instances,
|
|
10870
|
+
const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
|
|
10341
10871
|
if (workerClient) {
|
|
10342
10872
|
__privateGet(this, _pendingWorkerCleanups).add(key);
|
|
10343
10873
|
__privateGet(this, _pendingWorkerCleanupClients).set(key, workerClient);
|
|
@@ -10420,6 +10950,59 @@ markStartupPartial_fn = function(blockKey) {
|
|
|
10420
10950
|
));
|
|
10421
10951
|
}
|
|
10422
10952
|
};
|
|
10953
|
+
/**
|
|
10954
|
+
* Take a Worker client out of routing under one stream key, fail the blocks
|
|
10955
|
+
* whose completion it still owed, and report the stream's decoder
|
|
10956
|
+
* unavailable together with whatever still decodes the stream.
|
|
10957
|
+
*/
|
|
10958
|
+
retireWorker_fn = function(streamKey, client, reason) {
|
|
10959
|
+
__privateGet(this, _workerClients).delete(streamKey);
|
|
10960
|
+
__privateMethod(this, _FECManager_instances, releaseWorkerClient_fn).call(this, client);
|
|
10961
|
+
__privateWrapper(this, _decoderUnavailableEvents)._++;
|
|
10962
|
+
const stream = __privateMethod(this, _FECManager_instances, parseStreamKey_fn).call(this, streamKey);
|
|
10963
|
+
const fallbackDecoder = __privateMethod(this, _FECManager_instances, decoderForPacket_fn).call(this, stream.packetId, stream.catalogTrackName) ? "sync" : "none";
|
|
10964
|
+
this.onDecoderUnavailable?.({
|
|
10965
|
+
packetId: stream.packetId,
|
|
10966
|
+
...stream.catalogTrackName === void 0 ? {} : { catalogTrackName: stream.catalogTrackName },
|
|
10967
|
+
reason,
|
|
10968
|
+
fallbackDecoder
|
|
10969
|
+
});
|
|
10970
|
+
};
|
|
10971
|
+
/**
|
|
10972
|
+
* Fail every block whose Worker cleanup acknowledgement is owed by `client`
|
|
10973
|
+
* and drop its block ownership, so no block waits on a message the Worker
|
|
10974
|
+
* will never post.
|
|
10975
|
+
*/
|
|
10976
|
+
releaseWorkerClient_fn = function(client) {
|
|
10977
|
+
const owed = [...__privateGet(this, _pendingWorkerCleanupClients)].filter(([, owner]) => owner === client).map(([key]) => key);
|
|
10978
|
+
for (const key of owed) __privateMethod(this, _FECManager_instances, settleOwedCleanup_fn).call(this, key, "decoder-unavailable");
|
|
10979
|
+
__privateGet(this, _decoderBlockOwners).delete(client);
|
|
10980
|
+
};
|
|
10981
|
+
/**
|
|
10982
|
+
* Resolve one owed Worker cleanup: drop the pending entry and fail the block
|
|
10983
|
+
* unless it already completed. Shared by the cleanup acknowledgement and by
|
|
10984
|
+
* Worker retirement, so the two paths cannot drift in their bookkeeping.
|
|
10985
|
+
*/
|
|
10986
|
+
settleOwedCleanup_fn = function(key, event) {
|
|
10987
|
+
__privateGet(this, _pendingWorkerCleanups).delete(key);
|
|
10988
|
+
__privateGet(this, _pendingWorkerCleanupClients).delete(key);
|
|
10989
|
+
if (__privateGet(this, _completedBlocks).has(key)) return;
|
|
10990
|
+
const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
|
|
10991
|
+
__privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, event, block.sbn, block.packetId, block.catalogTrackName);
|
|
10992
|
+
__privateMethod(this, _FECManager_instances, failBlock_fn).call(this, block.sbn, block.packetId, false, block.catalogTrackName);
|
|
10993
|
+
};
|
|
10994
|
+
/** Evict a disposed client under every stream key that still maps to it. */
|
|
10995
|
+
evictDisposedWorker_fn = function(client) {
|
|
10996
|
+
for (const [key, registered] of [...__privateGet(this, _workerClients)]) {
|
|
10997
|
+
if (registered !== client) continue;
|
|
10998
|
+
const stream = __privateMethod(this, _FECManager_instances, parseStreamKey_fn).call(this, key);
|
|
10999
|
+
const track = stream.catalogTrackName === void 0 ? "" : ` track=${stream.catalogTrackName}`;
|
|
11000
|
+
console.warn(
|
|
11001
|
+
`[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`
|
|
11002
|
+
);
|
|
11003
|
+
__privateMethod(this, _FECManager_instances, retireWorker_fn).call(this, key, client, "disposed");
|
|
11004
|
+
}
|
|
11005
|
+
};
|
|
10423
11006
|
routeSyncCleanup_fn = function(decoder, sbn, packetId) {
|
|
10424
11007
|
let framed;
|
|
10425
11008
|
try {
|
|
@@ -12038,7 +12621,7 @@ buildFrameMeta_fn = function(assembled, packetId, seq, timing, recovered = false
|
|
|
12038
12621
|
}
|
|
12039
12622
|
return meta;
|
|
12040
12623
|
};
|
|
12041
|
-
sequencerFor_fn = function(packetId,
|
|
12624
|
+
sequencerFor_fn = function(packetId, _trackType2, catalogTrackName) {
|
|
12042
12625
|
const windowSize = __privateGet(this, _reassemblyPolicy) ? mmtpFrameReorderWindowFrames(__privateGet(this, _reassemblyPolicy), __privateGet(this, _reorderWindow)) : __privateGet(this, _reorderWindow);
|
|
12043
12626
|
if (windowSize <= 0) return null;
|
|
12044
12627
|
const streamKey = __privateMethod(this, _MmtpContainerParser_instances, streamKey_fn2).call(this, packetId, catalogTrackName);
|
|
@@ -14332,52 +14915,1373 @@ var ReedSolomonCodec = class {
|
|
|
14332
14915
|
}
|
|
14333
14916
|
};
|
|
14334
14917
|
|
|
14335
|
-
//
|
|
14336
|
-
var
|
|
14337
|
-
function
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
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;
|
|
14349
|
-
var _MulticastTransport = class _MulticastTransport {
|
|
14350
|
-
constructor(onPacket) {
|
|
14351
|
-
__privateAdd(this, _MulticastTransport_instances);
|
|
14352
|
-
__privateAdd(this, _onPacket);
|
|
14353
|
-
__privateAdd(this, _subscription2, null);
|
|
14354
|
-
__privateAdd(this, _bytesListener, null);
|
|
14355
|
-
__privateAdd(this, _transportListener, null);
|
|
14356
|
-
__privateAdd(this, _resolvedTransport);
|
|
14357
|
-
__privateAdd(this, _videoConfirmed, false);
|
|
14358
|
-
__privateAdd(this, _audioConfirmed, false);
|
|
14359
|
-
__privateAdd(this, _watchdog, null);
|
|
14360
|
-
__privateAdd(this, _failbackWindowMs, null);
|
|
14361
|
-
__privateAdd(this, _onTakeover, null);
|
|
14362
|
-
__privateAdd(this, _onFailback, null);
|
|
14363
|
-
__privateAdd(this, _disposed4, false);
|
|
14364
|
-
__privateAdd(this, _stats3, {
|
|
14365
|
-
packetsReceived: 0,
|
|
14366
|
-
bytesReceived: 0,
|
|
14367
|
-
packetsDroppedUnregisteredPid: 0,
|
|
14368
|
-
packetsDroppedUnparseable: 0
|
|
14369
|
-
});
|
|
14370
|
-
__privateAdd(this, _droppedPidCounts, /* @__PURE__ */ new Map());
|
|
14371
|
-
__privateAdd(this, _packetIds, null);
|
|
14372
|
-
__privateAdd(this, _joinGeneration, 0);
|
|
14373
|
-
__privateSet(this, _onPacket, onPacket);
|
|
14374
|
-
}
|
|
14375
|
-
static isAvailable() {
|
|
14376
|
-
return hasMulticastGateway();
|
|
14918
|
+
// node_modules/@blockcast/mmt-transport/dist/types.js
|
|
14919
|
+
var MoqFecAlgorithm;
|
|
14920
|
+
(function(MoqFecAlgorithm2) {
|
|
14921
|
+
MoqFecAlgorithm2[MoqFecAlgorithm2["None"] = 0] = "None";
|
|
14922
|
+
MoqFecAlgorithm2[MoqFecAlgorithm2["Xor"] = 1] = "Xor";
|
|
14923
|
+
MoqFecAlgorithm2[MoqFecAlgorithm2["RaptorQ"] = 2] = "RaptorQ";
|
|
14924
|
+
})(MoqFecAlgorithm || (MoqFecAlgorithm = {}));
|
|
14925
|
+
|
|
14926
|
+
// node_modules/@blockcast/mmt-transport/dist/driad-discovery.js
|
|
14927
|
+
var DRIADDiscovery = class {
|
|
14928
|
+
constructor() {
|
|
14929
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
14930
|
+
__publicField(this, "cacheExpiry", 3e5);
|
|
14377
14931
|
}
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14932
|
+
// 5 minutes
|
|
14933
|
+
/**
|
|
14934
|
+
* Discover AMT relays for a multicast source address (RFC 8777)
|
|
14935
|
+
*
|
|
14936
|
+
* @param source - The multicast source IP address (NOT the group!)
|
|
14937
|
+
* @returns Array of discovered AMT relays that can tunnel traffic from this source
|
|
14938
|
+
*/
|
|
14939
|
+
async discoverRelays(source) {
|
|
14940
|
+
const cached = this.cache.get(source);
|
|
14941
|
+
if (cached) {
|
|
14942
|
+
return cached;
|
|
14943
|
+
}
|
|
14944
|
+
try {
|
|
14945
|
+
const reversedSource = source.split(".").reverse().join(".");
|
|
14946
|
+
const queryDomain = `${reversedSource}.amt.in-addr.arpa`;
|
|
14947
|
+
console.log(`[DRIAD] Discovering relays for source ${source} via ${queryDomain}`);
|
|
14948
|
+
const relays = await this.queryDRIADProxy(source);
|
|
14949
|
+
if (relays.length > 0) {
|
|
14950
|
+
this.cache.set(source, relays);
|
|
14951
|
+
setTimeout(() => this.cache.delete(source), this.cacheExpiry);
|
|
14952
|
+
}
|
|
14953
|
+
return relays;
|
|
14954
|
+
} catch (error) {
|
|
14955
|
+
console.warn(`[DRIAD] Discovery failed for source ${source}:`, error);
|
|
14956
|
+
return [];
|
|
14957
|
+
}
|
|
14958
|
+
}
|
|
14959
|
+
/**
|
|
14960
|
+
* Query DRIAD proxy service (browser-compatible)
|
|
14961
|
+
*/
|
|
14962
|
+
async queryDRIADProxy(source) {
|
|
14963
|
+
const proxyEndpoints = [
|
|
14964
|
+
"https://driad.amt.net/api/v1/relays",
|
|
14965
|
+
"https://multicast-gateway.cloudflare.com/driad"
|
|
14966
|
+
];
|
|
14967
|
+
for (const endpoint of proxyEndpoints) {
|
|
14968
|
+
try {
|
|
14969
|
+
const response = await fetch(`${endpoint}?source=${encodeURIComponent(source)}`, {
|
|
14970
|
+
method: "GET",
|
|
14971
|
+
headers: { Accept: "application/json" },
|
|
14972
|
+
signal: AbortSignal.timeout(5e3)
|
|
14973
|
+
});
|
|
14974
|
+
if (response.ok) {
|
|
14975
|
+
const data = await response.json();
|
|
14976
|
+
return data.relays || [];
|
|
14977
|
+
}
|
|
14978
|
+
} catch {
|
|
14979
|
+
}
|
|
14980
|
+
}
|
|
14981
|
+
const dohRelay = await this.queryDNSOverHTTPS(source);
|
|
14982
|
+
if (dohRelay) {
|
|
14983
|
+
return [dohRelay];
|
|
14984
|
+
}
|
|
14985
|
+
return [];
|
|
14986
|
+
}
|
|
14987
|
+
/**
|
|
14988
|
+
* Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)
|
|
14989
|
+
*/
|
|
14990
|
+
async queryDNSOverHTTPS(source) {
|
|
14991
|
+
try {
|
|
14992
|
+
const reversedSource = source.split(".").reverse().join(".");
|
|
14993
|
+
const queryName = `${reversedSource}.amt.in-addr.arpa`;
|
|
14994
|
+
const dohUrl = `https://cloudflare-dns.com/dns-query?name=${queryName}&type=A`;
|
|
14995
|
+
const response = await fetch(dohUrl, {
|
|
14996
|
+
headers: { Accept: "application/dns-json" },
|
|
14997
|
+
signal: AbortSignal.timeout(3e3)
|
|
14998
|
+
});
|
|
14999
|
+
if (!response.ok) {
|
|
15000
|
+
return null;
|
|
15001
|
+
}
|
|
15002
|
+
const result = await response.json();
|
|
15003
|
+
if (result.Answer && result.Answer.length > 0) {
|
|
15004
|
+
return {
|
|
15005
|
+
host: result.Answer[0].data,
|
|
15006
|
+
port: 2268,
|
|
15007
|
+
priority: 10,
|
|
15008
|
+
weight: 100,
|
|
15009
|
+
asn: 0
|
|
15010
|
+
};
|
|
15011
|
+
}
|
|
15012
|
+
return null;
|
|
15013
|
+
} catch (error) {
|
|
15014
|
+
console.warn("[DRIAD] DNS-over-HTTPS query failed:", error);
|
|
15015
|
+
return null;
|
|
15016
|
+
}
|
|
15017
|
+
}
|
|
15018
|
+
/**
|
|
15019
|
+
* Select best relay based on RTT measurements
|
|
15020
|
+
*/
|
|
15021
|
+
async selectBestRelay(relays) {
|
|
15022
|
+
if (relays.length === 0)
|
|
15023
|
+
return null;
|
|
15024
|
+
const measured = await Promise.all(relays.map(async (relay) => {
|
|
15025
|
+
const rtt = await this.measureRTT(relay.host, relay.port);
|
|
15026
|
+
return { ...relay, rtt };
|
|
15027
|
+
}));
|
|
15028
|
+
measured.sort((a, b) => {
|
|
15029
|
+
if (a.priority !== b.priority)
|
|
15030
|
+
return a.priority - b.priority;
|
|
15031
|
+
return (a.rtt || Infinity) - (b.rtt || Infinity);
|
|
15032
|
+
});
|
|
15033
|
+
console.log(`[DRIAD] Selected relay: ${measured[0].host} (RTT: ${measured[0].rtt}ms)`);
|
|
15034
|
+
return measured[0];
|
|
15035
|
+
}
|
|
15036
|
+
async measureRTT(host, _port) {
|
|
15037
|
+
const start = performance.now();
|
|
15038
|
+
try {
|
|
15039
|
+
await fetch(`https://${host}/health`, {
|
|
15040
|
+
method: "HEAD",
|
|
15041
|
+
signal: AbortSignal.timeout(2e3),
|
|
15042
|
+
mode: "no-cors"
|
|
15043
|
+
});
|
|
15044
|
+
return performance.now() - start;
|
|
15045
|
+
} catch {
|
|
15046
|
+
return Infinity;
|
|
15047
|
+
}
|
|
15048
|
+
}
|
|
15049
|
+
/**
|
|
15050
|
+
* Clear the relay cache
|
|
15051
|
+
*/
|
|
15052
|
+
clearCache() {
|
|
15053
|
+
this.cache.clear();
|
|
15054
|
+
}
|
|
15055
|
+
/**
|
|
15056
|
+
* Get cached relays for a source address
|
|
15057
|
+
*/
|
|
15058
|
+
getCached(source) {
|
|
15059
|
+
return this.cache.get(source);
|
|
15060
|
+
}
|
|
15061
|
+
};
|
|
15062
|
+
var driadDiscovery = new DRIADDiscovery();
|
|
15063
|
+
|
|
15064
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-tier.js
|
|
15065
|
+
function endpointKey(endpoint) {
|
|
15066
|
+
return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
|
|
15067
|
+
}
|
|
15068
|
+
function selectMulticastTier(endpoints, estimate) {
|
|
15069
|
+
if (endpoints.length <= 1)
|
|
15070
|
+
return endpoints[0];
|
|
15071
|
+
const ranked = endpoints.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
|
|
15072
|
+
if (ranked.length < 2)
|
|
15073
|
+
return endpoints[0];
|
|
15074
|
+
if (estimate === void 0)
|
|
15075
|
+
return ranked[0];
|
|
15076
|
+
const budget = estimate * 0.8;
|
|
15077
|
+
let selected;
|
|
15078
|
+
for (const endpoint of ranked) {
|
|
15079
|
+
if (endpoint.bandwidth <= budget)
|
|
15080
|
+
selected = endpoint;
|
|
15081
|
+
}
|
|
15082
|
+
return selected ?? ranked[0];
|
|
15083
|
+
}
|
|
15084
|
+
|
|
15085
|
+
// node_modules/@blockcast/mmt-transport/dist/abr-controller.js
|
|
15086
|
+
var QUALITY_LEVELS = [
|
|
15087
|
+
{ name: "240p", bitrate: 3e5, width: 426, height: 240, fps: 15 },
|
|
15088
|
+
{ name: "360p", bitrate: 5e5, width: 640, height: 360, fps: 24 },
|
|
15089
|
+
{ name: "480p", bitrate: 1e6, width: 854, height: 480, fps: 30 },
|
|
15090
|
+
{ name: "720p", bitrate: 25e5, width: 1280, height: 720, fps: 30 },
|
|
15091
|
+
{ name: "1080p", bitrate: 5e6, width: 1920, height: 1080, fps: 30 },
|
|
15092
|
+
{ name: "1440p", bitrate: 8e6, width: 2560, height: 1440, fps: 30 },
|
|
15093
|
+
{ name: "4K", bitrate: 15e6, width: 3840, height: 2160, fps: 30 }
|
|
15094
|
+
];
|
|
15095
|
+
var AdaptiveBitrateController = class {
|
|
15096
|
+
constructor(initialQuality) {
|
|
15097
|
+
__publicField(this, "currentQualityIndex");
|
|
15098
|
+
__publicField(this, "history", []);
|
|
15099
|
+
__publicField(this, "historyWindow", 1e4);
|
|
15100
|
+
// 10 second window
|
|
15101
|
+
__publicField(this, "upgradeThreshold", 0.8);
|
|
15102
|
+
// 80% of target bitrate sustained
|
|
15103
|
+
__publicField(this, "downgradeThreshold", 0.5);
|
|
15104
|
+
// 50% of target bitrate triggers downgrade
|
|
15105
|
+
__publicField(this, "stabilityPeriod", 5e3);
|
|
15106
|
+
// Wait 5s before upgrading
|
|
15107
|
+
__publicField(this, "lastQualityChange", 0);
|
|
15108
|
+
__publicField(this, "onQualityChange");
|
|
15109
|
+
// FEC stats for effective loss rate calculation
|
|
15110
|
+
__publicField(this, "fecStats", {
|
|
15111
|
+
rawLossRate: 0,
|
|
15112
|
+
effectiveLossRate: 0,
|
|
15113
|
+
recoveryRate: 1,
|
|
15114
|
+
fecEnabled: false
|
|
15115
|
+
});
|
|
15116
|
+
this.currentQualityIndex = initialQuality ? QUALITY_LEVELS.findIndex((q) => q.name === initialQuality.name) : 0;
|
|
15117
|
+
if (this.currentQualityIndex < 0)
|
|
15118
|
+
this.currentQualityIndex = 0;
|
|
15119
|
+
}
|
|
15120
|
+
setCallback(callback) {
|
|
15121
|
+
this.onQualityChange = callback;
|
|
15122
|
+
}
|
|
15123
|
+
getCurrentQuality() {
|
|
15124
|
+
return QUALITY_LEVELS[this.currentQualityIndex];
|
|
15125
|
+
}
|
|
15126
|
+
/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */
|
|
15127
|
+
selectMulticastTier(endpoints, estimate = this.getMetrics().measuredBitrate || void 0) {
|
|
15128
|
+
return selectMulticastTier(endpoints, estimate);
|
|
15129
|
+
}
|
|
15130
|
+
/**
|
|
15131
|
+
* Record network sample for bitrate adaptation
|
|
15132
|
+
*
|
|
15133
|
+
* @param bytesReceived - Bytes received in this sample
|
|
15134
|
+
* @param packetsLost - Packets lost (raw, before FEC recovery)
|
|
15135
|
+
*/
|
|
15136
|
+
recordSample(bytesReceived, packetsLost) {
|
|
15137
|
+
const now = Date.now();
|
|
15138
|
+
this.history.push({ timestamp: now, bytesReceived, packetsLost });
|
|
15139
|
+
this.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);
|
|
15140
|
+
const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
|
|
15141
|
+
const currentQuality = this.getCurrentQuality();
|
|
15142
|
+
const effectiveLossRate = this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate;
|
|
15143
|
+
if (now - this.lastQualityChange < this.stabilityPeriod) {
|
|
15144
|
+
return;
|
|
15145
|
+
}
|
|
15146
|
+
if (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {
|
|
15147
|
+
this.downgrade();
|
|
15148
|
+
return;
|
|
15149
|
+
}
|
|
15150
|
+
if (measuredBitrate > currentQuality.bitrate * this.upgradeThreshold && effectiveLossRate < 0.01 && this.currentQualityIndex < QUALITY_LEVELS.length - 1) {
|
|
15151
|
+
this.upgrade();
|
|
15152
|
+
}
|
|
15153
|
+
}
|
|
15154
|
+
/**
|
|
15155
|
+
* Update FEC statistics for ABR decisions
|
|
15156
|
+
*
|
|
15157
|
+
* Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate
|
|
15158
|
+
* (after FEC recovery) rather than raw transport loss rate.
|
|
15159
|
+
*
|
|
15160
|
+
* @param stats - FEC statistics from the FEC decoder
|
|
15161
|
+
*/
|
|
15162
|
+
setFecStats(stats) {
|
|
15163
|
+
this.fecStats = stats;
|
|
15164
|
+
}
|
|
15165
|
+
/**
|
|
15166
|
+
* Get current FEC statistics
|
|
15167
|
+
*/
|
|
15168
|
+
getFecStats() {
|
|
15169
|
+
return { ...this.fecStats };
|
|
15170
|
+
}
|
|
15171
|
+
calculateMetrics() {
|
|
15172
|
+
if (this.history.length < 2) {
|
|
15173
|
+
return { measuredBitrate: 0, lossRate: 0 };
|
|
15174
|
+
}
|
|
15175
|
+
const totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);
|
|
15176
|
+
const totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);
|
|
15177
|
+
const duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1e3;
|
|
15178
|
+
const measuredBitrate = duration > 0 ? totalBytes * 8 / duration : 0;
|
|
15179
|
+
const lossRate = totalLost / (this.history.length + totalLost);
|
|
15180
|
+
return { measuredBitrate, lossRate };
|
|
15181
|
+
}
|
|
15182
|
+
upgrade() {
|
|
15183
|
+
if (this.currentQualityIndex >= QUALITY_LEVELS.length - 1)
|
|
15184
|
+
return;
|
|
15185
|
+
this.currentQualityIndex++;
|
|
15186
|
+
this.lastQualityChange = Date.now();
|
|
15187
|
+
const newQuality = this.getCurrentQuality();
|
|
15188
|
+
console.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
|
|
15189
|
+
this.onQualityChange?.(newQuality);
|
|
15190
|
+
}
|
|
15191
|
+
downgrade() {
|
|
15192
|
+
if (this.currentQualityIndex <= 0)
|
|
15193
|
+
return;
|
|
15194
|
+
this.currentQualityIndex--;
|
|
15195
|
+
this.lastQualityChange = Date.now();
|
|
15196
|
+
const newQuality = this.getCurrentQuality();
|
|
15197
|
+
console.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
|
|
15198
|
+
this.onQualityChange?.(newQuality);
|
|
15199
|
+
}
|
|
15200
|
+
/**
|
|
15201
|
+
* Force quality level (for testing)
|
|
15202
|
+
*/
|
|
15203
|
+
setQuality(name) {
|
|
15204
|
+
const index = QUALITY_LEVELS.findIndex((q) => q.name === name);
|
|
15205
|
+
if (index >= 0) {
|
|
15206
|
+
this.currentQualityIndex = index;
|
|
15207
|
+
this.lastQualityChange = Date.now();
|
|
15208
|
+
this.onQualityChange?.(this.getCurrentQuality());
|
|
15209
|
+
}
|
|
15210
|
+
}
|
|
15211
|
+
/**
|
|
15212
|
+
* Get current metrics including both raw and effective loss rates
|
|
15213
|
+
*/
|
|
15214
|
+
getMetrics() {
|
|
15215
|
+
const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
|
|
15216
|
+
return {
|
|
15217
|
+
measuredBitrate,
|
|
15218
|
+
rawLossRate,
|
|
15219
|
+
effectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,
|
|
15220
|
+
fecEnabled: this.fecStats.fecEnabled,
|
|
15221
|
+
sampleCount: this.history.length
|
|
15222
|
+
};
|
|
15223
|
+
}
|
|
15224
|
+
/**
|
|
15225
|
+
* Reset history and start fresh
|
|
15226
|
+
*/
|
|
15227
|
+
reset() {
|
|
15228
|
+
this.history = [];
|
|
15229
|
+
this.lastQualityChange = 0;
|
|
15230
|
+
}
|
|
15231
|
+
};
|
|
15232
|
+
|
|
15233
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-orchestrator.js
|
|
15234
|
+
var DEFAULT_DWELL_MS = 3e3;
|
|
15235
|
+
var DEFAULT_JOIN_DEADLINE_MS = 5e3;
|
|
15236
|
+
var _state2, _current, _target, _estimate, _favored, _joinStartedAt, _lastCommittedTimestamp, _dwellMs, _joinDeadlineMs, _MulticastOrchestrator_instances, now_fn;
|
|
15237
|
+
var MulticastOrchestrator = class {
|
|
15238
|
+
constructor(options) {
|
|
15239
|
+
__privateAdd(this, _MulticastOrchestrator_instances);
|
|
15240
|
+
__publicField(this, "options");
|
|
15241
|
+
__privateAdd(this, _state2, "stable");
|
|
15242
|
+
__privateAdd(this, _current);
|
|
15243
|
+
__privateAdd(this, _target);
|
|
15244
|
+
__privateAdd(this, _estimate);
|
|
15245
|
+
__privateAdd(this, _favored);
|
|
15246
|
+
__privateAdd(this, _joinStartedAt, 0);
|
|
15247
|
+
__privateAdd(this, _lastCommittedTimestamp, Number.NEGATIVE_INFINITY);
|
|
15248
|
+
__privateAdd(this, _dwellMs);
|
|
15249
|
+
__privateAdd(this, _joinDeadlineMs);
|
|
15250
|
+
this.options = options;
|
|
15251
|
+
__privateSet(this, _dwellMs, options.dwellMs ?? DEFAULT_DWELL_MS);
|
|
15252
|
+
__privateSet(this, _joinDeadlineMs, options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS);
|
|
15253
|
+
if (!Number.isFinite(__privateGet(this, _dwellMs)) || !Number.isFinite(__privateGet(this, _joinDeadlineMs)) || __privateGet(this, _dwellMs) <= 0 || __privateGet(this, _joinDeadlineMs) <= 0) {
|
|
15254
|
+
throw new Error("Multicast dwell and join deadline must be positive");
|
|
15255
|
+
}
|
|
15256
|
+
}
|
|
15257
|
+
get state() {
|
|
15258
|
+
return __privateGet(this, _state2);
|
|
15259
|
+
}
|
|
15260
|
+
get current() {
|
|
15261
|
+
return __privateGet(this, _current);
|
|
15262
|
+
}
|
|
15263
|
+
get target() {
|
|
15264
|
+
return __privateGet(this, _target);
|
|
15265
|
+
}
|
|
15266
|
+
get estimate() {
|
|
15267
|
+
return __privateGet(this, _estimate);
|
|
15268
|
+
}
|
|
15269
|
+
start() {
|
|
15270
|
+
__privateGet(this, _current) ?? __privateSet(this, _current, this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, void 0));
|
|
15271
|
+
return __privateGet(this, _current);
|
|
15272
|
+
}
|
|
15273
|
+
onGoodput(sample) {
|
|
15274
|
+
const bitrate = sample.bitrate;
|
|
15275
|
+
if (!__privateGet(this, _current) || this.options.adaptive === false)
|
|
15276
|
+
return void 0;
|
|
15277
|
+
if (__privateGet(this, _state2) === "joining")
|
|
15278
|
+
return this.poll();
|
|
15279
|
+
if (bitrate === void 0 || !Number.isFinite(bitrate))
|
|
15280
|
+
return void 0;
|
|
15281
|
+
__privateSet(this, _estimate, bitrate);
|
|
15282
|
+
const desired = selectMulticastTier(this.options.endpoints, bitrate);
|
|
15283
|
+
if (!desired)
|
|
15284
|
+
return void 0;
|
|
15285
|
+
const desiredKey = endpointKey(desired);
|
|
15286
|
+
if (desiredKey === endpointKey(__privateGet(this, _current))) {
|
|
15287
|
+
if (__privateGet(this, _favored)?.source === "goodput")
|
|
15288
|
+
__privateSet(this, _favored, void 0);
|
|
15289
|
+
return void 0;
|
|
15290
|
+
}
|
|
15291
|
+
if (typeof desired.bandwidth !== "number" || typeof __privateGet(this, _current).bandwidth !== "number" || desired.bandwidth >= __privateGet(this, _current).bandwidth) {
|
|
15292
|
+
if (__privateGet(this, _favored)?.source === "goodput")
|
|
15293
|
+
__privateSet(this, _favored, void 0);
|
|
15294
|
+
return void 0;
|
|
15295
|
+
}
|
|
15296
|
+
const now = __privateMethod(this, _MulticastOrchestrator_instances, now_fn).call(this);
|
|
15297
|
+
if (!__privateGet(this, _favored) || __privateGet(this, _favored).key !== desiredKey || __privateGet(this, _favored).source !== "goodput") {
|
|
15298
|
+
__privateSet(this, _favored, { key: desiredKey, since: now, source: "goodput" });
|
|
15299
|
+
return void 0;
|
|
15300
|
+
}
|
|
15301
|
+
if (now - __privateGet(this, _favored).since < __privateGet(this, _dwellMs))
|
|
15302
|
+
return void 0;
|
|
15303
|
+
__privateSet(this, _state2, "joining");
|
|
15304
|
+
__privateSet(this, _target, desired);
|
|
15305
|
+
__privateSet(this, _joinStartedAt, now);
|
|
15306
|
+
__privateSet(this, _favored, void 0);
|
|
15307
|
+
return { kind: "join-target", current: __privateGet(this, _current), target: desired };
|
|
15308
|
+
}
|
|
15309
|
+
onHeadroom(sample) {
|
|
15310
|
+
if (!__privateGet(this, _current) || this.options.adaptive === false || __privateGet(this, _state2) === "joining")
|
|
15311
|
+
return void 0;
|
|
15312
|
+
if (!sample.healthy) {
|
|
15313
|
+
if (__privateGet(this, _favored)?.source === "headroom")
|
|
15314
|
+
__privateSet(this, _favored, void 0);
|
|
15315
|
+
return void 0;
|
|
15316
|
+
}
|
|
15317
|
+
if (__privateGet(this, _favored)?.source === "goodput")
|
|
15318
|
+
return void 0;
|
|
15319
|
+
const currentBandwidth = __privateGet(this, _current).bandwidth;
|
|
15320
|
+
if (typeof currentBandwidth !== "number")
|
|
15321
|
+
return void 0;
|
|
15322
|
+
const desired = this.options.endpoints.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth).sort((a, b) => a.bandwidth - b.bandwidth)[0];
|
|
15323
|
+
if (!desired) {
|
|
15324
|
+
__privateSet(this, _favored, void 0);
|
|
15325
|
+
return void 0;
|
|
15326
|
+
}
|
|
15327
|
+
const desiredKey = endpointKey(desired);
|
|
15328
|
+
if (desiredKey === endpointKey(__privateGet(this, _current))) {
|
|
15329
|
+
__privateSet(this, _favored, void 0);
|
|
15330
|
+
return void 0;
|
|
15331
|
+
}
|
|
15332
|
+
const now = __privateMethod(this, _MulticastOrchestrator_instances, now_fn).call(this);
|
|
15333
|
+
if (!__privateGet(this, _favored) || __privateGet(this, _favored).key !== desiredKey) {
|
|
15334
|
+
__privateSet(this, _favored, { key: desiredKey, since: now, source: "headroom" });
|
|
15335
|
+
return void 0;
|
|
15336
|
+
}
|
|
15337
|
+
if (now - __privateGet(this, _favored).since < __privateGet(this, _dwellMs))
|
|
15338
|
+
return void 0;
|
|
15339
|
+
__privateSet(this, _state2, "joining");
|
|
15340
|
+
__privateSet(this, _target, desired);
|
|
15341
|
+
__privateSet(this, _joinStartedAt, now);
|
|
15342
|
+
__privateSet(this, _favored, void 0);
|
|
15343
|
+
return { kind: "join-target", current: __privateGet(this, _current), target: desired };
|
|
15344
|
+
}
|
|
15345
|
+
onTierPacket(packet) {
|
|
15346
|
+
if (!__privateGet(this, _current))
|
|
15347
|
+
return void 0;
|
|
15348
|
+
if (packet.endpointKey === endpointKey(__privateGet(this, _current)) && Number.isFinite(packet.timestamp)) {
|
|
15349
|
+
__privateSet(this, _lastCommittedTimestamp, Math.max(__privateGet(this, _lastCommittedTimestamp), packet.timestamp));
|
|
15350
|
+
}
|
|
15351
|
+
if (__privateGet(this, _state2) !== "joining" || !__privateGet(this, _target))
|
|
15352
|
+
return void 0;
|
|
15353
|
+
const expired = this.poll();
|
|
15354
|
+
if (expired)
|
|
15355
|
+
return expired;
|
|
15356
|
+
if (packet.endpointKey !== endpointKey(__privateGet(this, _target)) || !packet.keyframe || !packet.hasValidCodecConfig || !Number.isFinite(packet.timestamp) || packet.timestamp < __privateGet(this, _lastCommittedTimestamp))
|
|
15357
|
+
return void 0;
|
|
15358
|
+
const command = {
|
|
15359
|
+
kind: "commit-swap",
|
|
15360
|
+
target: __privateGet(this, _target),
|
|
15361
|
+
old: __privateGet(this, _current)
|
|
15362
|
+
};
|
|
15363
|
+
__privateSet(this, _lastCommittedTimestamp, packet.timestamp);
|
|
15364
|
+
__privateSet(this, _current, __privateGet(this, _target));
|
|
15365
|
+
__privateSet(this, _target, void 0);
|
|
15366
|
+
__privateSet(this, _state2, "stable");
|
|
15367
|
+
return command;
|
|
15368
|
+
}
|
|
15369
|
+
poll() {
|
|
15370
|
+
if (__privateGet(this, _state2) !== "joining" || !__privateGet(this, _target) || !__privateGet(this, _current))
|
|
15371
|
+
return void 0;
|
|
15372
|
+
if (__privateMethod(this, _MulticastOrchestrator_instances, now_fn).call(this) - __privateGet(this, _joinStartedAt) < __privateGet(this, _joinDeadlineMs))
|
|
15373
|
+
return void 0;
|
|
15374
|
+
return this.cancel();
|
|
15375
|
+
}
|
|
15376
|
+
cancel() {
|
|
15377
|
+
if (__privateGet(this, _state2) !== "joining" || !__privateGet(this, _target) || !__privateGet(this, _current))
|
|
15378
|
+
return void 0;
|
|
15379
|
+
const command = {
|
|
15380
|
+
kind: "abandon-join",
|
|
15381
|
+
current: __privateGet(this, _current),
|
|
15382
|
+
target: __privateGet(this, _target)
|
|
15383
|
+
};
|
|
15384
|
+
__privateSet(this, _target, void 0);
|
|
15385
|
+
__privateSet(this, _state2, "stable");
|
|
15386
|
+
return command;
|
|
15387
|
+
}
|
|
15388
|
+
};
|
|
15389
|
+
_state2 = new WeakMap();
|
|
15390
|
+
_current = new WeakMap();
|
|
15391
|
+
_target = new WeakMap();
|
|
15392
|
+
_estimate = new WeakMap();
|
|
15393
|
+
_favored = new WeakMap();
|
|
15394
|
+
_joinStartedAt = new WeakMap();
|
|
15395
|
+
_lastCommittedTimestamp = new WeakMap();
|
|
15396
|
+
_dwellMs = new WeakMap();
|
|
15397
|
+
_joinDeadlineMs = new WeakMap();
|
|
15398
|
+
_MulticastOrchestrator_instances = new WeakSet();
|
|
15399
|
+
now_fn = function() {
|
|
15400
|
+
const now = this.options.now();
|
|
15401
|
+
if (!Number.isFinite(now))
|
|
15402
|
+
throw new Error("Multicast orchestrator clock must be finite");
|
|
15403
|
+
return now;
|
|
15404
|
+
};
|
|
15405
|
+
|
|
15406
|
+
// node_modules/@blockcast/mmt-transport/dist/endpoint-config.js
|
|
15407
|
+
function toWindowMulticastSubscribeConfig(input) {
|
|
15408
|
+
if (input.mode !== void 0 && !["auto", "native", "tunnel"].includes(input.mode)) {
|
|
15409
|
+
throw new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);
|
|
15410
|
+
}
|
|
15411
|
+
const base = {
|
|
15412
|
+
group: input.group,
|
|
15413
|
+
port: input.port,
|
|
15414
|
+
...input.source?.trim() ? { source: input.source.trim() } : {},
|
|
15415
|
+
consume: "bytes",
|
|
15416
|
+
...input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}
|
|
15417
|
+
};
|
|
15418
|
+
const relay = input.amtRelay?.trim();
|
|
15419
|
+
const wantsAmt = input.mode === "tunnel" || input.mode !== "native" && relay !== void 0 && relay !== "";
|
|
15420
|
+
if (!wantsAmt) {
|
|
15421
|
+
const native = {
|
|
15422
|
+
...base,
|
|
15423
|
+
origin: "ip-multicast",
|
|
15424
|
+
...input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}
|
|
15425
|
+
};
|
|
15426
|
+
return native;
|
|
15427
|
+
}
|
|
15428
|
+
const relayPort = input.amtRelayPort;
|
|
15429
|
+
if (!relay || relayPort === void 0 || !isPositivePort(relayPort)) {
|
|
15430
|
+
throw new TypeError("AMT requires a resolved relay and a valid relay port");
|
|
15431
|
+
}
|
|
15432
|
+
return {
|
|
15433
|
+
...base,
|
|
15434
|
+
origin: "amt",
|
|
15435
|
+
relay,
|
|
15436
|
+
relayPort
|
|
15437
|
+
};
|
|
15438
|
+
}
|
|
15439
|
+
function isValidMulticastGroupAddress(address) {
|
|
15440
|
+
const value = address?.trim().toLowerCase();
|
|
15441
|
+
if (!value)
|
|
15442
|
+
return false;
|
|
15443
|
+
if (value.startsWith("ff"))
|
|
15444
|
+
return isValidIpv6MulticastAddress(value);
|
|
15445
|
+
const parts = value.split(".");
|
|
15446
|
+
if (parts.length !== 4)
|
|
15447
|
+
return false;
|
|
15448
|
+
const octets = parts.map((part) => {
|
|
15449
|
+
if (!/^\d+$/.test(part))
|
|
15450
|
+
return Number.NaN;
|
|
15451
|
+
const octet = Number(part);
|
|
15452
|
+
return Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;
|
|
15453
|
+
});
|
|
15454
|
+
return octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;
|
|
15455
|
+
}
|
|
15456
|
+
function normalizeMulticastEndpoint(endpoint) {
|
|
15457
|
+
const groupAddress = endpoint.groupAddress.trim().toLowerCase();
|
|
15458
|
+
const sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || void 0;
|
|
15459
|
+
const amtRelay = endpoint.amtRelay?.trim().toLowerCase() || void 0;
|
|
15460
|
+
if (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port))
|
|
15461
|
+
return void 0;
|
|
15462
|
+
if (endpoint.mode && !["auto", "native", "tunnel"].includes(endpoint.mode))
|
|
15463
|
+
return void 0;
|
|
15464
|
+
if (endpoint.amtRelayPort !== void 0 && !isPositivePort(endpoint.amtRelayPort))
|
|
15465
|
+
return void 0;
|
|
15466
|
+
return {
|
|
15467
|
+
groupAddress,
|
|
15468
|
+
...sourceAddress ? { sourceAddress } : {},
|
|
15469
|
+
port: endpoint.port,
|
|
15470
|
+
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
15471
|
+
...amtRelay ? { amtRelay } : {},
|
|
15472
|
+
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}
|
|
15473
|
+
};
|
|
15474
|
+
}
|
|
15475
|
+
function multicastEndpointKey(endpoint) {
|
|
15476
|
+
const normalized = normalizeMulticastEndpoint(endpoint);
|
|
15477
|
+
if (!normalized)
|
|
15478
|
+
throw new Error("Invalid multicast endpoint");
|
|
15479
|
+
return JSON.stringify([
|
|
15480
|
+
normalized.sourceAddress ?? "",
|
|
15481
|
+
normalized.groupAddress,
|
|
15482
|
+
normalized.port,
|
|
15483
|
+
normalized.mode ?? "auto",
|
|
15484
|
+
normalized.amtRelay ?? "",
|
|
15485
|
+
normalized.amtRelayPort ?? ""
|
|
15486
|
+
]);
|
|
15487
|
+
}
|
|
15488
|
+
function isValidIpv6MulticastAddress(value) {
|
|
15489
|
+
if (!/^[0-9a-f:]+$/.test(value))
|
|
15490
|
+
return false;
|
|
15491
|
+
const compressed = value.includes("::");
|
|
15492
|
+
if (value.indexOf("::") !== value.lastIndexOf("::"))
|
|
15493
|
+
return false;
|
|
15494
|
+
const groups = value.split(":").filter(Boolean);
|
|
15495
|
+
if (groups.length === 0 || groups.length > 8)
|
|
15496
|
+
return false;
|
|
15497
|
+
if (!compressed && groups.length !== 8)
|
|
15498
|
+
return false;
|
|
15499
|
+
return groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));
|
|
15500
|
+
}
|
|
15501
|
+
function isPositivePort(port) {
|
|
15502
|
+
return Number.isInteger(port) && port > 0 && port <= 65535;
|
|
15503
|
+
}
|
|
15504
|
+
|
|
15505
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-subscription-pool.js
|
|
15506
|
+
var _entries, _gatewayIds, _nextGatewayId, _MulticastSubscriptionPool_instances, createEntry_fn, gatewayId_fn, detach_fn, dispatch_fn;
|
|
15507
|
+
var MulticastSubscriptionPool = class {
|
|
15508
|
+
constructor() {
|
|
15509
|
+
__privateAdd(this, _MulticastSubscriptionPool_instances);
|
|
15510
|
+
__privateAdd(this, _entries, /* @__PURE__ */ new Map());
|
|
15511
|
+
__privateAdd(this, _gatewayIds, /* @__PURE__ */ new WeakMap());
|
|
15512
|
+
__privateAdd(this, _nextGatewayId, 1);
|
|
15513
|
+
}
|
|
15514
|
+
async acquire(gateway, config, consumer) {
|
|
15515
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
15516
|
+
groupAddress: config.group,
|
|
15517
|
+
sourceAddress: config.source,
|
|
15518
|
+
port: config.port,
|
|
15519
|
+
mode: config.mode,
|
|
15520
|
+
amtRelay: config.amtRelay,
|
|
15521
|
+
amtRelayPort: config.amtRelayPort
|
|
15522
|
+
});
|
|
15523
|
+
if (!endpoint)
|
|
15524
|
+
throw new Error("Invalid multicast endpoint");
|
|
15525
|
+
if (typeof gateway.subscribe !== "function")
|
|
15526
|
+
throw new Error("window.multicast.subscribe unavailable");
|
|
15527
|
+
const key = multicastEndpointKey(endpoint);
|
|
15528
|
+
const internalKey = `${__privateMethod(this, _MulticastSubscriptionPool_instances, gatewayId_fn).call(this, gateway)}|${key}`;
|
|
15529
|
+
let record = __privateGet(this, _entries).get(internalKey);
|
|
15530
|
+
if (!record) {
|
|
15531
|
+
record = { claims: 0, entry: Promise.resolve(null) };
|
|
15532
|
+
const createdRecord = record;
|
|
15533
|
+
record.entry = __privateMethod(this, _MulticastSubscriptionPool_instances, createEntry_fn).call(this, gateway, endpoint, (entry2) => {
|
|
15534
|
+
if (__privateGet(this, _entries).get(internalKey) === createdRecord)
|
|
15535
|
+
__privateGet(this, _entries).delete(internalKey);
|
|
15536
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, detach_fn).call(this, entry2);
|
|
15537
|
+
}).catch((error) => {
|
|
15538
|
+
if (__privateGet(this, _entries).get(internalKey) === createdRecord)
|
|
15539
|
+
__privateGet(this, _entries).delete(internalKey);
|
|
15540
|
+
throw error;
|
|
15541
|
+
});
|
|
15542
|
+
__privateGet(this, _entries).set(internalKey, record);
|
|
15543
|
+
}
|
|
15544
|
+
record.claims++;
|
|
15545
|
+
let entry;
|
|
15546
|
+
try {
|
|
15547
|
+
entry = await record.entry;
|
|
15548
|
+
} catch (error) {
|
|
15549
|
+
record.claims--;
|
|
15550
|
+
throw error;
|
|
15551
|
+
}
|
|
15552
|
+
if (entry.closed) {
|
|
15553
|
+
record.claims--;
|
|
15554
|
+
return this.acquire(gateway, config, consumer);
|
|
15555
|
+
}
|
|
15556
|
+
const token = Symbol(key);
|
|
15557
|
+
entry.consumers.set(token, consumer);
|
|
15558
|
+
let released = false;
|
|
15559
|
+
return {
|
|
15560
|
+
key,
|
|
15561
|
+
release: async () => {
|
|
15562
|
+
if (released)
|
|
15563
|
+
return;
|
|
15564
|
+
released = true;
|
|
15565
|
+
entry.consumers.delete(token);
|
|
15566
|
+
record.claims--;
|
|
15567
|
+
if (record.claims > 0)
|
|
15568
|
+
return;
|
|
15569
|
+
if (__privateGet(this, _entries).get(internalKey) === record)
|
|
15570
|
+
__privateGet(this, _entries).delete(internalKey);
|
|
15571
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, detach_fn).call(this, entry);
|
|
15572
|
+
if (!entry.closed)
|
|
15573
|
+
await entry.subscription.unsubscribe();
|
|
15574
|
+
}
|
|
15575
|
+
};
|
|
15576
|
+
}
|
|
15577
|
+
get size() {
|
|
15578
|
+
return __privateGet(this, _entries).size;
|
|
15579
|
+
}
|
|
15580
|
+
};
|
|
15581
|
+
_entries = new WeakMap();
|
|
15582
|
+
_gatewayIds = new WeakMap();
|
|
15583
|
+
_nextGatewayId = new WeakMap();
|
|
15584
|
+
_MulticastSubscriptionPool_instances = new WeakSet();
|
|
15585
|
+
createEntry_fn = async function(gateway, endpoint, onClosed) {
|
|
15586
|
+
const subscription = await gateway.subscribe(toWindowMulticastSubscribeConfig({
|
|
15587
|
+
group: endpoint.groupAddress,
|
|
15588
|
+
...endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {},
|
|
15589
|
+
port: endpoint.port,
|
|
15590
|
+
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
15591
|
+
...endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {},
|
|
15592
|
+
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}
|
|
15593
|
+
}));
|
|
15594
|
+
const consumers = /* @__PURE__ */ new Map();
|
|
15595
|
+
let entry;
|
|
15596
|
+
const bytesListener = (event) => {
|
|
15597
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, dispatch_fn).call(this, consumers, (consumer) => consumer.onBytes(event.detail));
|
|
15598
|
+
};
|
|
15599
|
+
const stateListener = (event) => {
|
|
15600
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, dispatch_fn).call(this, consumers, (consumer) => consumer.onState?.(event.detail));
|
|
15601
|
+
if (event.detail === "closed" && !entry.closed) {
|
|
15602
|
+
entry.closed = true;
|
|
15603
|
+
onClosed(entry);
|
|
15604
|
+
}
|
|
15605
|
+
};
|
|
15606
|
+
const errorListener = (event) => {
|
|
15607
|
+
const error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));
|
|
15608
|
+
__privateMethod(this, _MulticastSubscriptionPool_instances, dispatch_fn).call(this, consumers, (consumer) => consumer.onError?.(error));
|
|
15609
|
+
};
|
|
15610
|
+
entry = { subscription, consumers, bytesListener, stateListener, errorListener, closed: false };
|
|
15611
|
+
subscription.addEventListener("bytes", bytesListener);
|
|
15612
|
+
subscription.addEventListener("state", stateListener);
|
|
15613
|
+
subscription.addEventListener("error", errorListener);
|
|
15614
|
+
return entry;
|
|
15615
|
+
};
|
|
15616
|
+
gatewayId_fn = function(gateway) {
|
|
15617
|
+
const object = gateway;
|
|
15618
|
+
let id = __privateGet(this, _gatewayIds).get(object);
|
|
15619
|
+
if (!id) {
|
|
15620
|
+
id = __privateWrapper(this, _nextGatewayId)._++;
|
|
15621
|
+
__privateGet(this, _gatewayIds).set(object, id);
|
|
15622
|
+
}
|
|
15623
|
+
return id;
|
|
15624
|
+
};
|
|
15625
|
+
detach_fn = function(entry) {
|
|
15626
|
+
entry.subscription.removeEventListener("bytes", entry.bytesListener);
|
|
15627
|
+
entry.subscription.removeEventListener("state", entry.stateListener);
|
|
15628
|
+
entry.subscription.removeEventListener("error", entry.errorListener);
|
|
15629
|
+
};
|
|
15630
|
+
dispatch_fn = function(consumers, callback) {
|
|
15631
|
+
for (const consumer of consumers.values()) {
|
|
15632
|
+
try {
|
|
15633
|
+
callback(consumer);
|
|
15634
|
+
} catch (error) {
|
|
15635
|
+
console.error("[Transport] multicast consumer callback failed", error);
|
|
15636
|
+
}
|
|
15637
|
+
}
|
|
15638
|
+
};
|
|
15639
|
+
var multicastSubscriptionPool = new MulticastSubscriptionPool();
|
|
15640
|
+
|
|
15641
|
+
// node_modules/@blockcast/mmt-transport/dist/transport-manager.js
|
|
15642
|
+
var TransportManager = class {
|
|
15643
|
+
constructor() {
|
|
15644
|
+
__publicField(this, "state", {
|
|
15645
|
+
mode: "disconnected",
|
|
15646
|
+
connected: false,
|
|
15647
|
+
packetsReceived: 0,
|
|
15648
|
+
bytesReceived: 0,
|
|
15649
|
+
lastPacketTime: 0
|
|
15650
|
+
});
|
|
15651
|
+
__publicField(this, "capabilities", null);
|
|
15652
|
+
__publicField(this, "subscription", null);
|
|
15653
|
+
__publicField(this, "driad", new DRIADDiscovery());
|
|
15654
|
+
__publicField(this, "abr");
|
|
15655
|
+
__publicField(this, "multicastOrchestrator", null);
|
|
15656
|
+
__publicField(this, "onOrchestratorCommand");
|
|
15657
|
+
__publicField(this, "connectionGeneration", 0);
|
|
15658
|
+
__publicField(this, "onPacket");
|
|
15659
|
+
__publicField(this, "onError");
|
|
15660
|
+
__publicField(this, "onStateChange");
|
|
15661
|
+
this.abr = new AdaptiveBitrateController();
|
|
15662
|
+
}
|
|
15663
|
+
getWindow() {
|
|
15664
|
+
return typeof window === "undefined" ? void 0 : window;
|
|
15665
|
+
}
|
|
15666
|
+
getWindowMulticastGateway() {
|
|
15667
|
+
return this.getWindow()?.multicast;
|
|
15668
|
+
}
|
|
15669
|
+
async getGatewayCapabilities(gateway) {
|
|
15670
|
+
if (!gateway?.capabilities)
|
|
15671
|
+
return void 0;
|
|
15672
|
+
try {
|
|
15673
|
+
return typeof gateway.capabilities === "function" ? await gateway.capabilities() : gateway.capabilities;
|
|
15674
|
+
} catch (error) {
|
|
15675
|
+
console.warn("[Transport] window.multicast capabilities failed:", error);
|
|
15676
|
+
return void 0;
|
|
15677
|
+
}
|
|
15678
|
+
}
|
|
15679
|
+
async gatewaySupportsBytes(gateway) {
|
|
15680
|
+
if (typeof gateway?.subscribe !== "function")
|
|
15681
|
+
return false;
|
|
15682
|
+
if (!gateway.capabilities)
|
|
15683
|
+
return true;
|
|
15684
|
+
const capabilities = await this.getGatewayCapabilities(gateway);
|
|
15685
|
+
if (!capabilities)
|
|
15686
|
+
return false;
|
|
15687
|
+
const consumeModes = capabilities.consumeModes;
|
|
15688
|
+
return !consumeModes || consumeModes.includes("bytes");
|
|
15689
|
+
}
|
|
15690
|
+
/**
|
|
15691
|
+
* Detect available transport capabilities
|
|
15692
|
+
*/
|
|
15693
|
+
async detectCapabilities() {
|
|
15694
|
+
if (this.capabilities)
|
|
15695
|
+
return this.capabilities;
|
|
15696
|
+
const gateway = this.getWindowMulticastGateway();
|
|
15697
|
+
const hasGatewayBytes = await this.gatewaySupportsBytes(gateway);
|
|
15698
|
+
const win = this.getWindow();
|
|
15699
|
+
this.capabilities = {
|
|
15700
|
+
nativeSSM: hasGatewayBytes,
|
|
15701
|
+
nativeIGMP: hasGatewayBytes,
|
|
15702
|
+
directSockets: typeof win?.UDPSocket !== "undefined",
|
|
15703
|
+
amt: false,
|
|
15704
|
+
driad: true,
|
|
15705
|
+
// DRIAD uses HTTP proxy, always available
|
|
15706
|
+
fecDecode: false
|
|
15707
|
+
};
|
|
15708
|
+
console.log("[Transport] Capabilities:", this.capabilities);
|
|
15709
|
+
return this.capabilities;
|
|
15710
|
+
}
|
|
15711
|
+
/**
|
|
15712
|
+
* Connect using best available transport
|
|
15713
|
+
*
|
|
15714
|
+
* Promotion hierarchy:
|
|
15715
|
+
* 1. window.multicast gateway bytes
|
|
15716
|
+
* 2. AMT proxy (specified or discovered via DRIAD)
|
|
15717
|
+
* 3. MOQ WebTransport (final fallback)
|
|
15718
|
+
*/
|
|
15719
|
+
async connect(config) {
|
|
15720
|
+
const generation = ++this.connectionGeneration;
|
|
15721
|
+
this.subscription?.close();
|
|
15722
|
+
this.subscription = null;
|
|
15723
|
+
this.state = {
|
|
15724
|
+
mode: "disconnected",
|
|
15725
|
+
connected: false,
|
|
15726
|
+
packetsReceived: 0,
|
|
15727
|
+
bytesReceived: 0,
|
|
15728
|
+
lastPacketTime: 0
|
|
15729
|
+
};
|
|
15730
|
+
this.onStateChange?.(this.state);
|
|
15731
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
15732
|
+
groupAddress: config.group,
|
|
15733
|
+
sourceAddress: config.source,
|
|
15734
|
+
port: config.port,
|
|
15735
|
+
mode: config.mode,
|
|
15736
|
+
amtRelay: config.amtRelay,
|
|
15737
|
+
amtRelayPort: config.amtRelayPort
|
|
15738
|
+
});
|
|
15739
|
+
if (!endpoint) {
|
|
15740
|
+
const error = new Error("Invalid multicast endpoint configuration");
|
|
15741
|
+
this.onError?.(error);
|
|
15742
|
+
return false;
|
|
15743
|
+
}
|
|
15744
|
+
config = {
|
|
15745
|
+
...config,
|
|
15746
|
+
group: endpoint.groupAddress,
|
|
15747
|
+
source: endpoint.sourceAddress ?? "",
|
|
15748
|
+
mode: endpoint.mode,
|
|
15749
|
+
amtRelay: endpoint.amtRelay,
|
|
15750
|
+
amtRelayPort: endpoint.amtRelayPort
|
|
15751
|
+
};
|
|
15752
|
+
const caps = await this.detectCapabilities();
|
|
15753
|
+
if (generation !== this.connectionGeneration)
|
|
15754
|
+
return false;
|
|
15755
|
+
if (config.initialQuality) {
|
|
15756
|
+
this.abr = new AdaptiveBitrateController(config.initialQuality);
|
|
15757
|
+
}
|
|
15758
|
+
this.abr.setCallback((quality) => {
|
|
15759
|
+
this.state.quality = quality;
|
|
15760
|
+
this.subscription?.onqualitychange?.(quality);
|
|
15761
|
+
this.onStateChange?.(this.state);
|
|
15762
|
+
});
|
|
15763
|
+
let connected = false;
|
|
15764
|
+
if (caps.nativeSSM || caps.nativeIGMP) {
|
|
15765
|
+
console.log("[Transport] Attempting window.multicast...");
|
|
15766
|
+
connected = await this.connectWindowMulticast(config, generation);
|
|
15767
|
+
if (connected) {
|
|
15768
|
+
if (generation !== this.connectionGeneration)
|
|
15769
|
+
return false;
|
|
15770
|
+
this.state.mode = "multicast";
|
|
15771
|
+
return true;
|
|
15772
|
+
}
|
|
15773
|
+
}
|
|
15774
|
+
if (!connected) {
|
|
15775
|
+
console.log("[Transport] Attempting AMT tunnel...");
|
|
15776
|
+
connected = await this.connectAmt(config, generation);
|
|
15777
|
+
if (connected) {
|
|
15778
|
+
if (generation !== this.connectionGeneration)
|
|
15779
|
+
return false;
|
|
15780
|
+
this.state.mode = "amt";
|
|
15781
|
+
return true;
|
|
15782
|
+
}
|
|
15783
|
+
}
|
|
15784
|
+
console.log("[Transport] All multicast transports failed, falling back to MOQ");
|
|
15785
|
+
return false;
|
|
15786
|
+
}
|
|
15787
|
+
/**
|
|
15788
|
+
* Connect via the lowercase window.multicast bytes gateway.
|
|
15789
|
+
*/
|
|
15790
|
+
async connectWindowMulticast(config, generation) {
|
|
15791
|
+
try {
|
|
15792
|
+
const gateway = this.getWindowMulticastGateway();
|
|
15793
|
+
if (typeof gateway?.subscribe !== "function")
|
|
15794
|
+
return false;
|
|
15795
|
+
const lease = await multicastSubscriptionPool.acquire(gateway, config, {
|
|
15796
|
+
onBytes: (data) => {
|
|
15797
|
+
if (generation === this.connectionGeneration)
|
|
15798
|
+
this.handlePacket(data);
|
|
15799
|
+
},
|
|
15800
|
+
onState: (state) => {
|
|
15801
|
+
if (generation !== this.connectionGeneration)
|
|
15802
|
+
return;
|
|
15803
|
+
this.state.connected = state !== "closed";
|
|
15804
|
+
this.onStateChange?.(this.state);
|
|
15805
|
+
},
|
|
15806
|
+
onError: (error) => {
|
|
15807
|
+
if (generation !== this.connectionGeneration)
|
|
15808
|
+
return;
|
|
15809
|
+
console.error("[Transport] window.multicast error:", error);
|
|
15810
|
+
this.onError?.(error);
|
|
15811
|
+
}
|
|
15812
|
+
});
|
|
15813
|
+
if (generation !== this.connectionGeneration) {
|
|
15814
|
+
await lease.release();
|
|
15815
|
+
return false;
|
|
15816
|
+
}
|
|
15817
|
+
this.subscription = {
|
|
15818
|
+
id: lease.key,
|
|
15819
|
+
config,
|
|
15820
|
+
close: () => {
|
|
15821
|
+
void lease.release();
|
|
15822
|
+
}
|
|
15823
|
+
};
|
|
15824
|
+
this.state = {
|
|
15825
|
+
mode: "multicast",
|
|
15826
|
+
connected: true,
|
|
15827
|
+
group: config.group,
|
|
15828
|
+
source: config.source,
|
|
15829
|
+
quality: this.abr.getCurrentQuality(),
|
|
15830
|
+
packetsReceived: 0,
|
|
15831
|
+
bytesReceived: 0,
|
|
15832
|
+
lastPacketTime: 0
|
|
15833
|
+
};
|
|
15834
|
+
console.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);
|
|
15835
|
+
this.onStateChange?.(this.state);
|
|
15836
|
+
return true;
|
|
15837
|
+
} catch (error) {
|
|
15838
|
+
console.warn("[Transport] window.multicast failed:", error);
|
|
15839
|
+
return false;
|
|
15840
|
+
}
|
|
15841
|
+
}
|
|
15842
|
+
/**
|
|
15843
|
+
* Connect via AMT proxy (with optional DRIAD discovery)
|
|
15844
|
+
*/
|
|
15845
|
+
async connectAmt(config, generation) {
|
|
15846
|
+
try {
|
|
15847
|
+
let amtRelay = config.amtRelay;
|
|
15848
|
+
if (!amtRelay && config.driadEnabled !== false && config.source) {
|
|
15849
|
+
console.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);
|
|
15850
|
+
const relays = await this.driad.discoverRelays(config.source);
|
|
15851
|
+
if (generation !== this.connectionGeneration)
|
|
15852
|
+
return false;
|
|
15853
|
+
const bestRelay = await this.driad.selectBestRelay(relays);
|
|
15854
|
+
if (generation !== this.connectionGeneration)
|
|
15855
|
+
return false;
|
|
15856
|
+
if (bestRelay) {
|
|
15857
|
+
amtRelay = bestRelay.host;
|
|
15858
|
+
this.state.amtRelay = amtRelay;
|
|
15859
|
+
}
|
|
15860
|
+
}
|
|
15861
|
+
if (!amtRelay) {
|
|
15862
|
+
console.warn("[Transport] No AMT relay available (DRIAD requires source address for SSM)");
|
|
15863
|
+
return false;
|
|
15864
|
+
}
|
|
15865
|
+
return await this.connectAmtProxy(config, amtRelay, generation);
|
|
15866
|
+
} catch (error) {
|
|
15867
|
+
console.warn("[Transport] AMT connection failed:", error);
|
|
15868
|
+
return false;
|
|
15869
|
+
}
|
|
15870
|
+
}
|
|
15871
|
+
/**
|
|
15872
|
+
* Connect via WebSocket-based AMT proxy (browser fallback)
|
|
15873
|
+
*/
|
|
15874
|
+
async connectAmtProxy(config, amtRelay, generation) {
|
|
15875
|
+
try {
|
|
15876
|
+
const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
|
|
15877
|
+
const relayAuthority = config.amtRelayPort ? `${relayHost}:${config.amtRelayPort}` : relayHost;
|
|
15878
|
+
const wsUrl = new URL(`wss://${relayAuthority}/amt-proxy`);
|
|
15879
|
+
wsUrl.searchParams.set("group", config.group);
|
|
15880
|
+
wsUrl.searchParams.set("source", config.source || "");
|
|
15881
|
+
wsUrl.searchParams.set("port", String(config.port));
|
|
15882
|
+
const ws = new WebSocket(wsUrl);
|
|
15883
|
+
return new Promise((resolve, reject) => {
|
|
15884
|
+
ws.binaryType = "arraybuffer";
|
|
15885
|
+
const timeout = setTimeout(() => {
|
|
15886
|
+
ws.close();
|
|
15887
|
+
reject(new Error("AMT proxy connection timeout"));
|
|
15888
|
+
}, 1e4);
|
|
15889
|
+
ws.onopen = () => {
|
|
15890
|
+
clearTimeout(timeout);
|
|
15891
|
+
if (generation !== this.connectionGeneration) {
|
|
15892
|
+
ws.close();
|
|
15893
|
+
resolve(false);
|
|
15894
|
+
return;
|
|
15895
|
+
}
|
|
15896
|
+
console.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);
|
|
15897
|
+
this.subscription = {
|
|
15898
|
+
id: `amt-proxy-${Date.now()}`,
|
|
15899
|
+
config,
|
|
15900
|
+
close: () => ws.close()
|
|
15901
|
+
};
|
|
15902
|
+
this.state = {
|
|
15903
|
+
mode: "amt",
|
|
15904
|
+
connected: true,
|
|
15905
|
+
group: config.group,
|
|
15906
|
+
source: config.source,
|
|
15907
|
+
amtRelay,
|
|
15908
|
+
quality: this.abr.getCurrentQuality(),
|
|
15909
|
+
packetsReceived: 0,
|
|
15910
|
+
bytesReceived: 0,
|
|
15911
|
+
lastPacketTime: 0
|
|
15912
|
+
};
|
|
15913
|
+
this.onStateChange?.(this.state);
|
|
15914
|
+
resolve(true);
|
|
15915
|
+
};
|
|
15916
|
+
ws.onmessage = (event) => {
|
|
15917
|
+
if (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {
|
|
15918
|
+
this.handlePacket(event.data);
|
|
15919
|
+
}
|
|
15920
|
+
};
|
|
15921
|
+
ws.onerror = (error) => {
|
|
15922
|
+
clearTimeout(timeout);
|
|
15923
|
+
if (generation !== this.connectionGeneration) {
|
|
15924
|
+
resolve(false);
|
|
15925
|
+
return;
|
|
15926
|
+
}
|
|
15927
|
+
console.error("[Transport] AMT proxy error:", error);
|
|
15928
|
+
reject(error);
|
|
15929
|
+
};
|
|
15930
|
+
ws.onclose = () => {
|
|
15931
|
+
clearTimeout(timeout);
|
|
15932
|
+
if (generation !== this.connectionGeneration) {
|
|
15933
|
+
resolve(false);
|
|
15934
|
+
return;
|
|
15935
|
+
}
|
|
15936
|
+
this.state.connected = false;
|
|
15937
|
+
this.onStateChange?.(this.state);
|
|
15938
|
+
resolve(false);
|
|
15939
|
+
};
|
|
15940
|
+
});
|
|
15941
|
+
} catch (error) {
|
|
15942
|
+
console.warn("[Transport] AMT proxy failed:", error);
|
|
15943
|
+
return false;
|
|
15944
|
+
}
|
|
15945
|
+
}
|
|
15946
|
+
/**
|
|
15947
|
+
* Handle received packet
|
|
15948
|
+
*/
|
|
15949
|
+
handlePacket(data) {
|
|
15950
|
+
this.state.packetsReceived++;
|
|
15951
|
+
this.state.bytesReceived += data.byteLength;
|
|
15952
|
+
this.state.lastPacketTime = Date.now();
|
|
15953
|
+
this.abr.recordSample(data.byteLength, 0);
|
|
15954
|
+
this.onPacket?.(data);
|
|
15955
|
+
}
|
|
15956
|
+
/**
|
|
15957
|
+
* Get current transport state
|
|
15958
|
+
*/
|
|
15959
|
+
getState() {
|
|
15960
|
+
return { ...this.state };
|
|
15961
|
+
}
|
|
15962
|
+
/**
|
|
15963
|
+
* Get current quality level
|
|
15964
|
+
*/
|
|
15965
|
+
getQuality() {
|
|
15966
|
+
return this.abr.getCurrentQuality();
|
|
15967
|
+
}
|
|
15968
|
+
/**
|
|
15969
|
+
* Force quality level
|
|
15970
|
+
*/
|
|
15971
|
+
setQuality(name) {
|
|
15972
|
+
this.abr.setQuality(name);
|
|
15973
|
+
}
|
|
15974
|
+
/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
|
|
15975
|
+
configureMulticastTiers(options, onCommand) {
|
|
15976
|
+
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
15977
|
+
this.multicastOrchestrator = new MulticastOrchestrator(options);
|
|
15978
|
+
this.onOrchestratorCommand = onCommand;
|
|
15979
|
+
return this.multicastOrchestrator.start();
|
|
15980
|
+
}
|
|
15981
|
+
recordMulticastGoodput(bitrate) {
|
|
15982
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
|
|
15983
|
+
}
|
|
15984
|
+
recordMulticastHeadroom(healthy) {
|
|
15985
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));
|
|
15986
|
+
}
|
|
15987
|
+
recordMulticastTierPacket(packet) {
|
|
15988
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
|
|
15989
|
+
}
|
|
15990
|
+
pollMulticastTierSwitch() {
|
|
15991
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());
|
|
15992
|
+
}
|
|
15993
|
+
/**
|
|
15994
|
+
* Set packet callback
|
|
15995
|
+
*/
|
|
15996
|
+
setPacketCallback(callback) {
|
|
15997
|
+
this.onPacket = callback;
|
|
15998
|
+
}
|
|
15999
|
+
/**
|
|
16000
|
+
* Set error callback
|
|
16001
|
+
*/
|
|
16002
|
+
setErrorCallback(callback) {
|
|
16003
|
+
this.onError = callback;
|
|
16004
|
+
}
|
|
16005
|
+
/**
|
|
16006
|
+
* Set state change callback
|
|
16007
|
+
*/
|
|
16008
|
+
setStateCallback(callback) {
|
|
16009
|
+
this.onStateChange = callback;
|
|
16010
|
+
}
|
|
16011
|
+
/**
|
|
16012
|
+
* Disconnect
|
|
16013
|
+
*/
|
|
16014
|
+
disconnect() {
|
|
16015
|
+
this.connectionGeneration++;
|
|
16016
|
+
this.subscription?.close();
|
|
16017
|
+
this.subscription = null;
|
|
16018
|
+
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
16019
|
+
this.multicastOrchestrator = null;
|
|
16020
|
+
this.onOrchestratorCommand = void 0;
|
|
16021
|
+
this.state = {
|
|
16022
|
+
mode: "disconnected",
|
|
16023
|
+
connected: false,
|
|
16024
|
+
packetsReceived: 0,
|
|
16025
|
+
bytesReceived: 0,
|
|
16026
|
+
lastPacketTime: 0
|
|
16027
|
+
};
|
|
16028
|
+
this.onStateChange?.(this.state);
|
|
16029
|
+
}
|
|
16030
|
+
dispatchOrchestratorCommand(command) {
|
|
16031
|
+
if (command)
|
|
16032
|
+
this.onOrchestratorCommand?.(command);
|
|
16033
|
+
return command;
|
|
16034
|
+
}
|
|
16035
|
+
};
|
|
16036
|
+
var transportManager = new TransportManager();
|
|
16037
|
+
|
|
16038
|
+
// node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
|
|
16039
|
+
var MAX_MOQT_PRIORITY = 255;
|
|
16040
|
+
function moqtPriority(value, field) {
|
|
16041
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {
|
|
16042
|
+
throw new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);
|
|
16043
|
+
}
|
|
16044
|
+
return value;
|
|
16045
|
+
}
|
|
16046
|
+
function moqtSubscriberPriority(value) {
|
|
16047
|
+
return moqtPriority(value, "MOQT subscriber priority");
|
|
16048
|
+
}
|
|
16049
|
+
function moqtPublisherPriority(value) {
|
|
16050
|
+
return moqtPriority(value, "MOQT publisher priority");
|
|
16051
|
+
}
|
|
16052
|
+
var MOQT_SUBSCRIBER_PRIORITY = Object.freeze({
|
|
16053
|
+
catalog: moqtSubscriberPriority(0),
|
|
16054
|
+
deadlineRepair: moqtSubscriberPriority(0),
|
|
16055
|
+
audio: moqtSubscriberPriority(1),
|
|
16056
|
+
video: moqtSubscriberPriority(2),
|
|
16057
|
+
backgroundRepair: moqtSubscriberPriority(3)
|
|
16058
|
+
});
|
|
16059
|
+
var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
16060
|
+
catalog: moqtPublisherPriority(32),
|
|
16061
|
+
source: moqtPublisherPriority(128),
|
|
16062
|
+
repair: moqtPublisherPriority(240)
|
|
16063
|
+
});
|
|
16064
|
+
|
|
16065
|
+
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
16066
|
+
var DEFAULT_AMT_PORT = 2268;
|
|
16067
|
+
var AMTGatewayManager = class {
|
|
16068
|
+
constructor() {
|
|
16069
|
+
__publicField(this, "subscriptions", /* @__PURE__ */ new Map());
|
|
16070
|
+
__publicField(this, "messageHandler");
|
|
16071
|
+
__publicField(this, "pendingPackets", /* @__PURE__ */ new Map());
|
|
16072
|
+
__publicField(this, "driad", new DRIADDiscovery());
|
|
16073
|
+
this.messageHandler = (event) => {
|
|
16074
|
+
const data = event.data;
|
|
16075
|
+
console.log(`[AMT] SW message received:`, data.type);
|
|
16076
|
+
if (data.type === "UDP_PACKET") {
|
|
16077
|
+
this.handlePacket(data);
|
|
16078
|
+
} else if (data.type === "SOCKET_ERROR") {
|
|
16079
|
+
this.handleSocketError(data);
|
|
16080
|
+
} else if (data.type === "DEBUG_LOG") {
|
|
16081
|
+
console.log(`[AMT SW Debug] ${data.message}`);
|
|
16082
|
+
}
|
|
16083
|
+
};
|
|
16084
|
+
if (typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
16085
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
16086
|
+
console.log("[AMT] Registered service worker message handler");
|
|
16087
|
+
} else {
|
|
16088
|
+
console.warn("[AMT] Service worker not available");
|
|
16089
|
+
}
|
|
16090
|
+
}
|
|
16091
|
+
/**
|
|
16092
|
+
* Subscribe via AMT tunnel
|
|
16093
|
+
*/
|
|
16094
|
+
async subscribe(config) {
|
|
16095
|
+
const { key } = config;
|
|
16096
|
+
const keyStr = this.getKeyString(key);
|
|
16097
|
+
if (this.subscriptions.has(keyStr)) {
|
|
16098
|
+
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
16099
|
+
return keyStr;
|
|
16100
|
+
}
|
|
16101
|
+
try {
|
|
16102
|
+
let relayAddress;
|
|
16103
|
+
let discoveryMethod;
|
|
16104
|
+
const relayPort = config.relayPort || DEFAULT_AMT_PORT;
|
|
16105
|
+
if (config.relayAddress) {
|
|
16106
|
+
relayAddress = config.relayAddress;
|
|
16107
|
+
discoveryMethod = "manual";
|
|
16108
|
+
console.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);
|
|
16109
|
+
} else if (key.source) {
|
|
16110
|
+
console.log(`[AMT] No relay configured, discovering via DRIAD for source ${key.source}...`);
|
|
16111
|
+
const relays = await this.driad.discoverRelays(key.source);
|
|
16112
|
+
const bestRelay = await this.driad.selectBestRelay(relays);
|
|
16113
|
+
if (!bestRelay) {
|
|
16114
|
+
throw new Error(`AMT relay discovery failed for source ${key.source}. Please specify a relay address in the subscription configuration.`);
|
|
16115
|
+
}
|
|
16116
|
+
relayAddress = bestRelay.host;
|
|
16117
|
+
discoveryMethod = "driad";
|
|
16118
|
+
console.log(`[AMT] Discovered relay via DRIAD: ${relayAddress}:${relayPort}`);
|
|
16119
|
+
} else {
|
|
16120
|
+
throw new Error(`AMT relay discovery requires a source address for SSM (RFC 8777). Please specify either a relay address or source address.`);
|
|
16121
|
+
}
|
|
16122
|
+
const subscriptionId = keyStr;
|
|
16123
|
+
this.pendingPackets.set(subscriptionId, []);
|
|
16124
|
+
const subscription = {
|
|
16125
|
+
key,
|
|
16126
|
+
subscriptionId,
|
|
16127
|
+
config,
|
|
16128
|
+
handshakeComplete: false,
|
|
16129
|
+
relay: {
|
|
16130
|
+
address: relayAddress,
|
|
16131
|
+
port: relayPort,
|
|
16132
|
+
discoveryMethod,
|
|
16133
|
+
discoveredAt: Date.now()
|
|
16134
|
+
}
|
|
16135
|
+
};
|
|
16136
|
+
this.subscriptions.set(keyStr, subscription);
|
|
16137
|
+
console.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);
|
|
16138
|
+
return keyStr;
|
|
16139
|
+
} catch (error) {
|
|
16140
|
+
const subscription = this.subscriptions.get(keyStr);
|
|
16141
|
+
if (subscription) {
|
|
16142
|
+
this.subscriptions.delete(keyStr);
|
|
16143
|
+
this.pendingPackets.delete(subscription.subscriptionId);
|
|
16144
|
+
}
|
|
16145
|
+
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
16146
|
+
}
|
|
16147
|
+
}
|
|
16148
|
+
/**
|
|
16149
|
+
* Unsubscribe from AMT tunnel
|
|
16150
|
+
*/
|
|
16151
|
+
async unsubscribe(handle) {
|
|
16152
|
+
const subscription = this.subscriptions.get(handle);
|
|
16153
|
+
if (!subscription) {
|
|
16154
|
+
throw new Error(`Subscription ${handle} not found`);
|
|
16155
|
+
}
|
|
16156
|
+
try {
|
|
16157
|
+
if (subscription.keepAliveTimer) {
|
|
16158
|
+
clearInterval(subscription.keepAliveTimer);
|
|
16159
|
+
}
|
|
16160
|
+
console.log(`[AMT] Unsubscribed from ${handle}`);
|
|
16161
|
+
} catch (error) {
|
|
16162
|
+
console.error("[AMT] Error during unsubscribe:", error);
|
|
16163
|
+
} finally {
|
|
16164
|
+
this.subscriptions.delete(handle);
|
|
16165
|
+
this.pendingPackets.delete(subscription.subscriptionId);
|
|
16166
|
+
}
|
|
16167
|
+
}
|
|
16168
|
+
/**
|
|
16169
|
+
* Get list of active subscriptions
|
|
16170
|
+
*/
|
|
16171
|
+
getSubscriptions() {
|
|
16172
|
+
return Array.from(this.subscriptions.keys());
|
|
16173
|
+
}
|
|
16174
|
+
/**
|
|
16175
|
+
* Handle packet from service worker
|
|
16176
|
+
*/
|
|
16177
|
+
handlePacket(message) {
|
|
16178
|
+
const { subscriptionId, data } = message;
|
|
16179
|
+
if (!subscriptionId)
|
|
16180
|
+
return;
|
|
16181
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
16182
|
+
if (!subscription) {
|
|
16183
|
+
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
16184
|
+
return;
|
|
16185
|
+
}
|
|
16186
|
+
if (!subscription.handshakeComplete) {
|
|
16187
|
+
const queue = this.pendingPackets.get(subscriptionId);
|
|
16188
|
+
const packetData2 = data;
|
|
16189
|
+
if (queue) {
|
|
16190
|
+
queue.push({
|
|
16191
|
+
data: packetData2.data,
|
|
16192
|
+
remoteAddress: packetData2.remoteAddress,
|
|
16193
|
+
remotePort: packetData2.remotePort
|
|
16194
|
+
});
|
|
16195
|
+
}
|
|
16196
|
+
return;
|
|
16197
|
+
}
|
|
16198
|
+
const packetData = data;
|
|
16199
|
+
const metadata = {
|
|
16200
|
+
sourceAddress: subscription.relay?.address || "unknown",
|
|
16201
|
+
sourcePort: subscription.relay?.port || 0,
|
|
16202
|
+
transport: "tunnel",
|
|
16203
|
+
timestamp: performance.now()
|
|
16204
|
+
};
|
|
16205
|
+
subscription.config.onPacket(packetData.data, metadata);
|
|
16206
|
+
}
|
|
16207
|
+
/**
|
|
16208
|
+
* Handle socket error from service worker
|
|
16209
|
+
*/
|
|
16210
|
+
handleSocketError(message) {
|
|
16211
|
+
const { subscriptionId, error } = message;
|
|
16212
|
+
if (!subscriptionId)
|
|
16213
|
+
return;
|
|
16214
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
16215
|
+
if (!subscription) {
|
|
16216
|
+
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
16217
|
+
return;
|
|
16218
|
+
}
|
|
16219
|
+
subscription.config.onError(new Error(error || "Unknown error"));
|
|
16220
|
+
}
|
|
16221
|
+
/**
|
|
16222
|
+
* Cleanup on destroy
|
|
16223
|
+
*/
|
|
16224
|
+
destroy() {
|
|
16225
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
16226
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
16227
|
+
}
|
|
16228
|
+
const handles = Array.from(this.subscriptions.keys());
|
|
16229
|
+
for (const handle of handles) {
|
|
16230
|
+
this.unsubscribe(handle).catch((err) => {
|
|
16231
|
+
console.error(`[AMT] Error unsubscribing ${handle}:`, err);
|
|
16232
|
+
});
|
|
16233
|
+
}
|
|
16234
|
+
}
|
|
16235
|
+
/**
|
|
16236
|
+
* Get key string for HashMap
|
|
16237
|
+
*/
|
|
16238
|
+
getKeyString(key) {
|
|
16239
|
+
const source = key.source ? `@${key.source}` : "";
|
|
16240
|
+
return `${key.group}:${key.port}${source}`;
|
|
16241
|
+
}
|
|
16242
|
+
};
|
|
16243
|
+
var amtGatewayManager = new AMTGatewayManager();
|
|
16244
|
+
|
|
16245
|
+
// src/multicast-transport.ts
|
|
16246
|
+
var MAX_TRACKED_DROPPED_PIDS = 16;
|
|
16247
|
+
function getMulticastGlobals() {
|
|
16248
|
+
return globalThis;
|
|
16249
|
+
}
|
|
16250
|
+
function hasMulticastGateway() {
|
|
16251
|
+
return typeof getMulticastGlobals().multicast?.subscribe === "function";
|
|
16252
|
+
}
|
|
16253
|
+
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;
|
|
16254
|
+
var _MulticastTransport = class _MulticastTransport {
|
|
16255
|
+
constructor(onPacket) {
|
|
16256
|
+
__privateAdd(this, _MulticastTransport_instances);
|
|
16257
|
+
__privateAdd(this, _onPacket);
|
|
16258
|
+
__privateAdd(this, _subscription2, null);
|
|
16259
|
+
__privateAdd(this, _bytesListener, null);
|
|
16260
|
+
__privateAdd(this, _resolvedTransport);
|
|
16261
|
+
__privateAdd(this, _videoConfirmed, false);
|
|
16262
|
+
__privateAdd(this, _audioConfirmed, false);
|
|
16263
|
+
__privateAdd(this, _watchdog, null);
|
|
16264
|
+
__privateAdd(this, _failbackWindowMs, null);
|
|
16265
|
+
__privateAdd(this, _onTakeover, null);
|
|
16266
|
+
__privateAdd(this, _onFailback, null);
|
|
16267
|
+
__privateAdd(this, _disposed4, false);
|
|
16268
|
+
__privateAdd(this, _stats3, {
|
|
16269
|
+
packetsReceived: 0,
|
|
16270
|
+
bytesReceived: 0,
|
|
16271
|
+
packetsDroppedUnregisteredPid: 0,
|
|
16272
|
+
packetsDroppedUnparseable: 0
|
|
16273
|
+
});
|
|
16274
|
+
__privateAdd(this, _droppedPidCounts, /* @__PURE__ */ new Map());
|
|
16275
|
+
__privateAdd(this, _packetIds, null);
|
|
16276
|
+
__privateAdd(this, _joinGeneration, 0);
|
|
16277
|
+
__privateSet(this, _onPacket, onPacket);
|
|
16278
|
+
}
|
|
16279
|
+
static isAvailable() {
|
|
16280
|
+
return hasMulticastGateway();
|
|
16281
|
+
}
|
|
16282
|
+
async join(config, context) {
|
|
16283
|
+
const generation = ++__privateWrapper(this, _joinGeneration)._;
|
|
16284
|
+
__privateMethod(this, _MulticastTransport_instances, releaseSubscription_fn).call(this);
|
|
14381
16285
|
if (__privateGet(this, _disposed4)) return false;
|
|
14382
16286
|
if (!_MulticastTransport.isAvailable()) {
|
|
14383
16287
|
console.warn("[MulticastTransport] Multicast API not available");
|
|
@@ -14388,17 +16292,17 @@ var _MulticastTransport = class _MulticastTransport {
|
|
|
14388
16292
|
console.log(
|
|
14389
16293
|
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress}, ${config.groupAddress}, ${config.port})`
|
|
14390
16294
|
);
|
|
14391
|
-
const
|
|
16295
|
+
const subscribeConfig = toWindowMulticastSubscribeConfig({
|
|
14392
16296
|
source: config.sourceAddress,
|
|
14393
16297
|
group: config.groupAddress,
|
|
14394
16298
|
port: config.port,
|
|
14395
|
-
consume: "bytes",
|
|
14396
16299
|
mode: config.mode,
|
|
14397
16300
|
amtRelay: config.amtRelay,
|
|
14398
16301
|
amtRelayPort: config.amtRelayPort,
|
|
14399
16302
|
packetIds: config.packetIds,
|
|
14400
|
-
tracks: config.tracks
|
|
16303
|
+
tracks: config.tracks
|
|
14401
16304
|
});
|
|
16305
|
+
const subscription = await getMulticastGlobals().multicast.subscribe(subscribeConfig);
|
|
14402
16306
|
console.log(`[MulticastTransport] window.multicast.subscribe returned:`, !!subscription);
|
|
14403
16307
|
if (__privateGet(this, _disposed4) || generation !== __privateGet(this, _joinGeneration)) {
|
|
14404
16308
|
void subscription.unsubscribe();
|
|
@@ -14420,18 +16324,11 @@ var _MulticastTransport = class _MulticastTransport {
|
|
|
14420
16324
|
__privateGet(this, _onPacket).call(this, packet, context);
|
|
14421
16325
|
__privateMethod(this, _MulticastTransport_instances, resetWatchdog_fn).call(this);
|
|
14422
16326
|
};
|
|
14423
|
-
const onTransportChange = (event) => {
|
|
14424
|
-
if (__privateGet(this, _disposed4) || generation !== __privateGet(this, _joinGeneration)) return;
|
|
14425
|
-
const transport = normalizeResolvedTransport(event.detail);
|
|
14426
|
-
if (transport) __privateSet(this, _resolvedTransport, transport);
|
|
14427
|
-
};
|
|
14428
16327
|
__privateSet(this, _subscription2, subscription);
|
|
14429
16328
|
__privateSet(this, _bytesListener, onBytes);
|
|
14430
|
-
__privateSet(this, _transportListener, onTransportChange);
|
|
14431
16329
|
__privateSet(this, _packetIds, packetIds);
|
|
14432
|
-
__privateSet(this, _resolvedTransport,
|
|
16330
|
+
__privateSet(this, _resolvedTransport, subscribeConfig.origin === "amt" ? "amt" : "native");
|
|
14433
16331
|
subscription.addEventListener("bytes", onBytes);
|
|
14434
|
-
subscription.addEventListener("transportchange", onTransportChange);
|
|
14435
16332
|
__privateMethod(this, _MulticastTransport_instances, resetWatchdog_fn).call(this);
|
|
14436
16333
|
console.log(
|
|
14437
16334
|
`[MulticastTransport] Joined ${config.groupAddress}:${config.port}`
|
|
@@ -14499,7 +16396,6 @@ var _MulticastTransport = class _MulticastTransport {
|
|
|
14499
16396
|
_onPacket = new WeakMap();
|
|
14500
16397
|
_subscription2 = new WeakMap();
|
|
14501
16398
|
_bytesListener = new WeakMap();
|
|
14502
|
-
_transportListener = new WeakMap();
|
|
14503
16399
|
_resolvedTransport = new WeakMap();
|
|
14504
16400
|
_videoConfirmed = new WeakMap();
|
|
14505
16401
|
_audioConfirmed = new WeakMap();
|
|
@@ -14520,14 +16416,10 @@ releaseSubscription_fn = function() {
|
|
|
14520
16416
|
if (__privateGet(this, _bytesListener)) {
|
|
14521
16417
|
__privateGet(this, _subscription2).removeEventListener("bytes", __privateGet(this, _bytesListener));
|
|
14522
16418
|
}
|
|
14523
|
-
if (__privateGet(this, _transportListener)) {
|
|
14524
|
-
__privateGet(this, _subscription2).removeEventListener("transportchange", __privateGet(this, _transportListener));
|
|
14525
|
-
}
|
|
14526
16419
|
void __privateGet(this, _subscription2).unsubscribe();
|
|
14527
16420
|
}
|
|
14528
16421
|
__privateSet(this, _subscription2, null);
|
|
14529
16422
|
__privateSet(this, _bytesListener, null);
|
|
14530
|
-
__privateSet(this, _transportListener, null);
|
|
14531
16423
|
__privateSet(this, _resolvedTransport, void 0);
|
|
14532
16424
|
__privateSet(this, _packetIds, null);
|
|
14533
16425
|
};
|
|
@@ -14603,12 +16495,22 @@ function createDefaultRepairLifecycleEvent(detail, options) {
|
|
|
14603
16495
|
function isRecord(value) {
|
|
14604
16496
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14605
16497
|
}
|
|
14606
|
-
function
|
|
16498
|
+
function isRepairStreamIdentity(value) {
|
|
14607
16499
|
if (!isRecord(value)) return false;
|
|
14608
16500
|
const packetId = value["packetId"];
|
|
14609
16501
|
const catalogTrackName = value["catalogTrackName"];
|
|
16502
|
+
return Number.isSafeInteger(packetId) && packetId >= 0 && (catalogTrackName === null || typeof catalogTrackName === "string" && catalogTrackName.length > 0);
|
|
16503
|
+
}
|
|
16504
|
+
function isRepairBlockIdentity(value) {
|
|
16505
|
+
if (!isRecord(value)) return false;
|
|
14610
16506
|
const sbn = value["sbn"];
|
|
14611
|
-
return
|
|
16507
|
+
return isRepairStreamIdentity(value) && Number.isSafeInteger(sbn) && sbn >= 0;
|
|
16508
|
+
}
|
|
16509
|
+
function isDecoderUnavailableReason(value) {
|
|
16510
|
+
return value === "cleared" || value === "disposed";
|
|
16511
|
+
}
|
|
16512
|
+
function isFecFallbackDecoder(value) {
|
|
16513
|
+
return value === "sync" || value === "none";
|
|
14612
16514
|
}
|
|
14613
16515
|
function isRepairLifecycleEvent(value) {
|
|
14614
16516
|
if (!isRecord(value)) return false;
|
|
@@ -14616,7 +16518,11 @@ function isRepairLifecycleEvent(value) {
|
|
|
14616
16518
|
const type = value["type"];
|
|
14617
16519
|
const block = value["block"];
|
|
14618
16520
|
const symbols = value["symbols"];
|
|
14619
|
-
if (version !== 1
|
|
16521
|
+
if (version !== 1) return false;
|
|
16522
|
+
if (type === "decoder-unavailable") {
|
|
16523
|
+
return isRepairStreamIdentity(value["stream"]) && isDecoderUnavailableReason(value["reason"]) && isFecFallbackDecoder(value["fallbackDecoder"]) && block === void 0 && symbols === void 0;
|
|
16524
|
+
}
|
|
16525
|
+
if (type !== "requested" && type !== "recovered" && type !== "failed" || !isRepairBlockIdentity(block) || value["stream"] !== void 0 || value["reason"] !== void 0 || value["fallbackDecoder"] !== void 0) {
|
|
14620
16526
|
return false;
|
|
14621
16527
|
}
|
|
14622
16528
|
if (type === "requested") return symbols === void 0;
|
|
@@ -14635,22 +16541,25 @@ function forwardRepairLifecycleEvents(source, target, options = {}) {
|
|
|
14635
16541
|
target.dispatchEvent(forwardedEvent);
|
|
14636
16542
|
});
|
|
14637
16543
|
}
|
|
14638
|
-
function
|
|
16544
|
+
function repairStreamIdentity(packetId, catalogTrackName) {
|
|
14639
16545
|
if (!Number.isSafeInteger(packetId) || packetId < 0) {
|
|
14640
16546
|
throw new TypeError("packetId must be a non-negative safe integer");
|
|
14641
16547
|
}
|
|
14642
|
-
if (!Number.isSafeInteger(sbn) || sbn < 0) {
|
|
14643
|
-
throw new TypeError("sbn must be a non-negative safe integer");
|
|
14644
|
-
}
|
|
14645
16548
|
if (catalogTrackName !== void 0 && (typeof catalogTrackName !== "string" || catalogTrackName.length === 0)) {
|
|
14646
16549
|
throw new TypeError("catalogTrackName must be a non-empty string when provided");
|
|
14647
16550
|
}
|
|
14648
16551
|
return {
|
|
14649
16552
|
packetId,
|
|
14650
|
-
catalogTrackName: catalogTrackName ?? null
|
|
14651
|
-
sbn
|
|
16553
|
+
catalogTrackName: catalogTrackName ?? null
|
|
14652
16554
|
};
|
|
14653
16555
|
}
|
|
16556
|
+
function repairBlockIdentity(packetId, sbn, catalogTrackName) {
|
|
16557
|
+
const stream = repairStreamIdentity(packetId, catalogTrackName);
|
|
16558
|
+
if (!Number.isSafeInteger(sbn) || sbn < 0) {
|
|
16559
|
+
throw new TypeError("sbn must be a non-negative safe integer");
|
|
16560
|
+
}
|
|
16561
|
+
return { ...stream, sbn };
|
|
16562
|
+
}
|
|
14654
16563
|
function assertPositiveGeometry(label, value) {
|
|
14655
16564
|
if (value === void 0) return;
|
|
14656
16565
|
if (!Number.isFinite(value) || value <= 0) {
|
|
@@ -14820,6 +16729,15 @@ var MulticastManager = class {
|
|
|
14820
16729
|
}
|
|
14821
16730
|
});
|
|
14822
16731
|
};
|
|
16732
|
+
__privateGet(this, _fec).onDecoderUnavailable = (event) => {
|
|
16733
|
+
__privateMethod(this, _MulticastManager_instances, emitRepairLifecycle_fn).call(this, {
|
|
16734
|
+
version: 1,
|
|
16735
|
+
type: "decoder-unavailable",
|
|
16736
|
+
stream: repairStreamIdentity(event.packetId, event.catalogTrackName),
|
|
16737
|
+
reason: event.reason,
|
|
16738
|
+
fallbackDecoder: event.fallbackDecoder
|
|
16739
|
+
});
|
|
16740
|
+
};
|
|
14823
16741
|
}
|
|
14824
16742
|
// ---------------------------------------------------------------------------
|
|
14825
16743
|
// Decomposed component accessors (for new code)
|
|
@@ -14874,6 +16792,17 @@ var MulticastManager = class {
|
|
|
14874
16792
|
__privateMethod(this, _MulticastManager_instances, configureTrackMapper_fn).call(this, packetId, config);
|
|
14875
16793
|
__privateGet(this, _router).setPacketIdType(packetId, trackType);
|
|
14876
16794
|
}
|
|
16795
|
+
/**
|
|
16796
|
+
* Stop routing a stream's symbols to its Worker decoder. Returns false when
|
|
16797
|
+
* none is registered; on success one `decoder-unavailable` repair lifecycle
|
|
16798
|
+
* event with reason 'cleared' reaches every observer, carrying whether a
|
|
16799
|
+
* synchronous WASM decoder still serves the stream. The packet mapping and
|
|
16800
|
+
* track type are untouched: the track still exists, it just has no Worker
|
|
16801
|
+
* decoder. Call this before disposing the client.
|
|
16802
|
+
*/
|
|
16803
|
+
clearFecWorker(packetId, catalogTrackName) {
|
|
16804
|
+
return __privateGet(this, _fec).clearFecWorker(packetId, catalogTrackName);
|
|
16805
|
+
}
|
|
14877
16806
|
/** Register callback for Worker-level FecTrackStats updates. */
|
|
14878
16807
|
setOnFecStatsUpdate(cb) {
|
|
14879
16808
|
__privateGet(this, _fec).onFecStatsUpdate = cb;
|
|
@@ -14919,8 +16848,9 @@ var MulticastManager = class {
|
|
|
14919
16848
|
repairSymbols: config.fecRepairSymbols,
|
|
14920
16849
|
symbolSize: config.fecSymbolSize,
|
|
14921
16850
|
mediaUnitsPerSecond: config.fps,
|
|
14922
|
-
bandwidthBps: config.bandwidth
|
|
14923
|
-
|
|
16851
|
+
bandwidthBps: config.bandwidth,
|
|
16852
|
+
jitterBufferMs: config.fecReorderToleranceMs
|
|
16853
|
+
}) : void 0;
|
|
14924
16854
|
const fecConfig = {
|
|
14925
16855
|
k: config.fecK,
|
|
14926
16856
|
symbolSize: config.fecSymbolSize,
|
|
@@ -14957,8 +16887,9 @@ var MulticastManager = class {
|
|
|
14957
16887
|
repairSymbols: config.fecRepairSymbols,
|
|
14958
16888
|
symbolSize: config.fecSymbolSize,
|
|
14959
16889
|
mediaUnitsPerSecond: config.fps,
|
|
14960
|
-
bandwidthBps: config.bandwidth
|
|
14961
|
-
|
|
16890
|
+
bandwidthBps: config.bandwidth,
|
|
16891
|
+
jitterBufferMs: config.fecReorderToleranceMs
|
|
16892
|
+
}) : void 0;
|
|
14962
16893
|
__privateGet(this, _fec).configure({
|
|
14963
16894
|
k: config.fecK,
|
|
14964
16895
|
symbolSize: config.fecSymbolSize,
|
|
@@ -15475,11 +17406,13 @@ applyStreamGlobalFecTiming_fn = function() {
|
|
|
15475
17406
|
};
|
|
15476
17407
|
_onTrackTypeDiscovered = new WeakMap();
|
|
15477
17408
|
emitRepairLifecycle_fn = function(event) {
|
|
15478
|
-
const immutableEvent = Object.freeze(
|
|
15479
|
-
...event,
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
17409
|
+
const immutableEvent = Object.freeze(
|
|
17410
|
+
event.type === "decoder-unavailable" ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
17411
|
+
...event,
|
|
17412
|
+
block: Object.freeze({ ...event.block }),
|
|
17413
|
+
...event.type === "requested" ? {} : { symbols: Object.freeze({ ...event.symbols }) }
|
|
17414
|
+
}
|
|
17415
|
+
);
|
|
15483
17416
|
console.info(
|
|
15484
17417
|
`${REPAIR_LIFECYCLE_LOG_PREFIX}${JSON.stringify(immutableEvent)}`
|
|
15485
17418
|
);
|
|
@@ -15503,7 +17436,8 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
15503
17436
|
sourceSymbols: opts.fecK,
|
|
15504
17437
|
repairSymbols: opts.fecRepairCount,
|
|
15505
17438
|
symbolSize: opts.fecSymbolSize,
|
|
15506
|
-
mediaUnitsPerSecond: 1e3 / opts.mediaUnitDurationMs
|
|
17439
|
+
mediaUnitsPerSecond: 1e3 / opts.mediaUnitDurationMs,
|
|
17440
|
+
jitterBufferMs: opts.fecReorderToleranceMs
|
|
15507
17441
|
});
|
|
15508
17442
|
const origin = typeof location !== "undefined" && location.origin ? location.origin : "http://localhost";
|
|
15509
17443
|
const workerUrl = new URL(opts.assetUrls.fecWorker, origin);
|
|
@@ -15549,15 +17483,23 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
15549
17483
|
relayBlockSigEnabled: false
|
|
15550
17484
|
};
|
|
15551
17485
|
client.configure(fecTrackConfig, rqWasm, altaKeyBuffer, altaWasmModule);
|
|
15552
|
-
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
17486
|
+
try {
|
|
17487
|
+
opts.multicastManager.setFecWorker(opts.packetId, client, opts.trackType, {
|
|
17488
|
+
k: opts.fecK,
|
|
17489
|
+
symbolSize: opts.fecSymbolSize,
|
|
17490
|
+
interleaveDepthMs: opts.fecInterleaveMs,
|
|
17491
|
+
// FEC timing is expressed in media units. For audio this is the access-unit
|
|
17492
|
+
// cadence (for example 1024/48kHz), not a video-only framerate field.
|
|
17493
|
+
fps: 1e3 / opts.mediaUnitDurationMs,
|
|
17494
|
+
timeoutMs: deliveryWindowMs
|
|
17495
|
+
}, opts.trackName);
|
|
17496
|
+
} catch (e) {
|
|
17497
|
+
try {
|
|
17498
|
+
client.dispose();
|
|
17499
|
+
} catch {
|
|
17500
|
+
}
|
|
17501
|
+
throw e;
|
|
17502
|
+
}
|
|
15561
17503
|
let disposed = false;
|
|
15562
17504
|
return {
|
|
15563
17505
|
client,
|
|
@@ -15565,6 +17507,11 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
15565
17507
|
dispose: () => {
|
|
15566
17508
|
if (disposed) return;
|
|
15567
17509
|
disposed = true;
|
|
17510
|
+
if (!opts.multicastManager.clearFecWorker(opts.packetId, opts.trackName)) {
|
|
17511
|
+
console.warn(
|
|
17512
|
+
`[fec-worker-setup] packetId=${opts.packetId} track=${opts.trackName} Worker was not registered when disposed`
|
|
17513
|
+
);
|
|
17514
|
+
}
|
|
15568
17515
|
try {
|
|
15569
17516
|
client.dispose();
|
|
15570
17517
|
} catch {
|
|
@@ -16448,7 +18395,7 @@ _readIndex = new WeakMap();
|
|
|
16448
18395
|
_stalled = new WeakMap();
|
|
16449
18396
|
|
|
16450
18397
|
// src/mmtp-decode-pipeline.ts
|
|
16451
|
-
var _parser2, _fecClient, _frameCb2, _initCb2, _needsKeyframe, _codec, _gateDropped, _gateEmitted, _fedObjects, _warnedNoParser, _feedOutcomes, _MmtpDecodePipeline_instances, gatedEmit_fn;
|
|
18398
|
+
var _parser2, _fecClient, _frameCb2, _initCb2, _needsKeyframe, _codec, _trackType, _gateDropped, _gateEmitted, _fedObjects, _warnedNoParser, _feedOutcomes, _MmtpDecodePipeline_instances, tag_fn, gatedEmit_fn;
|
|
16452
18399
|
var MmtpDecodePipeline = class {
|
|
16453
18400
|
constructor() {
|
|
16454
18401
|
__privateAdd(this, _MmtpDecodePipeline_instances);
|
|
@@ -16457,7 +18404,11 @@ var MmtpDecodePipeline = class {
|
|
|
16457
18404
|
__privateAdd(this, _frameCb2, null);
|
|
16458
18405
|
__privateAdd(this, _initCb2, null);
|
|
16459
18406
|
__privateAdd(this, _needsKeyframe, false);
|
|
16460
|
-
__privateAdd(this, _codec,
|
|
18407
|
+
__privateAdd(this, _codec, null);
|
|
18408
|
+
/** Set together with #codec at configure(). Also the diagnostic label: a
|
|
18409
|
+
* player builds one pipeline per track, and codec alone does not say which
|
|
18410
|
+
* role a line belongs to at a glance. */
|
|
18411
|
+
__privateAdd(this, _trackType, null);
|
|
16461
18412
|
/** Diagnostic counters. The keyframe gate drops silently, so a parser that
|
|
16462
18413
|
* never marks a frame as a keyframe stalls the whole pipeline with no
|
|
16463
18414
|
* output anywhere: frames are fed, frames are parsed, and nothing is
|
|
@@ -16486,6 +18437,7 @@ var MmtpDecodePipeline = class {
|
|
|
16486
18437
|
__privateSet(this, _warnedNoParser, false);
|
|
16487
18438
|
__privateSet(this, _codec, config.codec);
|
|
16488
18439
|
const trackType = isVideoCodec(config.codec) ? "video" : "audio";
|
|
18440
|
+
__privateSet(this, _trackType, trackType);
|
|
16489
18441
|
const trackInfo = {
|
|
16490
18442
|
type: trackType,
|
|
16491
18443
|
codec: config.codec,
|
|
@@ -16529,6 +18481,24 @@ var MmtpDecodePipeline = class {
|
|
|
16529
18481
|
get reorderStats() {
|
|
16530
18482
|
return __privateGet(this, _parser2)?.reorderStats ?? null;
|
|
16531
18483
|
}
|
|
18484
|
+
/**
|
|
18485
|
+
* Feed tallies for this track. A fresh snapshot per call, not a live view.
|
|
18486
|
+
*
|
|
18487
|
+
* Unlike reorderStats this never returns null: the counters belong to the
|
|
18488
|
+
* pipeline rather than the parser, so they survive dispose(). That is
|
|
18489
|
+
* deliberate — a wedged pipeline is usually read after teardown, which is
|
|
18490
|
+
* exactly when the parser is already gone.
|
|
18491
|
+
*/
|
|
18492
|
+
get feedStats() {
|
|
18493
|
+
return {
|
|
18494
|
+
trackType: __privateGet(this, _trackType),
|
|
18495
|
+
codec: __privateGet(this, _codec),
|
|
18496
|
+
fedObjects: __privateGet(this, _fedObjects),
|
|
18497
|
+
emitted: __privateGet(this, _gateEmitted),
|
|
18498
|
+
gateDropped: __privateGet(this, _gateDropped),
|
|
18499
|
+
outcomes: Object.fromEntries(__privateGet(this, _feedOutcomes))
|
|
18500
|
+
};
|
|
18501
|
+
}
|
|
16532
18502
|
/**
|
|
16533
18503
|
* Feed a raw MoQ object payload (one MMTP packet per MoQ object).
|
|
16534
18504
|
* Optionally feeds source symbols to FEC client before parsing.
|
|
@@ -16537,33 +18507,37 @@ var MmtpDecodePipeline = class {
|
|
|
16537
18507
|
if (!__privateGet(this, _parser2)) {
|
|
16538
18508
|
if (!__privateGet(this, _warnedNoParser)) {
|
|
16539
18509
|
__privateSet(this, _warnedNoParser, true);
|
|
16540
|
-
console.warn(
|
|
18510
|
+
console.warn(`[mmtp-pipeline] feedMoqObject called with NO PARSER configured ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} \u2014 packet dropped`);
|
|
16541
18511
|
}
|
|
16542
18512
|
return;
|
|
16543
18513
|
}
|
|
16544
18514
|
__privateSet(this, _fedObjects, __privateGet(this, _fedObjects) + 1);
|
|
16545
18515
|
if (__privateGet(this, _fedObjects) === 1) {
|
|
16546
|
-
console.log(`[mmtp-pipeline] first object fed
|
|
18516
|
+
console.log(`[mmtp-pipeline] first object fed ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${packet.length}`);
|
|
16547
18517
|
}
|
|
16548
|
-
|
|
16549
|
-
|
|
16550
|
-
if (
|
|
16551
|
-
__privateGet(this,
|
|
18518
|
+
let outcomeKind = "threw";
|
|
18519
|
+
try {
|
|
18520
|
+
if (__privateGet(this, _fecClient)?.addSourceSymbol) {
|
|
18521
|
+
const info = __privateGet(this, _parser2).parseHeader(packet);
|
|
18522
|
+
if (info?.fecPayloadId) {
|
|
18523
|
+
__privateGet(this, _fecClient).addSourceSymbol(info.fecPayloadId.ssId, packet);
|
|
18524
|
+
}
|
|
18525
|
+
}
|
|
18526
|
+
const record = __privateGet(this, _parser2).feed(packet);
|
|
18527
|
+
outcomeKind = record?.kind ?? "null(parse-or-extract-failed)";
|
|
18528
|
+
} finally {
|
|
18529
|
+
const seen = (__privateGet(this, _feedOutcomes).get(outcomeKind) ?? 0) + 1;
|
|
18530
|
+
__privateGet(this, _feedOutcomes).set(outcomeKind, seen);
|
|
18531
|
+
if (seen === 1) {
|
|
18532
|
+
console.log(
|
|
18533
|
+
`[mmtp-pipeline] feed outcome FIRST '${outcomeKind}' ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} at object #${__privateGet(this, _fedObjects)} bytes=${packet.length}`
|
|
18534
|
+
);
|
|
16552
18535
|
}
|
|
16553
|
-
}
|
|
16554
|
-
const record = __privateGet(this, _parser2).feed(packet);
|
|
16555
|
-
const kind = record?.kind ?? "null(parse-or-extract-failed)";
|
|
16556
|
-
const seen = (__privateGet(this, _feedOutcomes).get(kind) ?? 0) + 1;
|
|
16557
|
-
__privateGet(this, _feedOutcomes).set(kind, seen);
|
|
16558
|
-
if (seen === 1) {
|
|
16559
|
-
console.log(
|
|
16560
|
-
`[mmtp-pipeline] feed outcome FIRST '${kind}' at object #${__privateGet(this, _fedObjects)} bytes=${packet.length}`
|
|
16561
|
-
);
|
|
16562
18536
|
}
|
|
16563
18537
|
if (__privateGet(this, _fedObjects) % 200 === 0) {
|
|
16564
18538
|
const hist = [...__privateGet(this, _feedOutcomes)].map(([k, v]) => `${k}=${v}`).join(" ");
|
|
16565
18539
|
console.log(
|
|
16566
|
-
`[mmtp-pipeline] feed histogram @${__privateGet(this, _fedObjects)}: ${hist} | emitted=${__privateGet(this, _gateEmitted)} gateDropped=${__privateGet(this, _gateDropped)}`
|
|
18540
|
+
`[mmtp-pipeline] feed histogram ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} @${__privateGet(this, _fedObjects)}: ${hist} | emitted=${__privateGet(this, _gateEmitted)} gateDropped=${__privateGet(this, _gateDropped)}`
|
|
16567
18541
|
);
|
|
16568
18542
|
}
|
|
16569
18543
|
}
|
|
@@ -16583,7 +18557,10 @@ var MmtpDecodePipeline = class {
|
|
|
16583
18557
|
...meta.mmtpTimestamp === void 0 ? {} : { mmtpTimestamp: meta.mmtpTimestamp },
|
|
16584
18558
|
keyframe: meta.keyframe,
|
|
16585
18559
|
packetId: meta.packetId,
|
|
16586
|
-
|
|
18560
|
+
// Same derivation as configure(), read from the field it set rather than
|
|
18561
|
+
// recomputed. Pre-configure the parser is null and routeFrame below is a
|
|
18562
|
+
// no-op, so the fallback is unreachable in practice.
|
|
18563
|
+
trackType: __privateGet(this, _trackType) ?? "audio",
|
|
16587
18564
|
packetSequenceNumber: meta.packetSequenceNumber
|
|
16588
18565
|
};
|
|
16589
18566
|
if (meta.sourceDiscontinuity) frameMeta.sourceDiscontinuity = true;
|
|
@@ -16609,7 +18586,7 @@ var MmtpDecodePipeline = class {
|
|
|
16609
18586
|
const droppedFrames = (__privateGet(this, _parser2)?.reorderStats.droppedOnLiveDispose ?? droppedBefore) - droppedBefore;
|
|
16610
18587
|
if (droppedFrames > 0) {
|
|
16611
18588
|
console.warn(
|
|
16612
|
-
`[MmtpDecodePipeline] live dispose dropped ${droppedFrames} buffered frame(s); finite inputs must use dispose({ flush: true })`
|
|
18589
|
+
`[MmtpDecodePipeline] live dispose ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} dropped ${droppedFrames} buffered frame(s); finite inputs must use dispose({ flush: true })`
|
|
16613
18590
|
);
|
|
16614
18591
|
}
|
|
16615
18592
|
__privateSet(this, _parser2, null);
|
|
@@ -16624,12 +18601,19 @@ _frameCb2 = new WeakMap();
|
|
|
16624
18601
|
_initCb2 = new WeakMap();
|
|
16625
18602
|
_needsKeyframe = new WeakMap();
|
|
16626
18603
|
_codec = new WeakMap();
|
|
18604
|
+
_trackType = new WeakMap();
|
|
16627
18605
|
_gateDropped = new WeakMap();
|
|
16628
18606
|
_gateEmitted = new WeakMap();
|
|
16629
18607
|
_fedObjects = new WeakMap();
|
|
16630
18608
|
_warnedNoParser = new WeakMap();
|
|
16631
18609
|
_feedOutcomes = new WeakMap();
|
|
16632
18610
|
_MmtpDecodePipeline_instances = new WeakSet();
|
|
18611
|
+
/** Identity prefix for diagnostics: one pipeline per track means two
|
|
18612
|
+
* instances write to the same console, and unlabelled lines interleave into
|
|
18613
|
+
* a single unreadable stream. */
|
|
18614
|
+
tag_fn = function() {
|
|
18615
|
+
return `track=${__privateGet(this, _trackType) ?? "unconfigured"} codec=${__privateGet(this, _codec) ?? "none"}`;
|
|
18616
|
+
};
|
|
16633
18617
|
gatedEmit_fn = function(data, meta) {
|
|
16634
18618
|
var _a;
|
|
16635
18619
|
let sourceDiscontinuity = false;
|
|
@@ -16638,7 +18622,7 @@ gatedEmit_fn = function(data, meta) {
|
|
|
16638
18622
|
__privateSet(this, _gateDropped, __privateGet(this, _gateDropped) + 1);
|
|
16639
18623
|
if (__privateGet(this, _gateDropped) <= 3 || __privateGet(this, _gateDropped) % 200 === 0) {
|
|
16640
18624
|
console.warn(
|
|
16641
|
-
`[mmtp-pipeline] keyframe gate DROP #${__privateGet(this, _gateDropped)}
|
|
18625
|
+
`[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)}`
|
|
16642
18626
|
);
|
|
16643
18627
|
}
|
|
16644
18628
|
return;
|
|
@@ -16646,12 +18630,12 @@ gatedEmit_fn = function(data, meta) {
|
|
|
16646
18630
|
__privateSet(this, _needsKeyframe, false);
|
|
16647
18631
|
sourceDiscontinuity = true;
|
|
16648
18632
|
console.log(
|
|
16649
|
-
`[mmtp-pipeline] keyframe gate OPENED
|
|
18633
|
+
`[mmtp-pipeline] keyframe gate OPENED ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} after ${__privateGet(this, _gateDropped)} drops, fed=${__privateGet(this, _fedObjects)}`
|
|
16650
18634
|
);
|
|
16651
18635
|
}
|
|
16652
18636
|
__privateSet(this, _gateEmitted, __privateGet(this, _gateEmitted) + 1);
|
|
16653
18637
|
if (__privateGet(this, _gateEmitted) === 1) {
|
|
16654
|
-
console.log(`[mmtp-pipeline] first frame emitted
|
|
18638
|
+
console.log(`[mmtp-pipeline] first frame emitted ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${data.length}`);
|
|
16655
18639
|
}
|
|
16656
18640
|
const outMeta = sourceDiscontinuity ? { ...meta, sourceDiscontinuity: true } : meta;
|
|
16657
18641
|
(_a = __privateGet(this, _frameCb2)) == null ? void 0 : _a.call(this, data, outMeta);
|
|
@@ -17325,12 +19309,140 @@ emit_fn = function(params) {
|
|
|
17325
19309
|
};
|
|
17326
19310
|
|
|
17327
19311
|
// src/catalog-fec-params.ts
|
|
19312
|
+
var MAX_SAFE_TICKS_PER_HZ = Number.MAX_SAFE_INTEGER / 1e3;
|
|
19313
|
+
function timescaleHz(value) {
|
|
19314
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
19315
|
+
}
|
|
19316
|
+
function catalogGroupDurationMs(rendition) {
|
|
19317
|
+
if (!rendition) return void 0;
|
|
19318
|
+
const ticks = numericField(rendition.groupDurationTicks);
|
|
19319
|
+
const ms = numericField(rendition.groupDurationMs);
|
|
19320
|
+
const timescale = timescaleHz(rendition.container?.kind === "mmtp" ? rendition.container.timescale : void 0) ?? timescaleHz(rendition.timescale);
|
|
19321
|
+
if (ticks !== void 0 && timescale !== void 0 && Number.isSafeInteger(ticks) && ticks <= timescale * MAX_SAFE_TICKS_PER_HZ) {
|
|
19322
|
+
return resolveGroupDurationMs({ groupDurationTicks: ticks }, timescale);
|
|
19323
|
+
}
|
|
19324
|
+
return ms;
|
|
19325
|
+
}
|
|
19326
|
+
function resolveCatalogTrackGroupDurationMs(catalog, track) {
|
|
19327
|
+
const renditions = track.type === "audio" ? catalog?.audio?.renditions : catalog?.video?.renditions;
|
|
19328
|
+
return catalogGroupDurationMs(renditions?.[track.name]);
|
|
19329
|
+
}
|
|
17328
19330
|
var NO_FEC = {
|
|
17329
19331
|
fecK: 0,
|
|
17330
19332
|
fecRepairCount: 0,
|
|
19333
|
+
fecTotalRepairCount: 0,
|
|
17331
19334
|
fecSymbolSize: 0,
|
|
17332
19335
|
fecInterleaveMs: 0
|
|
17333
19336
|
};
|
|
19337
|
+
function recordOf(value) {
|
|
19338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
19339
|
+
}
|
|
19340
|
+
function msfTracks(catalog) {
|
|
19341
|
+
const raw = catalog.tracks;
|
|
19342
|
+
if (!Array.isArray(raw)) return void 0;
|
|
19343
|
+
const common = recordOf(catalog.commonTrackFields) ?? {};
|
|
19344
|
+
return { tracks: raw.map((t) => recordOf(t) ?? {}), common };
|
|
19345
|
+
}
|
|
19346
|
+
function resolveRepairLayers(catalog, source) {
|
|
19347
|
+
if (!catalog) return void 0;
|
|
19348
|
+
const sourceName = typeof source === "string" ? source : source.name;
|
|
19349
|
+
const hang = catalog;
|
|
19350
|
+
const roles = typeof source === "string" ? ["video", "audio"] : source.type === "audio" ? ["audio"] : source.type === "video" ? ["video"] : ["video", "audio"];
|
|
19351
|
+
const role = roles.find((r) => recordOf(hang[r]?.renditions?.[sourceName]) !== void 0);
|
|
19352
|
+
if (role !== void 0) {
|
|
19353
|
+
const renditionPath = `$.${role}.renditions[${JSON.stringify(sourceName)}]`;
|
|
19354
|
+
const rendition = recordOf(hang[role].renditions[sourceName]);
|
|
19355
|
+
rejectRepairFieldsOnMediaTrack(renditionPath, rendition, throwRepairContractError);
|
|
19356
|
+
const repairRenditions = recordOf(hang.repair?.renditions) ?? {};
|
|
19357
|
+
const descriptors2 = Object.entries(repairRenditions).map(([name, entry]) => {
|
|
19358
|
+
const path = `$.repair.renditions[${JSON.stringify(name)}]`;
|
|
19359
|
+
const fields = recordOf(entry);
|
|
19360
|
+
if (fields === void 0) {
|
|
19361
|
+
throwRepairContractError("track_repair_layer", path, "a repair rendition must be an object");
|
|
19362
|
+
}
|
|
19363
|
+
rejectRepairTrackGeometry(path, fields, throwRepairContractError);
|
|
19364
|
+
return describeRepairTrack(name, path, fields);
|
|
19365
|
+
});
|
|
19366
|
+
const fecOfRendition = (entry) => {
|
|
19367
|
+
const fec2 = recordOf(recordOf(entry)?.["fec"]);
|
|
19368
|
+
return fec2 !== void 0 && fec2["algorithm"] !== "none" ? fec2 : void 0;
|
|
19369
|
+
};
|
|
19370
|
+
const mediaRenditions = [
|
|
19371
|
+
...Object.values(recordOf(hang.video?.renditions) ?? {}),
|
|
19372
|
+
...Object.values(recordOf(hang.audio?.renditions) ?? {})
|
|
19373
|
+
];
|
|
19374
|
+
const reachability2 = repairReachability(mediaRenditions.map((entry) => recordOf(entry)?.["fec"]));
|
|
19375
|
+
for (const d of descriptors2) {
|
|
19376
|
+
validateRepairTrackFields(d, throwRepairContractError);
|
|
19377
|
+
assertRepairTrackReachable(d, reachability2, throwRepairContractError);
|
|
19378
|
+
const target = dependsTarget(d);
|
|
19379
|
+
if (target === void 0) continue;
|
|
19380
|
+
const targetFec = fecOfRendition(hang.video?.renditions?.[target]) ?? fecOfRendition(hang.audio?.renditions?.[target]);
|
|
19381
|
+
if (targetFec === void 0) {
|
|
19382
|
+
throwRepairContractError(
|
|
19383
|
+
"track_repair_depends",
|
|
19384
|
+
`${d.path}.depends`,
|
|
19385
|
+
`${JSON.stringify(target)} is not a video or audio rendition with FEC`
|
|
19386
|
+
);
|
|
19387
|
+
}
|
|
19388
|
+
}
|
|
19389
|
+
return assembleRepairLayers(
|
|
19390
|
+
geometryOf(sourceName, `${renditionPath}.fec`, recordOf(rendition["fec"])),
|
|
19391
|
+
descriptors2,
|
|
19392
|
+
throwRepairContractError
|
|
19393
|
+
);
|
|
19394
|
+
}
|
|
19395
|
+
const msf = msfTracks(catalog);
|
|
19396
|
+
const msfIndex = msf ? msf.tracks.findIndex((t) => t["name"] === sourceName) : -1;
|
|
19397
|
+
if (!msf || msfIndex < 0) return void 0;
|
|
19398
|
+
const track = msf.tracks[msfIndex];
|
|
19399
|
+
const fec = recordOf(track["fec"]);
|
|
19400
|
+
const packagingOf = (t) => t["packaging"] ?? msf.common["packaging"];
|
|
19401
|
+
const fecOfTrack = (t) => {
|
|
19402
|
+
const trackFec = recordOf(t?.["fec"]);
|
|
19403
|
+
return trackFec !== void 0 && trackFec["algorithm"] !== "none" ? trackFec : void 0;
|
|
19404
|
+
};
|
|
19405
|
+
const descriptors = [];
|
|
19406
|
+
const mediaTracks = [];
|
|
19407
|
+
msf.tracks.forEach((t, i) => {
|
|
19408
|
+
const path = `$.tracks[${i}]`;
|
|
19409
|
+
if (packagingOf(t) === "fec-repair") {
|
|
19410
|
+
rejectRepairTrackGeometry(path, t, throwRepairContractError);
|
|
19411
|
+
if (typeof t["name"] === "string") descriptors.push(describeRepairTrack(t["name"], path, t));
|
|
19412
|
+
return;
|
|
19413
|
+
}
|
|
19414
|
+
rejectRepairFieldsOnMediaTrack(path, t, throwRepairContractError);
|
|
19415
|
+
mediaTracks.push(t);
|
|
19416
|
+
});
|
|
19417
|
+
const reachability = repairReachability(mediaTracks.map((t) => t["fec"]));
|
|
19418
|
+
for (const d of descriptors) {
|
|
19419
|
+
validateRepairTrackFields(d, throwRepairContractError);
|
|
19420
|
+
assertRepairTrackReachable(d, reachability, throwRepairContractError);
|
|
19421
|
+
const target = dependsTarget(d);
|
|
19422
|
+
if (target === void 0) continue;
|
|
19423
|
+
const targetTrack = msf.tracks.find((t) => t["name"] === target);
|
|
19424
|
+
if (targetTrack === void 0 || packagingOf(targetTrack) !== "mmtp" || fecOfTrack(targetTrack) === void 0) {
|
|
19425
|
+
throwRepairContractError(
|
|
19426
|
+
"track_repair_depends",
|
|
19427
|
+
`${d.path}.depends`,
|
|
19428
|
+
`${JSON.stringify(target)} is not an mmtp track with FEC`
|
|
19429
|
+
);
|
|
19430
|
+
}
|
|
19431
|
+
}
|
|
19432
|
+
return assembleRepairLayers(geometryOf(sourceName, `$.tracks[${msfIndex}].fec`, fec), descriptors, throwRepairContractError, {
|
|
19433
|
+
layerZeroMustBeDeclared: true
|
|
19434
|
+
});
|
|
19435
|
+
}
|
|
19436
|
+
function geometryOf(name, path, fec) {
|
|
19437
|
+
return {
|
|
19438
|
+
name,
|
|
19439
|
+
path,
|
|
19440
|
+
algorithm: fec?.["algorithm"],
|
|
19441
|
+
sourceSymbols: fec?.["sourceSymbols"],
|
|
19442
|
+
repairSymbols: fec?.["repairSymbols"],
|
|
19443
|
+
repairTrack: fec?.["repairTrack"]
|
|
19444
|
+
};
|
|
19445
|
+
}
|
|
17334
19446
|
function numericField(value) {
|
|
17335
19447
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
17336
19448
|
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
@@ -17339,13 +19451,16 @@ function nonNegativeField(value) {
|
|
|
17339
19451
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
17340
19452
|
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
17341
19453
|
}
|
|
17342
|
-
function fecParams(rendition, fps, sampleRate) {
|
|
19454
|
+
function fecParams(catalog, track, rendition, fps, sampleRate) {
|
|
17343
19455
|
const mediaRate = sampleRate === void 0 ? { fps } : { fps, sampleRate };
|
|
17344
19456
|
const fec = rendition?.fec;
|
|
17345
19457
|
if (!fec) return { ...NO_FEC, ...mediaRate };
|
|
19458
|
+
const repair = track ? resolveRepairLayers(catalog, track) : void 0;
|
|
17346
19459
|
return {
|
|
17347
19460
|
fecK: fec.sourceSymbols ?? 0,
|
|
17348
19461
|
fecRepairCount: fec.repairSymbols ?? 0,
|
|
19462
|
+
// The wire RSB_length: every base layer's packets carry the base total (§7.1).
|
|
19463
|
+
fecTotalRepairCount: repair?.totalBaseRepairSymbols ?? fec.repairSymbols ?? 0,
|
|
17349
19464
|
fecSymbolSize: fec.symbolSize ?? 0,
|
|
17350
19465
|
// interleaveDepthMs is the FEC block span in MILLISECONDS (§5.1) — the
|
|
17351
19466
|
// only interleave key the catalog carries post-BLO-14526. Receivers
|
|
@@ -17357,6 +19472,8 @@ function fecParams(rendition, fps, sampleRate) {
|
|
|
17357
19472
|
// so it cannot be reused here — a declared 0 is a legitimate value.
|
|
17358
19473
|
fecReorderToleranceMs: nonNegativeField(fec.reorderToleranceMs),
|
|
17359
19474
|
repairTrackName: fec.repairTrack,
|
|
19475
|
+
repairLayers: repair?.baseLayers,
|
|
19476
|
+
repairOverlays: repair?.overlays,
|
|
17360
19477
|
...mediaRate
|
|
17361
19478
|
};
|
|
17362
19479
|
}
|
|
@@ -17370,8 +19487,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
|
|
|
17370
19487
|
const catalogFps = numericField(video?.framerate);
|
|
17371
19488
|
const sampleRate = numericField(audio?.sampleRate ?? audio?.samplerate);
|
|
17372
19489
|
const audioFps = sampleRate ? sampleRate / 1024 : void 0;
|
|
17373
|
-
const videoParams = fecParams(video, catalogFps);
|
|
17374
|
-
const audioParams = fecParams(
|
|
19490
|
+
const videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: "video" } : void 0, video, catalogFps);
|
|
19491
|
+
const audioParams = fecParams(
|
|
19492
|
+
catalog,
|
|
19493
|
+
audioTrackName ? { name: audioTrackName, type: "audio" } : void 0,
|
|
19494
|
+
audio,
|
|
19495
|
+
audioFps,
|
|
19496
|
+
sampleRate
|
|
19497
|
+
);
|
|
17375
19498
|
return {
|
|
17376
19499
|
videoTrackName,
|
|
17377
19500
|
audioTrackName,
|
|
@@ -17534,6 +19657,7 @@ export {
|
|
|
17534
19657
|
CmafGroupBatcher,
|
|
17535
19658
|
CmcdProducer,
|
|
17536
19659
|
CmsfCatalogValidationError,
|
|
19660
|
+
DEFAULT_REPAIR_PRIORITY,
|
|
17537
19661
|
FECManager,
|
|
17538
19662
|
FEC_ESI_MAX,
|
|
17539
19663
|
FEC_SBN_MAX,
|
|
@@ -17541,6 +19665,10 @@ export {
|
|
|
17541
19665
|
FecStarvationDetector,
|
|
17542
19666
|
KEYFRAME_LOST_TIMEOUT_SLACK,
|
|
17543
19667
|
LOCHeaderExtensionId,
|
|
19668
|
+
MAX_ENCODING_SYMBOL_ID,
|
|
19669
|
+
MAX_RAPTORQ_SOURCE_SYMBOLS,
|
|
19670
|
+
MAX_REPAIR_SYMBOLS,
|
|
19671
|
+
MAX_TAKEOVER_DEADLINE_MS,
|
|
17544
19672
|
MFUReassembler,
|
|
17545
19673
|
MediaRouter,
|
|
17546
19674
|
MfuReassemblyError,
|
|
@@ -17555,7 +19683,12 @@ export {
|
|
|
17555
19683
|
NoneCodec,
|
|
17556
19684
|
REPAIR_LIFECYCLE_EVENT_TYPE,
|
|
17557
19685
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
19686
|
+
REPAIR_PRIORITY_MAX,
|
|
19687
|
+
REPAIR_PRIORITY_MIN,
|
|
19688
|
+
REPAIR_TRACK_GEOMETRY_KEYS,
|
|
19689
|
+
REPAIR_TRACK_ONLY_KEYS,
|
|
17558
19690
|
ReedSolomonCodec,
|
|
19691
|
+
RepairContractError,
|
|
17559
19692
|
SampleCombineError,
|
|
17560
19693
|
SampleCombiner,
|
|
17561
19694
|
SessionManager,
|
|
@@ -17566,11 +19699,14 @@ export {
|
|
|
17566
19699
|
WasmMfuReassembler,
|
|
17567
19700
|
annexBToAvcc,
|
|
17568
19701
|
asSeconds,
|
|
19702
|
+
assembleRepairLayers,
|
|
19703
|
+
assertRepairTrackReachable,
|
|
17569
19704
|
avccToAnnexB,
|
|
17570
19705
|
buildAudioInitSegment,
|
|
17571
19706
|
buildVideoInitSegment,
|
|
17572
19707
|
catalogAmtNetworkSource,
|
|
17573
19708
|
catalogFecInterleaveMs,
|
|
19709
|
+
catalogGroupDurationMs,
|
|
17574
19710
|
classifyH264Keyframe,
|
|
17575
19711
|
classifyPacket,
|
|
17576
19712
|
codecStringFromAvcC,
|
|
@@ -17585,6 +19721,8 @@ export {
|
|
|
17585
19721
|
decodeLocHeader,
|
|
17586
19722
|
decodeLocTimestamp,
|
|
17587
19723
|
decodeTimestamp,
|
|
19724
|
+
dependsTarget,
|
|
19725
|
+
describeRepairTrack,
|
|
17588
19726
|
detectH264Keyframe,
|
|
17589
19727
|
driftCheck,
|
|
17590
19728
|
encodeDataSegment,
|
|
@@ -17638,12 +19776,17 @@ export {
|
|
|
17638
19776
|
parseAacSampleRate,
|
|
17639
19777
|
parseOpusFrameSamples,
|
|
17640
19778
|
processMMTPWithReassembly,
|
|
19779
|
+
rejectRepairFieldsOnMediaTrack,
|
|
19780
|
+
rejectRepairTrackGeometry,
|
|
17641
19781
|
reliableMoqMfuReassembly,
|
|
17642
19782
|
removeEmulationPrevention,
|
|
19783
|
+
repairReachability,
|
|
17643
19784
|
resolveCatalogTrackFec,
|
|
19785
|
+
resolveCatalogTrackGroupDurationMs,
|
|
17644
19786
|
resolveGlobalMmtWasmBindings,
|
|
17645
19787
|
resolveGroupDurationMs,
|
|
17646
19788
|
resolveGroupDurationTicks,
|
|
19789
|
+
resolveRepairLayers,
|
|
17647
19790
|
secondsFromTicks,
|
|
17648
19791
|
setupFecWorkerForTrack,
|
|
17649
19792
|
setupTrackPipeline,
|
|
@@ -17653,6 +19796,7 @@ export {
|
|
|
17653
19796
|
unwrapMulticastPacket,
|
|
17654
19797
|
validateCmsfMmtpCatalog,
|
|
17655
19798
|
validateMulticastContainerContract,
|
|
19799
|
+
validateRepairTrackFields,
|
|
17656
19800
|
validateSwitchingSetTiming,
|
|
17657
19801
|
wireRepairFetch,
|
|
17658
19802
|
wrapAdtsFrame
|