@blockcast/mmt-container 0.1.0-main.149f78a4adae → 0.1.0-main.1fa0d55ca4da

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 (102) hide show
  1. package/dist/browser-runtime.js +1859 -934
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/carp-catalog.d.ts +2 -5
  4. package/dist/carp-catalog.d.ts.map +1 -1
  5. package/dist/carp-catalog.js.map +2 -2
  6. package/dist/catalog-fec-params.d.ts +39 -0
  7. package/dist/catalog-fec-params.d.ts.map +1 -1
  8. package/dist/catalog-fec-params.js +70 -1
  9. package/dist/catalog-fec-params.js.map +3 -3
  10. package/dist/cmaf-block-assembler.d.ts +20 -2
  11. package/dist/cmaf-block-assembler.d.ts.map +1 -1
  12. package/dist/cmaf-block-assembler.js +12 -8
  13. package/dist/cmaf-block-assembler.js.map +2 -2
  14. package/dist/codec-utils.d.ts +13 -0
  15. package/dist/codec-utils.d.ts.map +1 -1
  16. package/dist/codec-utils.js +44 -1
  17. package/dist/codec-utils.js.map +2 -2
  18. package/dist/fec-manager.d.ts +46 -1
  19. package/dist/fec-manager.d.ts.map +1 -1
  20. package/dist/fec-manager.js +129 -34
  21. package/dist/fec-manager.js.map +3 -3
  22. package/dist/fec-worker-setup.d.ts +6 -2
  23. package/dist/fec-worker-setup.d.ts.map +1 -1
  24. package/dist/fec-worker-setup.js +45 -36
  25. package/dist/fec-worker-setup.js.map +3 -3
  26. package/dist/index.d.ts +7 -7
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +11 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +25 -0
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +75 -15
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.d.ts +12 -0
  35. package/dist/media-router.d.ts.map +1 -1
  36. package/dist/media-router.js +5 -19
  37. package/dist/media-router.js.map +2 -2
  38. package/dist/mfu-reassembler.d.ts +135 -7
  39. package/dist/mfu-reassembler.d.ts.map +1 -1
  40. package/dist/mfu-reassembler.js +1123 -758
  41. package/dist/mfu-reassembler.js.map +3 -3
  42. package/dist/mmtp-container-parser.d.ts +29 -0
  43. package/dist/mmtp-container-parser.d.ts.map +1 -1
  44. package/dist/mmtp-container-parser.js +1387 -786
  45. package/dist/mmtp-container-parser.js.map +3 -3
  46. package/dist/mmtp-container-parser.no-modules.js +1385 -787
  47. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  48. package/dist/mmtp-decode-pipeline.d.ts +36 -0
  49. package/dist/mmtp-decode-pipeline.d.ts.map +1 -1
  50. package/dist/mmtp-decode-pipeline.js +1452 -813
  51. package/dist/mmtp-decode-pipeline.js.map +3 -3
  52. package/dist/mmtp-router.d.ts +13 -6
  53. package/dist/mmtp-router.d.ts.map +1 -1
  54. package/dist/mmtp-router.js +1432 -800
  55. package/dist/mmtp-router.js.map +3 -3
  56. package/dist/multicast-manager.d.ts +30 -3
  57. package/dist/multicast-manager.d.ts.map +1 -1
  58. package/dist/multicast-manager.js +1622 -854
  59. package/dist/multicast-manager.js.map +3 -3
  60. package/dist/no-modules.js +1385 -787
  61. package/dist/no-modules.js.map +3 -3
  62. package/dist/repair-fetch.d.ts.map +1 -1
  63. package/dist/repair-fetch.js +10 -0
  64. package/dist/repair-fetch.js.map +2 -2
  65. package/dist/sample-combiner.d.ts +30 -0
  66. package/dist/sample-combiner.d.ts.map +1 -1
  67. package/dist/sample-combiner.js +47 -2
  68. package/dist/sample-combiner.js.map +2 -2
  69. package/dist/track-session.d.ts +64 -24
  70. package/dist/track-session.d.ts.map +1 -1
  71. package/dist/track-session.js +137 -65
  72. package/dist/track-session.js.map +2 -2
  73. package/dist/wasm-mfu-reassembler.d.ts +11 -0
  74. package/dist/wasm-mfu-reassembler.d.ts.map +1 -1
  75. package/dist/wasm-mfu-reassembler.js +803 -635
  76. package/dist/wasm-mfu-reassembler.js.map +2 -2
  77. package/dist/wasm-mmtp-container-parser.js +1385 -787
  78. package/dist/wasm-mmtp-container-parser.js.map +3 -3
  79. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +7 -0
  80. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -1
  81. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +14 -5
  82. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -1
  83. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +15 -5
  84. package/package.json +6 -6
  85. package/src/carp-catalog.ts +2 -5
  86. package/src/catalog-fec-params.ts +76 -0
  87. package/src/cmaf-block-assembler.ts +51 -1
  88. package/src/codec-utils.ts +73 -1
  89. package/src/fec-manager.ts +168 -18
  90. package/src/fec-worker-setup.ts +37 -11
  91. package/src/index.ts +11 -0
  92. package/src/iso-bmff-timing.ts +77 -7
  93. package/src/media-router.ts +13 -6
  94. package/src/mfu-reassembler.ts +1732 -940
  95. package/src/mmtp-container-parser.ts +111 -9
  96. package/src/mmtp-decode-pipeline.ts +98 -24
  97. package/src/mmtp-router.ts +76 -16
  98. package/src/multicast-manager.ts +106 -22
  99. package/src/repair-fetch.ts +21 -0
  100. package/src/sample-combiner.ts +93 -1
  101. package/src/track-session.ts +198 -119
  102. package/src/wasm-mfu-reassembler.ts +18 -1
@@ -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.
@@ -920,7 +929,8 @@ var MFU_REASSEMBLY_ERROR_CODES = [
920
929
  "data-unit-identity-mismatch",
921
930
  "offset-overflow",
922
931
  "active-data-unit-limit",
923
- "incomplete-terminal-fragment"
932
+ "incomplete-terminal-fragment",
933
+ "ambiguous-continuation"
924
934
  ];
925
935
  var mfuReassemblyErrorCodes = new Set(MFU_REASSEMBLY_ERROR_CODES);
926
936
  function isMfuReassemblyErrorCode(value) {
@@ -933,6 +943,11 @@ var MfuReassemblyError = class extends Error {
933
943
  this.name = "MfuReassemblyError";
934
944
  }
935
945
  };
946
+ function isMfuReassemblyError(error) {
947
+ if (error instanceof MfuReassemblyError) return true;
948
+ if (typeof error !== "object" || error === null) return false;
949
+ return Reflect.get(error, "name") === "MfuReassemblyError" && isMfuReassemblyErrorCode(Reflect.get(error, "code"));
950
+ }
936
951
  function assembledPacketSequences(fragments) {
937
952
  const packetSequenceNumbers = [];
938
953
  const repairedPacketSequenceNumbers = [];
@@ -962,15 +977,11 @@ function streamScope(fragment) {
962
977
  function isU32(value) {
963
978
  return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
964
979
  }
965
- function fragmentRunKey(fragment) {
966
- if (!isU32(fragment.packetSequenceNumber)) {
967
- throw new Error("[MFUReassembler] fragmented value has no validated packet sequence");
968
- }
969
- const terminalPacketSequence = (fragment.packetSequenceNumber + fragment.fragmentCounter) % 4294967296;
980
+ function dataUnitAddress(fragment, packetSequenceNumber) {
970
981
  return JSON.stringify([
971
982
  ...streamScope(fragment),
972
983
  fragment.mpuSequenceNumber,
973
- terminalPacketSequence
984
+ packetSequenceNumber + fragment.fragmentCounter >>> 0
974
985
  ]);
975
986
  }
976
987
  function hasAnySampleIdentityField(fragment) {
@@ -991,12 +1002,7 @@ function fragmentCounterOrder(fragment) {
991
1002
  function objectKey(fragment) {
992
1003
  const scope = streamScope(fragment);
993
1004
  if (fragment.itemId !== void 0) {
994
- return JSON.stringify([
995
- ...scope,
996
- fragment.mpuSequenceNumber,
997
- "item",
998
- fragment.itemId
999
- ]);
1005
+ return JSON.stringify([...scope, fragment.mpuSequenceNumber, "item", fragment.itemId]);
1000
1006
  }
1001
1007
  if (hasSampleIdentity(fragment)) {
1002
1008
  return JSON.stringify([
@@ -1013,11 +1019,7 @@ function objectKey(fragment) {
1013
1019
  fragment.sampleOffset
1014
1020
  ]);
1015
1021
  }
1016
- return JSON.stringify([
1017
- ...scope,
1018
- fragment.mpuSequenceNumber,
1019
- "unidentified"
1020
- ]);
1022
+ return JSON.stringify([...scope, fragment.mpuSequenceNumber, "unidentified"]);
1021
1023
  }
1022
1024
  function ordersByOffset(fragments) {
1023
1025
  return fragments.every((fragment) => hasSampleIdentity(fragment));
@@ -1026,26 +1028,43 @@ function checkedSampleEnd(offset, byteLength) {
1026
1028
  const end = offset + byteLength;
1027
1029
  return Number.isSafeInteger(end) && end <= 4294967295 ? end : null;
1028
1030
  }
1029
- var _buffers, _timeouts, _activeDataUnits, _pendingContinuations, _emitted, _emittedOrder, _timeoutMs, _maxBuffered, _MFUReassembler_instances, addAssociatedFragment_fn, parkContinuation_fn, drainPendingContinuations_fn, incompleteRunCount_fn, reject_fn, associateDataUnit_fn, tryReassemble_fn, assemble_fn, setTimeout_fn, clearPendingTimeout_fn, deletePendingRun_fn, onPendingTimeout_fn, clearTimeout_fn, markEmitted_fn, orderValue_fn, removeActiveDataUnitForBuffer_fn, onTimeout_fn, pendingDebugSamples_fn;
1031
+ var COMPLETED_ADDRESS_WINDOW_MULTIPLE = 2;
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;
1030
1036
  var MFUReassembler = class {
1031
1037
  constructor(options) {
1032
1038
  __privateAdd(this, _MFUReassembler_instances);
1033
- // Fragment buffers use only the authored MFU DU identity. Continuations are
1034
- // admitted through bounded producer-profile runs keyed by physical stream,
1035
- // MPU, and the wrapping terminal packet sequence. This requires contiguous
1036
- // source-packet sequences within a fragmented DU; interleaving stays
1037
- // separated and incomplete. Continuations may wait in a counter-indexed queue
1038
- // until FI=1 supplies intrinsic identity; MMTP packet send time is never an
1039
- // association key.
1039
+ // Fragment buffers and active runs use the authored MFU DU identity.
1040
+ // Continuations may wait in one counter-indexed queue per data-unit address
1041
+ // until FI=1 supplies that identity. The address is transport association
1042
+ // only; the object key stays the intrinsic DU identity.
1040
1043
  __privateAdd(this, _buffers, /* @__PURE__ */ new Map());
1041
1044
  __privateAdd(this, _timeouts, /* @__PURE__ */ new Map());
1042
1045
  __privateAdd(this, _activeDataUnits, /* @__PURE__ */ new Map());
1043
1046
  __privateAdd(this, _pendingContinuations, /* @__PURE__ */ new Map());
1044
1047
  __privateAdd(this, _emitted, /* @__PURE__ */ new Set());
1045
- // same sample keys — reject FEC-recovered duplicates
1048
+ // same sample keys, so FEC-recovered duplicate heads are dropped
1046
1049
  __privateAdd(this, _emittedOrder, []);
1050
+ // Addresses of emitted data units. A continuation addressed to one of them
1051
+ // is a late copy of a fragment the run already consumed (FEC recovery
1052
+ // supplied it first), so it is rejected as stale rather than parked as a
1053
+ // headless data unit that would hold a slot until reset. Bounded at
1054
+ // COMPLETED_ADDRESS_WINDOW_MULTIPLE x maxBuffered, the same window the Rust
1055
+ // engine keeps, so both engines forget an address after the same number of
1056
+ // completions. (#emitted is a separate, per-engine window: this engine keeps
1057
+ // maxBuffered sample keys where Rust keeps 2x; the completed set does not
1058
+ // derive from it.)
1059
+ __privateAdd(this, _completed, /* @__PURE__ */ new Set());
1060
+ __privateAdd(this, _completedOrder, []);
1047
1061
  __privateAdd(this, _timeoutMs);
1048
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);
1049
1068
  /** Statistics for debugging */
1050
1069
  this.stats = {
1051
1070
  totalFragments: 0,
@@ -1076,20 +1095,14 @@ var MFUReassembler = class {
1076
1095
  if (state.timeoutId !== void 0) clearTimeout(state.timeoutId);
1077
1096
  delete state.timeoutId;
1078
1097
  if (ms !== null) {
1079
- state.timeoutId = setTimeout(
1080
- () => __privateMethod(this, _MFUReassembler_instances, onTimeout_fn).call(this, bufferKey),
1081
- Math.max(0, state.startedAt + ms - now)
1082
- );
1098
+ state.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onTimeout_fn).call(this, bufferKey), Math.max(0, state.startedAt + ms - now));
1083
1099
  }
1084
1100
  }
1085
- for (const [runKey, pending] of __privateGet(this, _pendingContinuations)) {
1101
+ for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
1086
1102
  if (pending.timeoutId !== void 0) clearTimeout(pending.timeoutId);
1087
1103
  delete pending.timeoutId;
1088
- if (ms !== null && !__privateGet(this, _activeDataUnits).has(runKey)) {
1089
- pending.timeoutId = setTimeout(
1090
- () => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, runKey),
1091
- Math.max(0, pending.startedAt + ms - now)
1092
- );
1104
+ if (ms !== null && parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
1105
+ pending.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address), Math.max(0, pending.startedAt + ms - now));
1093
1106
  }
1094
1107
  }
1095
1108
  }
@@ -1139,38 +1152,50 @@ var MFUReassembler = class {
1139
1152
  catalogTrackName: fragment.catalogTrackName
1140
1153
  };
1141
1154
  }
1142
- if (!isU32(fragment.packetSequenceNumber)) {
1155
+ const sequence = fragment.packetSequenceNumber;
1156
+ if (!isU32(sequence)) {
1143
1157
  __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "missing-packet-sequence", "fragmented MFU has no MMTP packet sequence number");
1144
1158
  }
1145
- const runKey = fragmentRunKey(fragment);
1146
- if (fragmentationIndicator === 1 && (hasSampleIdentity(fragment) || isU32(fragment.itemId)) && __privateGet(this, _emitted).has(objectKey(fragment))) {
1147
- __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, runKey);
1148
- return null;
1149
- }
1159
+ const address = dataUnitAddress(fragment, sequence);
1150
1160
  if (fragmentationIndicator !== 1) {
1151
- const active = __privateGet(this, _activeDataUnits).get(runKey);
1152
- if (active !== void 0 && fragment.fragmentCounter > active.nextFragmentCounter) {
1153
- __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "MFU continuation is behind the authored fragment-counter countdown");
1161
+ const open = __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address);
1162
+ if (open === void 0) {
1163
+ if (__privateGet(this, _completed).has(address)) {
1164
+ __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "MFU continuation names a data unit that already completed");
1165
+ }
1166
+ __privateMethod(this, _MFUReassembler_instances, parkContinuation_fn).call(this, address, fragment);
1167
+ return null;
1154
1168
  }
1155
- if (active === void 0 || fragment.fragmentCounter < active.nextFragmentCounter) {
1156
- __privateMethod(this, _MFUReassembler_instances, parkContinuation_fn).call(this, runKey, fragment);
1169
+ const [runKey2, active] = open;
1170
+ if (active.nextFragmentCounter > fragment.fragmentCounter) {
1171
+ __privateMethod(this, _MFUReassembler_instances, parkContinuation_fn).call(this, address, fragment);
1157
1172
  return null;
1158
1173
  }
1159
- } else if (__privateGet(this, _activeDataUnits).has(runKey)) {
1160
- __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "overlapping-data-unit", "fragmented MFU overlaps an active data unit");
1161
- } else {
1162
- const pending = __privateGet(this, _pendingContinuations).get(runKey);
1163
- if (pending !== void 0 && [...pending.fragments.keys()].some(
1164
- (counter) => counter >= fragment.fragmentCounter
1165
- )) {
1166
- __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "queued continuation counter is not below the authored first-fragment counter");
1174
+ if (active.nextFragmentCounter < fragment.fragmentCounter) {
1175
+ __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "MFU continuation is behind the authored fragment-counter countdown");
1167
1176
  }
1177
+ const result2 = __privateMethod(this, _MFUReassembler_instances, addAssociatedFragment_fn).call(this, fragment, runKey2, address);
1178
+ if (result2 !== null) return result2;
1179
+ return __privateMethod(this, _MFUReassembler_instances, drainPendingContinuations_fn).call(this, address, runKey2);
1168
1180
  }
1169
- const pendingStartedAt = fragmentationIndicator === 1 ? __privateGet(this, _pendingContinuations).get(runKey)?.startedAt : void 0;
1170
- const result = __privateMethod(this, _MFUReassembler_instances, addAssociatedFragment_fn).call(this, fragment, runKey, pendingStartedAt);
1171
- if (fragmentationIndicator === 1) __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, runKey);
1181
+ const runKey = objectKey(fragment);
1182
+ if ((hasSampleIdentity(fragment) || isU32(fragment.itemId)) && __privateGet(this, _emitted).has(runKey)) {
1183
+ return null;
1184
+ }
1185
+ if (__privateGet(this, _activeDataUnits).has(runKey)) {
1186
+ __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "overlapping-data-unit", "fragmented MFU overlaps an active data unit");
1187
+ }
1188
+ if (__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0) {
1189
+ __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "overlapping-data-unit", "fragmented MFU opens on an address held by an open data unit");
1190
+ }
1191
+ const pending = __privateGet(this, _pendingContinuations).get(address);
1192
+ if (pending !== void 0 && [...pending.fragments.keys()].some((counter) => counter >= fragment.fragmentCounter)) {
1193
+ __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "queued continuation counter is not below the authored first-fragment counter");
1194
+ }
1195
+ const result = __privateMethod(this, _MFUReassembler_instances, addAssociatedFragment_fn).call(this, fragment, runKey, address, pending?.startedAt);
1196
+ __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
1172
1197
  if (result !== null) return result;
1173
- return __privateMethod(this, _MFUReassembler_instances, drainPendingContinuations_fn).call(this, runKey);
1198
+ return __privateMethod(this, _MFUReassembler_instances, drainPendingContinuations_fn).call(this, address, runKey);
1174
1199
  }
1175
1200
  /**
1176
1201
  * Flush all buffered fragments
@@ -1182,12 +1207,14 @@ var MFUReassembler = class {
1182
1207
  __privateGet(this, _buffers).clear();
1183
1208
  __privateGet(this, _timeouts).clear();
1184
1209
  __privateGet(this, _activeDataUnits).clear();
1185
- for (const runKey of __privateGet(this, _pendingContinuations).keys()) {
1186
- __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, runKey);
1210
+ for (const address of __privateGet(this, _pendingContinuations).keys()) {
1211
+ __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
1187
1212
  }
1188
1213
  __privateGet(this, _pendingContinuations).clear();
1189
1214
  __privateGet(this, _emitted).clear();
1190
1215
  __privateGet(this, _emittedOrder).length = 0;
1216
+ __privateGet(this, _completed).clear();
1217
+ __privateGet(this, _completedOrder).length = 0;
1191
1218
  }
1192
1219
  /** Drop one catalog stream's incomplete and dedup state. */
1193
1220
  resetStream(catalogTrackName) {
@@ -1205,18 +1232,15 @@ var MFUReassembler = class {
1205
1232
  discarded.add(key);
1206
1233
  }
1207
1234
  }
1208
- for (const [runKey, active] of __privateGet(this, _activeDataUnits)) {
1235
+ for (const [objectKey2, active] of __privateGet(this, _activeDataUnits)) {
1209
1236
  if (active.catalogTrackName === catalogTrackName) {
1210
1237
  discarded.add(active.bufferKey);
1211
- __privateGet(this, _activeDataUnits).delete(runKey);
1238
+ __privateGet(this, _activeDataUnits).delete(objectKey2);
1212
1239
  }
1213
1240
  }
1214
- for (const [runKey, pending] of __privateGet(this, _pendingContinuations)) {
1215
- if ([...pending.fragments.values()].some(
1216
- (fragment) => fragment.catalogTrackName === catalogTrackName
1217
- )) {
1218
- __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, runKey);
1219
- }
1241
+ for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
1242
+ if ([...pending.fragments.values()].some((fragment) => fragment.catalogTrackName === catalogTrackName) || belongsToStream(address))
1243
+ __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, address);
1220
1244
  }
1221
1245
  for (const key of __privateGet(this, _emitted)) {
1222
1246
  if (belongsToStream(key)) discarded.add(key);
@@ -1227,6 +1251,10 @@ var MFUReassembler = class {
1227
1251
  __privateGet(this, _emitted).delete(key);
1228
1252
  }
1229
1253
  __privateSet(this, _emittedOrder, __privateGet(this, _emittedOrder).filter((key) => !discarded.has(key)));
1254
+ for (const address of __privateGet(this, _completed)) {
1255
+ if (belongsToStream(address)) __privateGet(this, _completed).delete(address);
1256
+ }
1257
+ __privateSet(this, _completedOrder, __privateGet(this, _completedOrder).filter((address) => __privateGet(this, _completed).has(address)));
1230
1258
  }
1231
1259
  /** Reset all state — flush buffers and zero stats */
1232
1260
  reset() {
@@ -1239,14 +1267,26 @@ var MFUReassembler = class {
1239
1267
  this.stats.outOfOrderFragments = 0;
1240
1268
  }
1241
1269
  /**
1242
- * Get number of pending MFUs
1270
+ * Get number of pending MFUs: engine buffers plus every headless parked data
1271
+ * unit that holds a slot.
1272
+ *
1273
+ * This is not one of the four charging sites, but it shares their second term,
1274
+ * so it takes the same rule — by delegating to `#unopenedParkedRunCount`, the
1275
+ * one site that applies it. Rust's public `buffered_count()` is
1276
+ * `inner.buffered_count() + unopened_parked_run_count()`, and that count is
1277
+ * `parked_run_holds_a_slot` — so filtering here on the open run alone would
1278
+ * report a phantom slot for an empty published queue in TypeScript and not in
1279
+ * Rust, out of a *host-observable* counter (`getStatus().bufferedMFUs`). Same
1280
+ * state, two numbers, decided by which engine the host loaded: the divergence
1281
+ * class this file exists to close, so the public counters have to agree too.
1282
+ *
1283
+ * Distinct from `#incompleteRunCount`, which sums `#activeDataUnits` rather
1284
+ * than `#buffers`. Those are different quantities by design and are not being
1285
+ * merged; only the parked term they share is required to agree, which is what
1286
+ * routing both through one method makes structural rather than conventional.
1243
1287
  */
1244
1288
  get pendingCount() {
1245
- let count = __privateGet(this, _buffers).size;
1246
- for (const runKey of __privateGet(this, _pendingContinuations).keys()) {
1247
- if (!__privateGet(this, _activeDataUnits).has(runKey)) count++;
1248
- }
1249
- return count;
1289
+ return __privateGet(this, _buffers).size + __privateMethod(this, _MFUReassembler_instances, unopenedParkedRunCount_fn).call(this);
1250
1290
  }
1251
1291
  /**
1252
1292
  * Get current buffer status
@@ -1273,11 +1313,14 @@ _activeDataUnits = new WeakMap();
1273
1313
  _pendingContinuations = new WeakMap();
1274
1314
  _emitted = new WeakMap();
1275
1315
  _emittedOrder = new WeakMap();
1316
+ _completed = new WeakMap();
1317
+ _completedOrder = new WeakMap();
1276
1318
  _timeoutMs = new WeakMap();
1277
1319
  _maxBuffered = new WeakMap();
1320
+ _nextAdmissionSeq = new WeakMap();
1278
1321
  _MFUReassembler_instances = new WeakSet();
1279
- addAssociatedFragment_fn = function(fragment, runKey, startedAt) {
1280
- const association = __privateMethod(this, _MFUReassembler_instances, associateDataUnit_fn).call(this, fragment, runKey);
1322
+ addAssociatedFragment_fn = function(fragment, runKey, address, startedAt) {
1323
+ const association = __privateMethod(this, _MFUReassembler_instances, associateDataUnit_fn).call(this, fragment, runKey, address);
1281
1324
  const { bufferKey, first, terminal } = association;
1282
1325
  let fragments = __privateGet(this, _buffers).get(bufferKey);
1283
1326
  if (first) {
@@ -1299,66 +1342,160 @@ addAssociatedFragment_fn = function(fragment, runKey, startedAt) {
1299
1342
  __privateGet(this, _buffers).delete(bufferKey);
1300
1343
  __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "incomplete-terminal-fragment", "validated terminal fragment did not complete its data unit");
1301
1344
  }
1345
+ __privateMethod(this, _MFUReassembler_instances, markCompleted_fn).call(this, address);
1302
1346
  return result;
1303
1347
  };
1304
- parkContinuation_fn = function(runKey, fragment) {
1305
- let pending = __privateGet(this, _pendingContinuations).get(runKey);
1306
- if (pending === void 0) {
1307
- const awaitingFirst = !__privateGet(this, _activeDataUnits).has(runKey);
1308
- if (awaitingFirst && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
1348
+ parkContinuation_fn = function(address, fragment) {
1349
+ let pending = __privateGet(this, _pendingContinuations).get(address);
1350
+ const open = __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address);
1351
+ const alreadyCharged = open !== void 0 || parkedRunHoldsASlot(open !== void 0, pending?.fragments.size ?? 0);
1352
+ if (!alreadyCharged && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
1353
+ if (!__privateMethod(this, _MFUReassembler_instances, evictLeastRecentlyAdmitted_fn).call(this)) {
1309
1354
  __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
1310
1355
  }
1311
- const startedAt = Date.now();
1312
- pending = { fragments: /* @__PURE__ */ new Map(), startedAt };
1313
- if (awaitingFirst && __privateGet(this, _timeoutMs) !== null) {
1314
- pending.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, runKey), __privateGet(this, _timeoutMs));
1356
+ }
1357
+ let reAnchored = false;
1358
+ if (pending === void 0) {
1359
+ pending = {
1360
+ fragments: /* @__PURE__ */ new Map(),
1361
+ startedAt: Date.now(),
1362
+ admissionSeq: open !== void 0 ? open[1].admissionSeq : __privateWrapper(this, _nextAdmissionSeq)._++
1363
+ };
1364
+ if (open === void 0 && __privateGet(this, _timeoutMs) !== null) {
1365
+ pending.timeoutId = setTimeout(() => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address), __privateGet(this, _timeoutMs));
1315
1366
  }
1316
- __privateGet(this, _pendingContinuations).set(runKey, pending);
1367
+ __privateGet(this, _pendingContinuations).set(address, pending);
1368
+ } else if (!alreadyCharged) {
1369
+ pending.admissionSeq = __privateWrapper(this, _nextAdmissionSeq)._++;
1370
+ pending.startedAt = Date.now();
1371
+ __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
1372
+ reAnchored = true;
1317
1373
  }
1318
1374
  if (pending.fragments.has(fragment.fragmentCounter)) {
1319
1375
  __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "fragment-counter-mismatch", "duplicate queued continuation counter would replace retained media");
1320
1376
  }
1321
1377
  pending.fragments.set(fragment.fragmentCounter, fragment);
1378
+ if (reAnchored) __privateMethod(this, _MFUReassembler_instances, armHeadlessPendingTimeout_fn).call(this, address);
1322
1379
  };
1323
- drainPendingContinuations_fn = function(runKey) {
1380
+ drainPendingContinuations_fn = function(address, runKey) {
1324
1381
  while (true) {
1325
1382
  const active = __privateGet(this, _activeDataUnits).get(runKey);
1383
+ const pending = __privateGet(this, _pendingContinuations).get(address);
1326
1384
  if (active === void 0) {
1327
- __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, runKey);
1385
+ if (pending?.fragments.size === 0) __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, address);
1328
1386
  return null;
1329
1387
  }
1330
- const pending = __privateGet(this, _pendingContinuations).get(runKey);
1331
1388
  const fragment = pending?.fragments.get(active.nextFragmentCounter);
1332
- if (fragment === void 0) return null;
1389
+ if (fragment === void 0) {
1390
+ if (pending?.fragments.size === 0) __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, address);
1391
+ return null;
1392
+ }
1333
1393
  pending.fragments.delete(active.nextFragmentCounter);
1334
- if (pending.fragments.size === 0) __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, runKey);
1335
- const result = __privateMethod(this, _MFUReassembler_instances, addAssociatedFragment_fn).call(this, fragment, runKey);
1394
+ if (pending.fragments.size === 0) __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, address);
1395
+ const result = __privateMethod(this, _MFUReassembler_instances, addAssociatedFragment_fn).call(this, fragment, runKey, address);
1336
1396
  if (result !== null) {
1337
- __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, runKey);
1338
1397
  return result;
1339
1398
  }
1340
1399
  }
1341
1400
  };
