@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.a16efba1d85a

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 (89) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +630 -191
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +60 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +3 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +26 -4
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.js.map +1 -1
  31. package/dist/iso-bmff-timing.d.ts +10 -1
  32. package/dist/iso-bmff-timing.d.ts.map +1 -1
  33. package/dist/iso-bmff-timing.js +36 -2
  34. package/dist/iso-bmff-timing.js.map +2 -2
  35. package/dist/media-router.js.map +1 -1
  36. package/dist/mfu-reassembler.js +12 -19
  37. package/dist/mfu-reassembler.js.map +2 -2
  38. package/dist/mmtp-container-parser.d.ts +35 -2
  39. package/dist/mmtp-container-parser.d.ts.map +1 -1
  40. package/dist/mmtp-container-parser.js +97 -28
  41. package/dist/mmtp-container-parser.js.map +2 -2
  42. package/dist/mmtp-container-parser.no-modules.js +222 -45
  43. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  44. package/dist/mmtp-decode-pipeline.js +97 -28
  45. package/dist/mmtp-decode-pipeline.js.map +2 -2
  46. package/dist/mmtp-fec-session.d.ts +41 -3
  47. package/dist/mmtp-fec-session.d.ts.map +1 -1
  48. package/dist/mmtp-fec-session.js +118 -13
  49. package/dist/mmtp-fec-session.js.map +3 -3
  50. package/dist/mmtp-router.d.ts +6 -0
  51. package/dist/mmtp-router.d.ts.map +1 -1
  52. package/dist/mmtp-router.js +150 -44
  53. package/dist/mmtp-router.js.map +2 -2
  54. package/dist/multicast-auth-catalog.js.map +1 -1
  55. package/dist/multicast-input.js.map +1 -1
  56. package/dist/multicast-manager.js +176 -48
  57. package/dist/multicast-manager.js.map +2 -2
  58. package/dist/multicast-transport.js +26 -16
  59. package/dist/multicast-transport.js.map +2 -2
  60. package/dist/multicast-unwrap.js +12 -19
  61. package/dist/multicast-unwrap.js.map +2 -2
  62. package/dist/no-modules.js +215 -42
  63. package/dist/no-modules.js.map +3 -3
  64. package/dist/repair-fetch.js.map +1 -1
  65. package/dist/repair-layer-subscriptions.d.ts +82 -0
  66. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  67. package/dist/repair-layer-subscriptions.js +192 -0
  68. package/dist/repair-layer-subscriptions.js.map +7 -0
  69. package/dist/signaling.js.map +1 -1
  70. package/dist/timestamp-codec.js +12 -19
  71. package/dist/timestamp-codec.js.map +2 -2
  72. package/dist/track-session.d.ts +1 -1
  73. package/dist/track-session.js.map +1 -1
  74. package/dist/wasm-mfu-reassembler.js.map +1 -1
  75. package/dist/wasm-mmtp-container-parser.js +97 -28
  76. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  77. package/package.json +8 -5
  78. package/schemas/catalog.schema.json +4 -0
  79. package/src/catalog-fec-params.ts +36 -1
  80. package/src/cmcd-producer.ts +189 -179
  81. package/src/fec-manager.ts +26 -4
  82. package/src/fec-worker-setup.ts +2 -2
  83. package/src/index.ts +18 -2
  84. package/src/iso-bmff-timing.ts +40 -1
  85. package/src/mmtp-container-parser.ts +92 -7
  86. package/src/mmtp-fec-session.ts +115 -15
  87. package/src/mmtp-router.ts +46 -0
  88. package/src/repair-layer-subscriptions.ts +261 -0
  89. package/src/track-session.ts +1 -1
@@ -171,30 +171,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
171
171
  const authLength = view2.getUint16(2, false);
172
172
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
173
173
  }
174
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
174
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
175
175
  if (declaredEnd > payload.length)
176
176
  return null;
177
177
  if (options?.skipFecId) {
178
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
178
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
179
179
  }
180
- const overlay = options?.overlayDeclared === true && rapFlag;
181
180
  if (fecType === 1) {
182
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
181
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
183
182
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
184
183
  return null;
185
184
  }
