@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.
Files changed (98) hide show
  1. package/dist/browser-runtime.js +2390 -246
  2. package/dist/browser-runtime.js.map +4 -4
  3. package/dist/carp-catalog.d.ts +7 -7
  4. package/dist/carp-catalog.d.ts.map +1 -1
  5. package/dist/carp-catalog.js +336 -12
  6. package/dist/carp-catalog.js.map +4 -4
  7. package/dist/catalog-fec-params.d.ts +105 -0
  8. package/dist/catalog-fec-params.d.ts.map +1 -1
  9. package/dist/catalog-fec-params.js +495 -4
  10. package/dist/catalog-fec-params.js.map +4 -4
  11. package/dist/cmaf-block-assembler.js.map +1 -1
  12. package/dist/cmaf-group-batcher.js.map +1 -1
  13. package/dist/fec-manager.d.ts +46 -1
  14. package/dist/fec-manager.d.ts.map +1 -1
  15. package/dist/fec-manager.js +129 -34
  16. package/dist/fec-manager.js.map +3 -3
  17. package/dist/fec-worker-setup.d.ts +6 -2
  18. package/dist/fec-worker-setup.d.ts.map +1 -1
  19. package/dist/fec-worker-setup.js +45 -36
  20. package/dist/fec-worker-setup.js.map +3 -3
  21. package/dist/index.d.ts +5 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +43 -1
  24. package/dist/index.js.map +2 -2
  25. package/dist/media-router.d.ts +12 -0
  26. package/dist/media-router.d.ts.map +1 -1
  27. package/dist/media-router.js +5 -19
  28. package/dist/media-router.js.map +2 -2
  29. package/dist/mfu-reassembler.d.ts +99 -1
  30. package/dist/mfu-reassembler.d.ts.map +1 -1
  31. package/dist/mfu-reassembler.js +95 -35
  32. package/dist/mfu-reassembler.js.map +2 -2
  33. package/dist/mmtp-container-parser.js +94 -35
  34. package/dist/mmtp-container-parser.js.map +2 -2
  35. package/dist/mmtp-container-parser.no-modules.js +432 -48
  36. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  37. package/dist/mmtp-decode-pipeline.d.ts +34 -0
  38. package/dist/mmtp-decode-pipeline.d.ts.map +1 -1
  39. package/dist/mmtp-decode-pipeline.js +155 -59
  40. package/dist/mmtp-decode-pipeline.js.map +3 -3
  41. package/dist/mmtp-router.js +94 -35
  42. package/dist/mmtp-router.js.map +2 -2
  43. package/dist/mmtp-to-cmaf.d.ts.map +1 -1
  44. package/dist/mmtp-to-cmaf.js +2 -1
  45. package/dist/mmtp-to-cmaf.js.map +2 -2
  46. package/dist/multicast-manager.d.ts +30 -3
  47. package/dist/multicast-manager.d.ts.map +1 -1
  48. package/dist/multicast-manager.js +1619 -112
  49. package/dist/multicast-manager.js.map +4 -4
  50. package/dist/multicast-transport.d.ts +1 -2
  51. package/dist/multicast-transport.d.ts.map +1 -1
  52. package/dist/multicast-transport.js +1335 -23
  53. package/dist/multicast-transport.js.map +4 -4
  54. package/dist/no-modules.js +432 -48
  55. package/dist/no-modules.js.map +4 -4
  56. package/dist/repair-layers.d.ts +141 -0
  57. package/dist/repair-layers.d.ts.map +1 -0
  58. package/dist/repair-layers.js +303 -0
  59. package/dist/repair-layers.js.map +7 -0
  60. package/dist/track-session.d.ts +64 -24
  61. package/dist/track-session.d.ts.map +1 -1
  62. package/dist/track-session.js +138 -65
  63. package/dist/track-session.js.map +2 -2
  64. package/dist/wasm-mfu-reassembler.js.map +2 -2
  65. package/dist/wasm-mmtp-container-parser.js +94 -35
  66. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  67. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +7 -0
  68. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -1
  69. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +14 -5
  70. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -1
  71. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +15 -5
  72. package/package.json +6 -6
  73. package/schemas/catalog.schema.json +82 -5
  74. package/src/carp-catalog.ts +80 -11
  75. package/src/catalog-fec-params.ts +342 -2
  76. package/src/fec-manager.ts +168 -18
  77. package/src/fec-worker-setup.ts +37 -11
  78. package/src/index.ts +38 -0
  79. package/src/media-router.ts +13 -6
  80. package/src/mfu-reassembler.ts +252 -38
  81. package/src/mmtp-decode-pipeline.ts +93 -24
  82. package/src/mmtp-to-cmaf.ts +25 -1
  83. package/src/multicast-manager.ts +106 -22
  84. package/src/multicast-transport.ts +13 -27
  85. package/src/repair-layers.ts +490 -0
  86. package/src/track-session.ts +201 -119
  87. package/test-vectors/catalog/README.md +16 -1
  88. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
  89. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
  90. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
  91. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
  92. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
  93. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
  94. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
  95. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  96. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +23 -0
  97. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +2 -1
  98. package/test-vectors/catalog/positive/fec-layered-repair.json +104 -0