1342
- incompleteRunCount_fn = function() {
1343
- let count = __privateGet(this, _activeDataUnits).size;
1344
- for (const runKey of __privateGet(this, _pendingContinuations).keys()) {
1345
- if (!__privateGet(this, _activeDataUnits).has(runKey)) count++;
1401
+ /** Parked data units that hold a `maxBuffered` slot of their own. Membership
1402
+ * is `parkedRunHoldsASlot`, the same rule #evictLeastRecentlyAdmitted offers
1403
+ * as candidates, so nothing can be counted here that eviction cannot reclaim.
1404
+ *
1405
+ * One site, two callers — `#incompleteRunCount` and `pendingCount` — mirroring
1406
+ * Rust's `unopened_parked_run_count`, which `incomplete_run_count` and
1407
+ * `buffered_count` both delegate to. Both callers used to inline this loop
1408
+ * verbatim, so converging the *rule* moved one copy and left the other as the
1409
+ * divergence (Ally's review of #400). The rule can no longer drift now that
1410
+ * every site routes through `parkedRunHoldsASlot`; keeping the loop itself at
1411
+ * one site is what stops the scaffolding drifting the next time this question
1412
+ * grows an argument. */
1413
+ unopenedParkedRunCount_fn = function() {
1414
+ let count = 0;
1415
+ for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
1416
+ if (parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
1417
+ count++;
1418
+ }
1346
1419
  }
1347
1420
  return count;
1348
1421
  };
1422
+ /** Every open run plus every headless parked data unit that holds a slot,
1423
+ * one slot each. This is the quantity maxBuffered bounds. The parked term is
1424
+ * `#unopenedParkedRunCount`, whose membership is `parkedRunHoldsASlot` — the
1425
+ * same rule #evictLeastRecentlyAdmitted offers as candidates, so nothing can
1426
+ * be counted here that eviction cannot reclaim. */
1427
+ incompleteRunCount_fn = function() {
1428
+ return __privateGet(this, _activeDataUnits).size + __privateMethod(this, _MFUReassembler_instances, unopenedParkedRunCount_fn).call(this);
1429
+ };
1430
+ /**
1431
+ * Drop the incomplete data unit admitted longest ago so a newly arriving one
1432
+ * can take its slot. Selection is by monotonic admission ordinal, never by
1433
+ * wall clock, so this is a bounded-capacity replacement policy and not a
1434
+ * timeout: it behaves identically on hosts that never drive `cleanup_timeouts`
1435
+ * (`timeoutMs: null`). Only incomplete data units are candidates — a data
1436
+ * unit that completed has already left both maps.
1437
+ *
1438
+ * Returns false only if no slot is charged, which cannot happen at the bound
1439
+ * (`maxBuffered` is a positive integer); admission then falls back to the
1440
+ * fail-closed `active-data-unit-limit` reject, which stays a stable host code.
1441
+ * That holds because the candidate set here is built from the same
1442
+ * `parkedRunHoldsASlot` rule `#incompleteRunCount` counts by, so reaching the
1443
+ * bound implies at least one candidate. Filtering one and not the other would
1444
+ * make the claim false: a queue counted but not offered charges a slot no
1445
+ * eviction can reclaim, and the receiver refuses every later data unit
1446
+ * permanently.
1447
+ */
1448
+ evictLeastRecentlyAdmitted_fn = function() {
1449
+ let victimAddress;
1450
+ let victimSeq = Number.POSITIVE_INFINITY;
1451
+ for (const [, active] of __privateGet(this, _activeDataUnits)) {
1452
+ if (active.admissionSeq < victimSeq) {
1453
+ victimSeq = active.admissionSeq;
1454
+ victimAddress = active.address;
1455
+ }
1456
+ }
1457
+ for (const [address, pending] of __privateGet(this, _pendingContinuations)) {
1458
+ if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
1459
+ continue;
1460
+ }
1461
+ if (pending.admissionSeq < victimSeq) {
1462
+ victimSeq = pending.admissionSeq;
1463
+ victimAddress = address;
1464
+ }
1465
+ }
1466
+ if (victimAddress === void 0) return false;
1467
+ __privateMethod(this, _MFUReassembler_instances, releaseSlot_fn).call(this, victimAddress);
1468
+ this.stats.evictedMemoryLimit++;
1469
+ return true;
1470
+ };
1471
+ /**
1472
+ * Release every form of state one address holds: its open run, that run's
1473
+ * buffered fragments and deadline, and its continuation queue. The address is
1474
+ * deliberately not marked completed — the data unit never emitted, so a later
1475
+ * fragment of it may open a fresh slot rather than being rejected as stale.
1476
+ */
1477
+ releaseSlot_fn = function(address) {
1478
+ const open = __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address);
1479
+ if (open !== void 0) {
1480
+ const [runKey, active] = open;
1481
+ __privateGet(this, _activeDataUnits).delete(runKey);
1482
+ __privateMethod(this, _MFUReassembler_instances, clearTimeout_fn).call(this, active.bufferKey);
1483
+ __privateGet(this, _buffers).delete(active.bufferKey);
1484
+ }
1485
+ __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, address);
1486
+ };
1349
1487
  reject_fn = function(code, message) {
1350
1488
  this.stats.rejectedFragments++;
1351
1489
  throw new MfuReassemblyError(code, message);
1352
1490
  };
1353
- associateDataUnit_fn = function(fragment, runKey) {
1491
+ associateDataUnit_fn = function(fragment, runKey, address) {
1354
1492
  const fi = fragment.fragmentationIndicator;
1355
1493
  if (fi === 1) {
1356
- if (__privateGet(this, _activeDataUnits).has(runKey)) {
1357
- __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "overlapping-data-unit", "fragmented MFU overlaps an active data unit");
1358
- }
1359
- if (!__privateGet(this, _pendingContinuations).has(runKey) && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
1360
- __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
1361
- }
1494
+ const parked = __privateGet(this, _pendingContinuations).get(address);
1495
+ const inheritsParkedSlot = parkedRunHoldsASlot(
1496
+ __privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0,
1497
+ parked?.fragments.size ?? 0
1498
+ );
1362
1499
  const sampleIdentity = hasSampleIdentity(fragment);
1363
1500
  const hasSampleFields = hasAnySampleIdentityField(fragment);
1364
1501
  const hasItemIdentity = fragment.itemId !== void 0;
@@ -1378,29 +1515,42 @@ associateDataUnit_fn = function(fragment, runKey) {
1378
1515
  if (__privateGet(this, _buffers).has(bufferKey)) {
1379
1516
  __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "overlapping-data-unit", "intrinsic data-unit buffer already exists");
1380
1517
  }
1518
+ let sampleEnd = null;
1519
+ if (sampleIdentity) {
1520
+ sampleEnd = checkedSampleEnd(fragment.sampleOffset, fragment.data.byteLength);
1521
+ if (sampleEnd === null) __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "offset-overflow", "sample byte offset overflow");
1522
+ }
1523
+ if (!inheritsParkedSlot && __privateMethod(this, _MFUReassembler_instances, incompleteRunCount_fn).call(this) >= __privateGet(this, _maxBuffered)) {
1524
+ if (!__privateMethod(this, _MFUReassembler_instances, evictLeastRecentlyAdmitted_fn).call(this)) {
1525
+ __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "active-data-unit-limit", "active MFU data-unit limit reached");
1526
+ }
1527
+ }
1528
+ const admissionSeq = inheritsParkedSlot ? parked.admissionSeq : __privateWrapper(this, _nextAdmissionSeq)._++;
1381
1529
  let active2;
1382
1530
  if (sampleIdentity) {
1383
- const nextOffset = checkedSampleEnd(fragment.sampleOffset, fragment.data.byteLength);
1384
- if (nextOffset === null) __privateMethod(this, _MFUReassembler_instances, reject_fn).call(this, "offset-overflow", "sample byte offset overflow");
1385
1531
  active2 = {
1532
+ address,
1386
1533
  kind: "sample",
1387
1534
  bufferKey,
1388
1535
  nextFragmentCounter: fragment.fragmentCounter - 1,
1536
+ admissionSeq,
1389
1537
  movieFragmentSequenceNumber: fragment.movieFragmentSequenceNumber,
1390
1538
  sampleNumber: fragment.sampleNumber,
1391
- nextOffset,
1539
+ nextOffset: sampleEnd,
1392
1540
  ...fragment.catalogTrackName === void 0 ? {} : { catalogTrackName: fragment.catalogTrackName }
1393
1541
  };
1394
1542
  } else {
1395
1543
  active2 = {
1544
+ address,
1396
1545
  kind: "item",
1397
1546
  bufferKey,
1398
1547
  nextFragmentCounter: fragment.fragmentCounter - 1,
1548
+ admissionSeq,
1399
1549
  itemId: fragment.itemId,
1400
1550
  ...fragment.catalogTrackName === void 0 ? {} : { catalogTrackName: fragment.catalogTrackName }
1401
1551
  };
1402
1552
  }
1403
- __privateGet(this, _activeDataUnits).set(runKey, active2);
1553
+ __privateGet(this, _activeDataUnits).set(objectKey(fragment), active2);
1404
1554
  return { bufferKey, first: true, terminal: false };
1405
1555
  }
1406
1556
  const active = __privateGet(this, _activeDataUnits).get(runKey);
@@ -1506,25 +1656,49 @@ setTimeout_fn = function(bufferKey, startedAt = Date.now()) {
1506
1656
  }
1507
1657
  __privateGet(this, _timeouts).set(bufferKey, state);
1508
1658
  };
1509
- clearPendingTimeout_fn = function(runKey) {
1510
- const pending = __privateGet(this, _pendingContinuations).get(runKey);
1659
+ clearPendingTimeout_fn = function(address) {
1660
+ const pending = __privateGet(this, _pendingContinuations).get(address);
1511
1661
  if (pending?.timeoutId !== void 0) clearTimeout(pending.timeoutId);
1512
1662
  if (pending !== void 0) {
1513
1663
  delete pending.timeoutId;
1514
1664
  }
1515
1665
  };
1516
- deletePendingRun_fn = function(runKey) {
1517
- __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, runKey);
1518
- __privateGet(this, _pendingContinuations).delete(runKey);
1666
+ /**
1667
+ * A queue whose run closed without completing is headless again and expires
1668
+ * from its own first arrival, exactly as it would had the head never come.
1669
+ *
1670
+ * An empty queue is not armed: it holds no data unit that could fail to
1671
+ * complete, so there is nothing for a deadline to expire. Both arming sites
1672
+ * take that test through `parkedRunHoldsASlot` rather than filtering on the
1673
+ * open run alone, which is how this engine reaches Rust's answer — its
1674
+ * `next_timeout_at` derives candidate deadlines from the queue's live
1675
+ * entries, so an empty queue yields none (BLO-32420).
1676
+ */
1677
+ armHeadlessPendingTimeout_fn = function(address) {
1678
+ const pending = __privateGet(this, _pendingContinuations).get(address);
1679
+ if (pending === void 0 || pending.timeoutId !== void 0 || __privateGet(this, _timeoutMs) === null) return;
1680
+ if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) return;
1681
+ pending.timeoutId = setTimeout(
1682
+ () => __privateMethod(this, _MFUReassembler_instances, onPendingTimeout_fn).call(this, address),
1683
+ Math.max(0, pending.startedAt + __privateGet(this, _timeoutMs) - Date.now())
1684
+ );
1685
+ };
1686
+ deletePendingRun_fn = function(address) {
1687
+ __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
1688
+ __privateGet(this, _pendingContinuations).delete(address);
1519
1689
  };
1520
- onPendingTimeout_fn = function(runKey) {
1521
- const pending = __privateGet(this, _pendingContinuations).get(runKey);
1690
+ onPendingTimeout_fn = function(address) {
1691
+ const pending = __privateGet(this, _pendingContinuations).get(address);
1522
1692
  if (pending === void 0) return;
1693
+ if (!parkedRunHoldsASlot(__privateMethod(this, _MFUReassembler_instances, openRunAt_fn).call(this, address) !== void 0, pending.fragments.size)) {
1694
+ __privateMethod(this, _MFUReassembler_instances, clearPendingTimeout_fn).call(this, address);
1695
+ return;
1696
+ }
1523
1697
  this.stats.timedOutMFUs++;
1524
1698
  console.warn(
1525
- `[MfuReassembler] timeout: run=${runKey} fragments=${pending.fragments.size} (continuations awaiting first fragment, discarded)`
1699
+ `[MfuReassembler] timeout: run=${address} fragments=${pending.fragments.size} (continuations awaiting first fragment, discarded)`
1526
1700
  );
1527
- __privateGet(this, _pendingContinuations).delete(runKey);
1701
+ __privateGet(this, _pendingContinuations).delete(address);
1528
1702
  };
1529
1703
  clearTimeout_fn = function(bufferKey) {
1530
1704
  const state = __privateGet(this, _timeouts).get(bufferKey);
@@ -1542,17 +1716,37 @@ markEmitted_fn = function(bufferKey) {
1542
1716
  if (oldest !== void 0) __privateGet(this, _emitted).delete(oldest);
1543
1717
  }
1544
1718
  };
1719
+ markCompleted_fn = function(address) {
1720
+ if (__privateGet(this, _completed).has(address)) return;
1721
+ __privateGet(this, _completed).add(address);
1722
+ __privateGet(this, _completedOrder).push(address);
1723
+ const cap = __privateGet(this, _maxBuffered) * COMPLETED_ADDRESS_WINDOW_MULTIPLE;
1724
+ while (__privateGet(this, _completedOrder).length > cap) {
1725
+ const oldest = __privateGet(this, _completedOrder).shift();
1726
+ if (oldest !== void 0) __privateGet(this, _completed).delete(oldest);
1727
+ }
1728
+ };
1545
1729
  orderValue_fn = function(fragment) {
1546
1730
  return fragmentCounterOrder(fragment);
1547
1731
  };
1732
+ /** Close the run behind an expired buffer. Early arrivals parked under its
1733
+ * address stay, now headless, and expire from their own arrival. */
1548
1734
  removeActiveDataUnitForBuffer_fn = function(bufferKey) {
1549
- for (const [runKey, active] of __privateGet(this, _activeDataUnits)) {
1735
+ for (const [objectKey2, active] of __privateGet(this, _activeDataUnits)) {
1550
1736
  if (active.bufferKey === bufferKey) {
1551
- __privateGet(this, _activeDataUnits).delete(runKey);
1552
- __privateMethod(this, _MFUReassembler_instances, deletePendingRun_fn).call(this, runKey);
1737
+ __privateGet(this, _activeDataUnits).delete(objectKey2);
1738
+ __privateMethod(this, _MFUReassembler_instances, armHeadlessPendingTimeout_fn).call(this, active.address);
1553
1739
  }
1554
1740
  }
1555
1741
  };
1742
+ /** The one open run holding this address, with its object key. Admission
1743
+ * rejects a second head on an open address, so at most one run can match. */
1744
+ openRunAt_fn = function(address) {
1745
+ for (const entry of __privateGet(this, _activeDataUnits)) {
1746
+ if (entry[1].address === address) return entry;
1747
+ }
1748
+ return void 0;
1749
+ };
1556
1750
  onTimeout_fn = function(bufferKey) {
1557
1751
  const fragments = __privateGet(this, _buffers).get(bufferKey);
1558
1752
  if (!fragments || fragments.length === 0) {
@@ -1564,9 +1758,7 @@ onTimeout_fn = function(bufferKey) {
1564
1758
  return;
1565
1759
  }
1566
1760
  this.stats.timedOutMFUs++;
1567
- console.warn(
1568
- `[MfuReassembler] timeout: key=${bufferKey} fragments=${fragments.length} (partial, discarded)`
1569
- );
1761
+ console.warn(`[MfuReassembler] timeout: key=${bufferKey} fragments=${fragments.length} (partial, discarded)`);
1570
1762
  __privateGet(this, _buffers).delete(bufferKey);
1571
1763
  __privateGet(this, _timeouts).delete(bufferKey);
1572
1764
  __privateMethod(this, _MFUReassembler_instances, removeActiveDataUnitForBuffer_fn).call(this, bufferKey);
@@ -1691,15 +1883,14 @@ if (void 0) {
1691
1883
  rapFlag: true,
1692
1884
  ...identity
1693
1885
  });
1886
+ expectRejection(() => reassembler.addFragment(complete({})), "missing-data-unit-identity");
1694
1887
  expectRejection(
1695
- () => reassembler.addFragment(complete({})),
1696
- "missing-data-unit-identity"
1697
- );
1698
- expectRejection(
1699
- () => reassembler.addFragment(complete({
1700
- movieFragmentSequenceNumber: 2,
1701
- sampleNumber: 1
1702
- })),
1888
+ () => reassembler.addFragment(
1889
+ complete({
1890
+ movieFragmentSequenceNumber: 2,
1891
+ sampleNumber: 1
1892
+ })
1893
+ ),
1703
1894
  "missing-data-unit-identity"
1704
1895
  );
1705
1896
  });
@@ -1721,18 +1912,20 @@ if (void 0) {
1721
1912
  });