186
- return { hasFecId: true, hasOverlayFecId: overlay };
185
+ return { hasFecId: true };
187
186
  }
188
- if (fecType !== 0 || overlay)
187
+ if (fecType !== 0)
189
188
  return null;
190
189
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
191
- return { hasFecId: false, hasOverlayFecId: false };
190
+ return { hasFecId: false };
192
191
  }
193
192
  return null;
194
193
  }
195
- function sourceFecTrailerSize(overlay) {
196
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
197
- }
198
194
  function planMfuExtraction(mmtpPacket, options, parsedHeader) {
199
195
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
200
196
  return {
@@ -247,11 +243,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
247
243
  if ("rejection" in layoutResult)
248
244
  return layoutResult;
249
245
  const { layout } = layoutResult;
250
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
246
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
251
247
  if (!trailerLayout) {
252
248
  if (!options?.skipFecId && header.fecType === 1) {
253
249
  const observed = payload.length - layout.dataEnd;
254
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
250
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
255
251
  if (observed !== expected) {
256
252
  return {
257
253
  rejection: {
@@ -283,13 +279,10 @@ function extractPlannedMfu(mmtpPacket, result) {
283
279
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
284
280
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
285
281
  const data = payload.subarray(mfuOffset, dataEnd);
286
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
287
- const ssIdAt = (offset) => ({
288
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
289
- });
290
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
291
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
292
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
282
+ const fecPayloadId = trailerLayout.hasFecId ? {
283
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
284
+ } : null;
285
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
293
286
  }
294
287
  function sourceFecPayloadIdOf(mmtpPacket, header) {
295
288
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
@@ -2299,6 +2292,10 @@ var FECManager = class _FECManager {
2299
2292
  // ── Keyframe-protected repair policy (block-policy.ts) ──
2300
2293
  // One classifier per physical stream, built from that stream's geometry.
2301
2294
  this.#classifiers = /* @__PURE__ */ new Map();
2295
+ // A stream without catalog timing has no classifier, so the last RAP it
2296
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
2297
+ // a second RAP is released inside one MPU (BLO-52085).
2298
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
2302
2299
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
2303
2300
  // rungs can share a packetId with a different framerate or GOP, so the
2304
2301
  // packet-level geometry is only the geometry of a packet whose streams
@@ -2498,6 +2495,7 @@ var FECManager = class _FECManager {
2498
2495
  #orderedFrontiers;
2499
2496
  #orderedPendingBlocks;
2500
2497
  #classifiers;
2498
+ #noSpanRaps;
2501
2499
  #streamGeometries;
2502
2500
  #failedBlocks;
2503
2501
  #resolvedBlockClasses;
@@ -3159,6 +3157,7 @@ var FECManager = class _FECManager {
3159
3157
  };
3160
3158
  const discontinuity = () => {
3161
3159
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
3160
+ this.#noSpanRaps.delete(streamKey);
3162
3161
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
3163
3162
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
3164
3163
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -3571,6 +3570,9 @@ var FECManager = class _FECManager {
3571
3570
  }
3572
3571
  if (policyChanged) {
3573
3572
  this.#bumpEpoch(packetId);
3573
+ for (const key of [...this.#noSpanRaps.keys()]) {
3574
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
3575
+ }
3574
3576
  for (const [key, classifier] of [...this.#classifiers]) {
3575
3577
  if (!this.#belongsToPacket(key, packetId)) continue;
3576
3578
  const stream = this.#parseStreamKey(key);
@@ -3806,6 +3808,7 @@ var FECManager = class _FECManager {
3806
3808
  resetMap(this.#isoLocators);
3807
3809
  resetMap(this.#sourceAnchors);
3808
3810
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
3811
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
3809
3812
  this.#bumpEpoch(packetId);
3810
3813
  for (const [sbn, keys] of this.#blockKeysBySbn) {
3811
3814
  for (const key of keys.keys()) {
@@ -3912,6 +3915,7 @@ var FECManager = class _FECManager {
3912
3915
  dropMap(this.#isoLocators);
3913
3916
  dropMap(this.#sourceAnchors);
3914
3917
  dropMap(this.#classifiers);
3918
+ dropMap(this.#noSpanRaps);
3915
3919
  dropMap(this.#streamGeometries);
3916
3920
  dropSet(this.#pendingWorkerCleanups);
3917
3921
  dropSet(this.#completedBlocks);
@@ -4542,11 +4546,14 @@ var FECManager = class _FECManager {
4542
4546
  /**
4543
4547
  * Whether an MPU boundary still bounds this stream's RAPs (see
4544
4548
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
4545
- * it does not. A stream with no classifier yet has shown nothing against it.
4549
+ * it does not. A stream with no classifier yet has shown nothing against it,
4550
+ * and neither has a stream without catalog timing until it releases two
4551
+ * RAPs inside one MPU.
4546
4552
  */
4547
4553
  mpuBoundsRapsFor(packetId, catalogTrackName) {
4548
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
4549
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
4554
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4555
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
4556
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
4550
4557
  }
4551
4558
  /** A block's class now, or frozen at its resolution. */
4552
4559
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -4572,7 +4579,14 @@ var FECManager = class _FECManager {
4572
4579
  * invariant (consecutive MPUs exactly one keyframe interval apart).
4573
4580
  */
4574
4581
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
4575
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
4582
+ if (!this.#trackConfigs.has(packetId)) return;
4583
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
4584
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4585
+ const last = this.#noSpanRaps.get(streamKey);
4586
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
4587
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
4588
+ return;
4589
+ }
4576
4590
  this.#applyClassifierChanges(
4577
4591
  packetId,
4578
4592
  catalogTrackName,
@@ -5587,6 +5601,7 @@ var FECManager = class _FECManager {
5587
5601
  this.#blockAnchorsMs.clear();
5588
5602
  this.#blockCatalogTrackNames.clear();
5589
5603
  this.#classifiers.clear();
5604
+ this.#noSpanRaps.clear();
5590
5605
  this.#streamGeometries.clear();
5591
5606
  this.#failedBlocks.clear();
5592
5607
  this.#resolvedBlockClasses.clear();
@@ -7061,14 +7076,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
7061
7076
  });
7062
7077
 
7063
7078
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
7079
+ var MAX_PENDING_PACKETS = 256;
7064
7080
  var AMTGatewayManager = class {
7065
7081
  subscriptions = /* @__PURE__ */ new Map();
7066
7082
  messageHandler;
7083
+ messageListenerRegistered = false;
7067
7084
  pendingPackets = /* @__PURE__ */ new Map();
7068
7085
  constructor() {
7069
7086
  this.messageHandler = (event) => {
7070
7087
  const data = event.data;
7071
- console.log(`[AMT] SW message received:`, data.type);
7072
7088
  if (data.type === "UDP_PACKET") {
7073
7089
  this.handlePacket(data);
7074
7090
  } else if (data.type === "SOCKET_ERROR") {
@@ -7077,12 +7093,22 @@ var AMTGatewayManager = class {
7077
7093
  console.log(`[AMT SW Debug] ${data.message}`);
7078
7094
  }
7079
7095
  };
7080
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
7081
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
7082
- console.log("[AMT] Registered service worker message handler");
7083
- } else {
7084
- console.warn("[AMT] Service worker not available");
7096
+ }
7097
+ ensureMessageListener() {
7098
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
7099
+ return;
7100
+ }
7101
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
7102
+ this.messageListenerRegistered = true;
7103
+ console.log("[AMT] Registered service worker message handler");
7104
+ }
7105
+ removeMessageListener() {
7106
+ if (!this.messageListenerRegistered)
7107
+ return;
7108
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7109
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7085
7110
  }
7111
+ this.messageListenerRegistered = false;
7086
7112
  }
7087
7113
  /**
7088
7114
  * Subscribe via AMT tunnel
@@ -7092,6 +7118,7 @@ var AMTGatewayManager = class {
7092
7118
  if (config.protected) {
7093
7119
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
7094
7120
  }
7121
+ this.ensureMessageListener();
7095
7122
  const keyStr = this.getKeyString(key);
7096
7123
  if (this.subscriptions.has(keyStr)) {
7097
7124
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -7127,6 +7154,8 @@ var AMTGatewayManager = class {
7127
7154
  this.subscriptions.delete(keyStr);
7128
7155
  this.pendingPackets.delete(subscription.subscriptionId);
7129
7156
  }
7157
+ if (this.subscriptions.size === 0)
7158
+ this.removeMessageListener();
7130
7159
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
7131
7160
  }
7132
7161
  }
@@ -7148,6 +7177,8 @@ var AMTGatewayManager = class {
7148
7177
  } finally {
7149
7178
  this.subscriptions.delete(handle);
7150
7179
  this.pendingPackets.delete(subscription.subscriptionId);
7180
+ if (this.subscriptions.size === 0)
7181
+ this.removeMessageListener();
7151
7182
  }
7152
7183
  }
7153
7184
  /**
@@ -7164,14 +7195,12 @@ var AMTGatewayManager = class {
7164
7195
  if (!subscriptionId)
7165
7196
  return;
7166
7197
  const subscription = this.subscriptions.get(subscriptionId);
7167
- if (!subscription) {
7168
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
7198
+ if (!subscription)
7169
7199
  return;
7170
- }
7171
7200
  if (!subscription.handshakeComplete) {
7172
7201
  const queue = this.pendingPackets.get(subscriptionId);
7173
7202
  const packetData2 = data;
7174
- if (queue) {
7203
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
7175
7204
  queue.push({
7176
7205
  data: packetData2.data,
7177
7206
  remoteAddress: packetData2.remoteAddress,
@@ -7197,19 +7226,15 @@ var AMTGatewayManager = class {
7197
7226
  if (!subscriptionId)
7198
7227
  return;
7199
7228
  const subscription = this.subscriptions.get(subscriptionId);
7200
- if (!subscription) {
7201
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
7229
+ if (!subscription)
7202
7230
  return;
7203
- }
7204
7231
  subscription.config.onError(new Error(error || "Unknown error"));
7205
7232
  }
7206
7233
  /**
7207
7234
  * Cleanup on destroy
7208
7235
  */
7209
7236
  destroy() {
7210
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7211
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7212
- }
7237
+ this.removeMessageListener();
7213
7238
  const handles = Array.from(this.subscriptions.keys());
7214
7239
  for (const handle of handles) {
7215
7240
  this.unsubscribe(handle).catch((err) => {
@@ -13118,9 +13143,21 @@ var IsoBmffTimingResolver = class {
13118
13143
  }
13119
13144
  #track = null;
13120
13145
  #fragments = /* @__PURE__ */ new Map();
13146
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
13147
+ #firstSamples = /* @__PURE__ */ new Map();
13148
+ /**
13149
+ * First sample and sample count of the newest fragments whose every slot was
13150
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
13151
+ * matches the combiner, since it retires an abandoned sample as well.
13152
+ */
13153
+ #delivered = /* @__PURE__ */ new Map();
13121
13154
  #pendingSamples = 0;
13122
13155
  #evictedSamples = 0;
13123
13156
  #lastMovieFragmentRejection = null;
13157
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
13158
+ get initialized() {
13159
+ return this.#track !== null;
13160
+ }
13124
13161
  get pendingSampleCount() {
13125
13162
  return this.#pendingSamples;
13126
13163
  }
@@ -13144,7 +13181,14 @@ var IsoBmffTimingResolver = class {
13144
13181
  this.#track = timing;
13145
13182
  return true;
13146
13183
  }
13147
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
13184
+ /**
13185
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
13186
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
13187
+ * fragment whose samples are all done is a duplicate: admitting it again
13188
+ * would charge stale timing and could displace the live MPU's. A source
13189
+ * copy is still admitted again (a rendition switch re-arms timing that way).
13190
+ */
13191
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
13148
13192
  this.#lastMovieFragmentRejection = null;
13149
13193
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
13150
13194
  this.#lastMovieFragmentRejection = "invalid";
@@ -13170,6 +13214,8 @@ var IsoBmffTimingResolver = class {
13170
13214
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
13171
13215
  );
13172
13216
  }
13217
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
13218
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
13173
13219
  let freed = 0;
13174
13220
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
13175
13221
  freed += this.#evictOldestFragment();
@@ -13183,6 +13229,7 @@ var IsoBmffTimingResolver = class {
13183
13229
  return false;
13184
13230
  }
13185
13231
  this.#fragments.set(key, [...parsed.samples]);
13232
+ this.#firstSamples.set(key, parsed.samples[0]);
13186
13233
  this.#pendingSamples += parsed.samples.length;
13187
13234
  return true;
13188
13235
  }
@@ -13240,6 +13287,7 @@ var IsoBmffTimingResolver = class {
13240
13287
  let live = 0;
13241
13288
  for (const sample of samples) if (sample !== null) live++;
13242
13289
  this.#fragments.delete(oldest);
13290
+ this.#firstSamples.delete(oldest);
13243
13291
  this.#pendingSamples -= live;
13244
13292
  this.#evictedSamples += live;
13245
13293
  return live;
@@ -13255,12 +13303,23 @@ var IsoBmffTimingResolver = class {
13255
13303
  if (!samples || !samples[index]) return false;
13256
13304
  samples[index] = null;
13257
13305
  this.#pendingSamples--;
13258
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
13306
+ if (samples.every((value) => value === null)) {
13307
+ this.#fragments.delete(key);
13308
+ const first = this.#firstSamples.get(key);
13309
+ this.#firstSamples.delete(key);
13310
+ this.#delivered.delete(key);
13311
+ this.#delivered.set(key, { first, sampleCount: samples.length });
13312
+ if (this.#delivered.size > this.maxPendingSamples) {
13313
+ this.#delivered.delete(this.#delivered.keys().next().value);
13314
+ }
13315
+ }
13259
13316
  return true;
13260
13317
  }
13261
13318
  reset() {
13262
13319
  this.#track = null;
13263
13320
  this.#fragments.clear();
13321
+ this.#firstSamples.clear();
13322
+ this.#delivered.clear();
13264
13323
  this.#pendingSamples = 0;
13265
13324
  }
13266
13325
  };
@@ -13742,6 +13801,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13742
13801
  this.#reclaimedTimingSamples = 0;
13743
13802
  this.#evictedTimingSamples = 0;
13744
13803
  this.#rejectedMovieFragments = 0;
13804
+ this.#recoveredHeadsAwaitingTiming = {
13805
+ held: 0,
13806
+ expired: 0,
13807
+ discarded: 0,
13808
+ superseded: 0
13809
+ };
13745
13810
  this.#sequencers = /* @__PURE__ */ new Map();
13746
13811
  this.#fecGate = null;
13747
13812
  this.#fecSequencer = null;
@@ -13850,6 +13915,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
13850
13915
  #reclaimedTimingSamples;
13851
13916
  #evictedTimingSamples;
13852
13917
  #rejectedMovieFragments;
13918
+ #recoveredHeadsAwaitingTiming;
13853
13919
  #sequencers;
13854
13920
  #fecGate;
13855
13921
  #fecSequencer;
@@ -13970,6 +14036,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
13970
14036
  rejectedMovieFragments: this.#rejectedMovieFragments
13971
14037
  };
13972
14038
  }
14039
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
14040
+ get recoveredHeadsAwaitingTiming() {
14041
+ let pending = 0;
14042
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14043
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
14044
+ }
14045
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
14046
+ }
13973
14047
  get reorderStats() {
13974
14048
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
13975
14049
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -14143,7 +14217,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14143
14217
  const now = Date.now();
14144
14218
  for (const { awaitingTiming } of this.#timingStreams.values()) {
14145
14219
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
14146
- awaitingTiming.shift();
14220
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14147
14221
  }
14148
14222
  }
14149
14223
  }
@@ -14261,11 +14335,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
14261
14335
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
14262
14336
  return this.#drop("fragment-unexpected-fragmentation");
14263
14337
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
14264
- if (!resolver2) return this.#drop("fragment-no-resolver");
14338
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
14265
14339
  const pendingBefore = resolver2.pendingSampleCount;
14266
14340
  const evictedBefore = resolver2.evictedSampleCount;
14267
14341
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
14268
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
14342
+ const admitted = resolver2.addMovieFragment(
14343
+ mpu.mpuSequenceNumber,
14344
+ data,
14345
+ remaining,
14346
+ options?.recovered === true
14347
+ );
14269
14348
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
14270
14349
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
14271
14350
  if (!admitted) {
@@ -14285,7 +14364,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14285
14364
  };
14286
14365
  }
14287
14366
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
14288
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
14367
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
14289
14368
  if (!resolver) return this.#drop("mfu-no-resolver");
14290
14369
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
14291
14370
  return this.#drop("mfu-empty-payload");
@@ -14306,6 +14385,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14306
14385
  mpuSequenceNumber: mpu.mpuSequenceNumber,
14307
14386
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
14308
14387
  sampleNumber: mfuDuHeader.sampleNumber,
14388
+ sequenceNumber: info.sequenceNumber,
14309
14389
  arrivedAt: Date.now()
14310
14390
  });
14311
14391
  return this.#drop("mfu-sample-timing-missing");
@@ -14629,6 +14709,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
14629
14709
  }
14630
14710
  this.#emitFrame(data, meta);
14631
14711
  }
14712
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
14713
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
14714
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
14715
+ }
14632
14716
  /**
14633
14717
  * Claim and return the authored timing for a recovered sample. Recovery does
14634
14718
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -14690,6 +14774,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
14690
14774
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
14691
14775
  this.#sequencers.clear();
14692
14776
  this.#fecSequencer?.discard();
14777
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14778
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14779
+ }
14693
14780
  this.#timingStreams.clear();
14694
14781
  this.#pendingTimingSamples = 0;
14695
14782
  this.#reliableMoqStartupGate.clear();
@@ -14708,10 +14795,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
14708
14795
  return resolver;
14709
14796
  }
14710
14797
  #awaitTiming(streamKey, head) {
14711
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
14712
- if (!awaitingTiming) return;
14798
+ const state = this.#timingStreams.get(streamKey);
14799
+ if (!state) return;
14800
+ if (!head.options?.recovered) {
14801
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
14802
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
14803
+ state.awaitingTiming = kept;
14804
+ }
14805
+ const awaitingTiming = state.awaitingTiming;
14713
14806
  awaitingTiming.push(head);
14714
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
14807
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
14808
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
14809
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14810
+ }
14811
+ }
14812
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
14813
+ #leaveAwaitingTiming(head, outcome) {
14814
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
14715
14815
  }
14716
14816
  /**
14717
14817
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -14739,6 +14839,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14739
14839
  const state = this.#timingStreams.get(streamKey);
14740
14840
  if (!state) return;
14741
14841
  this.#timingStreams.delete(streamKey);
14842
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14742
14843
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
14743
14844
  if (this.#pendingTimingSamples < 0) {
14744
14845
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -18743,6 +18844,10 @@ var MmtpRouter = class {
18743
18844
  get keyframeOverlayStats() {
18744
18845
  return { ...this.#overlayStats };
18745
18846
  }
18847
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
18848
+ get recoveredHeadsAwaitingTiming() {
18849
+ return this.#parser.recoveredHeadsAwaitingTiming;
18850
+ }
18746
18851
  #isOverlayRepair(repairPacketId, repairTrackName) {
18747
18852
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
18748
18853
  }
@@ -21152,6 +21257,29 @@ var MmtpRouter = class {
21152
21257
  }
21153
21258
  );
21154
21259
  }
21260
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
21261
+ ownerPacketId,
21262
+ sym.fragment.mpuSequenceNumber,
21263
+ sym.fragment.movieFragmentSequenceNumber,
21264
+ sym.fragment.sampleNumber,
21265
+ catalogTrackName
21266
+ );
21267
+ for (const sym of result.symbols) {
21268
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
21269
+ this.#feedViaParser(
21270
+ sym.rawSymbol,
21271
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
21272
+ void 0,
21273
+ {
21274
+ skipFecPayloadId: true,
21275
+ recovered: true,
21276
+ deliveryProvenance,
21277
+ ...fecSbnOf(sym.index),
21278
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
21279
+ }
21280
+ );
21281
+ }
21282
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
21155
21283
  const gated = this.#isFecTrack(ownerPacketId);
21156
21284
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
21157
21285
  if (assembler) {