@@ -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
- return;
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
- return;
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
- return;
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
- return;
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
- return;
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
- var _buffers, _timeouts, _activeDataUnits, _pendingContinuations, _emitted, _emittedOrder, _completed, _completedOrder, _timeoutMs, _maxBuffered, _nextAdmissionSeq, _MFUReassembler_instances, addAssociatedFragment_fn, parkContinuation_fn, drainPendingContinuations_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;
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 [address, pending] of __privateGet(this, _pendingContinuations)) {
1090
- if (pending.timeoutId !== void 0) clearTimeout(pending.timeoutId);
1091
- delete pending.timeoutId;
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
- let count = __privateGet(this, _buffers).size;
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
- if (pending === void 0) {
1327
- const open = __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address);
1328
- const awaitingFirst = open === void 0;
1329
- if (awaitingFirst && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
1330
- if (!__privateMethod(this, _MFUReassembler_instances, evictLeastRecentlyAdmitted_fn).call(this)) {
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
- const startedAt = Date.now();
1353
+ }
1354
+ let armAfterInsert = false;
1355
+ if (pending === void 0) {
1335
1356
  pending = {
1336
1357
  fragments: /* @__PURE__ */ new Map(),
1337
- startedAt,
1338
- admissionSeq: open === void 0 ? __privateWrapper(this, _nextAdmissionSeq)._++ : open[1].admissionSeq
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
- /** Every open run plus every headless parked data unit, one slot each.
1372
- * This is the quantity maxBuffered bounds. */
1373
- incompleteRunCount_fn = function() {
1374
- let count = __privateGet(this, _activeDataUnits).size;
1375
- for (const address of __privateGet(this, _pendingContinuations).keys()) {
1376
- if (__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) === void 0) count++;
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) continue;
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 = (parked?.fragments.size ?? 0) > 0;
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 positiveInteger = (input) => Number.isSafeInteger(input) && input > 0;
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 && !positiveInteger(groupDurationMs)) {
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 && !positiveInteger(groupDurationTicks)) {
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 && !positiveInteger(keyframeIntervalTicks)) {
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 (!positiveInteger(timescale)) {
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 (config["interleaveDepthMs"] !== void 0 && !positiveInteger(config["interleaveDepthMs"])) {
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 (!positiveInteger(config[field])) {
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
- const repairTrack = fec["repairTrack"];
5961
- const target = trackRecords.find((candidate) => candidate["name"] === repairTrack);
5962
- if (!target || effective(target, "packaging") !== "fec-repair") {
5963
- catalogError("track_repair_track", `$.tracks[${index}].fec.repairTrack`, `unknown FEC repair track ${JSON.stringify(repairTrack)}`);
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
- compositionTimeOffset
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
- const spansMs = [fecInterleaveMs];
8616
- if (config.mediaUnitsPerSecond && config.mediaUnitsPerSecond > 0) {
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
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
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
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
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(this.config.fecInterleaveMs, this.config.fecK);
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
- if (nextMode === "moq") {
8815
- __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
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
- if (this.config.trackType === "audio") {
8828
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
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 (to === "joining") __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this);
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(from) && !fecActive(to)) {
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
- const deadlineMs = __privateGet(this, _manager).takeoverDeadlineMs;
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. If a valid RAP arrives
8974
- * later, allow that late-RAP edge to suppress unicast and promote multicast.
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`) AND unsuppressed — unsubscribe() unwinds
9007
- * any outstanding suppression regardless of the state it tears down from
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
- if (session.state !== "idle" && session.state !== "unicast" && session.state !== "unicast-fallback") {
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, workerForPacket_fn).call(this, packetId, catalogTrackName);
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, workerForPacket_fn).call(this, packetId, catalogTrackName);
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, workerForPacket_fn).call(this, packetId, catalogTrackName);
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, _trackType, catalogTrackName) {
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
- // src/multicast-transport.ts
14336
- var MAX_TRACKED_DROPPED_PIDS = 16;
14337
- function getMulticastGlobals() {
14338
- return globalThis;
14339
- }
14340
- function hasMulticastGateway() {
14341
- return typeof getMulticastGlobals().multicast?.subscribe === "function";
14342
- }
14343
- function normalizeResolvedTransport(value) {
14344
- if (value === "native") return "native";
14345
- if (value === "amt") return "amt";
14346
- return void 0;
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
- async join(config, context) {
14379
- const generation = ++__privateWrapper(this, _joinGeneration)._;
14380
- __privateMethod(this, _MulticastTransport_instances, releaseSubscription_fn).call(this);
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 subscription = await getMulticastGlobals().multicast.subscribe({
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?.map(({ name, packetId }) => ({ name, packetId }))
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, normalizeResolvedTransport(subscription.transport));
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 isRepairBlockIdentity(value) {
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 Number.isSafeInteger(packetId) && packetId >= 0 && (catalogTrackName === null || typeof catalogTrackName === "string" && catalogTrackName.length > 0) && Number.isSafeInteger(sbn) && sbn >= 0;
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 || type !== "requested" && type !== "recovered" && type !== "failed" || !isRepairBlockIdentity(block)) {
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 repairBlockIdentity(packetId, sbn, catalogTrackName) {
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
- }) + (config.fecReorderToleranceMs ?? 0) : void 0;
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
- }) + (config.fecReorderToleranceMs ?? 0) : void 0;
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
- block: Object.freeze({ ...event.block }),
15481
- ...event.type === "requested" ? {} : { symbols: Object.freeze({ ...event.symbols }) }
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
- opts.multicastManager.setFecWorker(opts.packetId, client, opts.trackType, {
15553
- k: opts.fecK,
15554
- symbolSize: opts.fecSymbolSize,
15555
- interleaveDepthMs: opts.fecInterleaveMs,
15556
- // FEC timing is expressed in media units. For audio this is the access-unit
15557
- // cadence (for example 1024/48kHz), not a video-only framerate field.
15558
- fps: 1e3 / opts.mediaUnitDurationMs,
15559
- timeoutMs: deliveryWindowMs
15560
- }, opts.trackName);
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("[mmtp-pipeline] feedMoqObject called with NO PARSER configured \u2014 packet dropped");
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 codec=${__privateGet(this, _codec)} bytes=${packet.length}`);
18516
+ console.log(`[mmtp-pipeline] first object fed ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${packet.length}`);
16547
18517
  }
16548
- if (__privateGet(this, _fecClient)?.addSourceSymbol) {
16549
- const info = __privateGet(this, _parser2).parseHeader(packet);
16550
- if (info?.fecPayloadId) {
16551
- __privateGet(this, _fecClient).addSourceSymbol(info.fecPayloadId.ssId, packet);
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
- trackType: isVideoCodec(__privateGet(this, _codec)) ? "video" : "audio",
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)} codec=${__privateGet(this, _codec)} bytes=${data.length} fed=${__privateGet(this, _fedObjects)} emitted=${__privateGet(this, _gateEmitted)}`
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 codec=${__privateGet(this, _codec)} after ${__privateGet(this, _gateDropped)} drops, fed=${__privateGet(this, _fedObjects)}`
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 codec=${__privateGet(this, _codec)} bytes=${data.length}`);
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(audio, audioFps, sampleRate);
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