1722
1913
  test("accepts a fragmented subsample DU whose first fragment has a nonzero authored offset", () => {
1723
1914
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
1724
- expect(reassembler.addFragment({
1725
- fragmentationIndicator: 1,
1726
- mpuSequenceNumber: 2,
1727
- fragmentCounter: 1,
1728
- movieFragmentSequenceNumber: 2,
1729
- sampleNumber: 1,
1730
- sampleOffset: 4,
1731
- packetSequenceNumber: testPacketSequence(2, 1),
1732
- data: new Uint8Array([161]),
1733
- timestamp: 1000n,
1734
- rapFlag: true
1735
- })).toBeNull();
1915
+ expect(
1916
+ reassembler.addFragment({
1917
+ fragmentationIndicator: 1,
1918
+ mpuSequenceNumber: 2,
1919
+ fragmentCounter: 1,
1920
+ movieFragmentSequenceNumber: 2,
1921
+ sampleNumber: 1,
1922
+ sampleOffset: 4,
1923
+ packetSequenceNumber: testPacketSequence(2, 1),
1924
+ data: new Uint8Array([161]),
1925
+ timestamp: 1000n,
1926
+ rapFlag: true
1927
+ })
1928
+ ).toBeNull();
1736
1929
  expect(reassembler.pendingCount).toBe(1);
1737
1930
  expect(reassembler.getStatus().pendingFragments).toBe(1);
1738
1931
  const out = reassembler.addFragment({
@@ -1749,30 +1942,34 @@ if (void 0) {
1749
1942
  });
1750
1943
  test("sibling subsample DUs of one sample reassemble independently by offset", () => {
1751
1944
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
1752
- expect(reassembler.addFragment({
1753
- mpuSequenceNumber: 3,
1754
- movieFragmentSequenceNumber: 5,
1755
- sampleNumber: 9,
1756
- fragmentationIndicator: 1,
1757
- fragmentCounter: 1,
1758
- sampleOffset: 0,
1759
- data: new Uint8Array([161]),
1760
- timestamp: 2000n,
1761
- rapFlag: true,
1762
- packetSequenceNumber: testPacketSequence(3, 1, 0)
1763
- })).toBeNull();
1764
- expect(reassembler.addFragment({
1765
- mpuSequenceNumber: 3,
1766
- movieFragmentSequenceNumber: 5,
1767
- sampleNumber: 9,
1768
- fragmentationIndicator: 1,
1769
- fragmentCounter: 1,
1770
- sampleOffset: 3,
1771
- data: new Uint8Array([177]),
1772
- timestamp: 2001n,
1773
- rapFlag: false,
1774
- packetSequenceNumber: testPacketSequence(3, 1, 1)
1775
- })).toBeNull();
1945
+ expect(
1946
+ reassembler.addFragment({
1947
+ mpuSequenceNumber: 3,
1948
+ movieFragmentSequenceNumber: 5,
1949
+ sampleNumber: 9,
1950
+ fragmentationIndicator: 1,
1951
+ fragmentCounter: 1,
1952
+ sampleOffset: 0,
1953
+ data: new Uint8Array([161]),
1954
+ timestamp: 2000n,
1955
+ rapFlag: true,
1956
+ packetSequenceNumber: testPacketSequence(3, 1, 0)
1957
+ })
1958
+ ).toBeNull();
1959
+ expect(
1960
+ reassembler.addFragment({
1961
+ mpuSequenceNumber: 3,
1962
+ movieFragmentSequenceNumber: 5,
1963
+ sampleNumber: 9,
1964
+ fragmentationIndicator: 1,
1965
+ fragmentCounter: 2,
1966
+ sampleOffset: 3,
1967
+ data: new Uint8Array([177]),
1968
+ timestamp: 2001n,
1969
+ rapFlag: false,
1970
+ packetSequenceNumber: testPacketSequence(3, 1, 1)
1971
+ })
1972
+ ).toBeNull();
1776
1973
  expect(reassembler.pendingCount).toBe(2);
1777
1974
  const first = reassembler.addFragment({
1778
1975
  mpuSequenceNumber: 3,
@@ -1785,16 +1982,27 @@ if (void 0) {
1785
1982
  });
1786
1983
  expect(first?.data).toEqual(new Uint8Array([161, 162]));
1787
1984
  expect(first?.sampleOffset).toBe(0);
1985
+ expect(
1986
+ reassembler.addFragment({
1987
+ mpuSequenceNumber: 3,
1988
+ fragmentationIndicator: 2,
1989
+ fragmentCounter: 1,
1990
+ data: new Uint8Array([178]),
1991
+ timestamp: 2001n,
1992
+ rapFlag: false,
1993
+ packetSequenceNumber: testPacketSequence(3, 0, 1)
1994
+ })
1995
+ ).toBeNull();
1788
1996
  const second = reassembler.addFragment({
1789
1997
  mpuSequenceNumber: 3,
1790
1998
  fragmentationIndicator: 3,
1791
1999
  fragmentCounter: 0,
1792
- data: new Uint8Array([178]),
2000
+ data: new Uint8Array([179]),
1793
2001
  timestamp: 2001n,
1794
2002
  rapFlag: false,
1795
- packetSequenceNumber: testPacketSequence(3, 0, 1)
2003
+ packetSequenceNumber: testPacketSequence(3, 0, 2)
1796
2004
  });
1797
- expect(second?.data).toEqual(new Uint8Array([177, 178]));
2005
+ expect(second?.data).toEqual(new Uint8Array([177, 178, 179]));
1798
2006
  expect(second?.sampleOffset).toBe(3);
1799
2007
  expect(reassembler.pendingCount).toBe(0);
1800
2008
  });
@@ -1833,10 +2041,7 @@ if (void 0) {
1833
2041
  packetSequenceNumber: testPacketSequence(1, fragmentCounter)
1834
2042
  });
1835
2043
  expect(reassembler.addFragment(fragment(10, 1, 1, 16))).toBeNull();
1836
- expectRejection(
1837
- () => reassembler.addFragment(fragment(20, 1, 1, 32)),
1838
- "overlapping-data-unit"
1839
- );
2044
+ expectRejection(() => reassembler.addFragment(fragment(20, 1, 1, 32)), "overlapping-data-unit");
1840
2045
  expect(reassembler.pendingCount).toBe(1);
1841
2046
  expect(reassembler.addFragment(fragment(10, 3, 0, 17))?.data).toEqual(new Uint8Array([16, 17]));
1842
2047
  expect(reassembler.pendingCount).toBe(0);
@@ -1888,15 +2093,17 @@ if (void 0) {
1888
2093
  });
1889
2094
  test("buffers a terminal continuation until its authored first fragment arrives", () => {
1890
2095
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
1891
- expect(reassembler.addFragment({
1892
- fragmentationIndicator: 3,
1893
- mpuSequenceNumber: 1,
1894
- fragmentCounter: 0,
1895
- data: new Uint8Array([3, 4]),
1896
- timestamp: 1000n,
1897
- rapFlag: false,
1898
- packetSequenceNumber: testPacketSequence(1, 0)
1899
- })).toBeNull();
2096
+ expect(
2097
+ reassembler.addFragment({
2098
+ fragmentationIndicator: 3,
2099
+ mpuSequenceNumber: 1,
2100
+ fragmentCounter: 0,
2101
+ data: new Uint8Array([3, 4]),
2102
+ timestamp: 1000n,
2103
+ rapFlag: false,
2104
+ packetSequenceNumber: testPacketSequence(1, 0)
2105
+ })
2106
+ ).toBeNull();
1900
2107
  expect(reassembler.pendingCount).toBe(1);
1901
2108
  const completed = reassembler.addFragment({
1902
2109
  fragmentationIndicator: 1,
@@ -1957,25 +2164,29 @@ if (void 0) {
1957
2164
  });
1958
2165
  test("should reassemble 3 fragments (FI=1, FI=2, FI=3)", () => {
1959
2166
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
1960
- expect(reassembler.addFragment({
1961
- fragmentationIndicator: 1,
1962
- mpuSequenceNumber: 10,
1963
- fragmentCounter: 2,
1964
- itemId: 1,
1965
- data: new Uint8Array([170, 187]),
1966
- timestamp: 5000n,
1967
- rapFlag: true,
1968
- packetSequenceNumber: testPacketSequence(10, 2)
1969
- })).toBeNull();
1970
- expect(reassembler.addFragment({
1971
- fragmentationIndicator: 2,
1972
- mpuSequenceNumber: 10,
1973
- fragmentCounter: 1,
1974
- data: new Uint8Array([204, 221]),
1975
- timestamp: 5000n,
1976
- rapFlag: false,
1977
- packetSequenceNumber: testPacketSequence(10, 1)
1978
- })).toBeNull();
2167
+ expect(
2168
+ reassembler.addFragment({
2169
+ fragmentationIndicator: 1,
2170
+ mpuSequenceNumber: 10,
2171
+ fragmentCounter: 2,
2172
+ itemId: 1,
2173
+ data: new Uint8Array([170, 187]),
2174
+ timestamp: 5000n,
2175
+ rapFlag: true,
2176
+ packetSequenceNumber: testPacketSequence(10, 2)
2177
+ })
2178
+ ).toBeNull();
2179
+ expect(
2180
+ reassembler.addFragment({
2181
+ fragmentationIndicator: 2,
2182
+ mpuSequenceNumber: 10,
2183
+ fragmentCounter: 1,
2184
+ data: new Uint8Array([204, 221]),
2185
+ timestamp: 5000n,
2186
+ rapFlag: false,
2187
+ packetSequenceNumber: testPacketSequence(10, 1)
2188
+ })
2189
+ ).toBeNull();
1979
2190
  const result = reassembler.addFragment({
1980
2191
  fragmentationIndicator: 3,
1981
2192
  mpuSequenceNumber: 10,
@@ -2067,16 +2278,18 @@ if (void 0) {
2067
2278
  expect(a).not.toBeNull();
2068
2279
  expect(a.data).toEqual(new Uint8Array([161, 162]));
2069
2280
  expect(a.rapFlag).toBe(true);
2070
- expect(reassembler.addFragment({
2071
- fragmentationIndicator: 1,
2072
- mpuSequenceNumber: mpuSeq,
2073
- fragmentCounter: 1,
2074
- itemId: 20,
2075
- data: new Uint8Array([177]),
2076
- timestamp: 200n,
2077
- rapFlag: false,
2078
- packetSequenceNumber: testPacketSequence(mpuSeq, 1, 10)
2079
- })).toBeNull();
2281
+ expect(
2282
+ reassembler.addFragment({
2283
+ fragmentationIndicator: 1,
2284
+ mpuSequenceNumber: mpuSeq,
2285
+ fragmentCounter: 1,
2286
+ itemId: 20,
2287
+ data: new Uint8Array([177]),
2288
+ timestamp: 200n,
2289
+ rapFlag: false,
2290
+ packetSequenceNumber: testPacketSequence(mpuSeq, 1, 10)
2291
+ })
2292
+ ).toBeNull();
2080
2293
  const b = reassembler.addFragment({
2081
2294
  fragmentationIndicator: 3,
2082
2295
  mpuSequenceNumber: mpuSeq,
@@ -2096,26 +2309,30 @@ if (void 0) {
2096
2309
  movieFragmentSequenceNumber: 9,
2097
2310
  sampleNumber: 17
2098
2311
  };
2099
- expect(reassembler.addFragment({
2100
- ...common,
2101
- fragmentationIndicator: 1,
2102
- fragmentCounter: 2,
2103
- sampleOffset: 0,
2104
- data: new Uint8Array([161]),
2105
- timestamp: 1000n,
2106
- rapFlag: true,
2107
- packetSequenceNumber: testPacketSequence(0, 2)
2108
- })).toBeNull();
2109
- expect(reassembler.addFragment({
2110
- ...common,
2111
- fragmentationIndicator: 2,
2112
- fragmentCounter: 1,
2113
- sampleOffset: 1,
2114
- data: new Uint8Array([178]),
2115
- timestamp: 1001n,
2116
- rapFlag: false,
2117
- packetSequenceNumber: testPacketSequence(0, 1)
2118
- })).toBeNull();
2312
+ expect(
2313
+ reassembler.addFragment({
2314
+ ...common,
2315
+ fragmentationIndicator: 1,
2316
+ fragmentCounter: 2,
2317
+ sampleOffset: 0,
2318
+ data: new Uint8Array([161]),
2319
+ timestamp: 1000n,
2320
+ rapFlag: true,
2321
+ packetSequenceNumber: testPacketSequence(0, 2)
2322
+ })
2323
+ ).toBeNull();
2324
+ expect(
2325
+ reassembler.addFragment({
2326
+ ...common,
2327
+ fragmentationIndicator: 2,
2328
+ fragmentCounter: 1,
2329
+ sampleOffset: 1,
2330
+ data: new Uint8Array([178]),
2331
+ timestamp: 1001n,
2332
+ rapFlag: false,
2333
+ packetSequenceNumber: testPacketSequence(0, 1)
2334
+ })
2335
+ ).toBeNull();
2119
2336
  const out = reassembler.addFragment({
2120
2337
  ...common,
2121
2338
  fragmentationIndicator: 3,
@@ -2141,23 +2358,27 @@ if (void 0) {
2141
2358
  timestamp: 1000n,
2142
2359
  rapFlag: true
2143
2360
  };
2144
- expect(reassembler.addFragment({
2145
- ...common,
2146
- fragmentationIndicator: 1,
2147
- fragmentCounter: 2,
2148
- sampleOffset: 0,
2149
- data: new Uint8Array([161, 162]),
2150
- packetSequenceNumber: testPacketSequence(0, 2)
2151
- })).toBeNull();
2152
- expect(reassembler.addFragment({
2153
- ...common,
2154
- fragmentationIndicator: 3,
2155
- fragmentCounter: 0,
2156
- sampleOffset: 4,
2157
- data: new Uint8Array([193, 194]),
2158
- packetSequenceNumber: testPacketSequence(0, 0)
2159
- })).toBeNull();
2160
- expect(reassembler.pendingCount).toBe(1);
2361
+ expect(
2362
+ reassembler.addFragment({
2363
+ ...common,
2364
+ fragmentationIndicator: 1,
2365
+ fragmentCounter: 2,
2366
+ sampleOffset: 0,
2367
+ data: new Uint8Array([161, 162]),
2368
+ packetSequenceNumber: testPacketSequence(0, 2)
2369
+ })
2370
+ ).toBeNull();
2371
+ expect(
2372
+ reassembler.addFragment({
2373
+ ...common,
2374
+ fragmentationIndicator: 3,
2375
+ fragmentCounter: 0,
2376
+ sampleOffset: 4,
2377
+ data: new Uint8Array([193, 194]),
2378
+ packetSequenceNumber: testPacketSequence(0, 0)
2379
+ })
2380
+ ).toBeNull();
2381
+ expect(reassembler.pendingCount).toBe(1);
2161
2382
  const out = reassembler.addFragment({
2162
2383
  ...common,
2163
2384
  fragmentationIndicator: 2,
@@ -2166,33 +2387,35 @@ if (void 0) {
2166
2387
  data: new Uint8Array([177, 178]),
2167
2388
  packetSequenceNumber: testPacketSequence(0, 1)
2168
2389
  });
2169
- expect(out?.data).toEqual(
2170
- new Uint8Array([161, 162, 177, 178, 193, 194])
2171
- );
2390
+ expect(out?.data).toEqual(new Uint8Array([161, 162, 177, 178, 193, 194]));
2172
2391
  });
2173
2392
  test("reassembles first-only DU-header fragments across changing packet timestamps", () => {
2174
2393
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2175
- expect(reassembler.addFragment({
2176
- fragmentationIndicator: 1,
2177
- mpuSequenceNumber: 44,
2178
- fragmentCounter: 2,
2179
- movieFragmentSequenceNumber: 9,
2180
- sampleNumber: 17,
2181
- sampleOffset: 0,
2182
- data: new Uint8Array([161]),
2183
- timestamp: 12345n,
2184
- rapFlag: true,
2185
- packetSequenceNumber: testPacketSequence(44, 2)
2186
- })).toBeNull();
2187
- expect(reassembler.addFragment({
2188
- fragmentationIndicator: 2,
2189
- mpuSequenceNumber: 44,
2190
- fragmentCounter: 1,
2191
- data: new Uint8Array([178]),
2192
- timestamp: 12346n,
2193
- rapFlag: false,
2194
- packetSequenceNumber: testPacketSequence(44, 1)
2195
- })).toBeNull();
2394
+ expect(
2395
+ reassembler.addFragment({
2396
+ fragmentationIndicator: 1,
2397
+ mpuSequenceNumber: 44,
2398
+ fragmentCounter: 2,
2399
+ movieFragmentSequenceNumber: 9,
2400
+ sampleNumber: 17,
2401
+ sampleOffset: 0,
2402
+ data: new Uint8Array([161]),
2403
+ timestamp: 12345n,
2404
+ rapFlag: true,
2405
+ packetSequenceNumber: testPacketSequence(44, 2)
2406
+ })
2407
+ ).toBeNull();
2408
+ expect(
2409
+ reassembler.addFragment({
2410
+ fragmentationIndicator: 2,
2411
+ mpuSequenceNumber: 44,
2412
+ fragmentCounter: 1,
2413
+ data: new Uint8Array([178]),
2414
+ timestamp: 12346n,
2415
+ rapFlag: false,
2416
+ packetSequenceNumber: testPacketSequence(44, 1)
2417
+ })
2418
+ ).toBeNull();
2196
2419
  const out = reassembler.addFragment({
2197
2420
  fragmentationIndicator: 3,
2198
2421
  mpuSequenceNumber: 44,
@@ -2211,36 +2434,42 @@ if (void 0) {
2211
2434
  test("reassembles first-only DU-header fragments with an ISO countdown", () => {
2212
2435
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2213
2436
  const timestamp = 12345n;
2214
- expect(reassembler.addFragment({
2215
- fragmentationIndicator: 1,
2216
- mpuSequenceNumber: 44,
2217
- fragmentCounter: 3,
2218
- movieFragmentSequenceNumber: 9,
2219
- sampleNumber: 17,
2220
- sampleOffset: 0,
2221
- data: new Uint8Array([161]),
2222
- timestamp,
2223
- rapFlag: true,
2224
- packetSequenceNumber: testPacketSequence(44, 3)
2225
- })).toBeNull();
2226
- expect(reassembler.addFragment({
2227
- fragmentationIndicator: 2,
2228
- mpuSequenceNumber: 44,
2229
- fragmentCounter: 2,
2230
- data: new Uint8Array([178]),
2231
- timestamp,
2232
- rapFlag: false,
2233
- packetSequenceNumber: testPacketSequence(44, 2)
2234
- })).toBeNull();
2235
- expect(reassembler.addFragment({
2236
- fragmentationIndicator: 2,
2237
- mpuSequenceNumber: 44,
2238
- fragmentCounter: 1,
2239
- data: new Uint8Array([195]),
2240
- timestamp,
2241
- rapFlag: false,
2242
- packetSequenceNumber: testPacketSequence(44, 1)
2243
- })).toBeNull();
2437
+ expect(
2438
+ reassembler.addFragment({
2439
+ fragmentationIndicator: 1,
2440
+ mpuSequenceNumber: 44,
2441
+ fragmentCounter: 3,
2442
+ movieFragmentSequenceNumber: 9,
2443
+ sampleNumber: 17,
2444
+ sampleOffset: 0,
2445
+ data: new Uint8Array([161]),
2446
+ timestamp,
2447
+ rapFlag: true,
2448
+ packetSequenceNumber: testPacketSequence(44, 3)
2449
+ })
2450
+ ).toBeNull();
2451
+ expect(
2452
+ reassembler.addFragment({
2453
+ fragmentationIndicator: 2,
2454
+ mpuSequenceNumber: 44,
2455
+ fragmentCounter: 2,
2456
+ data: new Uint8Array([178]),
2457
+ timestamp,
2458
+ rapFlag: false,
2459
+ packetSequenceNumber: testPacketSequence(44, 2)
2460
+ })
2461
+ ).toBeNull();
2462
+ expect(
2463
+ reassembler.addFragment({
2464
+ fragmentationIndicator: 2,
2465
+ mpuSequenceNumber: 44,
2466
+ fragmentCounter: 1,
2467
+ data: new Uint8Array([195]),
2468
+ timestamp,
2469
+ rapFlag: false,
2470
+ packetSequenceNumber: testPacketSequence(44, 1)
2471
+ })
2472
+ ).toBeNull();
2244
2473
  const out = reassembler.addFragment({
2245
2474
  fragmentationIndicator: 3,
2246
2475
  mpuSequenceNumber: 44,
@@ -2258,107 +2487,134 @@ if (void 0) {
2258
2487
  test("rejects fragmented MFUs without an authored DU identity", () => {
2259
2488
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2260
2489
  const timestamp = 22222n;
2261
- expectRejection(() => reassembler.addFragment({
2262
- fragmentationIndicator: 1,
2263
- mpuSequenceNumber: 55,
2264
- fragmentCounter: 2,
2265
- data: new Uint8Array([161]),
2266
- timestamp,
2267
- rapFlag: true,
2268
- packetSequenceNumber: testPacketSequence(55, 2)
2269
- }), "missing-data-unit-identity");
2490
+ expectRejection(
2491
+ () => reassembler.addFragment({
2492
+ fragmentationIndicator: 1,
2493
+ mpuSequenceNumber: 55,
2494
+ fragmentCounter: 2,
2495
+ data: new Uint8Array([161]),
2496
+ timestamp,
2497
+ rapFlag: true,
2498
+ packetSequenceNumber: testPacketSequence(55, 2)
2499
+ }),
2500
+ "missing-data-unit-identity"
2501
+ );
2270
2502
  expect(reassembler.pendingCount).toBe(0);
2271
2503
  });
2272
2504
  test("rejects fragmented MFUs without an authored packet sequence", () => {
2273
2505
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2274
- expectRejection(() => reassembler.addFragment({
2275
- fragmentationIndicator: 1,
2276
- mpuSequenceNumber: 55,
2277
- fragmentCounter: 1,
2278
- itemId: 1,
2279
- data: new Uint8Array([161]),
2280
- timestamp: 22222n,
2281
- rapFlag: true
2282
- }), "missing-packet-sequence");
2283
- expect(reassembler.pendingCount).toBe(0);
2284
- });
2285
- test("rejects fragmented MFUs whose authored packet sequence is not u32", () => {
2286
- const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2287
- for (const packetSequenceNumber of [-1, 4294967296, 1.5, Number.NaN]) {
2288
- expectRejection(() => reassembler.addFragment({
2506
+ expectRejection(
2507
+ () => reassembler.addFragment({
2289
2508
  fragmentationIndicator: 1,
2290
2509
  mpuSequenceNumber: 55,
2291
2510
  fragmentCounter: 1,
2292
2511
  itemId: 1,
2293
2512
  data: new Uint8Array([161]),
2294
2513
  timestamp: 22222n,
2295
- rapFlag: true,
2296
- packetSequenceNumber
2297
- }), "missing-packet-sequence");
2514
+ rapFlag: true
2515
+ }),
2516
+ "missing-packet-sequence"
2517
+ );
2518
+ expect(reassembler.pendingCount).toBe(0);
2519
+ });
2520
+ test("rejects fragmented MFUs whose authored packet sequence is not u32", () => {
2521
+ const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2522
+ for (const packetSequenceNumber of [-1, 4294967296, 1.5, Number.NaN]) {
2523
+ expectRejection(
2524
+ () => reassembler.addFragment({
2525
+ fragmentationIndicator: 1,
2526
+ mpuSequenceNumber: 55,
2527
+ fragmentCounter: 1,
2528
+ itemId: 1,
2529
+ data: new Uint8Array([161]),
2530
+ timestamp: 22222n,
2531
+ rapFlag: true,
2532
+ packetSequenceNumber
2533
+ }),
2534
+ "missing-packet-sequence"
2535
+ );
2298
2536
  }
2299
2537
  expect(reassembler.pendingCount).toBe(0);
2300
2538
  });
2301
- test("fails closed when one fragmented DU has interleaved packet sequence gaps", () => {
2539
+ test("a packet sequence gap inside a fragmented DU splits it and never completes", () => {
2302
2540
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2303
2541
  const timestamp = 66666n;
2304
- expect(reassembler.addFragment({
2305
- fragmentationIndicator: 1,
2306
- mpuSequenceNumber: 80,
2307
- fragmentCounter: 17,
2308
- movieFragmentSequenceNumber: 241,
2309
- sampleNumber: 1,
2310
- sampleOffset: 0,
2311
- packetSequenceNumber: 1e3,
2312
- data: new Uint8Array([160]),
2313
- timestamp,
2314
- rapFlag: true
2315
- })).toBeNull();
2316
- for (let i = 1; i < 17; i++) {
2317
- expect(reassembler.addFragment({
2318
- fragmentationIndicator: 2,
2542
+ expect(
2543
+ reassembler.addFragment({
2544
+ fragmentationIndicator: 1,
2319
2545
  mpuSequenceNumber: 80,
2320
- fragmentCounter: 17 - i,
2321
- packetSequenceNumber: 1e3 + i * 2,
2322
- data: new Uint8Array([160 + i]),
2546
+ fragmentCounter: 3,
2547
+ movieFragmentSequenceNumber: 241,
2548
+ sampleNumber: 1,
2549
+ sampleOffset: 0,
2550
+ packetSequenceNumber: 1e3,
2551
+ data: new Uint8Array([160]),
2323
2552
  timestamp,
2324
- rapFlag: false
2325
- })).toBeNull();
2553
+ rapFlag: true
2554
+ })
2555
+ ).toBeNull();
2556
+ for (const [counter, psn] of [
2557
+ [2, 1002],
2558
+ [1, 1003]
2559
+ ]) {
2560
+ expect(
2561
+ reassembler.addFragment({
2562
+ fragmentationIndicator: 2,
2563
+ mpuSequenceNumber: 80,
2564
+ fragmentCounter: counter,
2565
+ packetSequenceNumber: psn,
2566
+ data: new Uint8Array([160 + counter]),
2567
+ timestamp,
2568
+ rapFlag: false
2569
+ })
2570
+ ).toBeNull();
2326
2571
  }
2327
- const out = reassembler.addFragment({
2328
- fragmentationIndicator: 3,
2329
- mpuSequenceNumber: 80,
2330
- fragmentCounter: 0,
2331
- packetSequenceNumber: 1040,
2332
- data: new Uint8Array([177]),
2333
- timestamp,
2334
- rapFlag: false
2335
- });
2336
- expect(out).toBeNull();
2337
- expect(reassembler.pendingCount).toBe(18);
2572
+ expect(
2573
+ reassembler.addFragment({
2574
+ fragmentationIndicator: 3,
2575
+ mpuSequenceNumber: 80,
2576
+ fragmentCounter: 0,
2577
+ packetSequenceNumber: 1004,
2578
+ data: new Uint8Array([177]),
2579
+ timestamp,
2580
+ rapFlag: false
2581
+ })
2582
+ ).toBeNull();
2583
+ expect(reassembler.pendingCount).toBe(2);
2584
+ expect(reassembler.getStatus().pendingFragments).toBe(4);
2338
2585
  expect(reassembler.stats.reassembledMFUs).toBe(0);
2586
+ expect(reassembler.stats.rejectedFragments).toBe(0);
2339
2587
  });
2340
- test("does not combine adjacent reordered MFUs in one MPU", () => {
2588
+ test("keeps adjacent MFUs with equal countdowns in one MPU separate", () => {
2341
2589
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2342
2590
  const common = { mpuSequenceNumber: 80, packetId: 7, timestamp: 66666n };
2343
- expect(reassembler.addFragment({
2344
- ...common,
2345
- fragmentationIndicator: 3,
2346
- fragmentCounter: 0,
2347
- packetSequenceNumber: 103,
2348
- data: new Uint8Array([178]),
2349
- rapFlag: false
2350
- })).toBeNull();
2351
- expect(reassembler.addFragment({
2352
- ...common,
2353
- fragmentationIndicator: 1,
2354
- fragmentCounter: 1,
2355
- packetSequenceNumber: 100,
2356
- movieFragmentSequenceNumber: 241,
2357
- sampleNumber: 1,
2358
- sampleOffset: 0,
2359
- data: new Uint8Array([161]),
2360
- rapFlag: true
2361
- })).toBeNull();
2591
+ expect(
2592
+ reassembler.addFragment({
2593
+ ...common,
2594
+ fragmentationIndicator: 1,
2595
+ fragmentCounter: 1,
2596
+ packetSequenceNumber: 100,
2597
+ movieFragmentSequenceNumber: 241,
2598
+ sampleNumber: 1,
2599
+ sampleOffset: 0,
2600
+ data: new Uint8Array([161]),
2601
+ rapFlag: true
2602
+ })
2603
+ ).toBeNull();
2604
+ expect(
2605
+ reassembler.addFragment({
2606
+ ...common,
2607
+ fragmentationIndicator: 1,
2608
+ fragmentCounter: 1,
2609
+ packetSequenceNumber: 102,
2610
+ movieFragmentSequenceNumber: 241,
2611
+ sampleNumber: 2,
2612
+ sampleOffset: 0,
2613
+ data: new Uint8Array([177]),
2614
+ rapFlag: false
2615
+ })
2616
+ ).toBeNull();
2617
+ expect(reassembler.pendingCount).toBe(2);
2362
2618
  const earlier = reassembler.addFragment({
2363
2619
  ...common,
2364
2620
  fragmentationIndicator: 3,
@@ -2368,105 +2624,122 @@ if (void 0) {
2368
2624
  rapFlag: false
2369
2625
  });
2370
2626
  expect(earlier?.data).toEqual(new Uint8Array([161, 162]));
2627
+ expect(earlier?.packetSequenceNumbers).toEqual([100, 101]);
2371
2628
  const later = reassembler.addFragment({
2372
2629
  ...common,
2373
- fragmentationIndicator: 1,
2374
- fragmentCounter: 1,
2375
- packetSequenceNumber: 102,
2376
- movieFragmentSequenceNumber: 241,
2377
- sampleNumber: 2,
2378
- sampleOffset: 0,
2379
- data: new Uint8Array([177]),
2630
+ fragmentationIndicator: 3,
2631
+ fragmentCounter: 0,
2632
+ packetSequenceNumber: 103,
2633
+ data: new Uint8Array([178]),
2380
2634
  rapFlag: false
2381
2635
  });
2382
2636
  expect(later?.data).toEqual(new Uint8Array([177, 178]));
2637
+ expect(later?.packetSequenceNumbers).toEqual([102, 103]);
2638
+ expect(reassembler.pendingCount).toBe(0);
2383
2639
  });
2384
2640
  test("wraps the fragmented run coordinate at u32 packet sequence", () => {
2385
2641
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2386
- expect(reassembler.addFragment({
2387
- fragmentationIndicator: 1,
2388
- mpuSequenceNumber: 81,
2389
- fragmentCounter: 1,
2390
- packetSequenceNumber: 4294967295,
2391
- itemId: 1,
2392
- data: new Uint8Array([161]),
2393
- timestamp: 66666n,
2394
- rapFlag: true
2395
- })).toBeNull();
2396
- expect(reassembler.addFragment({
2397
- fragmentationIndicator: 3,
2398
- mpuSequenceNumber: 81,
2399
- fragmentCounter: 0,
2400
- packetSequenceNumber: 0,
2401
- data: new Uint8Array([162]),
2402
- timestamp: 66667n,
2403
- rapFlag: false
2404
- })?.data).toEqual(new Uint8Array([161, 162]));
2642
+ expect(
2643
+ reassembler.addFragment({
2644
+ fragmentationIndicator: 1,
2645
+ mpuSequenceNumber: 81,
2646
+ fragmentCounter: 1,
2647
+ packetSequenceNumber: 4294967295,
2648
+ itemId: 1,
2649
+ data: new Uint8Array([161]),
2650
+ timestamp: 66666n,
2651
+ rapFlag: true
2652
+ })
2653
+ ).toBeNull();
2654
+ expect(
2655
+ reassembler.addFragment({
2656
+ fragmentationIndicator: 3,
2657
+ mpuSequenceNumber: 81,
2658
+ fragmentCounter: 0,
2659
+ packetSequenceNumber: 0,
2660
+ data: new Uint8Array([162]),
2661
+ timestamp: 66667n,
2662
+ rapFlag: false
2663
+ })?.data
2664
+ ).toEqual(new Uint8Array([161, 162]));
2405
2665
  });
2406
2666
  test("dedupes recovered copies by ISO fragment-counter position", () => {
2407
2667
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2408
2668
  const timestamp = 44444n;
2409
- expect(reassembler.addFragment({
2410
- fragmentationIndicator: 1,
2411
- mpuSequenceNumber: 77,
2412
- fragmentCounter: 2,
2413
- movieFragmentSequenceNumber: 12,
2414
- sampleNumber: 1,
2415
- sampleOffset: 0,
2416
- packetSequenceNumber: 2e3,
2417
- data: new Uint8Array([161]),
2418
- timestamp,
2419
- rapFlag: true
2420
- })).toBeNull();
2421
- expect(reassembler.addFragment({
2422
- fragmentationIndicator: 2,
2423
- mpuSequenceNumber: 77,
2424
- fragmentCounter: 1,
2425
- packetSequenceNumber: 2001,
2426
- data: new Uint8Array([178]),
2427
- timestamp,
2428
- rapFlag: false
2429
- })).toBeNull();
2430
- expect(reassembler.addFragment({
2431
- fragmentationIndicator: 3,
2432
- mpuSequenceNumber: 77,
2433
- fragmentCounter: 0,
2434
- packetSequenceNumber: 2002,
2435
- data: new Uint8Array([195]),
2436
- timestamp,
2437
- rapFlag: false
2438
- })).not.toBeNull();
2669
+ expect(
2670
+ reassembler.addFragment({
2671
+ fragmentationIndicator: 1,
2672
+ mpuSequenceNumber: 77,
2673
+ fragmentCounter: 2,
2674
+ movieFragmentSequenceNumber: 12,
2675
+ sampleNumber: 1,
2676
+ sampleOffset: 0,
2677
+ packetSequenceNumber: 2e3,
2678
+ data: new Uint8Array([161]),
2679
+ timestamp,
2680
+ rapFlag: true
2681
+ })
2682
+ ).toBeNull();
2683
+ expect(
2684
+ reassembler.addFragment({
2685
+ fragmentationIndicator: 2,
2686
+ mpuSequenceNumber: 77,
2687
+ fragmentCounter: 1,
2688
+ packetSequenceNumber: 2001,
2689
+ data: new Uint8Array([178]),
2690
+ timestamp,
2691
+ rapFlag: false
2692
+ })
2693
+ ).toBeNull();
2694
+ expect(
2695
+ reassembler.addFragment({
2696
+ fragmentationIndicator: 3,
2697
+ mpuSequenceNumber: 77,
2698
+ fragmentCounter: 0,
2699
+ packetSequenceNumber: 2002,
2700
+ data: new Uint8Array([195]),
2701
+ timestamp,
2702
+ rapFlag: false
2703
+ })
2704
+ ).not.toBeNull();
2439
2705
  reassembler.flush();
2440
- expect(reassembler.addFragment({
2441
- fragmentationIndicator: 1,
2442
- mpuSequenceNumber: 78,
2443
- fragmentCounter: 2,
2444
- movieFragmentSequenceNumber: 13,
2445
- sampleNumber: 1,
2446
- sampleOffset: 0,
2447
- packetSequenceNumber: 3e3,
2448
- data: new Uint8Array([212]),
2449
- timestamp,
2450
- rapFlag: true
2451
- })).toBeNull();
2452
- expect(reassembler.addFragment({
2453
- fragmentationIndicator: 2,
2454
- mpuSequenceNumber: 78,
2455
- fragmentCounter: 1,
2456
- packetSequenceNumber: 3001,
2457
- data: new Uint8Array([229]),
2458
- timestamp,
2459
- rapFlag: false
2460
- })).toBeNull();
2461
- expectRejection(() => reassembler.addFragment({
2462
- fragmentationIndicator: 2,
2463
- mpuSequenceNumber: 78,
2464
- fragmentCounter: 1,
2465
- packetSequenceNumber: 3001,
2466
- data: new Uint8Array([229]),
2467
- timestamp,
2468
- rapFlag: false
2469
- }), "fragment-counter-mismatch");
2706
+ expect(
2707
+ reassembler.addFragment({
2708
+ fragmentationIndicator: 1,
2709
+ mpuSequenceNumber: 78,
2710
+ fragmentCounter: 2,
2711
+ movieFragmentSequenceNumber: 13,
2712
+ sampleNumber: 1,
2713
+ sampleOffset: 0,
2714
+ packetSequenceNumber: 3e3,
2715
+ data: new Uint8Array([212]),
2716
+ timestamp,
2717
+ rapFlag: true
2718
+ })
2719
+ ).toBeNull();
2720
+ expect(
2721
+ reassembler.addFragment({
2722
+ fragmentationIndicator: 2,
2723
+ mpuSequenceNumber: 78,
2724
+ fragmentCounter: 1,
2725
+ packetSequenceNumber: 3001,
2726
+ data: new Uint8Array([229]),
2727
+ timestamp,
2728
+ rapFlag: false
2729
+ })
2730
+ ).toBeNull();
2731
+ expectRejection(
2732
+ () => reassembler.addFragment({
2733
+ fragmentationIndicator: 2,
2734
+ mpuSequenceNumber: 78,
2735
+ fragmentCounter: 1,
2736
+ packetSequenceNumber: 3001,
2737
+ data: new Uint8Array([229]),
2738
+ timestamp,
2739
+ rapFlag: false
2740
+ }),
2741
+ "fragment-counter-mismatch"
2742
+ );
2470
2743
  const out = reassembler.addFragment({
2471
2744
  fragmentationIndicator: 3,
2472
2745
  mpuSequenceNumber: 78,
@@ -2482,24 +2755,28 @@ if (void 0) {
2482
2755
  test("reassembles middle and terminal continuations that arrive before first", () => {
2483
2756
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2484
2757
  const timestamp = 54321n;
2485
- expect(reassembler.addFragment({
2486
- fragmentationIndicator: 2,
2487
- mpuSequenceNumber: 45,
2488
- fragmentCounter: 1,
2489
- data: new Uint8Array([178]),
2490
- timestamp,
2491
- rapFlag: false,
2492
- packetSequenceNumber: testPacketSequence(45, 1)
2493
- })).toBeNull();
2494
- expect(reassembler.addFragment({
2495
- fragmentationIndicator: 3,
2496
- mpuSequenceNumber: 45,
2497
- fragmentCounter: 0,
2498
- data: new Uint8Array([195]),
2499
- timestamp: timestamp + 1n,
2500
- rapFlag: false,
2501
- packetSequenceNumber: testPacketSequence(45, 0)
2502
- })).toBeNull();
2758
+ expect(
2759
+ reassembler.addFragment({
2760
+ fragmentationIndicator: 2,
2761
+ mpuSequenceNumber: 45,
2762
+ fragmentCounter: 1,
2763
+ data: new Uint8Array([178]),
2764
+ timestamp,
2765
+ rapFlag: false,
2766
+ packetSequenceNumber: testPacketSequence(45, 1)
2767
+ })
2768
+ ).toBeNull();
2769
+ expect(
2770
+ reassembler.addFragment({
2771
+ fragmentationIndicator: 3,
2772
+ mpuSequenceNumber: 45,
2773
+ fragmentCounter: 0,
2774
+ data: new Uint8Array([195]),
2775
+ timestamp: timestamp + 1n,
2776
+ rapFlag: false,
2777
+ packetSequenceNumber: testPacketSequence(45, 0)
2778
+ })
2779
+ ).toBeNull();
2503
2780
  expect(reassembler.pendingCount).toBe(1);
2504
2781
  const out = reassembler.addFragment({
2505
2782
  fragmentationIndicator: 1,
@@ -2522,33 +2799,40 @@ if (void 0) {
2522
2799
  test("rejects a duplicate continuation before first without replacing retained media", () => {
2523
2800
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2524
2801
  const timestamp = 54322n;
2525
- expect(reassembler.addFragment({
2526
- fragmentationIndicator: 3,
2527
- mpuSequenceNumber: 46,
2528
- fragmentCounter: 0,
2529
- data: new Uint8Array([195]),
2530
- timestamp,
2531
- rapFlag: false,
2532
- packetSequenceNumber: testPacketSequence(46, 0)
2533
- })).toBeNull();
2534
- expectRejection(() => reassembler.addFragment({
2535
- fragmentationIndicator: 3,
2536
- mpuSequenceNumber: 46,
2537
- fragmentCounter: 0,
2538
- data: new Uint8Array([255]),
2539
- timestamp: timestamp + 1n,
2540
- rapFlag: false,
2541
- packetSequenceNumber: testPacketSequence(46, 0)
2542
- }), "fragment-counter-mismatch");
2543
- expect(reassembler.addFragment({
2544
- fragmentationIndicator: 2,
2545
- mpuSequenceNumber: 46,
2546
- fragmentCounter: 1,
2547
- data: new Uint8Array([178]),
2548
- timestamp: timestamp + 2n,
2549
- rapFlag: false,
2550
- packetSequenceNumber: testPacketSequence(46, 1)
2551
- })).toBeNull();
2802
+ expect(
2803
+ reassembler.addFragment({
2804
+ fragmentationIndicator: 3,
2805
+ mpuSequenceNumber: 46,
2806
+ fragmentCounter: 0,
2807
+ data: new Uint8Array([195]),
2808
+ timestamp,
2809
+ rapFlag: false,
2810
+ packetSequenceNumber: testPacketSequence(46, 0)
2811
+ })
2812
+ ).toBeNull();
2813
+ expectRejection(
2814
+ () => reassembler.addFragment({
2815
+ fragmentationIndicator: 3,
2816
+ mpuSequenceNumber: 46,
2817
+ fragmentCounter: 0,
2818
+ data: new Uint8Array([255]),
2819
+ timestamp: timestamp + 1n,
2820
+ rapFlag: false,
2821
+ packetSequenceNumber: testPacketSequence(46, 0)
2822
+ }),
2823
+ "fragment-counter-mismatch"
2824
+ );
2825
+ expect(
2826
+ reassembler.addFragment({
2827
+ fragmentationIndicator: 2,
2828
+ mpuSequenceNumber: 46,
2829
+ fragmentCounter: 1,
2830
+ data: new Uint8Array([178]),
2831
+ timestamp: timestamp + 2n,
2832
+ rapFlag: false,
2833
+ packetSequenceNumber: testPacketSequence(46, 1)
2834
+ })
2835
+ ).toBeNull();
2552
2836
  const out = reassembler.addFragment({
2553
2837
  fragmentationIndicator: 1,
2554
2838
  mpuSequenceNumber: 46,
@@ -2581,13 +2865,16 @@ if (void 0) {
2581
2865
  { fragmentationIndicator: 1, fragmentCounter: Number.NaN }
2582
2866
  ];
2583
2867
  for (const header of invalidHeaders) {
2584
- expectRejection(() => reassembler.addFragment({
2585
- ...header,
2586
- mpuSequenceNumber: 48,
2587
- data: new Uint8Array([161]),
2588
- timestamp: 54324n,
2589
- rapFlag: false
2590
- }), "invalid-fragment-state");
2868
+ expectRejection(
2869
+ () => reassembler.addFragment({
2870
+ ...header,
2871
+ mpuSequenceNumber: 48,
2872
+ data: new Uint8Array([161]),
2873
+ timestamp: 54324n,
2874
+ rapFlag: false
2875
+ }),
2876
+ "invalid-fragment-state"
2877
+ );
2591
2878
  }
2592
2879
  expect(reassembler.pendingCount).toBe(0);
2593
2880
  expect(reassembler.stats.reassembledMFUs).toBe(0);
@@ -2599,23 +2886,27 @@ if (void 0) {
2599
2886
  timestamp: 54323n,
2600
2887
  rapFlag: false
2601
2888
  };
2602
- expect(reassembler.addFragment({
2603
- ...common,
2604
- fragmentationIndicator: 1,
2605
- fragmentCounter: 255,
2606
- itemId: 1,
2607
- data: new Uint8Array([0]),
2608
- rapFlag: true,
2609
- packetSequenceNumber: testPacketSequence(47, 255)
2610
- })).toBeNull();
2611
- for (let counter = 254; counter > 0; counter--) {
2612
- expect(reassembler.addFragment({
2889
+ expect(
2890
+ reassembler.addFragment({
2613
2891
  ...common,
2614
- fragmentationIndicator: 2,
2615
- fragmentCounter: counter,
2616
- data: new Uint8Array([255 - counter]),
2617
- packetSequenceNumber: testPacketSequence(47, counter)
2618
- })).toBeNull();
2892
+ fragmentationIndicator: 1,
2893
+ fragmentCounter: 255,
2894
+ itemId: 1,
2895
+ data: new Uint8Array([0]),
2896
+ rapFlag: true,
2897
+ packetSequenceNumber: testPacketSequence(47, 255)
2898
+ })
2899
+ ).toBeNull();
2900
+ for (let counter = 254; counter > 0; counter--) {
2901
+ expect(
2902
+ reassembler.addFragment({
2903
+ ...common,
2904
+ fragmentationIndicator: 2,
2905
+ fragmentCounter: counter,
2906
+ data: new Uint8Array([255 - counter]),
2907
+ packetSequenceNumber: testPacketSequence(47, counter)
2908
+ })
2909
+ ).toBeNull();
2619
2910
  }
2620
2911
  const out = reassembler.addFragment({
2621
2912
  ...common,
@@ -2652,37 +2943,49 @@ if (void 0) {
2652
2943
  expect(reassembler.pendingCount).toBe(1);
2653
2944
  });
2654
2945
  test("bounds continuations that arrive before their first fragments", () => {
2655
- const reassembler = new MFUReassembler({ timeoutMs: null, maxBuffered: 1 });
2656
- expect(reassembler.addFragment({
2657
- fragmentationIndicator: 3,
2658
- mpuSequenceNumber: 1,
2659
- fragmentCounter: 0,
2660
- data: new Uint8Array([1]),
2661
- timestamp: 1000n,
2662
- rapFlag: false,
2663
- packetSequenceNumber: testPacketSequence(1, 0)
2664
- })).toBeNull();
2665
- expectRejection(() => reassembler.addFragment({
2666
- fragmentationIndicator: 3,
2667
- mpuSequenceNumber: 2,
2668
- fragmentCounter: 0,
2669
- data: new Uint8Array([2]),
2670
- timestamp: 1000n,
2671
- rapFlag: false,
2672
- packetSequenceNumber: testPacketSequence(2, 0)
2673
- }), "active-data-unit-limit");
2674
- expectRejection(() => reassembler.addFragment({
2946
+ const reassembler = new MFUReassembler({
2947
+ timeoutMs: null,
2948
+ maxBuffered: 1
2949
+ });
2950
+ expect(
2951
+ reassembler.addFragment({
2952
+ fragmentationIndicator: 3,
2953
+ mpuSequenceNumber: 1,
2954
+ fragmentCounter: 0,
2955
+ data: new Uint8Array([1]),
2956
+ timestamp: 1000n,
2957
+ rapFlag: false,
2958
+ packetSequenceNumber: testPacketSequence(1, 0)
2959
+ })
2960
+ ).toBeNull();
2961
+ expect(reassembler.pendingCount).toBe(1);
2962
+ expect(
2963
+ reassembler.addFragment({
2964
+ fragmentationIndicator: 3,
2965
+ mpuSequenceNumber: 2,
2966
+ fragmentCounter: 0,
2967
+ data: new Uint8Array([2]),
2968
+ timestamp: 1000n,
2969
+ rapFlag: false,
2970
+ packetSequenceNumber: testPacketSequence(2, 0)
2971
+ })
2972
+ ).toBeNull();
2973
+ expect(reassembler.pendingCount).toBe(1);
2974
+ expect(reassembler.getStatus().pendingFragments).toBe(1);
2975
+ expect(reassembler.stats.evictedMemoryLimit).toBe(1);
2976
+ const completed = reassembler.addFragment({
2675
2977
  fragmentationIndicator: 1,
2676
2978
  mpuSequenceNumber: 2,
2677
2979
  fragmentCounter: 1,
2678
2980
  itemId: 2,
2679
- data: new Uint8Array([2]),
2981
+ data: new Uint8Array([4]),
2680
2982
  timestamp: 1000n,
2681
2983
  rapFlag: true,
2682
2984
  packetSequenceNumber: testPacketSequence(2, 1)
2683
- }), "active-data-unit-limit");
2684
- expect(reassembler.pendingCount).toBe(1);
2685
- expect(reassembler.getStatus().pendingFragments).toBe(1);
2985
+ });
2986
+ expect(completed?.data).toEqual(new Uint8Array([4, 2]));
2987
+ expect(reassembler.stats.evictedMemoryLimit).toBe(1);
2988
+ expect(reassembler.pendingCount).toBe(0);
2686
2989
  const result = reassembler.addFragment({
2687
2990
  fragmentationIndicator: 1,
2688
2991
  mpuSequenceNumber: 1,
@@ -2693,21 +2996,27 @@ if (void 0) {
2693
2996
  rapFlag: true,
2694
2997
  packetSequenceNumber: testPacketSequence(1, 1)
2695
2998
  });
2696
- expect(result?.data).toEqual(new Uint8Array([3, 1]));
2999
+ expect(result).toBeNull();
3000
+ expect(reassembler.pendingCount).toBe(1);
2697
3001
  });
2698
3002
  test("expires a continuation that never receives its first fragment", () => {
2699
3003
  vi.useFakeTimers();
2700
3004
  try {
2701
- const reassembler = new MFUReassembler({ timeoutMs: 50, maxBuffered: 1 });
2702
- expect(reassembler.addFragment({
2703
- fragmentationIndicator: 3,
2704
- mpuSequenceNumber: 1,
2705
- fragmentCounter: 0,
2706
- data: new Uint8Array([1]),
2707
- timestamp: 1000n,
2708
- rapFlag: false,
2709
- packetSequenceNumber: testPacketSequence(1, 0)
2710
- })).toBeNull();
3005
+ const reassembler = new MFUReassembler({
3006
+ timeoutMs: 50,
3007
+ maxBuffered: 1
3008
+ });
3009
+ expect(
3010
+ reassembler.addFragment({
3011
+ fragmentationIndicator: 3,
3012
+ mpuSequenceNumber: 1,
3013
+ fragmentCounter: 0,
3014
+ data: new Uint8Array([1]),
3015
+ timestamp: 1000n,
3016
+ rapFlag: false,
3017
+ packetSequenceNumber: testPacketSequence(1, 0)
3018
+ })
3019
+ ).toBeNull();
2711
3020
  vi.advanceTimersByTime(50);
2712
3021
  expect(reassembler.pendingCount).toBe(0);
2713
3022
  expect(reassembler.stats.timedOutMFUs).toBe(1);
@@ -2719,28 +3028,32 @@ if (void 0) {
2719
3028
  vi.useFakeTimers();
2720
3029
  try {
2721
3030
  const enabled = new MFUReassembler({ timeoutMs: null, maxBuffered: 1 });
2722
- expect(enabled.addFragment({
2723
- fragmentationIndicator: 3,
2724
- mpuSequenceNumber: 1,
2725
- fragmentCounter: 0,
2726
- data: new Uint8Array([1]),
2727
- timestamp: 1000n,
2728
- rapFlag: false,
2729
- packetSequenceNumber: testPacketSequence(1, 0)
2730
- })).toBeNull();
3031
+ expect(
3032
+ enabled.addFragment({
3033
+ fragmentationIndicator: 3,
3034
+ mpuSequenceNumber: 1,
3035
+ fragmentCounter: 0,
3036
+ data: new Uint8Array([1]),
3037
+ timestamp: 1000n,
3038
+ rapFlag: false,
3039
+ packetSequenceNumber: testPacketSequence(1, 0)
3040
+ })
3041
+ ).toBeNull();
2731
3042
  enabled.timeoutMs = 10;
2732
3043
  vi.advanceTimersByTime(10);
2733
3044
  expect(enabled.pendingCount).toBe(0);
2734
3045
  const disabled = new MFUReassembler({ timeoutMs: 10, maxBuffered: 1 });
2735
- expect(disabled.addFragment({
2736
- fragmentationIndicator: 3,
2737
- mpuSequenceNumber: 2,
2738
- fragmentCounter: 0,
2739
- data: new Uint8Array([2]),
2740
- timestamp: 1000n,
2741
- rapFlag: false,
2742
- packetSequenceNumber: testPacketSequence(2, 0)
2743
- })).toBeNull();
3046
+ expect(
3047
+ disabled.addFragment({
3048
+ fragmentationIndicator: 3,
3049
+ mpuSequenceNumber: 2,
3050
+ fragmentCounter: 0,
3051
+ data: new Uint8Array([2]),
3052
+ timestamp: 1000n,
3053
+ rapFlag: false,
3054
+ packetSequenceNumber: testPacketSequence(2, 0)
3055
+ })
3056
+ ).toBeNull();
2744
3057
  disabled.timeoutMs = null;
2745
3058
  vi.advanceTimersByTime(10);
2746
3059
  expect(disabled.pendingCount).toBe(1);
@@ -2749,8 +3062,11 @@ if (void 0) {
2749
3062
  vi.useRealTimers();
2750
3063
  }
2751
3064
  });
2752
- test("bounds active data units and rejects a new first at maxBuffered", () => {
2753
- const reassembler = new MFUReassembler({ timeoutMs: 1e3, maxBuffered: 2 });
3065
+ test("bounds active data units by evicting the oldest at maxBuffered", () => {
3066
+ const reassembler = new MFUReassembler({
3067
+ timeoutMs: 1e3,
3068
+ maxBuffered: 2
3069
+ });
2754
3070
  for (let i = 0; i < 2; i++) {
2755
3071
  reassembler.addFragment({
2756
3072
  fragmentationIndicator: 1,
@@ -2763,19 +3079,35 @@ if (void 0) {
2763
3079
  packetSequenceNumber: testPacketSequence(i, 1)
2764
3080
  });
2765
3081
  }
2766
- expectRejection(() => reassembler.addFragment({
2767
- fragmentationIndicator: 1,
2768
- mpuSequenceNumber: 2,
2769
- fragmentCounter: 1,
2770
- itemId: 2,
2771
- data: new Uint8Array([2]),
2772
- timestamp: 200n,
2773
- rapFlag: false,
2774
- packetSequenceNumber: testPacketSequence(2, 1)
2775
- }), "active-data-unit-limit");
3082
+ expect(
3083
+ reassembler.addFragment({
3084
+ fragmentationIndicator: 1,
3085
+ mpuSequenceNumber: 2,
3086
+ fragmentCounter: 1,
3087
+ itemId: 2,
3088
+ data: new Uint8Array([2]),
3089
+ timestamp: 200n,
3090
+ rapFlag: false,
3091
+ packetSequenceNumber: testPacketSequence(2, 1)
3092
+ })
3093
+ ).toBeNull();
3094
+ expect(reassembler.pendingCount).toBe(2);
3095
+ expect(reassembler.stats.timedOutMFUs).toBe(0);
3096
+ expect(reassembler.stats.evictedMemoryLimit).toBe(1);
3097
+ expect(
3098
+ reassembler.addFragment({
3099
+ fragmentationIndicator: 3,
3100
+ mpuSequenceNumber: 0,
3101
+ fragmentCounter: 0,
3102
+ data: new Uint8Array([9]),
3103
+ timestamp: 300n,
3104
+ rapFlag: false,
3105
+ packetSequenceNumber: testPacketSequence(0, 0)
3106
+ })
3107
+ ).toBeNull();
2776
3108
  expect(reassembler.pendingCount).toBe(2);
3109
+ expect(reassembler.stats.evictedMemoryLimit).toBe(2);
2777
3110
  expect(reassembler.stats.timedOutMFUs).toBe(0);
2778
- expect(reassembler.stats.evictedMemoryLimit).toBe(0);
2779
3111
  });
2780
3112
  test("should report stats correctly", () => {
2781
3113
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
@@ -2806,17 +3138,20 @@ if (void 0) {
2806
3138
  source: "multicast",
2807
3139
  packetSequenceNumber: testPacketSequence(1, 1)
2808
3140
  });
2809
- expectRejection(() => reassembler.addFragment({
2810
- fragmentationIndicator: 1,
2811
- mpuSequenceNumber: 1,
2812
- fragmentCounter: 1,
2813
- itemId: 2,
2814
- data: new Uint8Array([187]),
2815
- timestamp: 1000n,
2816
- rapFlag: true,
2817
- source: "moq",
2818
- packetSequenceNumber: testPacketSequence(1, 1)
2819
- }), "overlapping-data-unit");
3141
+ expectRejection(
3142
+ () => reassembler.addFragment({
3143
+ fragmentationIndicator: 1,
3144
+ mpuSequenceNumber: 1,
3145
+ fragmentCounter: 1,
3146
+ itemId: 2,
3147
+ data: new Uint8Array([187]),
3148
+ timestamp: 1000n,
3149
+ rapFlag: true,
3150
+ source: "moq",
3151
+ packetSequenceNumber: testPacketSequence(1, 1)
3152
+ }),
3153
+ "overlapping-data-unit"
3154
+ );
2820
3155
  const result = reassembler.addFragment({
2821
3156
  fragmentationIndicator: 3,
2822
3157
  mpuSequenceNumber: 1,
@@ -2844,24 +3179,28 @@ if (void 0) {
2844
3179
  rapFlag: true,
2845
3180
  packetSequenceNumber: testPacketSequence(1, 3)
2846
3181
  });
2847
- expect(reassembler.addFragment({
2848
- fragmentationIndicator: 2,
2849
- mpuSequenceNumber: 1,
2850
- fragmentCounter: 1,
2851
- data: new Uint8Array([204]),
2852
- timestamp: 1000n,
2853
- rapFlag: false,
2854
- packetSequenceNumber: testPacketSequence(1, 1)
2855
- })).toBeNull();
2856
- expect(reassembler.addFragment({
2857
- fragmentationIndicator: 3,
2858
- mpuSequenceNumber: 1,
2859
- fragmentCounter: 0,
2860
- data: new Uint8Array([221]),
2861
- timestamp: 1000n,
2862
- rapFlag: false,
2863
- packetSequenceNumber: testPacketSequence(1, 0)
2864
- })).toBeNull();
3182
+ expect(
3183
+ reassembler.addFragment({
3184
+ fragmentationIndicator: 2,
3185
+ mpuSequenceNumber: 1,
3186
+ fragmentCounter: 1,
3187
+ data: new Uint8Array([204]),
3188
+ timestamp: 1000n,
3189
+ rapFlag: false,
3190
+ packetSequenceNumber: testPacketSequence(1, 1)
3191
+ })
3192
+ ).toBeNull();
3193
+ expect(
3194
+ reassembler.addFragment({
3195
+ fragmentationIndicator: 3,
3196
+ mpuSequenceNumber: 1,
3197
+ fragmentCounter: 0,
3198
+ data: new Uint8Array([221]),
3199
+ timestamp: 1000n,
3200
+ rapFlag: false,
3201
+ packetSequenceNumber: testPacketSequence(1, 0)
3202
+ })
3203
+ ).toBeNull();
2865
3204
  expect(reassembler.pendingCount).toBe(1);
2866
3205
  expect(reassembler.getStatus().pendingFragments).toBe(3);
2867
3206
  });
@@ -2877,33 +3216,40 @@ if (void 0) {
2877
3216
  rapFlag: true,
2878
3217
  packetSequenceNumber: testPacketSequence(1, 3)
2879
3218
  });
2880
- expect(reassembler.addFragment({
2881
- fragmentationIndicator: 2,
2882
- mpuSequenceNumber: 1,
2883
- fragmentCounter: 1,
2884
- data: new Uint8Array([204]),
2885
- timestamp: 1000n,
2886
- rapFlag: false,
2887
- packetSequenceNumber: testPacketSequence(1, 1)
2888
- })).toBeNull();
2889
- expect(reassembler.addFragment({
2890
- fragmentationIndicator: 2,
2891
- mpuSequenceNumber: 1,
2892
- fragmentCounter: 2,
2893
- data: new Uint8Array([187]),
2894
- timestamp: 1000n,
2895
- rapFlag: false,
2896
- packetSequenceNumber: testPacketSequence(1, 2)
2897
- })).toBeNull();
2898
- expectRejection(() => reassembler.addFragment({
2899
- fragmentationIndicator: 2,
2900
- mpuSequenceNumber: 1,
2901
- fragmentCounter: 1,
2902
- data: new Uint8Array([204]),
2903
- timestamp: 1001n,
2904
- rapFlag: false,
2905
- packetSequenceNumber: testPacketSequence(1, 1)
2906
- }), "fragment-counter-mismatch");
3219
+ expect(
3220
+ reassembler.addFragment({
3221
+ fragmentationIndicator: 2,
3222
+ mpuSequenceNumber: 1,
3223
+ fragmentCounter: 1,
3224
+ data: new Uint8Array([204]),
3225
+ timestamp: 1000n,
3226
+ rapFlag: false,
3227
+ packetSequenceNumber: testPacketSequence(1, 1)
3228
+ })
3229
+ ).toBeNull();
3230
+ expect(
3231
+ reassembler.addFragment({
3232
+ fragmentationIndicator: 2,
3233
+ mpuSequenceNumber: 1,
3234
+ fragmentCounter: 2,
3235
+ data: new Uint8Array([187]),
3236
+ timestamp: 1000n,
3237
+ rapFlag: false,
3238
+ packetSequenceNumber: testPacketSequence(1, 2)
3239
+ })
3240
+ ).toBeNull();
3241
+ expectRejection(
3242
+ () => reassembler.addFragment({
3243
+ fragmentationIndicator: 2,
3244
+ mpuSequenceNumber: 1,
3245
+ fragmentCounter: 1,
3246
+ data: new Uint8Array([204]),
3247
+ timestamp: 1001n,
3248
+ rapFlag: false,
3249
+ packetSequenceNumber: testPacketSequence(1, 1)
3250
+ }),
3251
+ "fragment-counter-mismatch"
3252
+ );
2907
3253
  const result = reassembler.addFragment({
2908
3254
  fragmentationIndicator: 3,
2909
3255
  mpuSequenceNumber: 1,
@@ -2931,57 +3277,60 @@ if (void 0) {
2931
3277
  });
2932
3278
  expect(reassembler.addFragment(fragment(1, 3, 170))).toBeNull();
2933
3279
  expect(reassembler.addFragment(fragment(2, 1, 204))).toBeNull();
2934
- expectRejection(
2935
- () => reassembler.addFragment(fragment(1, 1, 238)),
2936
- "overlapping-data-unit"
2937
- );
3280
+ expectRejection(() => reassembler.addFragment(fragment(1, 1, 238)), "overlapping-data-unit");
2938
3281
  expect(reassembler.addFragment(fragment(2, 2, 187))).toBeNull();
2939
- expect(reassembler.addFragment(fragment(3, 0, 221))?.data).toEqual(
2940
- new Uint8Array([170, 187, 204, 221])
2941
- );
3282
+ expect(reassembler.addFragment(fragment(3, 0, 221))?.data).toEqual(new Uint8Array([170, 187, 204, 221]));
2942
3283
  });
2943
3284
  test("rejects a duplicate emitted intrinsic identity", () => {
2944
3285
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
2945
- expect(reassembler.addFragment({
2946
- fragmentationIndicator: 1,
2947
- mpuSequenceNumber: 42,
2948
- fragmentCounter: 1,
2949
- itemId: 1,
2950
- data: new Uint8Array([170]),
2951
- timestamp: 1000n,
2952
- rapFlag: true,
2953
- packetSequenceNumber: testPacketSequence(42, 1)
2954
- })).toBeNull();
2955
- expect(reassembler.addFragment({
2956
- fragmentationIndicator: 3,
2957
- mpuSequenceNumber: 42,
2958
- fragmentCounter: 0,
2959
- data: new Uint8Array([171]),
2960
- timestamp: 1001n,
2961
- rapFlag: false,
2962
- packetSequenceNumber: testPacketSequence(42, 0)
2963
- })).not.toBeNull();
2964
- expect(reassembler.addFragment({
2965
- fragmentationIndicator: 3,
2966
- mpuSequenceNumber: 42,
2967
- fragmentCounter: 0,
2968
- data: new Uint8Array([188]),
2969
- timestamp: 1002n,
2970
- rapFlag: false,
2971
- packetSequenceNumber: testPacketSequence(42, 0, 10)
2972
- })).toBeNull();
3286
+ expect(
3287
+ reassembler.addFragment({
3288
+ fragmentationIndicator: 1,
3289
+ mpuSequenceNumber: 42,
3290
+ fragmentCounter: 1,
3291
+ itemId: 1,
3292
+ data: new Uint8Array([170]),
3293
+ timestamp: 1000n,
3294
+ rapFlag: true,
3295
+ packetSequenceNumber: testPacketSequence(42, 1)
3296
+ })
3297
+ ).toBeNull();
3298
+ expect(
3299
+ reassembler.addFragment({
3300
+ fragmentationIndicator: 3,
3301
+ mpuSequenceNumber: 42,
3302
+ fragmentCounter: 0,
3303
+ data: new Uint8Array([171]),
3304
+ timestamp: 1001n,
3305
+ rapFlag: false,
3306
+ packetSequenceNumber: testPacketSequence(42, 0)
3307
+ })
3308
+ ).not.toBeNull();
3309
+ expect(
3310
+ reassembler.addFragment({
3311
+ fragmentationIndicator: 3,
3312
+ mpuSequenceNumber: 42,
3313
+ fragmentCounter: 0,
3314
+ data: new Uint8Array([188]),
3315
+ timestamp: 1002n,
3316
+ rapFlag: false,
3317
+ packetSequenceNumber: testPacketSequence(42, 0, 10)
3318
+ })
3319
+ ).toBeNull();
3320
+ expect(reassembler.pendingCount).toBe(1);
3321
+ expect(
3322
+ reassembler.addFragment({
3323
+ fragmentationIndicator: 1,
3324
+ mpuSequenceNumber: 42,
3325
+ fragmentCounter: 1,
3326
+ itemId: 1,
3327
+ data: new Uint8Array([187]),
3328
+ timestamp: 2000n,
3329
+ rapFlag: true,
3330
+ packetSequenceNumber: testPacketSequence(42, 1, 10)
3331
+ })
3332
+ ).toBeNull();
2973
3333
  expect(reassembler.pendingCount).toBe(1);
2974
- expect(reassembler.addFragment({
2975
- fragmentationIndicator: 1,
2976
- mpuSequenceNumber: 42,
2977
- fragmentCounter: 1,
2978
- itemId: 1,
2979
- data: new Uint8Array([187]),
2980
- timestamp: 2000n,
2981
- rapFlag: true,
2982
- packetSequenceNumber: testPacketSequence(42, 1, 10)
2983
- })).toBeNull();
2984
- expect(reassembler.pendingCount).toBe(0);
2985
3334
  const result = reassembler.addFragment({
2986
3335
  fragmentationIndicator: 1,
2987
3336
  mpuSequenceNumber: 43,
@@ -2993,52 +3342,60 @@ if (void 0) {
2993
3342
  packetSequenceNumber: testPacketSequence(43, 1)
2994
3343
  });
2995
3344
  expect(result).toBeNull();
2996
- expect(reassembler.addFragment({
2997
- fragmentationIndicator: 3,
2998
- mpuSequenceNumber: 43,
2999
- fragmentCounter: 0,
3000
- data: new Uint8Array([221]),
3001
- timestamp: 2001n,
3002
- rapFlag: false,
3003
- packetSequenceNumber: testPacketSequence(43, 0)
3004
- })?.data).toEqual(new Uint8Array([204, 221]));
3345
+ expect(
3346
+ reassembler.addFragment({
3347
+ fragmentationIndicator: 3,
3348
+ mpuSequenceNumber: 43,
3349
+ fragmentCounter: 0,
3350
+ data: new Uint8Array([221]),
3351
+ timestamp: 2001n,
3352
+ rapFlag: false,
3353
+ packetSequenceNumber: testPacketSequence(43, 0)
3354
+ })?.data
3355
+ ).toEqual(new Uint8Array([204, 221]));
3005
3356
  });
3006
3357
  test("does not create active state for an emitted identity duplicate", () => {
3007
3358
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
3008
- expect(reassembler.addFragment({
3009
- fragmentationIndicator: 1,
3010
- mpuSequenceNumber: 50,
3011
- fragmentCounter: 1,
3012
- movieFragmentSequenceNumber: 10,
3013
- sampleNumber: 18,
3014
- sampleOffset: 0,
3015
- data: new Uint8Array([161]),
3016
- timestamp: 1000n,
3017
- rapFlag: true,
3018
- packetSequenceNumber: testPacketSequence(50, 1)
3019
- })).toBeNull();
3020
- expect(reassembler.addFragment({
3021
- fragmentationIndicator: 3,
3022
- mpuSequenceNumber: 50,
3023
- fragmentCounter: 0,
3024
- data: new Uint8Array([178]),
3025
- timestamp: 1000n,
3026
- rapFlag: false,
3027
- packetSequenceNumber: testPacketSequence(50, 0)
3028
- })?.data).toEqual(new Uint8Array([161, 178]));
3359
+ expect(
3360
+ reassembler.addFragment({
3361
+ fragmentationIndicator: 1,
3362
+ mpuSequenceNumber: 50,
3363
+ fragmentCounter: 1,
3364
+ movieFragmentSequenceNumber: 10,
3365
+ sampleNumber: 18,
3366
+ sampleOffset: 0,
3367
+ data: new Uint8Array([161]),
3368
+ timestamp: 1000n,
3369
+ rapFlag: true,
3370
+ packetSequenceNumber: testPacketSequence(50, 1)
3371
+ })
3372
+ ).toBeNull();
3373
+ expect(
3374
+ reassembler.addFragment({
3375
+ fragmentationIndicator: 3,
3376
+ mpuSequenceNumber: 50,
3377
+ fragmentCounter: 0,
3378
+ data: new Uint8Array([178]),
3379
+ timestamp: 1000n,
3380
+ rapFlag: false,
3381
+ packetSequenceNumber: testPacketSequence(50, 0)
3382
+ })?.data
3383
+ ).toEqual(new Uint8Array([161, 178]));
3029
3384
  expect(reassembler.pendingCount).toBe(0);
3030
- expect(reassembler.addFragment({
3031
- fragmentationIndicator: 1,
3032
- mpuSequenceNumber: 50,
3033
- fragmentCounter: 1,
3034
- movieFragmentSequenceNumber: 10,
3035
- sampleNumber: 18,
3036
- sampleOffset: 0,
3037
- data: new Uint8Array([161]),
3038
- timestamp: 2000n,
3039
- rapFlag: true,
3040
- packetSequenceNumber: testPacketSequence(50, 1, 10)
3041
- })).toBeNull();
3385
+ expect(
3386
+ reassembler.addFragment({
3387
+ fragmentationIndicator: 1,
3388
+ mpuSequenceNumber: 50,
3389
+ fragmentCounter: 1,
3390
+ movieFragmentSequenceNumber: 10,
3391
+ sampleNumber: 18,
3392
+ sampleOffset: 0,
3393
+ data: new Uint8Array([161]),
3394
+ timestamp: 2000n,
3395
+ rapFlag: true,
3396
+ packetSequenceNumber: testPacketSequence(50, 1, 10)
3397
+ })
3398
+ ).toBeNull();
3042
3399
  expect(reassembler.pendingCount).toBe(0);
3043
3400
  });
3044
3401
  test("flush should clear emitted set and allow re-assembly", () => {
@@ -3064,16 +3421,18 @@ if (void 0) {
3064
3421
  packetSequenceNumber: testPacketSequence(42, 0)
3065
3422
  });
3066
3423
  expect(assembled).not.toBeNull();
3067
- expect(reassembler.addFragment({
3068
- fragmentationIndicator: 1,
3069
- mpuSequenceNumber: 42,
3070
- fragmentCounter: 1,
3071
- itemId: 1,
3072
- data: new Uint8Array([204]),
3073
- timestamp: 1000n,
3074
- rapFlag: true,
3075
- packetSequenceNumber: testPacketSequence(42, 1, 10)
3076
- })).toBeNull();
3424
+ expect(
3425
+ reassembler.addFragment({
3426
+ fragmentationIndicator: 1,
3427
+ mpuSequenceNumber: 42,
3428
+ fragmentCounter: 1,
3429
+ itemId: 1,
3430
+ data: new Uint8Array([204]),
3431
+ timestamp: 1000n,
3432
+ rapFlag: true,
3433
+ packetSequenceNumber: testPacketSequence(42, 1, 10)
3434
+ })
3435
+ ).toBeNull();
3077
3436
  reassembler.flush();
3078
3437
  const r2 = reassembler.addFragment({
3079
3438
  fragmentationIndicator: 1,
@@ -3278,6 +3637,15 @@ function trackNameOfKey(key) {
3278
3637
  const name = JSON.parse(key)[0];
3279
3638
  return name ?? void 0;
3280
3639
  }
3640
+ function identityOfKey(key) {
3641
+ const [name, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber] = JSON.parse(key);
3642
+ return {
3643
+ catalogTrackName: name ?? void 0,
3644
+ mpuSequenceNumber,
3645
+ movieFragmentSequenceNumber,
3646
+ sampleNumber
3647
+ };
3648
+ }
3281
3649
  var SampleCombineError = class extends Error {
3282
3650
  constructor(code, message) {
3283
3651
  super(message);
@@ -3285,13 +3653,14 @@ var SampleCombineError = class extends Error {
3285
3653
  this.name = "SampleCombineError";
3286
3654
  }
3287
3655
  };
3288
- var _maxBuffered2, _requireAuthoredSize, _maxSegmentsPerSample, _authoredSizes, _pending, _retired, _stats, _SampleCombiner_instances, tryComplete_fn, retire_fn, evictOldestPending_fn;
3656
+ var _maxBuffered2, _requireAuthoredSize, _maxSegmentsPerSample, _onAbandon, _authoredSizes, _pending, _retired, _stats, _SampleCombiner_instances, abandon_fn, tryComplete_fn, retire_fn, evictOldestPending_fn;
3289
3657
  var SampleCombiner = class {
3290
3658
  constructor(options) {
3291
3659
  __privateAdd(this, _SampleCombiner_instances);
3292
3660
  __privateAdd(this, _maxBuffered2);
3293
3661
  __privateAdd(this, _requireAuthoredSize);
3294
3662
  __privateAdd(this, _maxSegmentsPerSample);
3663
+ __privateAdd(this, _onAbandon);
3295
3664
  __privateAdd(this, _authoredSizes, /* @__PURE__ */ new Map());
3296
3665
  /** Map iteration is insertion-ordered, so the first key is the oldest. */
3297
3666
  __privateAdd(this, _pending, /* @__PURE__ */ new Map());
@@ -3307,7 +3676,7 @@ var SampleCombiner = class {
3307
3676
  timedOutSamples: 0,
3308
3677
  evictedSamples: 0
3309
3678
  });
3310
- const { maxBuffered, requireAuthoredSize, maxSegmentsPerSample } = options;
3679
+ const { maxBuffered, requireAuthoredSize, maxSegmentsPerSample, onAbandon } = options;
3311
3680
  if (!Number.isInteger(maxBuffered) || maxBuffered <= 0) {
3312
3681
  throw new Error("[SampleCombiner] maxBuffered must be a positive integer");
3313
3682
  }
@@ -3317,6 +3686,7 @@ var SampleCombiner = class {
3317
3686
  __privateSet(this, _maxBuffered2, maxBuffered);
3318
3687
  __privateSet(this, _requireAuthoredSize, requireAuthoredSize);
3319
3688
  __privateSet(this, _maxSegmentsPerSample, maxSegmentsPerSample);
3689
+ __privateSet(this, _onAbandon, onAbandon);
3320
3690
  }
3321
3691
  /**
3322
3692
  * Register the authored sample size from the host's own FT=1 parse. Per
@@ -3431,6 +3801,7 @@ var SampleCombiner = class {
3431
3801
  __privateGet(this, _pending).delete(key);
3432
3802
  __privateGet(this, _stats).timedOutSamples++;
3433
3803
  __privateMethod(this, _SampleCombiner_instances, retire_fn).call(this, key);
3804
+ __privateMethod(this, _SampleCombiner_instances, abandon_fn).call(this, key);
3434
3805
  }
3435
3806
  }
3436
3807
  }
@@ -3477,11 +3848,17 @@ var SampleCombiner = class {
3477
3848
  _maxBuffered2 = new WeakMap();
3478
3849
  _requireAuthoredSize = new WeakMap();
3479
3850
  _maxSegmentsPerSample = new WeakMap();
3851
+ _onAbandon = new WeakMap();
3480
3852
  _authoredSizes = new WeakMap();
3481
3853
  _pending = new WeakMap();
3482
3854
  _retired = new WeakMap();
3483
3855
  _stats = new WeakMap();
3484
3856
  _SampleCombiner_instances = new WeakSet();
3857
+ /** Report one abandoned sample to the owner of any state keyed on it. */
3858
+ abandon_fn = function(key) {
3859
+ var _a;
3860
+ (_a = __privateGet(this, _onAbandon)) == null ? void 0 : _a.call(this, identityOfKey(key));
3861
+ };
3485
3862
  /**
3486
3863
  * Emit the sample when its held data units are byte-adjacent from zero and
3487
3864
  * total `size`. Completion is a protocol event, never a timer.
@@ -3536,6 +3913,7 @@ evictOldestPending_fn = function() {
3536
3913
  __privateGet(this, _pending).delete(oldest);
3537
3914
  __privateGet(this, _stats).evictedSamples++;
3538
3915
  __privateMethod(this, _SampleCombiner_instances, retire_fn).call(this, oldest);
3916
+ __privateMethod(this, _SampleCombiner_instances, abandon_fn).call(this, oldest);
3539
3917
  };
3540
3918
  function fromPassthrough(mfu) {
3541
3919
  return {
@@ -3543,6 +3921,7 @@ function fromPassthrough(mfu) {
3543
3921
  movieFragmentSequenceNumber: mfu.movieFragmentSequenceNumber ?? 0,
3544
3922
  sampleNumber: mfu.sampleNumber ?? 0,
3545
3923
  data: mfu.data,
3924
+ mmtpTimestamp: mfu.timestamp,
3546
3925
  timestamp: mfu.timestamp,
3547
3926
  rapFlag: mfu.rapFlag,
3548
3927
  priority: mfu.priority,
@@ -3584,6 +3963,7 @@ function fromSegments(segments, sortedOffsets) {
3584
3963
  movieFragmentSequenceNumber: first.movieFragmentSequenceNumber,
3585
3964
  sampleNumber: first.sampleNumber,
3586
3965
  data,
3966
+ mmtpTimestamp: first.timestamp,
3587
3967
  timestamp: first.timestamp,
3588
3968
  rapFlag: first.rapFlag,
3589
3969
  priority: first.priority,
@@ -3690,6 +4070,30 @@ if (void 0) {
3690
4070
  expect(c.addMfu(mfu(1, 9, 17, 2, [3, 4], 1001n, false))).toBeNull();
3691
4071
  expect(() => c.addMfu(mfu(1, 9, 17, 4, [5, 6], 1002n, false))).toThrow(SampleCombineError);
3692
4072
  });
4073
+ test("reports every abandoned sample so its per-sample state can be freed", () => {
4074
+ const abandoned = [];
4075
+ const c = combiner({ maxBuffered: 2, onAbandon: (s) => abandoned.push(s) });
4076
+ c.registerAuthoredSize(void 0, 1, 9, 17, 10);
4077
+ c.registerAuthoredSize(void 0, 1, 9, 18, 10);
4078
+ c.registerAuthoredSize(void 0, 1, 9, 19, 10);
4079
+ expect(c.addMfu(mfu(1, 9, 17, 0, [1, 1, 1, 1, 1], 1000n, true))).toBeNull();
4080
+ expect(c.addMfu(mfu(1, 9, 18, 0, [2, 2, 2, 2, 2], 1001n, true))).toBeNull();
4081
+ expect(abandoned).toEqual([]);
4082
+ expect(c.addMfu(mfu(1, 9, 19, 0, [3, 3, 3, 3, 3], 1002n, true))).toBeNull();
4083
+ expect(abandoned).toEqual([{
4084
+ catalogTrackName: void 0,
4085
+ mpuSequenceNumber: 1,
4086
+ movieFragmentSequenceNumber: 9,
4087
+ sampleNumber: 17
4088
+ }]);
4089
+ c.cleanupTimeouts(-1);
4090
+ expect(abandoned.map((s) => s.sampleNumber)).toEqual([17, 18, 19]);
4091
+ const d = combiner({ maxBuffered: 2, onAbandon: (s) => abandoned.push(s) });
4092
+ d.registerAuthoredSize(void 0, 1, 9, 20, 10);
4093
+ expect(d.addMfu(mfu(1, 9, 20, 0, [4, 4, 4, 4, 4], 1003n, true))).toBeNull();
4094
+ expect(d.addMfu(mfu(1, 9, 20, 5, [5, 5, 5, 5, 5], 1004n, false))).not.toBeNull();
4095
+ expect(abandoned.map((s) => s.sampleNumber)).toEqual([17, 18, 19]);
4096
+ });
3693
4097
  test("a full combiner evicts the oldest pending sample instead of throwing", () => {
3694
4098
  const c = combiner({ maxBuffered: 2 });
3695
4099
  c.registerAuthoredSize(void 0, 1, 9, 17, 10);
@@ -4270,7 +4674,10 @@ function detectH264Keyframe(data, options = {}) {
4270
4674
  return classifyH264Keyframe(data, options) === true;
4271
4675
  }
4272
4676
  function mmtpTimestampToMicros(rawTs) {
4273
- const ts = Number(rawTs) >>> 0;
4677
+ const ts = typeof rawTs === "bigint" ? rawTs >= 0n && rawTs <= 0xffffffffn ? Number(rawTs) : void 0 : Number.isSafeInteger(rawTs) && rawTs >= 0 && rawTs <= 4294967295 ? rawTs : void 0;
4678
+ if (ts === void 0) {
4679
+ throw new RangeError(`MMTP timestamp must be an unsigned 32-bit integer, got ${String(rawTs)}`);
4680
+ }
4274
4681
  if (ts === 0) return 0;
4275
4682
  const seconds = ts >>> 16;
4276
4683
  const frac = ts & 65535;
@@ -4295,6 +4702,7 @@ function isFmp4InitSegment(data) {
4295
4702
  size = data.byteLength - offset;
4296
4703
  }
4297
4704
  if (size < headerSize || offset + size > data.byteLength) return false;
4705
+ if (type === "mmpu") return false;
4298
4706
  if (type === "ftyp") sawFtyp = true;
4299
4707
  if (type === "moov") {
4300
4708
  if (!sawFtyp) return false;
@@ -4304,6 +4712,44 @@ function isFmp4InitSegment(data) {
4304
4712
  }
4305
4713
  return offset === data.byteLength && sawFtyp && sawMoov;
4306
4714
  }
4715
+ function normalizeMmtpInitSegment(data) {
4716
+ if (isFmp4InitSegment(data)) return data;
4717
+ const parts = [];
4718
+ let offset = 0;
4719
+ let removedMpu = false;
4720
+ while (offset + 8 <= data.byteLength) {
4721
+ const view2 = new DataView(data.buffer, data.byteOffset + offset, data.byteLength - offset);
4722
+ let size = view2.getUint32(0, false);
4723
+ let headerSize = 8;
4724
+ if (size === 1) {
4725
+ if (offset + 16 > data.byteLength) return void 0;
4726
+ const largeSize = view2.getBigUint64(8, false);
4727
+ if (largeSize > BigInt(Number.MAX_SAFE_INTEGER)) return void 0;
4728
+ size = Number(largeSize);
4729
+ headerSize = 16;
4730
+ } else if (size === 0) {
4731
+ size = data.byteLength - offset;
4732
+ }
4733
+ if (size < headerSize || offset + size > data.byteLength) return void 0;
4734
+ const type = String.fromCharCode(
4735
+ data[offset + 4],
4736
+ data[offset + 5],
4737
+ data[offset + 6],
4738
+ data[offset + 7]
4739
+ );
4740
+ if (type === "mmpu") removedMpu = true;
4741
+ else parts.push(data.subarray(offset, offset + size));
4742
+ offset += size;
4743
+ }
4744
+ if (!removedMpu || offset !== data.byteLength) return void 0;
4745
+ const normalized = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
4746
+ let writeOffset = 0;
4747
+ for (const part of parts) {
4748
+ normalized.set(part, writeOffset);
4749
+ writeOffset += part.byteLength;
4750
+ }
4751
+ return isFmp4InitSegment(normalized) ? normalized : void 0;
4752
+ }
4307
4753
  function hasIsoBmffMagic(data) {
4308
4754
  for (let i = 0; i + 8 <= data.length && i < 32; i += 4) {
4309
4755
  const tag = String.fromCharCode(data[i + 4], data[i + 5], data[i + 6], data[i + 7]);
@@ -7283,13 +7729,13 @@ var CmafBlockAssembler = class {
7283
7729
  * is guarded by the per-frame `lastEmittedTs` check (also enforced in
7284
7730
  * `#emitPerSample`). The `sbn`/`esi` parameters are kept for the call-site
7285
7731
  * signature but no longer affect routing. */
7286
- addFrame(sbn, esi, data, timestamp, keyframe, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance) {
7732
+ addFrame(sbn, esi, data, timestamp, keyframe, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber) {
7287
7733
  void sbn;
7288
7734
  void esi;
7289
7735
  __privateWrapper(this, _addFrameCount)._++;
7290
7736
  __privateMethod(this, _CmafBlockAssembler_instances, startAggregateTimer_fn).call(this);
7291
7737
  if (!__privateMethod(this, _CmafBlockAssembler_instances, ordersSamples_fn).call(this)) __privateMethod(this, _CmafBlockAssembler_instances, drainRecoveredUpTo_fn).call(this, timestamp);
7292
- __privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, data, timestamp, __privateGet(this, _frameDuration2), keyframe, true, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance);
7738
+ __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);
7293
7739
  __privateMethod(this, _CmafBlockAssembler_instances, dumpEmit_fn).call(this, "addFrame-direct", [{ data, timestamp, keyframe }]);
7294
7740
  }
7295
7741
  /** Mark a block as complete (all K source symbols received or FEC recovery succeeded).
@@ -7334,9 +7780,9 @@ var CmafBlockAssembler = class {
7334
7780
  }
7335
7781
  }
7336
7782
  /** Emit a single frame immediately as a one-sample segment (for oversized packets bypassing FEC). */
7337
- addBypassFrame(data, timestamp, keyframe, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance) {
7783
+ addBypassFrame(data, timestamp, keyframe, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber) {
7338
7784
  if (!__privateMethod(this, _CmafBlockAssembler_instances, ordersSamples_fn).call(this)) __privateMethod(this, _CmafBlockAssembler_instances, drainRecoveredUpTo_fn).call(this, timestamp);
7339
- __privateMethod(this, _CmafBlockAssembler_instances, emitSample_fn).call(this, data, timestamp, __privateGet(this, _frameDuration2), keyframe, true, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance);
7785
+ __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);
7340
7786
  }
7341
7787
  /** Build and emit init segment from catalog config. */
7342
7788
  emitInit() {
@@ -7486,7 +7932,7 @@ unwrapTimestamp_fn = function(timestamp) {
7486
7932
  if (reference < 0) return timestamp;
7487
7933
  return timestamp + Math.round((reference - timestamp) / wrapPeriod) * wrapPeriod;
7488
7934
  };
7489
- emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance) {
7935
+ emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, catalogTrackName, mpuSequenceNumber, packetSequenceNumber, packetSequenceNumbers, repairedPacketSequenceNumbers, authoredTiming, deliveryProvenance, mmtpTimestamp, movieFragmentSequenceNumber, sampleNumber) {
7490
7936
  const scaledTiming = authoredTiming === void 0 ? null : scaleAuthoredTiming(authoredTiming, __privateGet(this, _config).timescale);
7491
7937
  if (authoredTiming !== void 0 && scaledTiming === null) return;
7492
7938
  const authoredTimestamp = scaledTiming?.timestamp ?? timestamp;
@@ -7513,6 +7959,7 @@ emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, c
7513
7959
  timestamp: unwrappedTimestamp,
7514
7960
  duration: authoredDuration,
7515
7961
  keyframe,
7962
+ ...mmtpTimestamp === void 0 ? {} : { mmtpTimestamp },
7516
7963
  ...compositionTimeOffset === void 0 ? {} : { compositionTimeOffset },
7517
7964
  advancesHorizon,
7518
7965
  ...authoredTiming === void 0 ? {} : { authoredTiming },
@@ -7520,6 +7967,8 @@ emitSample_fn = function(data, timestamp, duration, keyframe, advancesHorizon, c
7520
7967
  ...packetSequenceNumbers === void 0 ? {} : { packetSequenceNumbers },
7521
7968
  ...repairedPacketSequenceNumbers === void 0 ? {} : { repairedPacketSequenceNumbers },
7522
7969
  ...mpuSequenceNumber === void 0 ? {} : { mpuSequenceNumber },
7970
+ ...movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber },
7971
+ ...sampleNumber === void 0 ? {} : { sampleNumber },
7523
7972
  ...catalogTrackName === void 0 ? {} : { catalogTrackName },
7524
7973
  ...deliveryProvenance === void 0 ? {} : { deliveryProvenance }
7525
7974
  });
@@ -7677,7 +8126,7 @@ emitPerSample_fn = function(frames, audioDurationOverride, advancesHorizon = fal
7677
8126
  duration = audioDurationOverride ?? __privateGet(this, _frameDuration2);
7678
8127
  }
7679
8128
  if (duration <= 0) duration = audioDurationOverride ?? __privateGet(this, _frameDuration2);
7680
- __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);
8129
+ __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);
7681
8130
  }
7682
8131
  };
7683
8132
  _dumpCount = new WeakMap();
@@ -7692,8 +8141,9 @@ _aggregateTimer = new WeakMap();
7692
8141
  startAggregateTimer_fn = function() {
7693
8142
  if (__privateGet(this, _aggregateTimer)) return;
7694
8143
  __privateSet(this, _aggregateTimer, setInterval(() => {
8144
+ const order = __privateGet(this, _sampleOrderStats);
7695
8145
  console.log(
7696
- `[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)}`
8146
+ `[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}`
7697
8147
  );
7698
8148
  __privateSet(this, _emitCount, 0);
7699
8149
  __privateSet(this, _sampleCount, 0);
@@ -7751,7 +8201,7 @@ drainRecoveredUpTo_fn = function(horizonTs) {
7751
8201
  const ready = [...__privateGet(this, _pendingRecovered).values()].filter((frame) => frame.timestamp <= horizonTs).sort((a, b) => a.timestamp - b.timestamp);
7752
8202
  for (const frame of ready) {
7753
8203
  __privateGet(this, _pendingRecovered).delete(frame.timestamp);
7754
- __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);
8204
+ __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);
7755
8205
  __privateMethod(this, _CmafBlockAssembler_instances, dumpEmit_fn).call(this, "recovered-drain", [frame]);
7756
8206
  }
7757
8207
  };
@@ -8123,6 +8573,15 @@ function deriveFecInterleaveDepth(interleaveDepthMs, mediaUnitDurationMs) {
8123
8573
  }
8124
8574
  return Math.max(1, Math.floor(interleaveDepthMs / mediaUnitDurationMs + 0.5));
8125
8575
  }
8576
+ function fecBlockBudgetMs(interleaveDepthMs, reorderToleranceMs) {
8577
+ if (!Number.isFinite(interleaveDepthMs) || !(interleaveDepthMs > 0)) {
8578
+ throw new Error(`[FEC] interleaveDepthMs must be a finite value > 0 (got ${interleaveDepthMs}ms). Fix encoder config.`);
8579
+ }
8580
+ if (!(reorderToleranceMs >= 0) || !Number.isFinite(reorderToleranceMs)) {
8581
+ 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.`);
8582
+ }
8583
+ return Math.ceil(interleaveDepthMs + reorderToleranceMs);
8584
+ }
8126
8585
  function parseRepairPacket(packet) {
8127
8586
  const parsed = parseMoqRepairFrame(packet);
8128
8587
  if (!parsed)
@@ -8224,32 +8683,18 @@ function computeFecLossWindowMs(fecInterleaveMs, fecK, jitterBufferMs) {
8224
8683
  function computeFecDeliveryWindowMs(config) {
8225
8684
  const fecInterleaveMs = config.interleaveDepthMs;
8226
8685
  const fecK = config.sourceSymbols;
8227
- if (fecInterleaveMs <= 0) {
8686
+ if (!Number.isFinite(fecInterleaveMs) || fecInterleaveMs <= 0) {
8228
8687
  throw new Error(
8229
8688
  `[TrackSession] fecInterleaveMs=${fecInterleaveMs}. Derive a positive FEC interleave window from the stream catalog.`
8230
8689
  );
8231
8690
  }
8232
- if (fecK <= 0) {
8691
+ if (!Number.isFinite(fecK) || fecK <= 0) {
8233
8692
  throw new Error(
8234
8693
  `[TrackSession] fecK=${fecK}. Derive a positive FEC source-symbol count from the stream catalog.`
8235
8694
  );
8236
8695
  }
8237
- const spansMs = [fecInterleaveMs];
8238
- if (config.mediaUnitsPerSecond && config.mediaUnitsPerSecond > 0) {
8239
- const mediaUnitMs = 1e3 / config.mediaUnitsPerSecond;
8240
- const interleaveUnits = deriveFecInterleaveDepth(fecInterleaveMs, mediaUnitMs);
8241
- const deliverySymbols = fecK + Math.max(0, config.repairSymbols ?? 0);
8242
- spansMs.push(deliverySymbols * interleaveUnits * mediaUnitMs);
8243
- }
8244
- if (config.symbolSize && config.symbolSize > 0 && config.bandwidthBps && config.bandwidthBps > 0) {
8245
- const repairSymbols = Math.max(0, config.repairSymbols ?? 0);
8246
- spansMs.push(
8247
- (fecK + repairSymbols) * config.symbolSize * 8 * 1e3 / config.bandwidthBps
8248
- );
8249
- }
8250
- const coreMs = Math.max(...spansMs);
8251
- const marginMs = config.jitterBufferMs !== void 0 && config.jitterBufferMs >= 0 ? config.jitterBufferMs : fecInterleaveMs;
8252
- return Math.ceil(coreMs) + Math.ceil(marginMs);
8696
+ if (config.jitterBufferMs === void 0) return Math.ceil(fecInterleaveMs);
8697
+ return fecBlockBudgetMs(fecInterleaveMs, config.jitterBufferMs);
8253
8698
  }
8254
8699
  function computeFecDeliveryWindowMediaUnits(config, timestampTimescale) {
8255
8700
  if (!Number.isFinite(timestampTimescale) || timestampTimescale <= 0) {
@@ -8265,7 +8710,7 @@ function computeFecDeliveryWindowMediaUnits(config, timestampTimescale) {
8265
8710
  }
8266
8711
  return deliveryWindow;
8267
8712
  }
8268
- var _state, _subscription, _hysteresisTimer, _joinDeadlineTimer, _previousPrimary, _manager, _suppressFired, _TrackSession_instances, transition_fn, clearHysteresis_fn, armJoinDeadline_fn, clearJoinDeadline_fn, suppress_fn, unsuppress_fn;
8713
+ 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;
8269
8714
  var TrackSession = class {
8270
8715
  constructor(config, manager) {
8271
8716
  __privateAdd(this, _TrackSession_instances);
@@ -8282,6 +8727,12 @@ var TrackSession = class {
8282
8727
  // from `failing` (exactly where a starving track sits) was re-subscribed to
8283
8728
  // unicast with the host's media still muted: MoQ playing into a dead sink.
8284
8729
  __privateAdd(this, _suppressFired, false);
8730
+ // Single FEC authority flag: set by the fec-setup edges (joinStarted,
8731
+ // enterUnicastFallback), cleared by the fec-teardown edge in #transition.
8732
+ // Setup and teardown are paired by construction, so a takeover timeout that
8733
+ // parks the session in `unicast` keeps the decoder the still-joined
8734
+ // transport is feeding, and the later unsubscribe still tears it down.
8735
+ __privateAdd(this, _fecArmed, false);
8285
8736
  this.config = config;
8286
8737
  __privateSet(this, _manager, manager);
8287
8738
  }
@@ -8356,7 +8807,7 @@ var TrackSession = class {
8356
8807
  joinStarted() {
8357
8808
  if (__privateGet(this, _state) !== "unicast") return;
8358
8809
  __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
8359
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
8810
+ __privateMethod(this, _TrackSession_instances, armFec_fn).call(this);
8360
8811
  }
8361
8812
  /**
8362
8813
  * any non-fallback state → unicast-fallback. Used when the SSM bridge is
@@ -8377,7 +8828,7 @@ var TrackSession = class {
8377
8828
  __privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
8378
8829
  __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast-fallback");
8379
8830
  __privateMethod(this, _TrackSession_instances, suppress_fn).call(this);
8380
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
8831
+ __privateMethod(this, _TrackSession_instances, armFec_fn).call(this);
8381
8832
  }
8382
8833
  /**
8383
8834
  * unicast-fallback → unicast. Called when the SSM bridge appears mid-session
@@ -8408,7 +8859,11 @@ var TrackSession = class {
8408
8859
  if (__privateGet(this, _state) !== "multicast" && __privateGet(this, _state) !== "amt") return;
8409
8860
  __privateSet(this, _previousPrimary, __privateGet(this, _state));
8410
8861
  __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "failing");
8411
- const hysteresisMs = computeFecLossWindowMs(this.config.fecInterleaveMs, this.config.fecK);
8862
+ const hysteresisMs = computeFecLossWindowMs(
8863
+ this.config.fecInterleaveMs,
8864
+ this.config.fecK,
8865
+ this.config.fecReorderToleranceMs
8866
+ );
8412
8867
  __privateSet(this, _hysteresisTimer, setTimeout(() => {
8413
8868
  this.hysteresisExpired();
8414
8869
  }, hysteresisMs));
@@ -8432,23 +8887,18 @@ var TrackSession = class {
8432
8887
  const prefs = this.config.preferredTransports;
8433
8888
  const currentIdx = current ? prefs.indexOf(current) : -1;
8434
8889
  const nextMode = prefs[currentIdx + 1];
8435
- if (nextMode) {
8436
- if (nextMode === "moq") {
8437
- __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
8438
- __privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
8439
- if (this.config.trackType === "audio") {
8440
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
8441
- }
8442
- } else {
8443
- __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
8444
- __privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
8445
- }
8446
- } else {
8447
- __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
8890
+ if (nextMode !== void 0 && nextMode !== "moq") {
8891
+ __privateSet(this, _previousPrimary, nextMode);
8892
+ __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "joining");
8448
8893
  __privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
8449
- if (this.config.trackType === "audio") {
8450
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
8451
- }
8894
+ return;
8895
+ }
8896
+ __privateSet(this, _previousPrimary, null);
8897
+ __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
8898
+ __privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
8899
+ __privateMethod(this, _TrackSession_instances, unsuppress_fn).call(this);
8900
+ if (this.config.trackType === "audio") {
8901
+ __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "reset" }, this.config);
8452
8902
  }
8453
8903
  }
8454
8904
  /** Hysteresis expired → failback to next transport (Claim 9g). */
@@ -8472,11 +8922,20 @@ var TrackSession = class {
8472
8922
  get isUnicastFallback() {
8473
8923
  return __privateGet(this, _state) === "unicast-fallback";
8474
8924
  }
8925
+ /**
8926
+ * True while this track owns a FEC decoder (fec-setup fired, fec-teardown
8927
+ * not yet). Stays true across a takeover timeout that parked the session in
8928
+ * `unicast`; SessionManager.failbackAll uses it to release such sessions.
8929
+ */
8930
+ get isFecArmed() {
8931
+ return __privateGet(this, _fecArmed);
8932
+ }
8475
8933
  dispose() {
8476
8934
  __privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
8477
8935
  __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
8478
8936
  __privateSet(this, _state, "idle");
8479
8937
  __privateSet(this, _subscription, null);
8938
+ __privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
8480
8939
  }
8481
8940
  };
8482
8941
  _state = new WeakMap();
@@ -8486,20 +8945,20 @@ _joinDeadlineTimer = new WeakMap();
8486
8945
  _previousPrimary = new WeakMap();
8487
8946
  _manager = new WeakMap();
8488
8947
  _suppressFired = new WeakMap();
8948
+ _fecArmed = new WeakMap();
8489
8949
  _TrackSession_instances = new WeakSet();
8490
8950
  // -----------------------------------------------------------------------
8491
8951
  // Internal
8492
8952
  // -----------------------------------------------------------------------
8493
- transition_fn = function(to) {
8953
+ transition_fn = function(to, opts) {
8494
8954
  const from = __privateGet(this, _state);
8495
8955
  if (from === to) return;
8956
+ const joinDeadlineMs = to === "joining" ? __privateGet(this, _manager).takeoverDeadlineMs : void 0;
8496
8957
  __privateSet(this, _state, to);
8497
8958
  if (from === "joining") __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
8498
- if (to === "joining") __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this);
8959
+ if (joinDeadlineMs !== void 0) __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this, joinDeadlineMs);
8499
8960
  const fecActive = (s) => s === "joining" || s === "multicast" || s === "amt" || s === "failing" || s === "unicast-fallback";
8500
- if (fecActive(from) && !fecActive(to)) {
8501
- __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-teardown" }, this.config);
8502
- }
8961
+ if (!fecActive(to) && !opts?.retainFec) __privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
8503
8962
  __privateGet(this, _manager).callbacks?.onStateChange?.(this.config.trackName, from, to);
8504
8963
  };
8505
8964
  clearHysteresis_fn = function() {
@@ -8508,14 +8967,13 @@ clearHysteresis_fn = function() {
8508
8967
  __privateSet(this, _hysteresisTimer, null);
8509
8968
  }
8510
8969
  };
8511
- armJoinDeadline_fn = function() {
8970
+ armJoinDeadline_fn = function(deadlineMs) {
8512
8971
  __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
8513
- const deadlineMs = __privateGet(this, _manager).takeoverDeadlineMs;
8514
- if (deadlineMs <= 0) return;
8972
+ if (deadlineMs === 0) return;
8515
8973
  __privateSet(this, _joinDeadlineTimer, setTimeout(() => {
8516
8974
  __privateSet(this, _joinDeadlineTimer, null);
8517
8975
  if (__privateGet(this, _state) !== "joining") return;
8518
- __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast");
8976
+ __privateMethod(this, _TrackSession_instances, transition_fn).call(this, "unicast", { retainFec: true });
8519
8977
  __privateGet(this, _manager).callbacks?.onTakeoverTimeout?.(this.config.trackName);
8520
8978
  }, deadlineMs));
8521
8979
  };
@@ -8525,6 +8983,18 @@ clearJoinDeadline_fn = function() {
8525
8983
  __privateSet(this, _joinDeadlineTimer, null);
8526
8984
  }
8527
8985
  };
8986
+ /** Fire fec-setup exactly once per FEC epoch. */
8987
+ armFec_fn = function() {
8988
+ if (__privateGet(this, _fecArmed)) return;
8989
+ __privateSet(this, _fecArmed, true);
8990
+ __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-setup" }, this.config);
8991
+ };
8992
+ /** Fire fec-teardown for an armed decoder; no-op when none is armed. */
8993
+ disarmFec_fn = function() {
8994
+ if (!__privateGet(this, _fecArmed)) return;
8995
+ __privateSet(this, _fecArmed, false);
8996
+ __privateGet(this, _manager).mmtp?.onTrackEvent?.(this.config.trackName, { type: "fec-teardown" }, this.config);
8997
+ };
8528
8998
  /** Fire onSuppress(true) exactly once per suppression epoch. */
8529
8999
  suppress_fn = function() {
8530
9000
  if (__privateGet(this, _suppressFired)) return;
@@ -8537,18 +9007,14 @@ unsuppress_fn = function() {
8537
9007
  __privateSet(this, _suppressFired, false);
8538
9008
  __privateGet(this, _manager).callbacks?.onSuppress?.(this.config.trackName, false);
8539
9009
  };
9010
+ var MAX_TAKEOVER_DEADLINE_MS = 2147483647;
9011
+ var _takeoverDeadlineMs, _SessionManager_instances, requireTakeoverDeadlineMs_fn;
8540
9012
  var SessionManager = class {
8541
9013
  constructor(poolOptions) {
9014
+ __privateAdd(this, _SessionManager_instances);
8542
9015
  this.sessions = /* @__PURE__ */ new Map();
8543
9016
  this.callbacks = null;
8544
- /**
8545
- * Takeover deadline (BLO-9413/D9): max time a session may sit in `joining`
8546
- * without a RAP before returning to `unicast` and firing onTakeoverTimeout.
8547
- * MoQ is never suppressed during `joining`, so expiry is a silent abandon —
8548
- * playback never glitches. Hosts tune per stream (e.g. GOP length); 0
8549
- * disables.
8550
- */
8551
- this.takeoverDeadlineMs = 5e3;
9017
+ __privateAdd(this, _takeoverDeadlineMs);
8552
9018
  this.pool = new TransportPool(poolOptions);
8553
9019
  this.pool.onSubscriptionFailed((sub) => {
8554
9020
  for (const trackName of sub.consumers) {
@@ -8557,18 +9023,56 @@ var SessionManager = class {
8557
9023
  }
8558
9024
  });
8559
9025
  }
9026
+ /**
9027
+ * Takeover deadline (BLO-9413/D9): the time a session may sit in `joining`
9028
+ * without a RAP before returning to `unicast` and firing onTakeoverTimeout.
9029
+ * MoQ is never suppressed during `joining`, so expiry abandons the takeover
9030
+ * without a playback glitch. The host derives it from the catalog (RAP
9031
+ * cadence from groupDurationTicks/timescale plus the signaled FEC delivery
9032
+ * window or reorder tolerance) and sets it before addTrack(). There is no
9033
+ * default: an unset value fails addTrack() closed, and reading the getter
9034
+ * before assignment throws the same way, so no host ever holds a
9035
+ * `number | undefined` it could coalesce back into a default.
9036
+ * `hasTakeoverDeadline` answers whether one was admitted. A declared 0
9037
+ * disables the deadline. The value is an integer number of milliseconds:
9038
+ * setTimeout truncates fractions, so a host that divides ticks by a
9039
+ * timescale must round before assigning.
9040
+ *
9041
+ * Assignment validates, so the diagnostic lands at the host's own call site.
9042
+ * That is what bounds the blast radius: `joining` is entered from
9043
+ * joinStarted(), from the transportFailed() failover branch -- itself
9044
+ * reachable from a setTimeout via hysteresisExpired(), where no caller could
9045
+ * catch a throw -- and from the activateMulticast()/activateAmt()/pool-failure
9046
+ * loops, where a throw on one session would abandon its siblings mid-loop
9047
+ * (the split-brain those loops exist to prevent). Refusing a bad value at
9048
+ * assignment means no host mutation can make an already-admitted session
9049
+ * throw out of a transport edge.
9050
+ */
9051
+ get takeoverDeadlineMs() {
9052
+ return __privateMethod(this, _SessionManager_instances, requireTakeoverDeadlineMs_fn).call(this);
9053
+ }
9054
+ set takeoverDeadlineMs(value) {
9055
+ if (!Number.isInteger(value) || value < 0 || value > MAX_TAKEOVER_DEADLINE_MS) {
9056
+ throw new Error(
9057
+ `[SessionManager] takeoverDeadlineMs must be an integer from 0 to ${MAX_TAKEOVER_DEADLINE_MS} (0 disables it, larger values overflow setTimeout); got ${String(value)}`
9058
+ );
9059
+ }
9060
+ __privateSet(this, _takeoverDeadlineMs, value);
9061
+ }
9062
+ /** True once the host has admitted a deadline; a declared 0 counts. */
9063
+ get hasTakeoverDeadline() {
9064
+ return __privateGet(this, _takeoverDeadlineMs) !== void 0;
9065
+ }
8560
9066
  /** Narrow to MMTP callbacks (onTrackEvent). Safe — optional chaining handles PIM-only consumers. */
8561
9067
  get mmtp() {
8562
9068
  return this.callbacks;
8563
9069
  }
8564
- // -----------------------------------------------------------------------
8565
- // Track management
8566
- // -----------------------------------------------------------------------
8567
9070
  /** Add a track session from catalog config. */
8568
9071
  addTrack(config) {
8569
9072
  if (this.sessions.has(config.trackName)) {
8570
9073
  throw new Error(`[SessionManager] Track "${config.trackName}" already exists`);
8571
9074
  }
9075
+ __privateMethod(this, _SessionManager_instances, requireTakeoverDeadlineMs_fn).call(this);
8572
9076
  const session = new TrackSession(config, this);
8573
9077
  this.sessions.set(config.trackName, session);
8574
9078
  return session;
@@ -8592,8 +9096,9 @@ var SessionManager = class {
8592
9096
  * Prevents split-brain between video and audio.
8593
9097
  *
8594
9098
  * A takeover deadline can return a track from `joining` to `unicast` while
8595
- * the multicast socket keeps delivering packets. If a valid RAP arrives
8596
- * later, allow that late-RAP edge to suppress unicast and promote multicast.
9099
+ * the multicast socket keeps delivering packets and the track keeps its FEC
9100
+ * decoder armed. If a valid RAP arrives later, that late-RAP edge suppresses
9101
+ * unicast and promotes multicast without a second fec-setup.
8597
9102
  */
8598
9103
  activateMulticast() {
8599
9104
  for (const [, session] of this.sessions) {
@@ -8625,13 +9130,17 @@ var SessionManager = class {
8625
9130
  * and they must drain uninterrupted across transport-state events.
8626
9131
  *
8627
9132
  * Coherence invariant (BLO-9413): after this returns, every non-fallback
8628
- * session is `unicast` (or `idle`) AND unsuppressed — unsubscribe() unwinds
8629
- * any outstanding suppression regardless of the state it tears down from
8630
- * (`failing` included; see TrackSession.#suppressFired).
9133
+ * session is `unicast` (or `idle`), unsuppressed, and without a FEC decoder.
9134
+ * unsubscribe() unwinds any outstanding suppression regardless of the state
9135
+ * it tears down from (`failing` included; see TrackSession.#suppressFired),
9136
+ * and a session that a takeover timeout parked in `unicast` with its decoder
9137
+ * still armed is cycled too, because this transport edge is what releases
9138
+ * that decoder (unsubscribe fires fec-teardown).
8631
9139
  */
8632
9140
  failbackAll() {
8633
9141
  for (const [, session] of this.sessions) {
8634
- if (session.state !== "idle" && session.state !== "unicast" && session.state !== "unicast-fallback") {
9142
+ const parkedWithFec = session.state === "unicast" && session.isFecArmed;
9143
+ if (parkedWithFec || session.state !== "idle" && session.state !== "unicast" && session.state !== "unicast-fallback") {
8635
9144
  session.unsubscribe();
8636
9145
  session.subscribe();
8637
9146
  }
@@ -8715,6 +9224,27 @@ var SessionManager = class {
8715
9224
  this.callbacks = null;
8716
9225
  }
8717
9226
  };
9227
+ _takeoverDeadlineMs = new WeakMap();
9228
+ _SessionManager_instances = new WeakSet();
9229
+ // -----------------------------------------------------------------------
9230
+ // Track management
9231
+ // -----------------------------------------------------------------------
9232
+ /**
9233
+ * The admitted takeover deadline. Throws only when the host never set one,
9234
+ * every other invalid value being refused by the setter, and returns a
9235
+ * declared 0 as 0. The getter and addTrack() both route through it, so a
9236
+ * session cannot exist without an admitted deadline, which is what makes
9237
+ * the #transition() read unable to throw on an already-admitted session.
9238
+ */
9239
+ requireTakeoverDeadlineMs_fn = function() {
9240
+ const value = __privateGet(this, _takeoverDeadlineMs);
9241
+ if (value === void 0) {
9242
+ throw new Error(
9243
+ "[SessionManager] takeoverDeadlineMs is unset; set a catalog-derived takeover deadline (0 disables it) before adding tracks"
9244
+ );
9245
+ }
9246
+ return value;
9247
+ };
8718
9248
 
8719
9249
  // src/fec-manager.ts
8720
9250
  var MIN_INTERLEAVE_DEPTH_MS = 33;
@@ -8733,7 +9263,7 @@ function newFecCounters() {
8733
9263
  greenFillFrames: 0
8734
9264
  };
8735
9265
  }
8736
- 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;
9266
+ 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;
8737
9267
  var FECManager = class {
8738
9268
  constructor() {
8739
9269
  __privateAdd(this, _FECManager_instances);
@@ -8837,6 +9367,7 @@ var FECManager = class {
8837
9367
  // validation harness — which caps headless warnings at 500 — fails even
8838
9368
  // though the errors are duplicates of a single underlying event.
8839
9369
  __privateAdd(this, _workerErrorCounts, /* @__PURE__ */ new Map());
9370
+ __privateAdd(this, _decoderUnavailableEvents, 0);
8840
9371
  __privateAdd(this, _repairSymbolSizeMismatchCount, 0);
8841
9372
  // Event callbacks — block lifecycle notifications for MmtpRouter wiring.
8842
9373
  // NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
@@ -8845,6 +9376,17 @@ var FECManager = class {
8845
9376
  this.onBlockStarted = null;
8846
9377
  this.onBlockComplete = null;
8847
9378
  this.onBlockFailed = null;
9379
+ /**
9380
+ * Fired once per stream when its Worker decoder stops being routable.
9381
+ * `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when
9382
+ * a symbol or reset found the registered client already disposed and
9383
+ * evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder
9384
+ * registered for the same stream keeps decoding its symbols, and 'none'
9385
+ * when nothing does. Only 'none' leaves the stream unprotected: the host
9386
+ * must then register a decoder again or leave the stream, because counting
9387
+ * its symbols as handled would hide the loss.
9388
+ */
9389
+ this.onDecoderUnavailable = null;
8848
9390
  // D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
8849
9391
  // block arms a one-shot repair deadline after the derived K-symbol source
8850
9392
  // horizon; a block still short of K when it fires requests a per-SBN MoQ
@@ -8878,6 +9420,7 @@ var FECManager = class {
8878
9420
  startupRepairReadyPackets: __privateMethod(this, _FECManager_instances, packetIdsForStreamKeys_fn).call(this, __privateGet(this, _startupRepairReadyPackets)),
8879
9421
  startupRepairFailOpenPackets: __privateMethod(this, _FECManager_instances, packetIdsForStreamKeys_fn).call(this, __privateGet(this, _startupRepairFailOpenPackets)),
8880
9422
  pendingWorkerCleanups: __privateGet(this, _pendingWorkerCleanups).size,
9423
+ decoderUnavailableEvents: __privateGet(this, _decoderUnavailableEvents),
8881
9424
  repairTrackedBlocks: __privateGet(this, _blockRepairRsIds).size,
8882
9425
  lifecycleTrace: __privateGet(this, _lifecycleTrace).map((entry) => ({ ...entry }))
8883
9426
  };
@@ -9012,8 +9555,12 @@ var FECManager = class {
9012
9555
  __privateMethod(this, _FECManager_instances, discardFecDecoderClient_fn).call(this, client);
9013
9556
  }
9014
9557
  }
9015
- for (const [key, client] of __privateGet(this, _workerClients)) {
9558
+ for (const [key, client] of [...__privateGet(this, _workerClients)]) {
9016
9559
  if (!__privateMethod(this, _FECManager_instances, belongsToStream_fn).call(this, key, packetId, catalogTrackName)) continue;
9560
+ if (client.disposed === true) {
9561
+ __privateMethod(this, _FECManager_instances, evictDisposedWorker_fn).call(this, client);
9562
+ continue;
9563
+ }
9017
9564
  client.reset();
9018
9565
  clients.add(client);
9019
9566
  }
@@ -9233,6 +9780,12 @@ var FECManager = class {
9233
9780
  }
9234
9781
  const streamKey = __privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName);
9235
9782
  __privateMethod(this, _FECManager_instances, assertExclusiveDecoderClient_fn).call(this, streamKey, client, __privateGet(this, _workerClients), "worker");
9783
+ const incumbent = __privateGet(this, _workerClients).get(streamKey);
9784
+ if (incumbent !== void 0 && incumbent !== client) {
9785
+ throw new Error(
9786
+ `[FECManager] stream ${streamKey} already routes to a Worker; call clearFecWorker before registering another`
9787
+ );
9788
+ }
9236
9789
  if (config) __privateMethod(this, _FECManager_instances, upsertTrackConfig_fn).call(this, packetId, config);
9237
9790
  const workerK = __privateMethod(this, _FECManager_instances, resolveTrackConfig_fn).call(this, packetId).k;
9238
9791
  __privateGet(this, _workerClients).set(streamKey, client);
@@ -9319,14 +9872,7 @@ var FECManager = class {
9319
9872
  const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
9320
9873
  return block.packetId === packetId && block.sbn === sbn && __privateGet(this, _pendingWorkerCleanupClients).get(key) === client;
9321
9874
  });
9322
- for (const key of cleanupKeys) {
9323
- __privateGet(this, _pendingWorkerCleanups).delete(key);
9324
- __privateGet(this, _pendingWorkerCleanupClients).delete(key);
9325
- if (__privateGet(this, _completedBlocks).has(key)) continue;
9326
- const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
9327
- __privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, "cleanup-ack", sbn, packetId, block.catalogTrackName);
9328
- __privateMethod(this, _FECManager_instances, failBlock_fn).call(this, sbn, packetId, false, block.catalogTrackName);
9329
- }
9875
+ for (const key of cleanupKeys) __privateMethod(this, _FECManager_instances, settleOwedCleanup_fn).call(this, key, "cleanup-ack");
9330
9876
  });
9331
9877
  client.onError((msg, code) => {
9332
9878
  const requestedKind = code ?? msg;
@@ -9342,6 +9888,25 @@ var FECManager = class {
9342
9888
  }
9343
9889
  });
9344
9890
  }
9891
+ /**
9892
+ * Remove a stream's Worker decoder from routing.
9893
+ *
9894
+ * Returns false when no Worker is registered under that exact stream key.
9895
+ * On success every cleanup acknowledgement still owed by the client is
9896
+ * resolved by failing its block now (a Worker that is going away cannot
9897
+ * post it), the client's block ownership is released, and
9898
+ * onDecoderUnavailable fires once with reason 'cleared' and the decoder
9899
+ * that remains for the stream ('sync' or 'none'). Call this before
9900
+ * disposing the client; a client disposed while still registered is
9901
+ * evicted on its next symbol or reset with reason 'disposed'.
9902
+ */
9903
+ clearFecWorker(packetId, catalogTrackName) {
9904
+ const streamKey = __privateMethod(this, _FECManager_instances, streamKey_fn).call(this, packetId, catalogTrackName);
9905
+ const client = __privateGet(this, _workerClients).get(streamKey);
9906
+ if (!client) return false;
9907
+ __privateMethod(this, _FECManager_instances, retireWorker_fn).call(this, streamKey, client, "cleared");
9908
+ return true;
9909
+ }
9345
9910
  /** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
9346
9911
  setFramerate(fps) {
9347
9912
  if (__privateGet(this, _fpsDetected) || fps <= 0) return;
@@ -9392,7 +9957,7 @@ var FECManager = class {
9392
9957
  const sbn = Math.floor(ssId / this.kForPacket(packetId));
9393
9958
  const blockKey = __privateMethod(this, _FECManager_instances, blockKey_fn).call(this, sbn, packetId, catalogTrackName);
9394
9959
  const syncBlockOwner = __privateGet(this, _syncDecoderBlockOwners).get(blockKey);
9395
- const workerClient = __privateMethod(this, _FECManager_instances, workerForPacket_fn).call(this, packetId, catalogTrackName);
9960
+ const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
9396
9961
  if (workerClient && !syncBlockOwner) {
9397
9962
  if (!__privateMethod(this, _FECManager_instances, claimDecoderBlock_fn).call(this, workerClient, sbn, catalogTrackName)) return null;
9398
9963
  const buf = fecData.buffer.slice(
@@ -9469,7 +10034,7 @@ var FECManager = class {
9469
10034
  if (__privateGet(this, _repairRequestedBlocks).has(blockKey)) {
9470
10035
  __privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, "repair-arrived", sbn, packetId, catalogTrackName, rsId);
9471
10036
  }
9472
- const workerClient = __privateMethod(this, _FECManager_instances, workerForPacket_fn).call(this, packetId, catalogTrackName);
10037
+ const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
9473
10038
  if (workerClient && !syncBlockOwner) {
9474
10039
  if (!__privateMethod(this, _FECManager_instances, claimDecoderBlock_fn).call(this, workerClient, sbn, catalogTrackName)) return null;
9475
10040
  const buf = fecData.buffer.slice(
@@ -9754,6 +10319,7 @@ _fpsDetected = new WeakMap();
9754
10319
  _fecFps = new WeakMap();
9755
10320
  _fecInterleaveDepthMs = new WeakMap();
9756
10321
  _workerErrorCounts = new WeakMap();
10322
+ _decoderUnavailableEvents = new WeakMap();
9757
10323
  _repairSymbolSizeMismatchCount = new WeakMap();
9758
10324
  _FECManager_instances = new WeakSet();
9759
10325
  streamKey_fn = function(packetId, catalogTrackName) {
@@ -9773,6 +10339,14 @@ belongsToStream_fn = function(key, packetId, catalogTrackName) {
9773
10339
  packetIdsForStreamKeys_fn = function(keys) {
9774
10340
  return [...new Set([...keys].map((key) => Number(key.split(":", 1)[0])))];
9775
10341
  };
10342
+ parseStreamKey_fn = function(key) {
10343
+ const separator = key.indexOf(":");
10344
+ if (separator < 0) return { packetId: Number(key) || 0 };
10345
+ return {
10346
+ packetId: Number(key.slice(0, separator)) || 0,
10347
+ catalogTrackName: decodeURIComponent(key.slice(separator + 1))
10348
+ };
10349
+ };
9776
10350
  blockKey_fn = function(sbn, packetId, catalogTrackName) {
9777
10351
  const base = `${packetId}:${sbn}`;
9778
10352
  return catalogTrackName === void 0 ? base : `${base}:${encodeURIComponent(catalogTrackName)}`;
@@ -9792,6 +10366,18 @@ decoderForPacket_fn = function(packetId, catalogTrackName) {
9792
10366
  workerForPacket_fn = function(packetId, catalogTrackName) {
9793
10367
  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));
9794
10368
  };
10369
+ /**
10370
+ * The stream's Worker client for routing, or undefined after evicting a
10371
+ * registered client that was already disposed. A disposed client throws on
10372
+ * every command, so it leaves routing here before a symbol reaches it and
10373
+ * the stream reports its decoder unavailable instead.
10374
+ */
10375
+ routableWorkerForPacket_fn = function(packetId, catalogTrackName) {
10376
+ const client = __privateMethod(this, _FECManager_instances, workerForPacket_fn).call(this, packetId, catalogTrackName);
10377
+ if (client === void 0 || client.disposed !== true) return client;
10378
+ __privateMethod(this, _FECManager_instances, evictDisposedWorker_fn).call(this, client);
10379
+ return void 0;
10380
+ };
9795
10381
  claimDecoderBlock_fn = function(client, sbn, catalogTrackName) {
9796
10382
  let blocks = __privateGet(this, _decoderBlockOwners).get(client);
9797
10383
  if (!blocks) {
@@ -9959,7 +10545,7 @@ armBlockTimeout_fn = function(sbn, packetId, catalogTrackName, delayMs) {
9959
10545
  __privateMethod(this, _FECManager_instances, failBlock_fn).call(this, sbn, packetId, false, catalogTrackName);
9960
10546
  return;
9961
10547
  }
9962
- const workerClient = __privateMethod(this, _FECManager_instances, workerForPacket_fn).call(this, packetId, catalogTrackName);
10548
+ const workerClient = __privateMethod(this, _FECManager_instances, routableWorkerForPacket_fn).call(this, packetId, catalogTrackName);
9963
10549
  if (workerClient) {
9964
10550
  __privateGet(this, _pendingWorkerCleanups).add(key);
9965
10551
  __privateGet(this, _pendingWorkerCleanupClients).set(key, workerClient);
@@ -10042,6 +10628,59 @@ markStartupPartial_fn = function(blockKey) {
10042
10628
  ));
10043
10629
  }
10044
10630
  };
10631
+ /**
10632
+ * Take a Worker client out of routing under one stream key, fail the blocks
10633
+ * whose completion it still owed, and report the stream's decoder
10634
+ * unavailable together with whatever still decodes the stream.
10635
+ */
10636
+ retireWorker_fn = function(streamKey, client, reason) {
10637
+ __privateGet(this, _workerClients).delete(streamKey);
10638
+ __privateMethod(this, _FECManager_instances, releaseWorkerClient_fn).call(this, client);
10639
+ __privateWrapper(this, _decoderUnavailableEvents)._++;
10640
+ const stream = __privateMethod(this, _FECManager_instances, parseStreamKey_fn).call(this, streamKey);
10641
+ const fallbackDecoder = __privateMethod(this, _FECManager_instances, decoderForPacket_fn).call(this, stream.packetId, stream.catalogTrackName) ? "sync" : "none";
10642
+ this.onDecoderUnavailable?.({
10643
+ packetId: stream.packetId,
10644
+ ...stream.catalogTrackName === void 0 ? {} : { catalogTrackName: stream.catalogTrackName },
10645
+ reason,
10646
+ fallbackDecoder
10647
+ });
10648
+ };
10649
+ /**
10650
+ * Fail every block whose Worker cleanup acknowledgement is owed by `client`
10651
+ * and drop its block ownership, so no block waits on a message the Worker
10652
+ * will never post.
10653
+ */
10654
+ releaseWorkerClient_fn = function(client) {
10655
+ const owed = [...__privateGet(this, _pendingWorkerCleanupClients)].filter(([, owner]) => owner === client).map(([key]) => key);
10656
+ for (const key of owed) __privateMethod(this, _FECManager_instances, settleOwedCleanup_fn).call(this, key, "decoder-unavailable");
10657
+ __privateGet(this, _decoderBlockOwners).delete(client);
10658
+ };
10659
+ /**
10660
+ * Resolve one owed Worker cleanup: drop the pending entry and fail the block
10661
+ * unless it already completed. Shared by the cleanup acknowledgement and by
10662
+ * Worker retirement, so the two paths cannot drift in their bookkeeping.
10663
+ */
10664
+ settleOwedCleanup_fn = function(key, event) {
10665
+ __privateGet(this, _pendingWorkerCleanups).delete(key);
10666
+ __privateGet(this, _pendingWorkerCleanupClients).delete(key);
10667
+ if (__privateGet(this, _completedBlocks).has(key)) return;
10668
+ const block = __privateMethod(this, _FECManager_instances, parseBlockKey_fn).call(this, key);
10669
+ __privateMethod(this, _FECManager_instances, traceLifecycle_fn).call(this, event, block.sbn, block.packetId, block.catalogTrackName);
10670
+ __privateMethod(this, _FECManager_instances, failBlock_fn).call(this, block.sbn, block.packetId, false, block.catalogTrackName);
10671
+ };
10672
+ /** Evict a disposed client under every stream key that still maps to it. */
10673
+ evictDisposedWorker_fn = function(client) {
10674
+ for (const [key, registered] of [...__privateGet(this, _workerClients)]) {
10675
+ if (registered !== client) continue;
10676
+ const stream = __privateMethod(this, _FECManager_instances, parseStreamKey_fn).call(this, key);
10677
+ const track = stream.catalogTrackName === void 0 ? "" : ` track=${stream.catalogTrackName}`;
10678
+ console.warn(
10679
+ `[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`
10680
+ );
10681
+ __privateMethod(this, _FECManager_instances, retireWorker_fn).call(this, key, client, "disposed");
10682
+ }
10683
+ };
10045
10684
  routeSyncCleanup_fn = function(decoder, sbn, packetId) {
10046
10685
  let framed;
10047
10686
  try {
@@ -10613,13 +11252,15 @@ function sameTrack(a, b) {
10613
11252
  function sameSample(a, b) {
10614
11253
  return a.decodeTime === b.decodeTime && a.presentationTime === b.presentationTime && a.duration === b.duration && a.timescale === b.timescale && a.size === b.size && a.bytePosition === b.bytePosition;
10615
11254
  }
10616
- var _track, _fragments, _pendingSamples;
11255
+ var _track, _fragments, _pendingSamples, _lastMovieFragmentRejection, _IsoBmffTimingResolver_instances, clearSlot_fn;
10617
11256
  var IsoBmffTimingResolver = class {
10618
11257
  constructor(maxPendingSamples) {
10619
11258
  this.maxPendingSamples = maxPendingSamples;
11259
+ __privateAdd(this, _IsoBmffTimingResolver_instances);
10620
11260
  __privateAdd(this, _track, null);
10621
11261
  __privateAdd(this, _fragments, /* @__PURE__ */ new Map());
10622
11262
  __privateAdd(this, _pendingSamples, 0);
11263
+ __privateAdd(this, _lastMovieFragmentRejection, null);
10623
11264
  if (!Number.isInteger(maxPendingSamples) || maxPendingSamples <= 0) {
10624
11265
  throw new Error("[IsoBmffTimingResolver] maxPendingSamples must be a positive integer");
10625
11266
  }
@@ -10627,6 +11268,10 @@ var IsoBmffTimingResolver = class {
10627
11268
  get pendingSampleCount() {
10628
11269
  return __privateGet(this, _pendingSamples);
10629
11270
  }
11271
+ /** Typed reason for the most recent rejected movie fragment. */
11272
+ get lastMovieFragmentRejection() {
11273
+ return __privateGet(this, _lastMovieFragmentRejection);
11274
+ }
10630
11275
  addInitialization(data) {
10631
11276
  const timing = parseIsoBmffInitializationTiming(data);
10632
11277
  if (!timing) return false;
@@ -10635,14 +11280,24 @@ var IsoBmffTimingResolver = class {
10635
11280
  return true;
10636
11281
  }
10637
11282
  addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
10638
- if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) return false;
10639
- if (!__privateGet(this, _track) || !isU322(mpuSequenceNumber)) return false;
11283
+ __privateSet(this, _lastMovieFragmentRejection, null);
11284
+ if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
11285
+ __privateSet(this, _lastMovieFragmentRejection, "invalid");
11286
+ return false;
11287
+ }
11288
+ if (!__privateGet(this, _track) || !isU322(mpuSequenceNumber)) {
11289
+ __privateSet(this, _lastMovieFragmentRejection, "invalid");
11290
+ return false;
11291
+ }
10640
11292
  const parsed = parseIsoBmffMovieFragmentTiming(
10641
11293
  data,
10642
11294
  __privateGet(this, _track),
10643
11295
  this.maxPendingSamples
10644
11296
  );
10645
- if (!parsed) return false;
11297
+ if (!parsed) {
11298
+ __privateSet(this, _lastMovieFragmentRejection, "invalid");
11299
+ return false;
11300
+ }
10646
11301
  const key = fragmentKey(mpuSequenceNumber, parsed.movieFragmentSequenceNumber);
10647
11302
  const existing = __privateGet(this, _fragments).get(key);
10648
11303
  if (existing) {
@@ -10650,8 +11305,14 @@ var IsoBmffTimingResolver = class {
10650
11305
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
10651
11306
  );
10652
11307
  }
10653
- if (parsed.samples.length > additionalSampleLimit) return false;
10654
- if (__privateGet(this, _pendingSamples) + parsed.samples.length > this.maxPendingSamples) return false;
11308
+ if (parsed.samples.length > additionalSampleLimit) {
11309
+ __privateSet(this, _lastMovieFragmentRejection, "budget");
11310
+ return false;
11311
+ }
11312
+ if (__privateGet(this, _pendingSamples) + parsed.samples.length > this.maxPendingSamples) {
11313
+ __privateSet(this, _lastMovieFragmentRejection, "budget");
11314
+ return false;
11315
+ }
10655
11316
  __privateGet(this, _fragments).set(key, [...parsed.samples]);
10656
11317
  __privateSet(this, _pendingSamples, __privateGet(this, _pendingSamples) + parsed.samples.length);
10657
11318
  return true;
@@ -10666,15 +11327,36 @@ var IsoBmffTimingResolver = class {
10666
11327
  return samples?.[sampleNumber - 1] ?? null;
10667
11328
  }
10668
11329
  consumeSample(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, expected) {
10669
- const key = fragmentKey(mpuSequenceNumber, movieFragmentSequenceNumber);
10670
- const samples = __privateGet(this, _fragments).get(key);
10671
- const index = sampleNumber - 1;
10672
- const sample = samples?.[index];
10673
- if (!samples || !sample || !sameSample(sample, expected)) return false;
10674
- samples[index] = null;
10675
- __privateWrapper(this, _pendingSamples)._--;
10676
- if (samples.every((value) => value === null)) __privateGet(this, _fragments).delete(key);
10677
- return true;
11330
+ const samples = __privateGet(this, _fragments).get(
11331
+ fragmentKey(mpuSequenceNumber, movieFragmentSequenceNumber)
11332
+ );
11333
+ const sample = samples?.[sampleNumber - 1];
11334
+ if (!sample || !sameSample(sample, expected)) return false;
11335
+ return __privateMethod(this, _IsoBmffTimingResolver_instances, clearSlot_fn).call(this, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber);
11336
+ }
11337
+ /**
11338
+ * Free the budget slot of a sample that will never be delivered.
11339
+ *
11340
+ * `consumeSample` is the delivery path and is the only decrement on a
11341
+ * healthy stream. But a sample whose data units never complete is never
11342
+ * delivered, and without this path its slot is held until `reset()` --
11343
+ * making `maxPendingSamples` a one-way ratchet. Once it saturates,
11344
+ * `addMovieFragment` rejects every subsequent moof and the stream is
11345
+ * wedged permanently: the consumer sees media stop with no error, and only
11346
+ * a full track reset clears it.
11347
+ *
11348
+ * The caller is whoever abandoned the sample, so the release is driven by
11349
+ * the abandonment itself rather than by a timer, an LRU, or an age bound --
11350
+ * the slot is freed exactly when its sample provably cannot arrive, which
11351
+ * is a protocol event the combiner already detects and counts.
11352
+ *
11353
+ * Deliberately takes no `expected` timing: the caller is discarding, not
11354
+ * matching, and requiring it would make the release fail exactly when the
11355
+ * timing entry is the thing being reclaimed. Returns false when the slot
11356
+ * was already freed, so a double release cannot drive the count negative.
11357
+ */
11358
+ releaseSample(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) {
11359
+ return __privateMethod(this, _IsoBmffTimingResolver_instances, clearSlot_fn).call(this, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber);
10678
11360
  }
10679
11361
  reset() {
10680
11362
  __privateSet(this, _track, null);
@@ -10685,6 +11367,22 @@ var IsoBmffTimingResolver = class {
10685
11367
  _track = new WeakMap();
10686
11368
  _fragments = new WeakMap();
10687
11369
  _pendingSamples = new WeakMap();
11370
+ _lastMovieFragmentRejection = new WeakMap();
11371
+ _IsoBmffTimingResolver_instances = new WeakSet();
11372
+ /** Null one slot, decrement the budget, and drop the fragment when empty. */
11373
+ clearSlot_fn = function(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) {
11374
+ if (!isU322(mpuSequenceNumber) || !isU322(movieFragmentSequenceNumber) || movieFragmentSequenceNumber === 0 || !isU322(sampleNumber) || sampleNumber === 0) {
11375
+ return false;
11376
+ }
11377
+ const key = fragmentKey(mpuSequenceNumber, movieFragmentSequenceNumber);
11378
+ const samples = __privateGet(this, _fragments).get(key);
11379
+ const index = sampleNumber - 1;
11380
+ if (!samples || !samples[index]) return false;
11381
+ samples[index] = null;
11382
+ __privateWrapper(this, _pendingSamples)._--;
11383
+ if (samples.every((value) => value === null)) __privateGet(this, _fragments).delete(key);
11384
+ return true;
11385
+ };
10688
11386
  function fragmentKey(mpuSequenceNumber, movieFragmentSequenceNumber) {
10689
11387
  return `${mpuSequenceNumber}:${movieFragmentSequenceNumber}`;
10690
11388
  }
@@ -10849,7 +11547,7 @@ function isFmp4Box(data) {
10849
11547
  const type = String.fromCharCode(data[4], data[5], data[6], data[7]);
10850
11548
  return FMPU_BOX_TYPES.has(type);
10851
11549
  }
10852
- var _tracks, _defaultTrack, _reassembler, _sampleCombiners, _sampleCombinerOptions, _retiredSampleCombinerStats, _timingStreams, _maxPendingTimingSamples, _pendingTimingSamples, _sequencers, _reorderWindow, _reassemblyPolicy, _frameCb, _initCb, _sourceSymbolCb, _trackFrameCbs, _reorderStats, _reliableMoqStartupGate, _disposed2, _MmtpContainerParser_instances, streamKey_fn2, sampleCombinerFor_fn, dropSampleCombiner_fn, emitCombinedSample_fn, passReliableMoqStartupGate_fn, timingResolver_fn, dropTimingStream_fn, validateReassemblyPolicy_fn, buildFrameMeta_fn, sequencerFor_fn, flushSequencers_fn, emitFrame_fn2, _MmtpContainerParser_static, scanForKeyframe_fn, scanLengthPrefixedHevcForKeyframe_fn, scanAnnexBForKeyframe_fn;
11550
+ var _tracks, _defaultTrack, _reassembler, _sampleCombiners, _sampleCombinerOptions, _retiredSampleCombinerStats, _timingStreams, _maxPendingTimingSamples, _pendingTimingSamples, _reclaimedTimingSamples, _rejectedMovieFragments, _sequencers, _reorderWindow, _reassemblyPolicy, _frameCb, _initCb, _sourceSymbolCb, _trackFrameCbs, _reorderStats, _reliableMoqStartupGate, _disposed2, _MmtpContainerParser_instances, streamKey_fn2, sampleCombinerFor_fn, releaseTimingSlot_fn, dropSampleCombiner_fn, emitCombinedSample_fn, passReliableMoqStartupGate_fn, timingResolver_fn, dropTimingStream_fn, validateReassemblyPolicy_fn, buildFrameMeta_fn, sequencerFor_fn, flushSequencers_fn, emitFrame_fn2, _MmtpContainerParser_static, scanForKeyframe_fn, scanLengthPrefixedHevcForKeyframe_fn, scanAnnexBForKeyframe_fn;
10853
11551
  var _MmtpContainerParser = class _MmtpContainerParser {
10854
11552
  constructor(config) {
10855
11553
  __privateAdd(this, _MmtpContainerParser_instances);
@@ -10882,6 +11580,8 @@ var _MmtpContainerParser = class _MmtpContainerParser {
10882
11580
  __privateAdd(this, _timingStreams, /* @__PURE__ */ new Map());
10883
11581
  __privateAdd(this, _maxPendingTimingSamples);
10884
11582
  __privateAdd(this, _pendingTimingSamples, 0);
11583
+ __privateAdd(this, _reclaimedTimingSamples, 0);
11584
+ __privateAdd(this, _rejectedMovieFragments, 0);
10885
11585
  __privateAdd(this, _sequencers, /* @__PURE__ */ new Map());
10886
11586
  __privateAdd(this, _reorderWindow, 0);
10887
11587
  __privateAdd(this, _reassemblyPolicy, null);
@@ -10995,6 +11695,23 @@ var _MmtpContainerParser = class _MmtpContainerParser {
10995
11695
  }
10996
11696
  return total;
10997
11697
  }
11698
+ /**
11699
+ * Typed view of the ISO-BMFF timing-sample budget.
11700
+ *
11701
+ * This is the acceptance signal for the budget ratchet: `pending` walking
11702
+ * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
11703
+ * and it is observable here rather than only as media silently stopping.
11704
+ * Per repo policy this is a producer-owned counter -- never infer budget
11705
+ * state by parsing logs.
11706
+ */
11707
+ get timingBudgetStats() {
11708
+ return {
11709
+ pending: __privateGet(this, _pendingTimingSamples),
11710
+ capacity: __privateGet(this, _maxPendingTimingSamples),
11711
+ reclaimed: __privateGet(this, _reclaimedTimingSamples),
11712
+ rejectedMovieFragments: __privateGet(this, _rejectedMovieFragments)
11713
+ };
11714
+ }
10998
11715
  get reorderStats() {
10999
11716
  let bufferedFrames = 0;
11000
11717
  for (const { sequencer } of __privateGet(this, _sequencers).values()) bufferedFrames += sequencer.buffered;
@@ -11007,6 +11724,20 @@ var _MmtpContainerParser = class _MmtpContainerParser {
11007
11724
  }
11008
11725
  return streams;
11009
11726
  }
11727
+ /**
11728
+ * Reclaim samples abandoned by the sample combiner's protocol deadline.
11729
+ *
11730
+ * This is deliberately driven by the parser's existing packet/lifecycle
11731
+ * cleanup path rather than by another timer. The reassembler timeout is the
11732
+ * same catalog/FEC-derived deadline that bounds each constituent MFU.
11733
+ */
11734
+ cleanupTimeouts() {
11735
+ const timeoutMs = __privateGet(this, _reassembler).timeoutMs;
11736
+ if (timeoutMs === null) return;
11737
+ for (const combiner of __privateGet(this, _sampleCombiners).values()) {
11738
+ combiner.cleanupTimeouts(timeoutMs);
11739
+ }
11740
+ }
11010
11741
  /**
11011
11742
  * Phase 1 of two-phase parsing. Returns structured header info for FEC feeding.
11012
11743
  * Call this before feed() when you need to extract FEC payload ID and feed
@@ -11037,6 +11768,7 @@ var _MmtpContainerParser = class _MmtpContainerParser {
11037
11768
  feed(packet, options) {
11038
11769
  var _a;
11039
11770
  if (__privateGet(this, _disposed2)) return null;
11771
+ this.cleanupTimeouts();
11040
11772
  const info = this.parseHeader(packet, options);
11041
11773
  if (!info) return null;
11042
11774
  if (info.packetType !== PACKET_TYPE_MPU2) {
@@ -11064,13 +11796,15 @@ var _MmtpContainerParser = class _MmtpContainerParser {
11064
11796
  const streamKey = __privateMethod(this, _MmtpContainerParser_instances, streamKey_fn2).call(this, info.packetId, options?.catalogTrackName);
11065
11797
  if (fragmentType === 0) {
11066
11798
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0) return null;
11799
+ const normalizedInit = normalizeMmtpInitSegment(data);
11800
+ if (!normalizedInit) return null;
11067
11801
  const resolver2 = __privateMethod(this, _MmtpContainerParser_instances, timingResolver_fn).call(this, streamKey, info.packetId, true);
11068
- if (!resolver2?.addInitialization(data)) return null;
11069
- (_a = __privateGet(this, _initCb)) == null ? void 0 : _a.call(this, data, info.packetId, options?.catalogTrackName);
11802
+ if (!resolver2?.addInitialization(normalizedInit)) return null;
11803
+ (_a = __privateGet(this, _initCb)) == null ? void 0 : _a.call(this, normalizedInit, info.packetId, options?.catalogTrackName);
11070
11804
  return {
11071
11805
  kind: "init",
11072
11806
  packet: info,
11073
- data,
11807
+ data: normalizedInit,
11074
11808
  mpuSequenceNumber: mpu.mpuSequenceNumber,
11075
11809
  fragmentationIndicator: mpu.fragmentationIndicator,
11076
11810
  fragmentCounter: mpu.fragmentCounter
@@ -11082,7 +11816,12 @@ var _MmtpContainerParser = class _MmtpContainerParser {
11082
11816
  if (!resolver2) return null;
11083
11817
  const pendingBefore = resolver2.pendingSampleCount;
11084
11818
  const remaining = __privateGet(this, _maxPendingTimingSamples) - __privateGet(this, _pendingTimingSamples);
11085
- if (!resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining)) return null;
11819
+ if (!resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining)) {
11820
+ if (resolver2.lastMovieFragmentRejection === "budget") {
11821
+ __privateWrapper(this, _rejectedMovieFragments)._++;
11822
+ }
11823
+ return null;
11824
+ }
11086
11825
  __privateSet(this, _pendingTimingSamples, __privateGet(this, _pendingTimingSamples) + (resolver2.pendingSampleCount - pendingBefore));
11087
11826
  return {
11088
11827
  kind: "fragment",
@@ -11144,7 +11883,7 @@ var _MmtpContainerParser = class _MmtpContainerParser {
11144
11883
  packetId: info.packetId
11145
11884
  });
11146
11885
  } catch (error) {
11147
- if (!(error instanceof MfuReassemblyError)) throw error;
11886
+ if (!isMfuReassemblyError(error)) throw error;
11148
11887
  console.warn(
11149
11888
  `[MmtpContainerParser] rejected MFU fragment: code=${error.code} mpu=${mpu.mpuSequenceNumber} fi=${mpu.fragmentationIndicator} counter=${mpu.fragmentCounter}`
11150
11889
  );
@@ -11368,6 +12107,8 @@ _retiredSampleCombinerStats = new WeakMap();
11368
12107
  _timingStreams = new WeakMap();
11369
12108
  _maxPendingTimingSamples = new WeakMap();
11370
12109
  _pendingTimingSamples = new WeakMap();
12110
+ _reclaimedTimingSamples = new WeakMap();
12111
+ _rejectedMovieFragments = new WeakMap();
11371
12112
  _sequencers = new WeakMap();
11372
12113
  _reorderWindow = new WeakMap();
11373
12114
  _reassemblyPolicy = new WeakMap();
@@ -11385,11 +12126,42 @@ streamKey_fn2 = function(packetId, catalogTrackName) {
11385
12126
  sampleCombinerFor_fn = function(streamKey) {
11386
12127
  let combiner = __privateGet(this, _sampleCombiners).get(streamKey);
11387
12128
  if (combiner === void 0) {
11388
- combiner = new SampleCombiner(__privateGet(this, _sampleCombinerOptions));
12129
+ combiner = new SampleCombiner({
12130
+ ...__privateGet(this, _sampleCombinerOptions),
12131
+ onAbandon: (sample) => __privateMethod(this, _MmtpContainerParser_instances, releaseTimingSlot_fn).call(this, streamKey, sample)
12132
+ });
11389
12133
  __privateGet(this, _sampleCombiners).set(streamKey, combiner);
11390
12134
  }
11391
12135
  return combiner;
11392
12136
  };
12137
+ /**
12138
+ * Return the timing budget slot of a sample the combiner abandoned.
12139
+ *
12140
+ * Without this, `#pendingTimingSamples` is a one-way ratchet: it is charged
12141
+ * for every sample a moof declares but credited only when that sample is
12142
+ * delivered, so each sample lost to eviction or timeout permanently burns a
12143
+ * slot. At saturation `addMovieFragment` rejects every subsequent moof and
12144
+ * every FT=2 data unit is dropped -- media stops with no error surfaced and
12145
+ * nothing short of a track reset recovers.
12146
+ *
12147
+ * The parser total is decremented only when the resolver reports it
12148
+ * actually freed a slot, so a resolver already dropped by a track reset, or
12149
+ * a sample released twice, cannot drive the accounting negative.
12150
+ */
12151
+ releaseTimingSlot_fn = function(streamKey, sample) {
12152
+ const resolver = __privateGet(this, _timingStreams).get(streamKey)?.resolver;
12153
+ if (!resolver) return;
12154
+ if (!resolver.releaseSample(
12155
+ sample.mpuSequenceNumber,
12156
+ sample.movieFragmentSequenceNumber,
12157
+ sample.sampleNumber
12158
+ )) return;
12159
+ __privateWrapper(this, _pendingTimingSamples)._--;
12160
+ __privateWrapper(this, _reclaimedTimingSamples)._++;
12161
+ if (__privateGet(this, _pendingTimingSamples) < 0) {
12162
+ throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
12163
+ }
12164
+ };
11393
12165
  /** Drop one stream's combiner, folding its counters into the retained
11394
12166
  * totals so a reset cannot make the exposed stats run backwards. */
11395
12167
  dropSampleCombiner_fn = function(streamKey) {
@@ -11473,7 +12245,7 @@ validateReassemblyPolicy_fn = function(policy = __privateGet(this, _reassemblyPo
11473
12245
  mmtpFrameReorderWindowFrames(policy, reorderWindow);
11474
12246
  };
11475
12247
  buildFrameMeta_fn = function(assembled, packetId, seq, timing, recovered = false) {
11476
- const { data, timestamp: mmtpTimestamp, rapFlag, priority } = assembled;
12248
+ const { data, rapFlag, priority } = assembled;
11477
12249
  if (isFmp4Box(data)) return null;
11478
12250
  const presentationTimestamp = mediaTicksToMicroseconds(
11479
12251
  timing.presentationTime,
@@ -11512,7 +12284,7 @@ buildFrameMeta_fn = function(assembled, packetId, seq, timing, recovered = false
11512
12284
  ...assembled.packetSequenceNumbers === void 0 ? {} : { packetSequenceNumbers: assembled.packetSequenceNumbers },
11513
12285
  ...assembled.repairedPacketSequenceNumbers === void 0 ? {} : { repairedPacketSequenceNumbers: assembled.repairedPacketSequenceNumbers },
11514
12286
  ...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName },
11515
- mmtpTimestamp,
12287
+ ...assembled.mmtpTimestamp === void 0 ? {} : { mmtpTimestamp: assembled.mmtpTimestamp },
11516
12288
  mpuSequenceNumber: assembled.mpuSequenceNumber,
11517
12289
  ...assembled.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber },
11518
12290
  ...assembled.sampleNumber === void 0 ? {} : { sampleNumber: assembled.sampleNumber },
@@ -11527,7 +12299,7 @@ buildFrameMeta_fn = function(assembled, packetId, seq, timing, recovered = false
11527
12299
  }
11528
12300
  return meta;
11529
12301
  };
11530
- sequencerFor_fn = function(packetId, _trackType, catalogTrackName) {
12302
+ sequencerFor_fn = function(packetId, _trackType2, catalogTrackName) {
11531
12303
  const windowSize = __privateGet(this, _reassemblyPolicy) ? mmtpFrameReorderWindowFrames(__privateGet(this, _reassemblyPolicy), __privateGet(this, _reorderWindow)) : __privateGet(this, _reorderWindow);
11532
12304
  if (windowSize <= 0) return null;
11533
12305
  const streamKey = __privateMethod(this, _MmtpContainerParser_instances, streamKey_fn2).call(this, packetId, catalogTrackName);
@@ -11843,6 +12615,7 @@ var MmtpRouter = class {
11843
12615
  const isVideo = __privateMethod(this, _MmtpRouter_instances, classifyPacketId_fn).call(this, packetId) === true;
11844
12616
  const frameMeta = {
11845
12617
  timestamp: meta.timestamp,
12618
+ ...meta.mmtpTimestamp === void 0 ? {} : { mmtpTimestamp: meta.mmtpTimestamp },
11846
12619
  keyframe: isVideo ? meta.keyframe : true,
11847
12620
  packetId,
11848
12621
  packetSequenceNumber: meta.packetSequenceNumber ?? __privateGet(this, _currentSeqNum),
@@ -11850,6 +12623,8 @@ var MmtpRouter = class {
11850
12623
  ...meta.packetSequenceNumbers === void 0 ? {} : { packetSequenceNumbers: meta.packetSequenceNumbers },
11851
12624
  ...meta.repairedPacketSequenceNumbers === void 0 ? {} : { repairedPacketSequenceNumbers: meta.repairedPacketSequenceNumbers },
11852
12625
  ...meta.mpuSequenceNumber === void 0 ? {} : { mpuSequenceNumber: meta.mpuSequenceNumber },
12626
+ ...meta.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: meta.movieFragmentSequenceNumber },
12627
+ ...meta.sampleNumber === void 0 ? {} : { sampleNumber: meta.sampleNumber },
11853
12628
  ...meta.catalogTrackName === void 0 ? {} : { catalogTrackName: meta.catalogTrackName }
11854
12629
  };
11855
12630
  if (meta.source) frameMeta.source = meta.source;
@@ -11873,7 +12648,10 @@ var MmtpRouter = class {
11873
12648
  frameMeta.packetSequenceNumbers,
11874
12649
  frameMeta.repairedPacketSequenceNumbers,
11875
12650
  frameMeta.authoredTiming,
11876
- frameMeta.deliveryProvenance
12651
+ frameMeta.deliveryProvenance,
12652
+ frameMeta.mmtpTimestamp,
12653
+ frameMeta.movieFragmentSequenceNumber,
12654
+ frameMeta.sampleNumber
11877
12655
  );
11878
12656
  } else {
11879
12657
  assembler.addBypassFrame(
@@ -11886,7 +12664,10 @@ var MmtpRouter = class {
11886
12664
  frameMeta.packetSequenceNumbers,
11887
12665
  frameMeta.repairedPacketSequenceNumbers,
11888
12666
  frameMeta.authoredTiming,
11889
- frameMeta.deliveryProvenance
12667
+ frameMeta.deliveryProvenance,
12668
+ frameMeta.mmtpTimestamp,
12669
+ frameMeta.movieFragmentSequenceNumber,
12670
+ frameMeta.sampleNumber
11890
12671
  );
11891
12672
  }
11892
12673
  }
@@ -12955,7 +13736,10 @@ isCatalogTrackUnambiguous_fn = function(packetId, catalogTrackName) {
12955
13736
  };
12956
13737
  resolveRecoveredTiming_fn = function(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, mmtpTimestamp, catalogTrackName) {
12957
13738
  if (movieFragmentSequenceNumber === void 0 || sampleNumber === void 0) {
12958
- return { timestamp: mmtpTimestampToMicros(Number(mmtpTimestamp)) };
13739
+ return {
13740
+ timestamp: mmtpTimestampToMicros(mmtpTimestamp),
13741
+ mmtpTimestamp
13742
+ };
12959
13743
  }
12960
13744
  const authoredTiming = __privateGet(this, _parser).consumeAuthoredTimingFor(
12961
13745
  packetId,
@@ -12967,7 +13751,7 @@ resolveRecoveredTiming_fn = function(packetId, mpuSequenceNumber, movieFragmentS
12967
13751
  if (authoredTiming === null) return null;
12968
13752
  const scaled = scaleAuthoredTiming(authoredTiming, 1e6);
12969
13753
  if (scaled === null) return null;
12970
- return { authoredTiming, timestamp: scaled.timestamp };
13754
+ return { authoredTiming, timestamp: scaled.timestamp, mmtpTimestamp };
12971
13755
  };
12972
13756
  packetSeqKey_fn = function(packetId, packetSequenceNumber, catalogTrackName) {
12973
13757
  return JSON.stringify([packetId, catalogTrackName ?? null, packetSequenceNumber]);
@@ -13266,12 +14050,15 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13266
14050
  frames.push({
13267
14051
  data: sym.fragment.data,
13268
14052
  timestamp: recoveredTiming.timestamp,
14053
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
13269
14054
  keyframe,
13270
14055
  authoredTiming: recoveredTiming.authoredTiming,
13271
14056
  packetSequenceNumber: sym.packetSequenceNumber,
13272
14057
  packetSequenceNumbers: [sym.packetSequenceNumber],
13273
14058
  repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
13274
14059
  mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
14060
+ ...sym.fragment.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber },
14061
+ ...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
13275
14062
  ...catalogTrackName === void 0 ? {} : { catalogTrackName },
13276
14063
  // A single-packet MFU needs the block verdict just as much as a
13277
14064
  // reassembled one. The multi-fragment branch below gets it via the
@@ -13311,7 +14098,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13311
14098
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13312
14099
  });
13313
14100
  } catch (error) {
13314
- if (!(error instanceof MfuReassemblyError)) throw error;
14101
+ if (!isMfuReassemblyError(error)) throw error;
13315
14102
  console.warn(
13316
14103
  `[MmtpRouter] dropped rejected recovered MFU fragment: block=${blockId} packetId=${ownerPacketId} code=${error.code}`
13317
14104
  );
@@ -13329,6 +14116,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13329
14116
  frames.push({
13330
14117
  data: assembled.data,
13331
14118
  timestamp: recoveredTiming.timestamp,
14119
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
13332
14120
  keyframe,
13333
14121
  authoredTiming: recoveredTiming.authoredTiming,
13334
14122
  // The singular delivery identity is the fragment that completed
@@ -13339,6 +14127,8 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13339
14127
  repairedPacketSequenceNumbers: assembled.repairedPacketSequenceNumbers
13340
14128
  },
13341
14129
  mpuSequenceNumber: assembled.mpuSequenceNumber,
14130
+ ...assembled.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber },
14131
+ ...assembled.sampleNumber === void 0 ? {} : { sampleNumber: assembled.sampleNumber },
13342
14132
  ...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName },
13343
14133
  deliveryProvenance: assembled.deliveryProvenance
13344
14134
  });
@@ -13384,6 +14174,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13384
14174
  if (!recoveredTiming) continue;
13385
14175
  __privateGet(this, _parser).routeFrame(d, {
13386
14176
  timestamp: recoveredTiming.timestamp,
14177
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
13387
14178
  authoredTiming: recoveredTiming.authoredTiming,
13388
14179
  keyframe,
13389
14180
  packetId: ownerPacketId,
@@ -13392,6 +14183,8 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13392
14183
  packetSequenceNumbers: [sym.packetSequenceNumber],
13393
14184
  repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
13394
14185
  mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
14186
+ ...sym.fragment.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber },
14187
+ ...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
13395
14188
  recovered: true,
13396
14189
  deliveryProvenance,
13397
14190
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
@@ -13426,7 +14219,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13426
14219
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13427
14220
  });
13428
14221
  } catch (error) {
13429
- if (!(error instanceof MfuReassemblyError)) throw error;
14222
+ if (!isMfuReassemblyError(error)) throw error;
13430
14223
  console.warn(
13431
14224
  `[MmtpRouter] dropped rejected recovered MFU fragment: block=${blockId} packetId=${ownerPacketId} code=${error.code}`
13432
14225
  );
@@ -13443,6 +14236,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13443
14236
  if (!recoveredTiming) continue;
13444
14237
  __privateGet(this, _parser).routeFrame(assembled.data, {
13445
14238
  timestamp: recoveredTiming.timestamp,
14239
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
13446
14240
  authoredTiming: recoveredTiming.authoredTiming,
13447
14241
  keyframe,
13448
14242
  packetId: ownerPacketId,
@@ -13453,6 +14247,8 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13453
14247
  repairedPacketSequenceNumbers: assembled.repairedPacketSequenceNumbers
13454
14248
  },
13455
14249
  mpuSequenceNumber: assembled.mpuSequenceNumber,
14250
+ ...assembled.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber },
14251
+ ...assembled.sampleNumber === void 0 ? {} : { sampleNumber: assembled.sampleNumber },
13456
14252
  recovered: true,
13457
14253
  deliveryProvenance,
13458
14254
  ...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName }
@@ -13470,6 +14266,7 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13470
14266
  if (recoveredTiming === null) continue;
13471
14267
  const meta = {
13472
14268
  timestamp: recoveredTiming.timestamp,
14269
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
13473
14270
  authoredTiming: recoveredTiming.authoredTiming,
13474
14271
  keyframe: true,
13475
14272
  packetId: ownerPacketId,
@@ -13477,6 +14274,8 @@ routeProcessedFecSymbols_fn = function(blockId, result, ownerPacketId, catalogTr
13477
14274
  packetSequenceNumbers: [sym.packetSequenceNumber],
13478
14275
  repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
13479
14276
  mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
14277
+ ...sym.fragment.movieFragmentSequenceNumber === void 0 ? {} : { movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber },
14278
+ ...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
13480
14279
  recovered: true,
13481
14280
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13482
14281
  };
@@ -13684,7 +14483,7 @@ refreshStats_fn = function() {
13684
14483
  this.stats.reassembledMFUs = asNumber(stats.reassembled_mfus);
13685
14484
  this.stats.rejectedFragments = __privateGet(this, _rejectedFragments);
13686
14485
  this.stats.timedOutMFUs = asNumber(stats.timed_out_mfus);
13687
- this.stats.evictedMemoryLimit = asNumber(stats.evicted_memory_limit);
14486
+ this.stats.evictedMemoryLimit = asNumber(stats.evicted_memory_limit) + asNumber(stats.evicted_data_units ?? 0);
13688
14487
  this.stats.outOfOrderFragments = asNumber(stats.out_of_order_fragments);
13689
14488
  stats.free?.();
13690
14489
  };
@@ -14065,12 +14864,22 @@ function createDefaultRepairLifecycleEvent(detail, options) {
14065
14864
  function isRecord(value) {
14066
14865
  return value !== null && typeof value === "object" && !Array.isArray(value);
14067
14866
  }
14068
- function isRepairBlockIdentity(value) {
14867
+ function isRepairStreamIdentity(value) {
14069
14868
  if (!isRecord(value)) return false;
14070
14869
  const packetId = value["packetId"];
14071
14870
  const catalogTrackName = value["catalogTrackName"];
14871
+ return Number.isSafeInteger(packetId) && packetId >= 0 && (catalogTrackName === null || typeof catalogTrackName === "string" && catalogTrackName.length > 0);
14872
+ }
14873
+ function isRepairBlockIdentity(value) {
14874
+ if (!isRecord(value)) return false;
14072
14875
  const sbn = value["sbn"];
14073
- return Number.isSafeInteger(packetId) && packetId >= 0 && (catalogTrackName === null || typeof catalogTrackName === "string" && catalogTrackName.length > 0) && Number.isSafeInteger(sbn) && sbn >= 0;
14876
+ return isRepairStreamIdentity(value) && Number.isSafeInteger(sbn) && sbn >= 0;
14877
+ }
14878
+ function isDecoderUnavailableReason(value) {
14879
+ return value === "cleared" || value === "disposed";
14880
+ }
14881
+ function isFecFallbackDecoder(value) {
14882
+ return value === "sync" || value === "none";
14074
14883
  }
14075
14884
  function isRepairLifecycleEvent(value) {
14076
14885
  if (!isRecord(value)) return false;
@@ -14078,7 +14887,11 @@ function isRepairLifecycleEvent(value) {
14078
14887
  const type = value["type"];
14079
14888
  const block = value["block"];
14080
14889
  const symbols = value["symbols"];
14081
- if (version !== 1 || type !== "requested" && type !== "recovered" && type !== "failed" || !isRepairBlockIdentity(block)) {
14890
+ if (version !== 1) return false;
14891
+ if (type === "decoder-unavailable") {
14892
+ return isRepairStreamIdentity(value["stream"]) && isDecoderUnavailableReason(value["reason"]) && isFecFallbackDecoder(value["fallbackDecoder"]) && block === void 0 && symbols === void 0;
14893
+ }
14894
+ if (type !== "requested" && type !== "recovered" && type !== "failed" || !isRepairBlockIdentity(block) || value["stream"] !== void 0 || value["reason"] !== void 0 || value["fallbackDecoder"] !== void 0) {
14082
14895
  return false;
14083
14896
  }
14084
14897
  if (type === "requested") return symbols === void 0;
@@ -14097,22 +14910,25 @@ function forwardRepairLifecycleEvents(source, target, options = {}) {
14097
14910
  target.dispatchEvent(forwardedEvent);
14098
14911
  });
14099
14912
  }
14100
- function repairBlockIdentity(packetId, sbn, catalogTrackName) {
14913
+ function repairStreamIdentity(packetId, catalogTrackName) {
14101
14914
  if (!Number.isSafeInteger(packetId) || packetId < 0) {
14102
14915
  throw new TypeError("packetId must be a non-negative safe integer");
14103
14916
  }
14104
- if (!Number.isSafeInteger(sbn) || sbn < 0) {
14105
- throw new TypeError("sbn must be a non-negative safe integer");
14106
- }
14107
14917
  if (catalogTrackName !== void 0 && (typeof catalogTrackName !== "string" || catalogTrackName.length === 0)) {
14108
14918
  throw new TypeError("catalogTrackName must be a non-empty string when provided");
14109
14919
  }
14110
14920
  return {
14111
14921
  packetId,
14112
- catalogTrackName: catalogTrackName ?? null,
14113
- sbn
14922
+ catalogTrackName: catalogTrackName ?? null
14114
14923
  };
14115
14924
  }
14925
+ function repairBlockIdentity(packetId, sbn, catalogTrackName) {
14926
+ const stream = repairStreamIdentity(packetId, catalogTrackName);
14927
+ if (!Number.isSafeInteger(sbn) || sbn < 0) {
14928
+ throw new TypeError("sbn must be a non-negative safe integer");
14929
+ }
14930
+ return { ...stream, sbn };
14931
+ }
14116
14932
  function assertPositiveGeometry(label, value) {
14117
14933
  if (value === void 0) return;
14118
14934
  if (!Number.isFinite(value) || value <= 0) {
@@ -14282,6 +15098,15 @@ var MulticastManager = class {
14282
15098
  }
14283
15099
  });
14284
15100
  };
15101
+ __privateGet(this, _fec).onDecoderUnavailable = (event) => {
15102
+ __privateMethod(this, _MulticastManager_instances, emitRepairLifecycle_fn).call(this, {
15103
+ version: 1,
15104
+ type: "decoder-unavailable",
15105
+ stream: repairStreamIdentity(event.packetId, event.catalogTrackName),
15106
+ reason: event.reason,
15107
+ fallbackDecoder: event.fallbackDecoder
15108
+ });
15109
+ };
14285
15110
  }
14286
15111
  // ---------------------------------------------------------------------------
14287
15112
  // Decomposed component accessors (for new code)
@@ -14336,6 +15161,17 @@ var MulticastManager = class {
14336
15161
  __privateMethod(this, _MulticastManager_instances, configureTrackMapper_fn).call(this, packetId, config);
14337
15162
  __privateGet(this, _router).setPacketIdType(packetId, trackType);
14338
15163
  }
15164
+ /**
15165
+ * Stop routing a stream's symbols to its Worker decoder. Returns false when
15166
+ * none is registered; on success one `decoder-unavailable` repair lifecycle
15167
+ * event with reason 'cleared' reaches every observer, carrying whether a
15168
+ * synchronous WASM decoder still serves the stream. The packet mapping and
15169
+ * track type are untouched: the track still exists, it just has no Worker
15170
+ * decoder. Call this before disposing the client.
15171
+ */
15172
+ clearFecWorker(packetId, catalogTrackName) {
15173
+ return __privateGet(this, _fec).clearFecWorker(packetId, catalogTrackName);
15174
+ }
14339
15175
  /** Register callback for Worker-level FecTrackStats updates. */
14340
15176
  setOnFecStatsUpdate(cb) {
14341
15177
  __privateGet(this, _fec).onFecStatsUpdate = cb;
@@ -14381,8 +15217,9 @@ var MulticastManager = class {
14381
15217
  repairSymbols: config.fecRepairSymbols,
14382
15218
  symbolSize: config.fecSymbolSize,
14383
15219
  mediaUnitsPerSecond: config.fps,
14384
- bandwidthBps: config.bandwidth
14385
- }) + (config.fecReorderToleranceMs ?? 0) : void 0;
15220
+ bandwidthBps: config.bandwidth,
15221
+ jitterBufferMs: config.fecReorderToleranceMs
15222
+ }) : void 0;
14386
15223
  const fecConfig = {
14387
15224
  k: config.fecK,
14388
15225
  symbolSize: config.fecSymbolSize,
@@ -14419,8 +15256,9 @@ var MulticastManager = class {
14419
15256
  repairSymbols: config.fecRepairSymbols,
14420
15257
  symbolSize: config.fecSymbolSize,
14421
15258
  mediaUnitsPerSecond: config.fps,
14422
- bandwidthBps: config.bandwidth
14423
- }) + (config.fecReorderToleranceMs ?? 0) : void 0;
15259
+ bandwidthBps: config.bandwidth,
15260
+ jitterBufferMs: config.fecReorderToleranceMs
15261
+ }) : void 0;
14424
15262
  __privateGet(this, _fec).configure({
14425
15263
  k: config.fecK,
14426
15264
  symbolSize: config.fecSymbolSize,
@@ -14937,11 +15775,13 @@ applyStreamGlobalFecTiming_fn = function() {
14937
15775
  };
14938
15776
  _onTrackTypeDiscovered = new WeakMap();
14939
15777
  emitRepairLifecycle_fn = function(event) {
14940
- const immutableEvent = Object.freeze({
14941
- ...event,
14942
- block: Object.freeze({ ...event.block }),
14943
- ...event.type === "requested" ? {} : { symbols: Object.freeze({ ...event.symbols }) }
14944
- });
15778
+ const immutableEvent = Object.freeze(
15779
+ event.type === "decoder-unavailable" ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
15780
+ ...event,
15781
+ block: Object.freeze({ ...event.block }),
15782
+ ...event.type === "requested" ? {} : { symbols: Object.freeze({ ...event.symbols }) }
15783
+ }
15784
+ );
14945
15785
  console.info(
14946
15786
  `${REPAIR_LIFECYCLE_LOG_PREFIX}${JSON.stringify(immutableEvent)}`
14947
15787
  );
@@ -14965,7 +15805,8 @@ async function setupFecWorkerForTrack(opts) {
14965
15805
  sourceSymbols: opts.fecK,
14966
15806
  repairSymbols: opts.fecRepairCount,
14967
15807
  symbolSize: opts.fecSymbolSize,
14968
- mediaUnitsPerSecond: 1e3 / opts.mediaUnitDurationMs
15808
+ mediaUnitsPerSecond: 1e3 / opts.mediaUnitDurationMs,
15809
+ jitterBufferMs: opts.fecReorderToleranceMs
14969
15810
  });
14970
15811
  const origin = typeof location !== "undefined" && location.origin ? location.origin : "http://localhost";
14971
15812
  const workerUrl = new URL(opts.assetUrls.fecWorker, origin);
@@ -15011,15 +15852,23 @@ async function setupFecWorkerForTrack(opts) {
15011
15852
  relayBlockSigEnabled: false
15012
15853
  };
15013
15854
  client.configure(fecTrackConfig, rqWasm, altaKeyBuffer, altaWasmModule);
15014
- opts.multicastManager.setFecWorker(opts.packetId, client, opts.trackType, {
15015
- k: opts.fecK,
15016
- symbolSize: opts.fecSymbolSize,
15017
- interleaveDepthMs: opts.fecInterleaveMs,
15018
- // FEC timing is expressed in media units. For audio this is the access-unit
15019
- // cadence (for example 1024/48kHz), not a video-only framerate field.
15020
- fps: 1e3 / opts.mediaUnitDurationMs,
15021
- timeoutMs: deliveryWindowMs
15022
- }, opts.trackName);
15855
+ try {
15856
+ opts.multicastManager.setFecWorker(opts.packetId, client, opts.trackType, {
15857
+ k: opts.fecK,
15858
+ symbolSize: opts.fecSymbolSize,
15859
+ interleaveDepthMs: opts.fecInterleaveMs,
15860
+ // FEC timing is expressed in media units. For audio this is the access-unit
15861
+ // cadence (for example 1024/48kHz), not a video-only framerate field.
15862
+ fps: 1e3 / opts.mediaUnitDurationMs,
15863
+ timeoutMs: deliveryWindowMs
15864
+ }, opts.trackName);
15865
+ } catch (e) {
15866
+ try {
15867
+ client.dispose();
15868
+ } catch {
15869
+ }
15870
+ throw e;
15871
+ }
15023
15872
  let disposed = false;
15024
15873
  return {
15025
15874
  client,
@@ -15027,6 +15876,11 @@ async function setupFecWorkerForTrack(opts) {
15027
15876
  dispose: () => {
15028
15877
  if (disposed) return;
15029
15878
  disposed = true;
15879
+ if (!opts.multicastManager.clearFecWorker(opts.packetId, opts.trackName)) {
15880
+ console.warn(
15881
+ `[fec-worker-setup] packetId=${opts.packetId} track=${opts.trackName} Worker was not registered when disposed`
15882
+ );
15883
+ }
15030
15884
  try {
15031
15885
  client.dispose();
15032
15886
  } catch {
@@ -15910,7 +16764,7 @@ _readIndex = new WeakMap();
15910
16764
  _stalled = new WeakMap();
15911
16765
 
15912
16766
  // src/mmtp-decode-pipeline.ts
15913
- var _parser2, _fecClient, _frameCb2, _initCb2, _needsKeyframe, _codec, _gateDropped, _gateEmitted, _fedObjects, _warnedNoParser, _feedOutcomes, _MmtpDecodePipeline_instances, gatedEmit_fn;
16767
+ var _parser2, _fecClient, _frameCb2, _initCb2, _needsKeyframe, _codec, _trackType, _gateDropped, _gateEmitted, _fedObjects, _warnedNoParser, _feedOutcomes, _MmtpDecodePipeline_instances, tag_fn, gatedEmit_fn;
15914
16768
  var MmtpDecodePipeline = class {
15915
16769
  constructor() {
15916
16770
  __privateAdd(this, _MmtpDecodePipeline_instances);
@@ -15919,7 +16773,11 @@ var MmtpDecodePipeline = class {
15919
16773
  __privateAdd(this, _frameCb2, null);
15920
16774
  __privateAdd(this, _initCb2, null);
15921
16775
  __privateAdd(this, _needsKeyframe, false);
15922
- __privateAdd(this, _codec, "");
16776
+ __privateAdd(this, _codec, null);
16777
+ /** Set together with #codec at configure(). Also the diagnostic label: a
16778
+ * player builds one pipeline per track, and codec alone does not say which
16779
+ * role a line belongs to at a glance. */
16780
+ __privateAdd(this, _trackType, null);
15923
16781
  /** Diagnostic counters. The keyframe gate drops silently, so a parser that
15924
16782
  * never marks a frame as a keyframe stalls the whole pipeline with no
15925
16783
  * output anywhere: frames are fed, frames are parsed, and nothing is
@@ -15948,6 +16806,7 @@ var MmtpDecodePipeline = class {
15948
16806
  __privateSet(this, _warnedNoParser, false);
15949
16807
  __privateSet(this, _codec, config.codec);
15950
16808
  const trackType = isVideoCodec(config.codec) ? "video" : "audio";
16809
+ __privateSet(this, _trackType, trackType);
15951
16810
  const trackInfo = {
15952
16811
  type: trackType,
15953
16812
  codec: config.codec,
@@ -15991,6 +16850,24 @@ var MmtpDecodePipeline = class {
15991
16850
  get reorderStats() {
15992
16851
  return __privateGet(this, _parser2)?.reorderStats ?? null;
15993
16852
  }
16853
+ /**
16854
+ * Feed tallies for this track. A fresh snapshot per call, not a live view.
16855
+ *
16856
+ * Unlike reorderStats this never returns null: the counters belong to the
16857
+ * pipeline rather than the parser, so they survive dispose(). That is
16858
+ * deliberate — a wedged pipeline is usually read after teardown, which is
16859
+ * exactly when the parser is already gone.
16860
+ */
16861
+ get feedStats() {
16862
+ return {
16863
+ trackType: __privateGet(this, _trackType),
16864
+ codec: __privateGet(this, _codec),
16865
+ fedObjects: __privateGet(this, _fedObjects),
16866
+ emitted: __privateGet(this, _gateEmitted),
16867
+ gateDropped: __privateGet(this, _gateDropped),
16868
+ outcomes: Object.fromEntries(__privateGet(this, _feedOutcomes))
16869
+ };
16870
+ }
15994
16871
  /**
15995
16872
  * Feed a raw MoQ object payload (one MMTP packet per MoQ object).
15996
16873
  * Optionally feeds source symbols to FEC client before parsing.
@@ -15999,33 +16876,37 @@ var MmtpDecodePipeline = class {
15999
16876
  if (!__privateGet(this, _parser2)) {
16000
16877
  if (!__privateGet(this, _warnedNoParser)) {
16001
16878
  __privateSet(this, _warnedNoParser, true);
16002
- console.warn("[mmtp-pipeline] feedMoqObject called with NO PARSER configured \u2014 packet dropped");
16879
+ console.warn(`[mmtp-pipeline] feedMoqObject called with NO PARSER configured ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} \u2014 packet dropped`);
16003
16880
  }
16004
16881
  return;
16005
16882
  }
16006
16883
  __privateSet(this, _fedObjects, __privateGet(this, _fedObjects) + 1);
16007
16884
  if (__privateGet(this, _fedObjects) === 1) {
16008
- console.log(`[mmtp-pipeline] first object fed codec=${__privateGet(this, _codec)} bytes=${packet.length}`);
16885
+ console.log(`[mmtp-pipeline] first object fed ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${packet.length}`);
16009
16886
  }
16010
- if (__privateGet(this, _fecClient)?.addSourceSymbol) {
16011
- const info = __privateGet(this, _parser2).parseHeader(packet);
16012
- if (info?.fecPayloadId) {
16013
- __privateGet(this, _fecClient).addSourceSymbol(info.fecPayloadId.ssId, packet);
16887
+ let outcomeKind = "threw";
16888
+ try {
16889
+ if (__privateGet(this, _fecClient)?.addSourceSymbol) {
16890
+ const info = __privateGet(this, _parser2).parseHeader(packet);
16891
+ if (info?.fecPayloadId) {
16892
+ __privateGet(this, _fecClient).addSourceSymbol(info.fecPayloadId.ssId, packet);
16893
+ }
16894
+ }
16895
+ const record = __privateGet(this, _parser2).feed(packet);
16896
+ outcomeKind = record?.kind ?? "null(parse-or-extract-failed)";
16897
+ } finally {
16898
+ const seen = (__privateGet(this, _feedOutcomes).get(outcomeKind) ?? 0) + 1;
16899
+ __privateGet(this, _feedOutcomes).set(outcomeKind, seen);
16900
+ if (seen === 1) {
16901
+ console.log(
16902
+ `[mmtp-pipeline] feed outcome FIRST '${outcomeKind}' ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} at object #${__privateGet(this, _fedObjects)} bytes=${packet.length}`
16903
+ );
16014
16904
  }
16015
- }
16016
- const record = __privateGet(this, _parser2).feed(packet);
16017
- const kind = record?.kind ?? "null(parse-or-extract-failed)";
16018
- const seen = (__privateGet(this, _feedOutcomes).get(kind) ?? 0) + 1;
16019
- __privateGet(this, _feedOutcomes).set(kind, seen);
16020
- if (seen === 1) {
16021
- console.log(
16022
- `[mmtp-pipeline] feed outcome FIRST '${kind}' at object #${__privateGet(this, _fedObjects)} bytes=${packet.length}`
16023
- );
16024
16905
  }
16025
16906
  if (__privateGet(this, _fedObjects) % 200 === 0) {
16026
16907
  const hist = [...__privateGet(this, _feedOutcomes)].map(([k, v]) => `${k}=${v}`).join(" ");
16027
16908
  console.log(
16028
- `[mmtp-pipeline] feed histogram @${__privateGet(this, _fedObjects)}: ${hist} | emitted=${__privateGet(this, _gateEmitted)} gateDropped=${__privateGet(this, _gateDropped)}`
16909
+ `[mmtp-pipeline] feed histogram ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} @${__privateGet(this, _fedObjects)}: ${hist} | emitted=${__privateGet(this, _gateEmitted)} gateDropped=${__privateGet(this, _gateDropped)}`
16029
16910
  );
16030
16911
  }
16031
16912
  }
@@ -16042,9 +16923,13 @@ var MmtpDecodePipeline = class {
16042
16923
  feedAssembledFrame(data, meta) {
16043
16924
  const frameMeta = {
16044
16925
  timestamp: meta.timestamp,
16926
+ ...meta.mmtpTimestamp === void 0 ? {} : { mmtpTimestamp: meta.mmtpTimestamp },
16045
16927
  keyframe: meta.keyframe,
16046
16928
  packetId: meta.packetId,
16047
- trackType: isVideoCodec(__privateGet(this, _codec)) ? "video" : "audio",
16929
+ // Same derivation as configure(), read from the field it set rather than
16930
+ // recomputed. Pre-configure the parser is null and routeFrame below is a
16931
+ // no-op, so the fallback is unreachable in practice.
16932
+ trackType: __privateGet(this, _trackType) ?? "audio",
16048
16933
  packetSequenceNumber: meta.packetSequenceNumber
16049
16934
  };
16050
16935
  if (meta.sourceDiscontinuity) frameMeta.sourceDiscontinuity = true;
@@ -16070,7 +16955,7 @@ var MmtpDecodePipeline = class {
16070
16955
  const droppedFrames = (__privateGet(this, _parser2)?.reorderStats.droppedOnLiveDispose ?? droppedBefore) - droppedBefore;
16071
16956
  if (droppedFrames > 0) {
16072
16957
  console.warn(
16073
- `[MmtpDecodePipeline] live dispose dropped ${droppedFrames} buffered frame(s); finite inputs must use dispose({ flush: true })`
16958
+ `[MmtpDecodePipeline] live dispose ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} dropped ${droppedFrames} buffered frame(s); finite inputs must use dispose({ flush: true })`
16074
16959
  );
16075
16960
  }
16076
16961
  __privateSet(this, _parser2, null);
@@ -16085,12 +16970,19 @@ _frameCb2 = new WeakMap();
16085
16970
  _initCb2 = new WeakMap();
16086
16971
  _needsKeyframe = new WeakMap();
16087
16972
  _codec = new WeakMap();
16973
+ _trackType = new WeakMap();
16088
16974
  _gateDropped = new WeakMap();
16089
16975
  _gateEmitted = new WeakMap();
16090
16976
  _fedObjects = new WeakMap();
16091
16977
  _warnedNoParser = new WeakMap();
16092
16978
  _feedOutcomes = new WeakMap();
16093
16979
  _MmtpDecodePipeline_instances = new WeakSet();
16980
+ /** Identity prefix for diagnostics: one pipeline per track means two
16981
+ * instances write to the same console, and unlabelled lines interleave into
16982
+ * a single unreadable stream. */
16983
+ tag_fn = function() {
16984
+ return `track=${__privateGet(this, _trackType) ?? "unconfigured"} codec=${__privateGet(this, _codec) ?? "none"}`;
16985
+ };
16094
16986
  gatedEmit_fn = function(data, meta) {
16095
16987
  var _a;
16096
16988
  let sourceDiscontinuity = false;
@@ -16099,7 +16991,7 @@ gatedEmit_fn = function(data, meta) {
16099
16991
  __privateSet(this, _gateDropped, __privateGet(this, _gateDropped) + 1);
16100
16992
  if (__privateGet(this, _gateDropped) <= 3 || __privateGet(this, _gateDropped) % 200 === 0) {
16101
16993
  console.warn(
16102
- `[mmtp-pipeline] keyframe gate DROP #${__privateGet(this, _gateDropped)} codec=${__privateGet(this, _codec)} bytes=${data.length} fed=${__privateGet(this, _fedObjects)} emitted=${__privateGet(this, _gateEmitted)}`
16994
+ `[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)}`
16103
16995
  );
16104
16996
  }
16105
16997
  return;
@@ -16107,12 +16999,12 @@ gatedEmit_fn = function(data, meta) {
16107
16999
  __privateSet(this, _needsKeyframe, false);
16108
17000
  sourceDiscontinuity = true;
16109
17001
  console.log(
16110
- `[mmtp-pipeline] keyframe gate OPENED codec=${__privateGet(this, _codec)} after ${__privateGet(this, _gateDropped)} drops, fed=${__privateGet(this, _fedObjects)}`
17002
+ `[mmtp-pipeline] keyframe gate OPENED ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} after ${__privateGet(this, _gateDropped)} drops, fed=${__privateGet(this, _fedObjects)}`
16111
17003
  );
16112
17004
  }
16113
17005
  __privateSet(this, _gateEmitted, __privateGet(this, _gateEmitted) + 1);
16114
17006
  if (__privateGet(this, _gateEmitted) === 1) {
16115
- console.log(`[mmtp-pipeline] first frame emitted codec=${__privateGet(this, _codec)} bytes=${data.length}`);
17007
+ console.log(`[mmtp-pipeline] first frame emitted ${__privateMethod(this, _MmtpDecodePipeline_instances, tag_fn).call(this)} bytes=${data.length}`);
16116
17008
  }
16117
17009
  const outMeta = sourceDiscontinuity ? { ...meta, sourceDiscontinuity: true } : meta;
16118
17010
  (_a = __privateGet(this, _frameCb2)) == null ? void 0 : _a.call(this, data, outMeta);
@@ -16786,6 +17678,24 @@ emit_fn = function(params) {
16786
17678
  };
16787
17679
 
16788
17680
  // src/catalog-fec-params.ts
17681
+ var MAX_SAFE_TICKS_PER_HZ = Number.MAX_SAFE_INTEGER / 1e3;
17682
+ function timescaleHz(value) {
17683
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
17684
+ }
17685
+ function catalogGroupDurationMs(rendition) {
17686
+ if (!rendition) return void 0;
17687
+ const ticks = numericField(rendition.groupDurationTicks);
17688
+ const ms = numericField(rendition.groupDurationMs);
17689
+ const timescale = timescaleHz(rendition.container?.kind === "mmtp" ? rendition.container.timescale : void 0) ?? timescaleHz(rendition.timescale);
17690
+ if (ticks !== void 0 && timescale !== void 0 && Number.isSafeInteger(ticks) && ticks <= timescale * MAX_SAFE_TICKS_PER_HZ) {
17691
+ return resolveGroupDurationMs({ groupDurationTicks: ticks }, timescale);
17692
+ }
17693
+ return ms;
17694
+ }
17695
+ function resolveCatalogTrackGroupDurationMs(catalog, track) {
17696
+ const renditions = track.type === "audio" ? catalog?.audio?.renditions : catalog?.video?.renditions;
17697
+ return catalogGroupDurationMs(renditions?.[track.name]);
17698
+ }
16789
17699
  var NO_FEC = {
16790
17700
  fecK: 0,
16791
17701
  fecRepairCount: 0,
@@ -16868,8 +17778,10 @@ function catalogAmtNetworkSource(networkSource) {
16868
17778
  // src/repair-fetch.ts
16869
17779
  function wireRepairFetch(mc, opts) {
16870
17780
  const inFlight = /* @__PURE__ */ new Map();
17781
+ const warnedNoTarget = /* @__PURE__ */ new Set();
16871
17782
  let disposed = false;
16872
17783
  const requestKey = (sbn, packetId, catalogTrackName) => JSON.stringify([packetId, catalogTrackName ?? null, sbn]);
17784
+ const noTargetKey = (packetId, catalogTrackName) => JSON.stringify([packetId, catalogTrackName ?? null]);
16873
17785
  const removeResetObserver = mc.addTrackResetObserver(
16874
17786
  (packetId, catalogTrackName) => {
16875
17787
  for (const [key, request] of inFlight) {
@@ -16878,6 +17790,7 @@ function wireRepairFetch(mc, opts) {
16878
17790
  inFlight.delete(key);
16879
17791
  }
16880
17792
  }
17793
+ warnedNoTarget.delete(noTargetKey(packetId, catalogTrackName));
16881
17794
  }
16882
17795
  );
16883
17796
  const timeoutFor = (sbn, packetId, catalogTrackName) => opts.timeoutMsForPacket?.(packetId) ?? mc.fecManager.repairFetchTimeoutMsForBlock(
@@ -16916,6 +17829,13 @@ function wireRepairFetch(mc, opts) {
16916
17829
  }
16917
17830
  const target = opts.resolveTarget(packetId, catalogTrackName);
16918
17831
  if (!target) {
17832
+ const warnKey = noTargetKey(packetId, catalogTrackName);
17833
+ if (!warnedNoTarget.has(warnKey)) {
17834
+ warnedNoTarget.add(warnKey);
17835
+ console.warn(
17836
+ `[RepairFetch] no repair target for pid=${packetId} sbn=${sbn}${catalogTrackName === void 0 ? "" : ` track=${catalogTrackName}`}; releasing without a fetch`
17837
+ );
17838
+ }
16919
17839
  clearRepair(sbn, packetId, catalogTrackName);
16920
17840
  return;
16921
17841
  }
@@ -16992,6 +17912,7 @@ export {
16992
17912
  FecStarvationDetector,
16993
17913
  KEYFRAME_LOST_TIMEOUT_SLACK,
16994
17914
  LOCHeaderExtensionId,
17915
+ MAX_TAKEOVER_DEADLINE_MS,
16995
17916
  MFUReassembler,
16996
17917
  MediaRouter,
16997
17918
  MfuReassemblyError,
@@ -17022,6 +17943,7 @@ export {
17022
17943
  buildVideoInitSegment,
17023
17944
  catalogAmtNetworkSource,
17024
17945
  catalogFecInterleaveMs,
17946
+ catalogGroupDurationMs,
17025
17947
  classifyH264Keyframe,
17026
17948
  classifyPacket,
17027
17949
  codecStringFromAvcC,
@@ -17068,6 +17990,7 @@ export {
17068
17990
  isFecCatalogConfig,
17069
17991
  isFmp4InitSegment,
17070
17992
  isHevcCodec,
17993
+ isMfuReassemblyError,
17071
17994
  isMulticastCatalogConfig,
17072
17995
  isMulticastEndpoint,
17073
17996
  isMulticastTrackObject,
@@ -17083,6 +18006,7 @@ export {
17083
18006
  multicastFromConfig,
17084
18007
  multicastFromEnv,
17085
18008
  normalizeH264SampleToAvcc,
18009
+ normalizeMmtpInitSegment,
17086
18010
  parseAacFrameSamples,
17087
18011
  parseAacSampleRate,
17088
18012
  parseOpusFrameSamples,
@@ -17090,6 +18014,7 @@ export {
17090
18014
  reliableMoqMfuReassembly,
17091
18015
  removeEmulationPrevention,
17092
18016
  resolveCatalogTrackFec,
18017
+ resolveCatalogTrackGroupDurationMs,
17093
18018
  resolveGlobalMmtWasmBindings,
17094
18019
  resolveGroupDurationMs,
17095
18020
  resolveGroupDurationTicks,