@blockcast/mmt-container 0.1.0-main.dc6b2132f53c → 0.1.0-main.dd5ce397d768

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