@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00

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 (105) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1949 -374
  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 +65 -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 +70 -12
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +403 -58
  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 +6 -5
  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.d.ts +48 -13
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +97 -25
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +79 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +219 -45
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +342 -63
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +218 -45
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +43 -9
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +2006 -95
  56. package/dist/mmtp-router.js.map +4 -4
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.d.ts +28 -5
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +16116 -15005
  62. package/dist/multicast-manager.js.map +4 -4
  63. package/dist/multicast-transport.d.ts +11 -3
  64. package/dist/multicast-transport.d.ts.map +1 -1
  65. package/dist/multicast-transport.js +281 -35
  66. package/dist/multicast-transport.js.map +4 -4
  67. package/dist/multicast-unwrap.js +12 -19
  68. package/dist/multicast-unwrap.js.map +2 -2
  69. package/dist/no-modules.js +336 -59
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.d.ts +36 -8
  72. package/dist/repair-fetch.d.ts.map +1 -1
  73. package/dist/repair-fetch.js +285 -26
  74. package/dist/repair-fetch.js.map +3 -3
  75. package/dist/repair-layer-subscriptions.d.ts +82 -0
  76. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  77. package/dist/repair-layer-subscriptions.js +192 -0
  78. package/dist/repair-layer-subscriptions.js.map +7 -0
  79. package/dist/signaling.js.map +2 -2
  80. package/dist/timestamp-codec.js +12 -19
  81. package/dist/timestamp-codec.js.map +2 -2
  82. package/dist/track-session.d.ts +1 -1
  83. package/dist/track-session.d.ts.map +1 -1
  84. package/dist/track-session.js +1 -0
  85. package/dist/track-session.js.map +2 -2
  86. package/dist/wasm-mfu-reassembler.js.map +1 -1
  87. package/dist/wasm-mmtp-container-parser.js +218 -45
  88. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  89. package/package.json +8 -5
  90. package/schemas/catalog.schema.json +4 -0
  91. package/src/catalog-fec-params.ts +36 -1
  92. package/src/cmcd-producer.ts +194 -179
  93. package/src/fec-manager.ts +433 -32
  94. package/src/fec-worker-setup.ts +2 -2
  95. package/src/index.ts +20 -2
  96. package/src/iso-block-locator.ts +133 -31
  97. package/src/iso-bmff-timing.ts +40 -1
  98. package/src/mmtp-container-parser.ts +306 -29
  99. package/src/mmtp-fec-session.ts +115 -15
  100. package/src/mmtp-router.ts +629 -54
  101. package/src/multicast-manager.ts +70 -11
  102. package/src/multicast-transport.ts +44 -20
  103. package/src/repair-fetch.ts +363 -49
  104. package/src/repair-layer-subscriptions.ts +261 -0
  105. package/src/track-session.ts +4 -1
@@ -489,6 +489,8 @@ var PACKET_TYPE_SIGNALING = 2;
489
489
  var PACKET_TYPE_REPAIR = 3;
490
490
  var MMTP_SIGNALING_PACKET_ID = 0;
491
491
  var MMTP_PACKET_ID_MAX = 65535;
492
+ var FEC_CODE_REED_SOLOMON = 1;
493
+ var FEC_CODE_RAPTORQ = 3;
492
494
  function parseMMTPHeader(buffer) {
493
495
  if (buffer.length < MMTP_HEADER_SIZE) {
494
496
  throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
@@ -655,30 +657,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
655
657
  const authLength = view2.getUint16(2, false);
656
658
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
657
659
  }
658
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
660
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
659
661
  if (declaredEnd > payload.length)
660
662
  return null;
661
663
  if (options?.skipFecId) {
662
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
664
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
663
665
  }
664
- const overlay = options?.overlayDeclared === true && rapFlag;
665
666
  if (fecType === 1) {
666
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
667
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
667
668
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
668
669
  return null;
669
670
  }
670
- return { hasFecId: true, hasOverlayFecId: overlay };
671
+ return { hasFecId: true };
671
672
  }
672
- if (fecType !== 0 || overlay)
673
+ if (fecType !== 0)
673
674
  return null;
674
675
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
675
- return { hasFecId: false, hasOverlayFecId: false };
676
+ return { hasFecId: false };
676
677
  }
677
678
  return null;
678
679
  }
679
- function sourceFecTrailerSize(overlay) {
680
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
681
- }
682
680
  function extractNALUnit(mmtpPacket) {
683
681
  const extraction = extractMFUPayload(mmtpPacket);
684
682
  if (!extraction)
@@ -737,11 +735,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
737
735
  if ("rejection" in layoutResult)
738
736
  return layoutResult;
739
737
  const { layout } = layoutResult;
740
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
738
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
741
739
  if (!trailerLayout) {
742
740
  if (!options?.skipFecId && header.fecType === 1) {
743
741
  const observed = payload.length - layout.dataEnd;
744
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
742
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
745
743
  if (observed !== expected) {
746
744
  return {
747
745
  rejection: {
@@ -776,13 +774,10 @@ function extractPlannedMfu(mmtpPacket, result) {
776
774
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
777
775
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
778
776
  const data = payload.subarray(mfuOffset, dataEnd);
779
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
780
- const ssIdAt = (offset) => ({
781
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
782
- });
783
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
784
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
785
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
777
+ const fecPayloadId = trailerLayout.hasFecId ? {
778
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
779
+ } : null;
780
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
786
781
  }
787
782
  function extractTimestamp(mmtpPacket) {
788
783
  const { header } = unwrapMMTP(mmtpPacket);
@@ -856,6 +851,17 @@ function readRepairFecPayloadIdFields(mmtpPacket, parsedHeader) {
856
851
  dataOffset: off + REPAIR_FEC_PID_SIZE
857
852
  };
858
853
  }
854
+ function extractRepairFecPayloadId(mmtpPacket) {
855
+ const fields = readRepairFecPayloadIdFields(mmtpPacket);
856
+ if (!fields)
857
+ return null;
858
+ return {
859
+ ssStart: fields.ssStart,
860
+ rsbLength: fields.rsbLength,
861
+ rsId: fields.rsId,
862
+ ssbLength: fields.ssbLength
863
+ };
864
+ }
859
865
  function parseMoqRepairFrame(frame) {
860
866
  return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame));
861
867
  }
@@ -903,6 +909,50 @@ var NTP_SHORT_BITS = 32;
903
909
  var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
904
910
  var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
905
911
 
912
+ // ../mmt-base/dist/signalling.js
913
+ var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
914
+ var SSBG_MODE_1 = 1;
915
+ var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
916
+ var REPAIR_SYMBOL_ID_MAX = 16777215;
917
+ var MIN_REPAIR_SYMBOL_SIZE = 14;
918
+ var REED_SOLOMON_MAX_BLOCK = 255;
919
+ function alFecRaptorQFlowConfig(flow) {
920
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
921
+ return null;
922
+ if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
923
+ return null;
924
+ if (flow.packetIds.length !== 1)
925
+ return null;
926
+ const repair = flow.repairFlows[0];
927
+ const { fecCodeId, maximumK: k, maximumP: p } = repair;
928
+ const t = flow.repairSymbolSize;
929
+ if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
930
+ return null;
931
+ if (fecCodeId === FEC_CODE_RAPTORQ) {
932
+ if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
933
+ return null;
934
+ } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
935
+ if (k + p > REED_SOLOMON_MAX_BLOCK)
936
+ return null;
937
+ } else {
938
+ return null;
939
+ }
940
+ if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
941
+ return null;
942
+ return {
943
+ sourcePacketIds: [...flow.packetIds],
944
+ repairFlowId: repair.repairFlowId,
945
+ fecCodeId,
946
+ ssbgMode: flow.ssbgMode,
947
+ fecPayloadIdMode: flow.fecPayloadIdMode,
948
+ repairSymbolSize: t,
949
+ maximumK: k,
950
+ maximumP: p,
951
+ protectionWindowTime: repair.protectionWindowTime,
952
+ protectionWindowSize: repair.protectionWindowSize
953
+ };
954
+ }
955
+
906
956
  // ../mmt-base/dist/serial-number.js
907
957
  var SERIAL32_SPAN = 4294967296;
908
958
  var SERIAL32_HALF = 2147483648;
@@ -7457,11 +7507,33 @@ var AdaptiveBitrateController = class {
7457
7507
  };
7458
7508
 
7459
7509
  // node_modules/@blockcast/mmt-base/dist/mmtp.js
7510
+ var MMTP_HEADER_SIZE2 = 12;
7460
7511
  var MPU_HEADER_SIZE2 = 8;
7461
7512
  var MPU_LENGTH_FIELD_SIZE2 = 2;
7462
7513
  var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
7463
7514
  var MMTP_SIGNALING_PACKET_ID2 = 0;
7464
7515
  var MMTP_PACKET_ID_MAX2 = 65535;
7516
+ function readMMTPPacketId2(buffer) {
7517
+ if (buffer.length < MMTP_HEADER_SIZE2)
7518
+ return null;
7519
+ const flags = buffer[0];
7520
+ if ((flags >> 6 & 3) !== 0)
7521
+ return null;
7522
+ let offset = MMTP_HEADER_SIZE2;
7523
+ if ((flags & 32) !== 0) {
7524
+ if (offset + 4 > buffer.length)
7525
+ return null;
7526
+ offset += 4;
7527
+ }
7528
+ if ((flags & 2) !== 0) {
7529
+ if (offset + 4 > buffer.length)
7530
+ return null;
7531
+ const length = buffer[offset + 2] << 8 | buffer[offset + 3];
7532
+ if (offset + 4 + length > buffer.length)
7533
+ return null;
7534
+ }
7535
+ return buffer[2] << 8 | buffer[3];
7536
+ }
7465
7537
 
7466
7538
  // node_modules/@blockcast/mmt-base/dist/net-constants.js
7467
7539
  var DEFAULT_AMT_RELAY_PORT2 = 2268;
@@ -7927,8 +7999,8 @@ var MulticastSubscriptionPool = class {
7927
7999
  record.claims--;
7928
8000
  return this.acquire(gateway, config, consumer);
7929
8001
  }
7930
- const token = Symbol(key);
7931
- entry.consumers.set(token, consumer);
8002
+ const token2 = Symbol(key);
8003
+ entry.consumers.set(token2, consumer);
7932
8004
  let released = false;
7933
8005
  return {
7934
8006
  key,
@@ -7936,7 +8008,7 @@ var MulticastSubscriptionPool = class {
7936
8008
  if (released)
7937
8009
  return;
7938
8010
  released = true;
7939
- entry.consumers.delete(token);
8011
+ entry.consumers.delete(token2);
7940
8012
  record.claims--;
7941
8013
  if (record.claims > 0)
7942
8014
  return;
@@ -8447,6 +8519,205 @@ var TransportManager = class {
8447
8519
  };
8448
8520
  var transportManager = new TransportManager();
8449
8521
 
8522
+ // node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
8523
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
8524
+ var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
8525
+ var INTEGRITY_SCHEME = "manifest-ed25519-2";
8526
+ var MulticastError = class extends Error {
8527
+ code;
8528
+ constructor(code, message, cause) {
8529
+ super(message, cause === void 0 ? void 0 : { cause });
8530
+ this.name = "MulticastError";
8531
+ this.code = code;
8532
+ }
8533
+ };
8534
+ function datagramsOf(chunk) {
8535
+ const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
8536
+ const out = [];
8537
+ for (const d of list) {
8538
+ const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
8539
+ if (data instanceof Uint8Array)
8540
+ out.push(data);
8541
+ else if (data instanceof ArrayBuffer)
8542
+ out.push(new Uint8Array(data));
8543
+ }
8544
+ return out;
8545
+ }
8546
+ function reasonOf(value) {
8547
+ return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
8548
+ }
8549
+ function registry() {
8550
+ const g = globalThis;
8551
+ if (g[REGISTRY] === void 0) {
8552
+ const providers = /* @__PURE__ */ new WeakSet();
8553
+ const byScope = /* @__PURE__ */ new WeakMap();
8554
+ Object.defineProperty(g, REGISTRY, {
8555
+ value: Object.freeze({
8556
+ isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
8557
+ providerFor: (scope) => {
8558
+ let provider = byScope.get(scope);
8559
+ if (provider === void 0) {
8560
+ provider = createProvider(scope);
8561
+ providers.add(provider);
8562
+ byScope.set(scope, provider);
8563
+ }
8564
+ return provider;
8565
+ }
8566
+ }),
8567
+ writable: false,
8568
+ configurable: false,
8569
+ enumerable: false
8570
+ });
8571
+ }
8572
+ return g[REGISTRY];
8573
+ }
8574
+ function resolveMulticastGateway(scope = globalThis) {
8575
+ const s = scope;
8576
+ if (typeof s.MulticastReceiver === "function")
8577
+ return registry().providerFor(scope);
8578
+ if (typeof s.multicast?.subscribe === "function")
8579
+ return s.multicast;
8580
+ return void 0;
8581
+ }
8582
+ function isUserAgentVerifiedGateway(gateway) {
8583
+ return registry().isProvider(gateway);
8584
+ }
8585
+ function userAgentVerifiedIngress(ingress, gateway) {
8586
+ if (isUserAgentVerifiedGateway(gateway))
8587
+ return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
8588
+ if (!("admittedBy" in ingress))
8589
+ return ingress;
8590
+ const { admittedBy: _, ...rest } = ingress;
8591
+ return rest;
8592
+ }
8593
+ function unsupported(message, cause) {
8594
+ return new MulticastError("capability-unsupported", message, cause);
8595
+ }
8596
+ function createProvider(scope) {
8597
+ const Receiver = scope.MulticastReceiver;
8598
+ let gate = null;
8599
+ return {
8600
+ capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
8601
+ subscribe: async (config) => {
8602
+ const c = config;
8603
+ if (c.consume !== "bytes")
8604
+ throw unsupported(`consume mode ${String(c.consume)} is not supported`);
8605
+ const origin = c.origin ?? "ip-multicast";
8606
+ if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
8607
+ throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
8608
+ }
8609
+ if (c.relay !== void 0)
8610
+ throw unsupported("MulticastReceiver picks its own AMT relay");
8611
+ if (!c.source)
8612
+ throw unsupported("MulticastReceiver needs an SSM source");
8613
+ if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
8614
+ throw unsupported("MulticastReceiver filters by source only");
8615
+ }
8616
+ const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
8617
+ if (gate !== null)
8618
+ await gate;
8619
+ let rx;
8620
+ try {
8621
+ rx = new Receiver(options);
8622
+ } catch (error) {
8623
+ throw unsupported("MulticastReceiver refused the request", error);
8624
+ }
8625
+ const opened = rx.opened;
8626
+ if (gate === null)
8627
+ gate = opened.then(() => void 0, () => void 0);
8628
+ return openSubscription(rx, opened, options, c.packetIds);
8629
+ }
8630
+ };
8631
+ }
8632
+ async function openSubscription(rx, openedPromise, options, packetIds) {
8633
+ let opened;
8634
+ try {
8635
+ opened = await openedPromise;
8636
+ } catch (error) {
8637
+ throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8638
+ }
8639
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
8640
+ await rx.close();
8641
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8642
+ }
8643
+ const events = new EventTarget();
8644
+ let state = "connected";
8645
+ let filter = packetIds?.length ? new Set(packetIds) : null;
8646
+ let closing = false;
8647
+ const setState = (next) => {
8648
+ if (state === next)
8649
+ return;
8650
+ state = next;
8651
+ events.dispatchEvent(new CustomEvent("state", { detail: next }));
8652
+ };
8653
+ const sourceAddress = options.sourceAddress;
8654
+ const subscription = {
8655
+ id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
8656
+ get state() {
8657
+ return state;
8658
+ },
8659
+ delivery: "datagram",
8660
+ eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
8661
+ setPacketIds(ids) {
8662
+ filter = ids.length ? new Set(ids) : null;
8663
+ },
8664
+ addEventListener: events.addEventListener.bind(events),
8665
+ removeEventListener: events.removeEventListener.bind(events),
8666
+ async unsubscribe() {
8667
+ closing = true;
8668
+ await rx.close();
8669
+ setState("closed");
8670
+ }
8671
+ };
8672
+ void (async () => {
8673
+ const reader = opened.readable.getReader();
8674
+ try {
8675
+ for (; ; ) {
8676
+ const { value, done } = await reader.read();
8677
+ if (done)
8678
+ break;
8679
+ const packets = [];
8680
+ for (const data of datagramsOf(value)) {
8681
+ if (filter !== null) {
8682
+ const id = readMMTPPacketId2(data);
8683
+ if (id === null || !filter.has(id))
8684
+ continue;
8685
+ }
8686
+ const copy = data.slice().buffer;
8687
+ packets.push(copy);
8688
+ events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
8689
+ }
8690
+ if (packets.length > 0)
8691
+ events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
8692
+ }
8693
+ } catch {
8694
+ }
8695
+ })();
8696
+ void rx.closed.then((value) => {
8697
+ const reason = reasonOf(value);
8698
+ if (!closing) {
8699
+ if (reason === "consent-withdrawn") {
8700
+ events.dispatchEvent(new CustomEvent("error", {
8701
+ detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
8702
+ }));
8703
+ } else if (reason === "oversubscribed" || reason === "network") {
8704
+ events.dispatchEvent(new CustomEvent("error", {
8705
+ detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
8706
+ }));
8707
+ }
8708
+ }
8709
+ setState("closed");
8710
+ }, (error) => {
8711
+ if (!closing) {
8712
+ events.dispatchEvent(new CustomEvent("error", {
8713
+ detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
8714
+ }));
8715
+ }
8716
+ setState("closed");
8717
+ });
8718
+ return subscription;
8719
+ }
8720
+
8450
8721
  // node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
8451
8722
  var MAX_MOQT_PRIORITY = 255;
8452
8723
  function moqtPriority(value, field) {
@@ -8486,14 +8757,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8486
8757
  });
8487
8758
 
8488
8759
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8760
+ var MAX_PENDING_PACKETS = 256;
8489
8761
  var AMTGatewayManager = class {
8490
8762
  subscriptions = /* @__PURE__ */ new Map();
8491
8763
  messageHandler;
8764
+ messageListenerRegistered = false;
8492
8765
  pendingPackets = /* @__PURE__ */ new Map();
8493
8766
  constructor() {
8494
8767
  this.messageHandler = (event) => {
8495
8768
  const data = event.data;
8496
- console.log(`[AMT] SW message received:`, data.type);
8497
8769
  if (data.type === "UDP_PACKET") {
8498
8770
  this.handlePacket(data);
8499
8771
  } else if (data.type === "SOCKET_ERROR") {
@@ -8502,12 +8774,22 @@ var AMTGatewayManager = class {
8502
8774
  console.log(`[AMT SW Debug] ${data.message}`);
8503
8775
  }
8504
8776
  };
8505
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
8506
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
8507
- console.log("[AMT] Registered service worker message handler");
8508
- } else {
8509
- console.warn("[AMT] Service worker not available");
8777
+ }
8778
+ ensureMessageListener() {
8779
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8780
+ return;
8781
+ }
8782
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8783
+ this.messageListenerRegistered = true;
8784
+ console.log("[AMT] Registered service worker message handler");
8785
+ }
8786
+ removeMessageListener() {
8787
+ if (!this.messageListenerRegistered)
8788
+ return;
8789
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8790
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8510
8791
  }
8792
+ this.messageListenerRegistered = false;
8511
8793
  }
8512
8794
  /**
8513
8795
  * Subscribe via AMT tunnel
@@ -8517,6 +8799,7 @@ var AMTGatewayManager = class {
8517
8799
  if (config.protected) {
8518
8800
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8519
8801
  }
8802
+ this.ensureMessageListener();
8520
8803
  const keyStr = this.getKeyString(key);
8521
8804
  if (this.subscriptions.has(keyStr)) {
8522
8805
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8552,6 +8835,8 @@ var AMTGatewayManager = class {
8552
8835
  this.subscriptions.delete(keyStr);
8553
8836
  this.pendingPackets.delete(subscription.subscriptionId);
8554
8837
  }
8838
+ if (this.subscriptions.size === 0)
8839
+ this.removeMessageListener();
8555
8840
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8556
8841
  }
8557
8842
  }
@@ -8573,6 +8858,8 @@ var AMTGatewayManager = class {
8573
8858
  } finally {
8574
8859
  this.subscriptions.delete(handle);
8575
8860
  this.pendingPackets.delete(subscription.subscriptionId);
8861
+ if (this.subscriptions.size === 0)
8862
+ this.removeMessageListener();
8576
8863
  }
8577
8864
  }
8578
8865
  /**
@@ -8589,14 +8876,12 @@ var AMTGatewayManager = class {
8589
8876
  if (!subscriptionId)
8590
8877
  return;
8591
8878
  const subscription = this.subscriptions.get(subscriptionId);
8592
- if (!subscription) {
8593
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8879
+ if (!subscription)
8594
8880
  return;
8595
- }
8596
8881
  if (!subscription.handshakeComplete) {
8597
8882
  const queue = this.pendingPackets.get(subscriptionId);
8598
8883
  const packetData2 = data;
8599
- if (queue) {
8884
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8600
8885
  queue.push({
8601
8886
  data: packetData2.data,
8602
8887
  remoteAddress: packetData2.remoteAddress,
@@ -8622,19 +8907,15 @@ var AMTGatewayManager = class {
8622
8907
  if (!subscriptionId)
8623
8908
  return;
8624
8909
  const subscription = this.subscriptions.get(subscriptionId);
8625
- if (!subscription) {
8626
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8910
+ if (!subscription)
8627
8911
  return;
8628
- }
8629
8912
  subscription.config.onError(new Error(error || "Unknown error"));
8630
8913
  }
8631
8914
  /**
8632
8915
  * Cleanup on destroy
8633
8916
  */
8634
8917
  destroy() {
8635
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8636
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8637
- }
8918
+ this.removeMessageListener();
8638
8919
  const handles = Array.from(this.subscriptions.keys());
8639
8920
  for (const handle of handles) {
8640
8921
  this.unsubscribe(handle).catch((err) => {
@@ -8733,9 +9014,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
8733
9014
  }
8734
9015
  function validateMmtpDashTemplate(template, path) {
8735
9016
  const variables = [...template.matchAll(TEMPLATE_VARIABLE)].map((match) => match[1]);
8736
- const unsupported = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
8737
- if (unsupported !== void 0) {
8738
- throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported}$`);
9017
+ const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
9018
+ if (unsupported2 !== void 0) {
9019
+ throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
8739
9020
  }
8740
9021
  if (variables.filter((variable) => variable === "Number").length !== 1) {
8741
9022
  throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
@@ -12165,6 +12446,7 @@ var TrackSession = class {
12165
12446
  this.#state = "idle";
12166
12447
  this.#subscription = null;
12167
12448
  this.#disarmFec();
12449
+ this.#unsuppress();
12168
12450
  }
12169
12451
  // -----------------------------------------------------------------------
12170
12452
  // Internal
@@ -12658,6 +12940,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
12658
12940
  this.#heldEvicted = 0;
12659
12941
  /** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
12660
12942
  this.#holes = [];
12943
+ /** Holes the horizon passed while pending: sources held until closed. */
12944
+ this.#expired = [];
12945
+ /** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
12946
+ this.#newlyExpired = [];
12947
+ /**
12948
+ * Spans closed from expired holes: behind the horizon, so kept only for
12949
+ * `locate` while their sources are fed, until the next retirement.
12950
+ */
12951
+ this.#closedBehind = [];
12661
12952
  /**
12662
12953
  * Keep a gap that is missing a source as a pending hole rather than a span,
12663
12954
  * so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
@@ -12679,7 +12970,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
12679
12970
  #retentionBlocks;
12680
12971
  #heldEvicted;
12681
12972
  #newest;
12973
+ #newestSource;
12682
12974
  #holes;
12975
+ #expired;
12976
+ #newlyExpired;
12977
+ #closedBehind;
12683
12978
  static #checkedRetention(retentionBlocks) {
12684
12979
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
12685
12980
  throw new Error(
@@ -12716,6 +13011,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12716
13011
  observe(position) {
12717
13012
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12718
13013
  }
13014
+ /**
13015
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
13016
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
13017
+ * a source cannot run ahead of the sources sent before it except by
13018
+ * reordering.
13019
+ */
13020
+ get newestSource() {
13021
+ return this.#newestSource;
13022
+ }
13023
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13024
+ noteSource(ssId) {
13025
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13026
+ this.#newestSource = ssId;
13027
+ return true;
13028
+ }
12719
13029
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12720
13030
  get heldEvicted() {
12721
13031
  return this.#heldEvicted;
@@ -12734,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
12734
13044
  }
12735
13045
  /** The known block or span containing `ssId`, with its ESI. */
12736
13046
  locate(ssId) {
12737
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
12738
- const block = this.#blocks[i];
12739
- if (isoBlockContains(block.start, block.length, ssId)) {
12740
- return { block, esi: serial32Distance(block.start, ssId) };
13047
+ for (const blocks of [this.#blocks, this.#closedBehind]) {
13048
+ for (let i = blocks.length - 1; i >= 0; i--) {
13049
+ const block = blocks[i];
13050
+ if (isoBlockContains(block.start, block.length, ssId)) {
13051
+ return { block, esi: serial32Distance(block.start, ssId) };
13052
+ }
12741
13053
  }
12742
13054
  }
12743
13055
  return null;
12744
13056
  }
12745
- /** Number of pending holes. */
13057
+ /** Number of holes not closed yet, pending or past the horizon. */
12746
13058
  get holeCount() {
12747
- return this.#holes.length;
13059
+ return this.#holes.length + this.#expired.length;
13060
+ }
13061
+ /**
13062
+ * The pending holes the horizon has passed since the last call, in the
13063
+ * order it passed them. Each is no longer pending: no repair states a block
13064
+ * in it (that repair is `stale`, as for a span the horizon retired) and the
13065
+ * release gate does not wait for it. Its owner must close it
13066
+ * (`closeHolesWithin`), which turns it into a span that is not kept here.
13067
+ */
13068
+ takeExpiredHoles() {
13069
+ return this.#newlyExpired.splice(0);
12748
13070
  }
12749
13071
  /** The stated block (never a span) ending exactly at `end`. */
12750
13072
  blockEndingAt(end) {
@@ -12800,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
12800
13122
  /**
12801
13123
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
12802
13124
  * block states its range and places every held source in it; later repair
12803
- * symbols of the same block change nothing. A range that contradicts a
12804
- * known block (a different length, or an overlap with another block or a
12805
- * span) fails closed: the repair symbol is dropped and no block changes.
13125
+ * symbols of the same block change nothing. A repair whose range is exactly
13126
+ * a repair-less span states that span's one block. A range that contradicts
13127
+ * a known block (a different length, or an overlap with another block or
13128
+ * with part of a span) fails closed: the repair symbol is dropped and no
13129
+ * block changes.
12806
13130
  * A newly stated block retires behind its SS_Start (see the class note).
12807
13131
  * A block so far past `newest` that stating it would retire the position
12808
13132
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -12821,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
12821
13145
  const known = this.blockAt(start);
12822
13146
  if (known !== void 0) {
12823
13147
  if (known.span) {
12824
- return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
13148
+ if (known.length === length) {
13149
+ const block2 = { start, length, span: false };
13150
+ this.#blocks[this.#blocks.indexOf(known)] = block2;
13151
+ return { status: "placed", block: block2, placed: [] };
13152
+ }
13153
+ return length < known.length ? {
13154
+ status: "overlap",
13155
+ reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
13156
+ ofSpan: known
13157
+ } : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
12825
13158
  }
12826
13159
  if (known.length !== length) {
12827
13160
  return {
@@ -12889,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
12889
13222
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
12890
13223
  }
12891
13224
  /**
12892
- * Turn every pending hole inside `[start, start + length)` into a
12893
- * repair-less span, claiming its held sources: its range FETCH ended, so
12894
- * nothing more will state its blocks. Returns the spans, in SS_ID order.
13225
+ * Turn every hole inside `[start, start + length)` into a repair-less span,
13226
+ * claiming its held sources: its range FETCH ended, so nothing more will
13227
+ * state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
13228
+ * span that is not kept among the blocks, as the horizon would retire it at
13229
+ * once; only `locate` finds it, until the next retirement, so its sources
13230
+ * can be fed. Returns the spans, in SS_ID order.
12895
13231
  */
12896
13232
  closeHolesWithin(start, length) {
12897
13233
  const closed = [];
12898
- for (const hole of [...this.#holes]) {
12899
- if (!isoBlockContains(start, length, hole.start)) continue;
12900
- this.#holes.splice(this.#holes.indexOf(hole), 1);
12901
- const spanBlock = { start: hole.start, length: hole.length, span: true };
12902
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
12903
- this.#blocks.push(spanBlock);
13234
+ for (const holes of [this.#holes, this.#expired]) {
13235
+ for (const hole of [...holes]) {
13236
+ if (!isoBlockContains(start, length, hole.start)) continue;
13237
+ holes.splice(holes.indexOf(hole), 1);
13238
+ const spanBlock = { start: hole.start, length: hole.length, span: true };
13239
+ closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
13240
+ if (holes === this.#holes) {
13241
+ this.#blocks.push(spanBlock);
13242
+ } else {
13243
+ this.#closedBehind.push(spanBlock);
13244
+ if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
13245
+ }
13246
+ }
12904
13247
  }
12905
13248
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
12906
13249
  }
@@ -12914,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
12914
13257
  return void 0;
12915
13258
  }
12916
13259
  /**
12917
- * Retire every block and span whose last SS_ID, and every held source,
12918
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
12919
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
12920
- * horizon uses. Returns the retired block starts.
13260
+ * Retire every block and span whose last SS_ID, and every held source
13261
+ * outside a hole, lies more than `retentionBlocks` blocks behind
13262
+ * `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
13263
+ * every ISO interleave horizon uses; a pending hole so far behind expires
13264
+ * (`takeExpiredHoles`). Returns the retired block starts.
12921
13265
  */
12922
13266
  retireBehind(position) {
12923
13267
  this.observe(position);
12924
13268
  const horizon = this.#retentionBlocks;
12925
13269
  const retired = [];
13270
+ this.#closedBehind.length = 0;
12926
13271
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
12927
13272
  const block = this.#blocks[i];
12928
13273
  const last = serial32Add(block.start, block.length - 1);
@@ -12934,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
12934
13279
  for (let i = this.#holes.length - 1; i >= 0; i--) {
12935
13280
  const hole = this.#holes[i];
12936
13281
  const last = serial32Add(hole.start, hole.length - 1);
12937
- if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) this.#holes.splice(i, 1);
13282
+ if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
13283
+ this.#holes.splice(i, 1);
13284
+ this.#expired.push(hole);
13285
+ this.#newlyExpired.push(hole);
12938
13286
  }
12939
13287
  for (const ssId of [...this.#held.keys()]) {
12940
- if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
13288
+ if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon && !this.#holes.some((hole) => isoBlockContains(hole.start, hole.length, ssId)) && !this.#expired.some((hole) => isoBlockContains(hole.start, hole.length, ssId))) {
13289
+ this.#held.delete(ssId);
13290
+ }
12941
13291
  }
12942
13292
  return retired;
12943
13293
  }
@@ -12945,9 +13295,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
12945
13295
  reset() {
12946
13296
  this.#blocks.length = 0;
12947
13297
  this.#holes.length = 0;
13298
+ this.#expired.length = 0;
13299
+ this.#newlyExpired.length = 0;
13300
+ this.#closedBehind.length = 0;
12948
13301
  this.#held.clear();
12949
13302
  this.#coverageEnd = void 0;
12950
13303
  this.#newest = void 0;
13304
+ this.#newestSource = void 0;
12951
13305
  }
12952
13306
  #claim(block) {
12953
13307
  const placed = [];
@@ -13567,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13567
13921
  var WORKER_ERROR_KIND_CAP = 32;
13568
13922
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13569
13923
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13924
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13570
13925
  function isMulticastSymbolDelivery(value) {
13571
13926
  return value === "datagram" || value === "ordered-relay";
13572
13927
  }
@@ -13660,10 +14015,18 @@ var FECManager = class _FECManager {
13660
14015
  // 'pending' forever, which would park every later frame of its stream in
13661
14016
  // the in-order release gate. Each entry owns its handle; the callback
13662
14017
  // checks it still owns the key, so a settled or reset key is never touched.
14018
+ /**
14019
+ * Per owed Worker cleanup: the bound on its acknowledgement, when its block
14020
+ * was due, and when its cleanup was asked for (see `onCleanupComplete`).
14021
+ */
13663
14022
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
13664
14023
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13665
14024
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13666
14025
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
14026
+ // Source ESIs per block that another FEC instance rebuilt and fed in
14027
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
14028
+ // not network arrivals and never make a block clean.
14029
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13667
14030
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13668
14031
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13669
14032
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -13672,6 +14035,9 @@ var FECManager = class _FECManager {
13672
14035
  // SSB_length source arrivals and recoverable from SSB_length received
13673
14036
  // symbols; waiting for K fails every one of them.
13674
14037
  this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
14038
+ // Keys whose authored length is a repair-less span's (#openIsoSpan), until
14039
+ // a repair states the block there or the span resolves.
14040
+ this.#isoSpanKeys = /* @__PURE__ */ new Set();
13675
14041
  this.#ssbLengthConflictCount = 0;
13676
14042
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
13677
14043
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -13792,6 +14158,10 @@ var FECManager = class _FECManager {
13792
14158
  // ── Keyframe-protected repair policy (block-policy.ts) ──
13793
14159
  // One classifier per physical stream, built from that stream's geometry.
13794
14160
  this.#classifiers = /* @__PURE__ */ new Map();
14161
+ // A stream without catalog timing has no classifier, so the last RAP it
14162
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14163
+ // a second RAP is released inside one MPU (BLO-52085).
14164
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
13795
14165
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
13796
14166
  // rungs can share a packetId with a different framerate or GOP, so the
13797
14167
  // packet-level geometry is only the geometry of a packet whose streams
@@ -13865,7 +14235,10 @@ var FECManager = class _FECManager {
13865
14235
  // arrives via onBlockRecovered callback.
13866
14236
  this.onBlockStarted = null;
13867
14237
  this.onBlockComplete = null;
13868
- /** A block reached its fail bound. `blockClass` is its class at failure. */
14238
+ /**
14239
+ * A block reached its fail bound. `blockClass` is its class at failure.
14240
+ * `span` is set when what failed is a repair-less span starting at `sbn`.
14241
+ */
13869
14242
  this.onBlockFailed = null;
13870
14243
  /**
13871
14244
  * Fired once per stream when its Worker decoder stops being routable.
@@ -13907,6 +14280,8 @@ var FECManager = class _FECManager {
13907
14280
  * for it, which it must be.
13908
14281
  */
13909
14282
  this.#onUndelimitedLoss = null;
14283
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
14284
+ this.#onIsoHoleExpired = null;
13910
14285
  /** A block every symbol of which vanished was created by SBN evidence. */
13911
14286
  this.onBlockVanished = null;
13912
14287
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -13949,7 +14324,9 @@ var FECManager = class _FECManager {
13949
14324
  #pendingWorkerCleanupBounds;
13950
14325
  #blockSourceCounts;
13951
14326
  #blockSourceEsis;
14327
+ #blockRebuiltEsis;
13952
14328
  #blockAuthoredLengths;
14329
+ #isoSpanKeys;
13953
14330
  #ssbLengthConflictCount;
13954
14331
  #blockRepairRsIds;
13955
14332
  #blockRsbLengths;
@@ -13991,6 +14368,7 @@ var FECManager = class _FECManager {
13991
14368
  #orderedFrontiers;
13992
14369
  #orderedPendingBlocks;
13993
14370
  #classifiers;
14371
+ #noSpanRaps;
13994
14372
  #streamGeometries;
13995
14373
  #failedBlocks;
13996
14374
  #resolvedBlockClasses;
@@ -14015,6 +14393,7 @@ var FECManager = class _FECManager {
14015
14393
  #decoderUnavailableEvents;
14016
14394
  #repairSymbolSizeMismatchCount;
14017
14395
  #onUndelimitedLoss;
14396
+ #onIsoHoleExpired;
14018
14397
  #encodeTrackName(catalogTrackName) {
14019
14398
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
14020
14399
  if (encoded === void 0) {
@@ -14486,7 +14865,10 @@ var FECManager = class _FECManager {
14486
14865
  timer.handle = null;
14487
14866
  this.#requestRepair(sbn, packetId, catalogTrackName);
14488
14867
  };
14489
- timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()));
14868
+ timer.handle = setTimeout(
14869
+ onFire,
14870
+ this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
14871
+ );
14490
14872
  this.#repairDeadlines.set(key, timer);
14491
14873
  }
14492
14874
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -14630,8 +15012,14 @@ var FECManager = class _FECManager {
14630
15012
  * Vanished blocks are not enumerated here: their starts are unknown until a
14631
15013
  * repair states them. The SS_IDs between two stated blocks become one
14632
15014
  * repair-less span when the later block's repair arrives (placeIsoRepair).
15015
+ *
15016
+ * A `held` source arrived before its block was known and is fed only now
15017
+ * that a repair, a span or a closed hole placed it: how far its block start
15018
+ * then lies behind the frontier measures how long the receiver held it, not
15019
+ * where the stream was, so only its SS_ID and packet_sequence_number judge
15020
+ * it (BLO-52085).
14633
15021
  */
14634
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
15022
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
14635
15023
  const streamKey = this.#streamKey(packetId, catalogTrackName);
14636
15024
  const now = performance.now();
14637
15025
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -14652,6 +15040,7 @@ var FECManager = class _FECManager {
14652
15040
  };
14653
15041
  const discontinuity = () => {
14654
15042
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
15043
+ this.#noSpanRaps.delete(streamKey);
14655
15044
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14656
15045
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14657
15046
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -14669,7 +15058,7 @@ var FECManager = class _FECManager {
14669
15058
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
14670
15059
  };
14671
15060
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
14672
- if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
15061
+ if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
14673
15062
  discontinuity();
14674
15063
  return;
14675
15064
  }
@@ -14931,17 +15320,21 @@ var FECManager = class _FECManager {
14931
15320
  this.#blockWaitDeadlinesMs.delete(key);
14932
15321
  this.#repairHoldUntilMs.delete(key);
14933
15322
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
14934
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
15323
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
14935
15324
  };
14936
- timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
15325
+ timer.handle = setTimeout(
15326
+ onFire,
15327
+ this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
15328
+ );
14937
15329
  this.#activeBlocks.set(key, timer);
14938
15330
  this.#indexBlockKey(key, sbn, packetId);
14939
15331
  }
14940
15332
  /**
14941
15333
  * Hand a block whose delivery is over to its decoder for finalization: the
14942
- * datagram deadline fired, or the ordered relay moved past it.
15334
+ * datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
15335
+ * is when it was due: its deadline, or now.
14943
15336
  */
14944
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
15337
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
14945
15338
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
14946
15339
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
14947
15340
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -14957,7 +15350,7 @@ var FECManager = class _FECManager {
14957
15350
  if (workerClient) {
14958
15351
  this.#pendingWorkerCleanups.add(key);
14959
15352
  this.#pendingWorkerCleanupClients.set(key, workerClient);
14960
- this.#boundOwedCleanup(key, packetId, workerClient);
15353
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
14961
15354
  workerClient.cleanup(sbn);
14962
15355
  } else {
14963
15356
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -15064,6 +15457,9 @@ var FECManager = class _FECManager {
15064
15457
  }
15065
15458
  if (policyChanged) {
15066
15459
  this.#bumpEpoch(packetId);
15460
+ for (const key of [...this.#noSpanRaps.keys()]) {
15461
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15462
+ }
15067
15463
  for (const [key, classifier] of [...this.#classifiers]) {
15068
15464
  if (!this.#belongsToPacket(key, packetId)) continue;
15069
15465
  const stream = this.#parseStreamKey(key);
@@ -15266,7 +15662,9 @@ var FECManager = class _FECManager {
15266
15662
  }
15267
15663
  resetMap(this.#blockSourceCounts);
15268
15664
  resetMap(this.#blockSourceEsis);
15665
+ resetMap(this.#blockRebuiltEsis);
15269
15666
  resetMap(this.#blockAuthoredLengths);
15667
+ resetSet(this.#isoSpanKeys);
15270
15668
  resetMap(this.#blockRepairRsIds);
15271
15669
  resetMap(this.#blockRsbLengths);
15272
15670
  resetMap(this.#heldLayerStates);
@@ -15299,6 +15697,7 @@ var FECManager = class _FECManager {
15299
15697
  resetMap(this.#isoLocators);
15300
15698
  resetMap(this.#sourceAnchors);
15301
15699
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15700
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15302
15701
  this.#bumpEpoch(packetId);
15303
15702
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15304
15703
  for (const key of keys.keys()) {
@@ -15389,7 +15788,9 @@ var FECManager = class _FECManager {
15389
15788
  dropMap(this.#pendingWorkerCleanupClients);
15390
15789
  dropMap(this.#blockSourceCounts);
15391
15790
  dropMap(this.#blockSourceEsis);
15791
+ dropMap(this.#blockRebuiltEsis);
15392
15792
  dropMap(this.#blockAuthoredLengths);
15793
+ dropSet(this.#isoSpanKeys);
15393
15794
  dropMap(this.#blockRepairRsIds);
15394
15795
  dropMap(this.#blockRsbLengths);
15395
15796
  dropMap(this.#blockPathCounts);
@@ -15405,6 +15806,7 @@ var FECManager = class _FECManager {
15405
15806
  dropMap(this.#isoLocators);
15406
15807
  dropMap(this.#sourceAnchors);
15407
15808
  dropMap(this.#classifiers);
15809
+ dropMap(this.#noSpanRaps);
15408
15810
  dropMap(this.#streamGeometries);
15409
15811
  dropSet(this.#pendingWorkerCleanups);
15410
15812
  dropSet(this.#completedBlocks);
@@ -15550,6 +15952,9 @@ var FECManager = class _FECManager {
15550
15952
  this.#unindexBlockKey(key);
15551
15953
  }
15552
15954
  }
15955
+ if (isSourceSymbol && source === "multicast") {
15956
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15957
+ }
15553
15958
  return true;
15554
15959
  }
15555
15960
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -15728,8 +16133,9 @@ var FECManager = class _FECManager {
15728
16133
  return true;
15729
16134
  }
15730
16135
  /**
15731
- * Drop a block's repair hold: its FETCH ended without delivering. The block
15732
- * falls back to its multicast wait, which may already have passed.
16136
+ * Drop a block's repair hold: its FETCH ended without delivering, or brought
16137
+ * back only a repair that cannot complete it. The block falls back to its
16138
+ * multicast wait, which may already have passed.
15733
16139
  */
15734
16140
  releaseRepairHold(sbn, packetId, catalogTrackName) {
15735
16141
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -15867,6 +16273,8 @@ var FECManager = class _FECManager {
15867
16273
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
15868
16274
  const evictedBefore = locator.heldEvicted;
15869
16275
  const held = locator.hold(ssId, source);
16276
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16277
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
15870
16278
  const evicted = locator.heldEvicted - evictedBefore;
15871
16279
  if (evicted > 0) {
15872
16280
  const before = this.#isoHeldEvictedCount;
@@ -15887,17 +16295,20 @@ var FECManager = class _FECManager {
15887
16295
  * contradicts a known block is refused (counted) and must be dropped.
15888
16296
  */
15889
16297
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
15890
- const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
16298
+ const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16299
+ const placement = locator.placeRepair(ssStart, ssbLength);
16300
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
15891
16301
  if (placement.status !== "placed") {
15892
16302
  this.#isoRepairRefusedCount++;
15893
16303
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
15894
16304
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
15895
16305
  }
16306
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
15896
16307
  return placement;
15897
16308
  }
16309
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
15898
16310
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
15899
16311
  if (placement.hole !== void 0) {
15900
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
15901
16312
  const hole = placement.hole.block;
15902
16313
  const previous = locator.blockEndingAt(hole.start);
15903
16314
  const loss = {
@@ -15918,17 +16329,29 @@ var FECManager = class _FECManager {
15918
16329
  * `handler`, instead of a repair-less span; null restores spans. Set by the
15919
16330
  * range repair scheduler; applies to every stream from its next gap. The
15920
16331
  * handler and the deferral are one setting, so no hole is ever created with
15921
- * nobody to close it.
16332
+ * nobody to close it. `onExpired` is told of each pending hole the stream
16333
+ * runs past the locator's horizon (BLO-52085): returning true, it closes
16334
+ * the hole (`closeIsoHoles`), so the span it leaves still fails and is
16335
+ * reported; otherwise the hole is discarded with its sources.
15922
16336
  */
15923
- setUndelimitedLossHandler(handler) {
16337
+ setUndelimitedLossHandler(handler, onExpired = null) {
15924
16338
  this.#onUndelimitedLoss = handler;
16339
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
15925
16340
  this.#deferIsoGaps = handler !== null;
15926
16341
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
15927
16342
  }
16343
+ /** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
16344
+ #settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
16345
+ for (const hole of locator.takeExpiredHoles()) {
16346
+ const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
16347
+ if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
16348
+ }
16349
+ }
15928
16350
  /**
15929
- * The range repair for a hole ended: every hole still pending inside
15930
- * `[start, start + length)` becomes a repair-less span. Returns those spans
15931
- * with the held sources they placed, for the caller to feed (MmtpRouter).
16351
+ * The range repair for a hole ended: every hole inside `[start, start +
16352
+ * length)`, pending or expired, becomes a repair-less span. Returns those
16353
+ * spans with the held sources they placed, for the caller to feed
16354
+ * (MmtpRouter).
15932
16355
  */
15933
16356
  closeIsoHoles(packetId, catalogTrackName, start, length) {
15934
16357
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -15947,7 +16370,10 @@ var FECManager = class _FECManager {
15947
16370
  */
15948
16371
  #openIsoSpan(span, packetId, catalogTrackName) {
15949
16372
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
15950
- if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, span.block.length);
16373
+ if (!this.#blockAuthoredLengths.has(key)) {
16374
+ this.#blockAuthoredLengths.set(key, span.block.length);
16375
+ this.#isoSpanKeys.add(key);
16376
+ }
15951
16377
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
15952
16378
  }
15953
16379
  /**
@@ -16035,11 +16461,14 @@ var FECManager = class _FECManager {
16035
16461
  /**
16036
16462
  * Whether an MPU boundary still bounds this stream's RAPs (see
16037
16463
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16038
- * it does not. A stream with no classifier yet has shown nothing against it.
16464
+ * it does not. A stream with no classifier yet has shown nothing against it,
16465
+ * and neither has a stream without catalog timing until it releases two
16466
+ * RAPs inside one MPU.
16039
16467
  */
16040
16468
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16041
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16042
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16469
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16470
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16471
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16043
16472
  }
16044
16473
  /** A block's class now, or frozen at its resolution. */
16045
16474
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16065,7 +16494,14 @@ var FECManager = class _FECManager {
16065
16494
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16066
16495
  */
16067
16496
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16068
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16497
+ if (!this.#trackConfigs.has(packetId)) return;
16498
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16499
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16500
+ const last = this.#noSpanRaps.get(streamKey);
16501
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16502
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16503
+ return;
16504
+ }
16069
16505
  this.#applyClassifierChanges(
16070
16506
  packetId,
16071
16507
  catalogTrackName,
@@ -16278,11 +16714,14 @@ var FECManager = class _FECManager {
16278
16714
  });
16279
16715
  client.onCleanupComplete((sbn) => {
16280
16716
  if (this.#releasedWorkerClients.has(client)) return;
16281
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
16717
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
16718
+ const cleanupKeys = owed.filter((key) => {
16282
16719
  const block = this.#parseBlockKey(key);
16283
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
16720
+ return block.packetId === packetId && block.sbn === sbn;
16284
16721
  });
16285
- for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
16722
+ const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
16723
+ const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
16724
+ for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
16286
16725
  });
16287
16726
  client.onError((msg, code) => {
16288
16727
  const requestedKind = code ?? msg;
@@ -16352,9 +16791,11 @@ var FECManager = class _FECManager {
16352
16791
  * fail deadline already passed, so failing it is the decision that deadline
16353
16792
  * made. A late ack then finds nothing owed and is a no-op.
16354
16793
  */
16355
- #boundOwedCleanup(key, packetId, client) {
16794
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
16356
16795
  this.#clearOwedCleanupBound(key);
16357
16796
  const bound = {
16797
+ dueAtMs,
16798
+ askedAtMs: performance.now(),
16358
16799
  handle: setTimeout(() => {
16359
16800
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
16360
16801
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -16452,7 +16893,15 @@ var FECManager = class _FECManager {
16452
16893
  this.#isoSourceOversizeCount++;
16453
16894
  return null;
16454
16895
  }
16455
- const sbn = located.block.start;
16896
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16897
+ }
16898
+ /**
16899
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16900
+ * answers through onFrame (null here), or its synchronous decoder, whose
16901
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16902
+ * stated for `ssId`.
16903
+ */
16904
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16456
16905
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16457
16906
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16458
16907
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16481,6 +16930,74 @@ var FECManager = class _FECManager {
16481
16930
  }
16482
16931
  return recovered;
16483
16932
  }
16933
+ /**
16934
+ * Feed a source symbol of this stream that another FEC instance over the
16935
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16936
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16937
+ * BLO-52085). It joins its block's decoder as a received source would, so
16938
+ * it reduces the erasures the block still has to solve, but it is not a
16939
+ * network arrival: no transport accounting, no classification evidence,
16940
+ * no deadline refresh, and it never makes a block clean.
16941
+ *
16942
+ * Only a block a repair stated takes one (a repair-less span has no
16943
+ * decoder block). A block that is terminal, or already holds that source,
16944
+ * does not. The symbol is the exact protected packet, unpadded, as
16945
+ * feedSourceSymbol takes it.
16946
+ *
16947
+ * `recovered` is the block the synchronous decoder completed with it; the
16948
+ * caller routes it and completes the block, as for any recovery. When
16949
+ * every source of the block is then present, received or rebuilt, and no
16950
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16951
+ * block as clean on its own side), the block is completed here as
16952
+ * recovered, with nothing left to route.
16953
+ */
16954
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16955
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16956
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16957
+ if (located === null || located.block.span) return NOT_TAKEN;
16958
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16959
+ const sbn = located.block.start;
16960
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16961
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16962
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16963
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16964
+ if (rebuilt === void 0) {
16965
+ rebuilt = /* @__PURE__ */ new Set();
16966
+ this.#blockRebuiltEsis.set(key, rebuilt);
16967
+ }
16968
+ const config = this.#trackConfigs.get(packetId);
16969
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16970
+ rebuilt.add(located.esi);
16971
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16972
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16973
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16974
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16975
+ }
16976
+ }
16977
+ return { taken: true, recovered };
16978
+ }
16979
+ /**
16980
+ * A block that holds every one of its `length` sources, received or
16981
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16982
+ * every packet of it was delivered, by its own arrival or by the instance
16983
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16984
+ */
16985
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16986
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16987
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16988
+ let present = rebuilt.size;
16989
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16990
+ if (present < length) return false;
16991
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16992
+ this.countRecovery(packetId, sbn, catalogTrackName);
16993
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16994
+ if (catalogTrackName === void 0) {
16995
+ this.onBlockComplete?.(sbn, packetId);
16996
+ } else {
16997
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16998
+ }
16999
+ return true;
17000
+ }
16484
17001
  /**
16485
17002
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16486
17003
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16683,8 +17200,75 @@ var FECManager = class _FECManager {
16683
17200
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16684
17201
  return null;
16685
17202
  }
17203
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16686
17204
  return recovered;
16687
17205
  }
17206
+ /**
17207
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17208
+ * its sources in send order, so one that moves the stream's newest source
17209
+ * position past a block leaves the sources that block still misses
17210
+ * reordered behind it at best: it shortens the wait of every block it
17211
+ * passed that only a late source can complete. MoQ is left out: its streams
17212
+ * can overtake one another, so a MoQ source shows nothing about the sources
17213
+ * sent before it.
17214
+ */
17215
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17216
+ const previous = locator.newestSource;
17217
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17218
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17219
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17220
+ this.#shortenUnrecoverableWait(
17221
+ this.#blockKey(block.start, packetId, catalogTrackName),
17222
+ block.start,
17223
+ packetId,
17224
+ catalogTrackName
17225
+ );
17226
+ }
17227
+ }
17228
+ /**
17229
+ * The wait of a block that only a late source can complete (BLO-52085): a
17230
+ * repair stated it, its received sources plus every symbol its repair group
17231
+ * carries (RSB_length) are short of K' (the repair scheduler's
17232
+ * group-cannot-complete), and a later source of the stream has arrived, so
17233
+ * a source it misses is lost or reordered behind that one. The span, which
17234
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17235
+ * the late-symbol allowance alone, the multicast wait less the span (the
17236
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17237
+ * block.
17238
+ */
17239
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17240
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17241
+ const rsbLength = this.#blockRsbLengths.get(key);
17242
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17243
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17244
+ const received = this.#blockSourceEsis.get(key);
17245
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17246
+ if (present + rsbLength >= authoredLength) return void 0;
17247
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17248
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17249
+ return void 0;
17250
+ }
17251
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17252
+ }
17253
+ /**
17254
+ * Bring an open block's wait in to its late-source wait, when it has one.
17255
+ * Never later than it is armed: only a source can complete such a block,
17256
+ * so a repair of its group (paced across the span) must not keep it open.
17257
+ */
17258
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17259
+ if (!this.#activeBlocks.has(key)) return;
17260
+ const config = this.#trackConfigs.get(packetId);
17261
+ if (config === void 0) return;
17262
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17263
+ if (waitMs === void 0) return;
17264
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17265
+ this.#armBlockTimeout(
17266
+ sbn,
17267
+ packetId,
17268
+ catalogTrackName,
17269
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17270
+ );
17271
+ }
16688
17272
  /**
16689
17273
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16690
17274
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -16782,12 +17366,13 @@ var FECManager = class _FECManager {
16782
17366
  * the inactivity deadline on each symbol, records the symbol's in-band
16783
17367
  * classification evidence, and marks the block clean when all of its
16784
17368
  * authored source symbols arrived: the SSB_length a repair symbol declared,
16785
- * otherwise K.
17369
+ * otherwise K. `held` marks a source held unplaced since it arrived and
17370
+ * placed only now (see #noteSbnSeen).
16786
17371
  *
16787
17372
  * Throws for a packetId that was never configured, so a classified block
16788
17373
  * never reads the manager-global K or timeout.
16789
17374
  */
16790
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
17375
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
16791
17376
  const trackConfig = this.#trackConfigs.get(packetId);
16792
17377
  if (trackConfig === void 0) {
16793
17378
  throw new Error(
@@ -16803,7 +17388,8 @@ var FECManager = class _FECManager {
16803
17388
  catalogTrackName,
16804
17389
  "source",
16805
17390
  evidence.packetSequenceNumber,
16806
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
17391
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
17392
+ held
16807
17393
  );
16808
17394
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
16809
17395
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -16839,6 +17425,15 @@ var FECManager = class _FECManager {
16839
17425
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
16840
17426
  return;
16841
17427
  }
17428
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17429
+ key,
17430
+ sbn,
17431
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17432
+ packetId,
17433
+ catalogTrackName
17434
+ )) {
17435
+ return;
17436
+ }
16842
17437
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
16843
17438
  }
16844
17439
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -16847,7 +17442,9 @@ var FECManager = class _FECManager {
16847
17442
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16848
17443
  this.#blockSourceCounts.delete(key);
16849
17444
  this.#blockSourceEsis.delete(key);
17445
+ this.#blockRebuiltEsis.delete(key);
16850
17446
  this.#blockAuthoredLengths.delete(key);
17447
+ this.#isoSpanKeys.delete(key);
16851
17448
  this.#blockRepairRsIds.delete(key);
16852
17449
  this.#blockRsbLengths.delete(key);
16853
17450
  this.#markTerminal(key, sbn, packetId, true);
@@ -16878,6 +17475,19 @@ var FECManager = class _FECManager {
16878
17475
  #receiptNow() {
16879
17476
  return this.#receiptAtMs ?? performance.now();
16880
17477
  }
17478
+ /**
17479
+ * The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
17480
+ * scope: counted from the receipt time, as the timer of a symbol fed when
17481
+ * it was received was, less the whole ms since. So it fires on the tick
17482
+ * that timer would have, never later than a delay counted from now, and
17483
+ * its handler still re-arms a timer that fires before its deadline.
17484
+ * Counted from now, a deadline set from the receipt time is rounded up
17485
+ * from another fraction of a ms (BLO-52085).
17486
+ */
17487
+ #receiptTimerDelayMs(deadlineMs, minMs) {
17488
+ const receivedAtMs = this.#receiptAtMs;
17489
+ return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
17490
+ }
16881
17491
  /**
16882
17492
  * A held packet admitted late arms from its (earlier) receipt time; that
16883
17493
  * never pulls in a deadline later activity already armed. Outside a
@@ -16913,10 +17523,20 @@ var FECManager = class _FECManager {
16913
17523
  this.#repairDeadlineMs(trackConfig)
16914
17524
  );
16915
17525
  }
16916
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17526
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17527
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17528
+ return;
17529
+ }
17530
+ this.#armBlockTimeout(
17531
+ sbn,
17532
+ packetId,
17533
+ catalogTrackName,
17534
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17535
+ );
16917
17536
  }
16918
17537
  #failBlock(sbn, packetId, catalogTrackName) {
16919
17538
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
17539
+ const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
16920
17540
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
16921
17541
  this.#clearRepairDeadline(key);
16922
17542
  this.#clearBlockTimer(key);
@@ -16926,6 +17546,7 @@ var FECManager = class _FECManager {
16926
17546
  }
16927
17547
  this.#blockSourceCounts.delete(key);
16928
17548
  this.#blockSourceEsis.delete(key);
17549
+ this.#blockRebuiltEsis.delete(key);
16929
17550
  this.#blockAuthoredLengths.delete(key);
16930
17551
  this.#blockRepairRsIds.delete(key);
16931
17552
  this.#blockRsbLengths.delete(key);
@@ -16935,7 +17556,8 @@ var FECManager = class _FECManager {
16935
17556
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
16936
17557
  this.#fecStats.blocksFailed++;
16937
17558
  this.#statsForPacket(packetId).blocksFailed++;
16938
- this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
17559
+ if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
17560
+ else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
16939
17561
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
16940
17562
  }
16941
17563
  /**
@@ -16964,7 +17586,9 @@ var FECManager = class _FECManager {
16964
17586
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
16965
17587
  this.#blockSourceCounts.delete(key);
16966
17588
  this.#blockSourceEsis.delete(key);
17589
+ this.#blockRebuiltEsis.delete(key);
16967
17590
  this.#blockAuthoredLengths.delete(key);
17591
+ this.#isoSpanKeys.delete(key);
16968
17592
  this.#blockRepairRsIds.delete(key);
16969
17593
  this.#blockRsbLengths.delete(key);
16970
17594
  this.#clearBlockTimer(key);
@@ -17055,7 +17679,9 @@ var FECManager = class _FECManager {
17055
17679
  this.#blockKeysBySbn.clear();
17056
17680
  this.#blockSourceCounts.clear();
17057
17681
  this.#blockSourceEsis.clear();
17682
+ this.#blockRebuiltEsis.clear();
17058
17683
  this.#blockAuthoredLengths.clear();
17684
+ this.#isoSpanKeys.clear();
17059
17685
  this.#blockRepairRsIds.clear();
17060
17686
  this.#blockRsbLengths.clear();
17061
17687
  this.#heldLayerStates.clear();
@@ -17080,6 +17706,7 @@ var FECManager = class _FECManager {
17080
17706
  this.#blockAnchorsMs.clear();
17081
17707
  this.#blockCatalogTrackNames.clear();
17082
17708
  this.#classifiers.clear();
17709
+ this.#noSpanRaps.clear();
17083
17710
  this.#streamGeometries.clear();
17084
17711
  this.#failedBlocks.clear();
17085
17712
  this.#resolvedBlockClasses.clear();
@@ -17098,17 +17725,17 @@ var FECManager = class _FECManager {
17098
17725
 
17099
17726
  // src/mfu-reassembly-policy.ts
17100
17727
  function validateReliableMoqOptionShape(value) {
17101
- const unsupported = Object.keys(value).filter(
17728
+ const unsupported2 = Object.keys(value).filter(
17102
17729
  (key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
17103
17730
  );
17104
- if (unsupported.length > 0) {
17105
- if (unsupported.includes("maxRacingGroups")) {
17731
+ if (unsupported2.length > 0) {
17732
+ if (unsupported2.includes("maxRacingGroups")) {
17106
17733
  throw new Error(
17107
17734
  "[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
17108
17735
  );
17109
17736
  }
17110
17737
  throw new Error(
17111
- `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported.join(", ")}`
17738
+ `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
17112
17739
  );
17113
17740
  }
17114
17741
  const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
@@ -17152,12 +17779,12 @@ function assertValidMfuReassemblyPolicy(policy) {
17152
17779
  return;
17153
17780
  }
17154
17781
  if (value["transport"] === "datagram") {
17155
- const unsupported = Object.keys(value).filter(
17782
+ const unsupported2 = Object.keys(value).filter(
17156
17783
  (key) => key !== "transport" && key !== "deadlineMs"
17157
17784
  );
17158
- if (unsupported.length > 0) {
17785
+ if (unsupported2.length > 0) {
17159
17786
  throw new Error(
17160
- `[MFU reassembly] unsupported datagram option(s): ${unsupported.join(", ")}`
17787
+ `[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
17161
17788
  );
17162
17789
  }
17163
17790
  assertValidDatagramDeadline(value["deadlineMs"]);
@@ -17610,9 +18237,21 @@ var IsoBmffTimingResolver = class {
17610
18237
  }
17611
18238
  #track = null;
17612
18239
  #fragments = /* @__PURE__ */ new Map();
18240
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18241
+ #firstSamples = /* @__PURE__ */ new Map();
18242
+ /**
18243
+ * First sample and sample count of the newest fragments whose every slot was
18244
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18245
+ * matches the combiner, since it retires an abandoned sample as well.
18246
+ */
18247
+ #delivered = /* @__PURE__ */ new Map();
17613
18248
  #pendingSamples = 0;
17614
18249
  #evictedSamples = 0;
17615
18250
  #lastMovieFragmentRejection = null;
18251
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18252
+ get initialized() {
18253
+ return this.#track !== null;
18254
+ }
17616
18255
  get pendingSampleCount() {
17617
18256
  return this.#pendingSamples;
17618
18257
  }
@@ -17636,7 +18275,14 @@ var IsoBmffTimingResolver = class {
17636
18275
  this.#track = timing;
17637
18276
  return true;
17638
18277
  }
17639
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18278
+ /**
18279
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18280
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18281
+ * fragment whose samples are all done is a duplicate: admitting it again
18282
+ * would charge stale timing and could displace the live MPU's. A source
18283
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18284
+ */
18285
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17640
18286
  this.#lastMovieFragmentRejection = null;
17641
18287
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17642
18288
  this.#lastMovieFragmentRejection = "invalid";
@@ -17662,6 +18308,8 @@ var IsoBmffTimingResolver = class {
17662
18308
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17663
18309
  );
17664
18310
  }
18311
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18312
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17665
18313
  let freed = 0;
17666
18314
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17667
18315
  freed += this.#evictOldestFragment();
@@ -17675,6 +18323,7 @@ var IsoBmffTimingResolver = class {
17675
18323
  return false;
17676
18324
  }
17677
18325
  this.#fragments.set(key, [...parsed.samples]);
18326
+ this.#firstSamples.set(key, parsed.samples[0]);
17678
18327
  this.#pendingSamples += parsed.samples.length;
17679
18328
  return true;
17680
18329
  }
@@ -17732,6 +18381,7 @@ var IsoBmffTimingResolver = class {
17732
18381
  let live = 0;
17733
18382
  for (const sample of samples) if (sample !== null) live++;
17734
18383
  this.#fragments.delete(oldest);
18384
+ this.#firstSamples.delete(oldest);
17735
18385
  this.#pendingSamples -= live;
17736
18386
  this.#evictedSamples += live;
17737
18387
  return live;
@@ -17747,12 +18397,23 @@ var IsoBmffTimingResolver = class {
17747
18397
  if (!samples || !samples[index]) return false;
17748
18398
  samples[index] = null;
17749
18399
  this.#pendingSamples--;
17750
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18400
+ if (samples.every((value) => value === null)) {
18401
+ this.#fragments.delete(key);
18402
+ const first = this.#firstSamples.get(key);
18403
+ this.#firstSamples.delete(key);
18404
+ this.#delivered.delete(key);
18405
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18406
+ if (this.#delivered.size > this.maxPendingSamples) {
18407
+ this.#delivered.delete(this.#delivered.keys().next().value);
18408
+ }
18409
+ }
17751
18410
  return true;
17752
18411
  }
17753
18412
  reset() {
17754
18413
  this.#track = null;
17755
18414
  this.#fragments.clear();
18415
+ this.#firstSamples.clear();
18416
+ this.#delivered.clear();
17756
18417
  this.#pendingSamples = 0;
17757
18418
  }
17758
18419
  };
@@ -17922,6 +18583,9 @@ function authoredGap(before, after) {
17922
18583
  function ssIdAfter(a, b) {
17923
18584
  return (a - b | 0) > 0;
17924
18585
  }
18586
+ function holdsRebuilt(stream) {
18587
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18588
+ }
17925
18589
  var FecReleaseSequencer = class {
17926
18590
  #streams = /* @__PURE__ */ new Map();
17927
18591
  #gate;
@@ -17984,6 +18648,7 @@ var FecReleaseSequencer = class {
17984
18648
  stream.lastSbn = void 0;
17985
18649
  stream.discontinuity = false;
17986
18650
  stream.rapSsId = void 0;
18651
+ stream.sourced = false;
17987
18652
  }
17988
18653
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
17989
18654
  this.#recordStat("droppedStale");
@@ -18010,6 +18675,7 @@ var FecReleaseSequencer = class {
18010
18675
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18011
18676
  else hi = mid;
18012
18677
  }
18678
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18013
18679
  stream.buf.splice(lo, 0, frame);
18014
18680
  this.#drainUnlessDeferred(stream);
18015
18681
  }
@@ -18045,6 +18711,41 @@ var FecReleaseSequencer = class {
18045
18711
  }
18046
18712
  return false;
18047
18713
  }
18714
+ /**
18715
+ * These streams' sink is being replaced by one that has seen no source
18716
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
18717
+ * release to the sink being replaced the frames the unanchored path had
18718
+ * already released to it (#cursorPassed): there they waited in its queue,
18719
+ * which its replacement flushes. A stream that holds none is not drained:
18720
+ * it holds nothing the anchor kept back, and a drain would still force out
18721
+ * a frame held past the hold limit, which before the anchor waited for the
18722
+ * next push. Then treat the streams as unsourced until a source frame is
18723
+ * released.
18724
+ */
18725
+ unsource(packetId, catalogTrackName) {
18726
+ for (const stream of this.#streams.values()) {
18727
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
18728
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
18729
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
18730
+ else this.#drain(stream, true);
18731
+ }
18732
+ stream.sourced = false;
18733
+ }
18734
+ }
18735
+ }
18736
+ /**
18737
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
18738
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
18739
+ * packetId.
18740
+ */
18741
+ holdsRebuilt(packetId, catalogTrackName) {
18742
+ for (const stream of this.#streams.values()) {
18743
+ if (stream.packetId !== packetId) continue;
18744
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
18745
+ if (holdsRebuilt(stream)) return true;
18746
+ }
18747
+ return false;
18748
+ }
18048
18749
  /** Re-evaluate held gaps after FEC state changed. */
18049
18750
  poke(streamKey) {
18050
18751
  if (streamKey !== void 0) {
@@ -18136,16 +18837,32 @@ var FecReleaseSequencer = class {
18136
18837
  last: null,
18137
18838
  lastSbn: void 0,
18138
18839
  discontinuity: false,
18139
- rapSsId: void 0
18840
+ rapSsId: void 0,
18841
+ sourced: false,
18842
+ draining: 0
18140
18843
  };
18141
18844
  this.#streams.set(streamKey, stream);
18142
18845
  }
18143
18846
  return stream;
18144
18847
  }
18145
- #drain(stream) {
18848
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
18849
+ #drain(stream, asUnanchored = false) {
18850
+ stream.draining++;
18851
+ try {
18852
+ this.#drainLoop(stream, asUnanchored);
18853
+ } finally {
18854
+ stream.draining--;
18855
+ }
18856
+ }
18857
+ #drainLoop(stream, asUnanchored) {
18146
18858
  while (stream.buf.length > 0 && stream.last !== null) {
18147
18859
  const head = stream.buf[0];
18148
18860
  const last = stream.last;
18861
+ if (compareFrameOrder(head, last) <= 0) {
18862
+ stream.buf.shift();
18863
+ this.#release(stream, head, void 0);
18864
+ continue;
18865
+ }
18149
18866
  if (stream.discontinuity) {
18150
18867
  stream.discontinuity = false;
18151
18868
  stream.buf.shift();
@@ -18159,9 +18876,10 @@ var FecReleaseSequencer = class {
18159
18876
  }
18160
18877
  const headSbn = head.meta.fecSbn;
18161
18878
  const from = stream.lastSbn ?? headSbn;
18162
- const to = headSbn ?? stream.lastSbn;
18879
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18163
18880
  let lost = [];
18164
18881
  const unanchored = from === void 0 && to === void 0;
18882
+ let unresolved = false;
18165
18883
  if (from !== void 0 && to !== void 0) {
18166
18884
  const resolved = this.#gate.resolvedLoss(
18167
18885
  stream.packetId,
@@ -18170,7 +18888,8 @@ var FecReleaseSequencer = class {
18170
18888
  Math.max(from, to),
18171
18889
  from
18172
18890
  );
18173
- if (resolved === null) {
18891
+ unresolved = resolved === null;
18892
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18174
18893
  const heldMs = performance.now() - head.arrivedAtMs;
18175
18894
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18176
18895
  this.#recordStat("forcedWindowFlushes");
@@ -18178,22 +18897,56 @@ var FecReleaseSequencer = class {
18178
18897
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18179
18898
  continue;
18180
18899
  }
18181
- lost = resolved;
18900
+ lost = resolved ?? [];
18182
18901
  }
18183
18902
  stream.buf.shift();
18184
18903
  this.#release(
18185
18904
  stream,
18186
18905
  head,
18187
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18906
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18188
18907
  );
18189
18908
  }
18190
18909
  }
18910
+ /**
18911
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
18912
+ * source cursor holds it unanchored, and neither does a later frame behind
18913
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
18914
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
18915
+ * like each frame that arrived in front of it since (stale there). The head
18916
+ * then goes, decided as it would be unanchored, once a frame the base path
18917
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
18918
+ * marked one: the base path has passed that one, so a frame still missing
18919
+ * before it can only come from a repair. It also goes while no source frame
18920
+ * has been released, when the direct path shows a recovered frame at once.
18921
+ * And it goes at once while a staged switch decides on when frames arrive
18922
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
18923
+ * sink being replaced: unanchored, it had already reached the sink.
18924
+ * The scan also writes: the `passed` marks it sets stay on the frames,
18925
+ * and push marks a frame it inserts in front of a marked one from them.
18926
+ */
18927
+ #cursorPassed(stream, asUnanchored) {
18928
+ let at2 = -1;
18929
+ for (let i = 0; i < stream.buf.length; i++) {
18930
+ const frame = stream.buf[i];
18931
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
18932
+ frame.passed = true;
18933
+ if (at2 < 0) at2 = i;
18934
+ } else if (frame.passed !== true) break;
18935
+ }
18936
+ if (at2 < 0) return false;
18937
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
18938
+ }
18191
18939
  #release(stream, frame, decision) {
18940
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18941
+ if (stale) this.#recordStat("droppedStale");
18192
18942
  if (decision) frame.meta.gapBefore = decision;
18193
18943
  stream.last = frame;
18194
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
18944
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
18945
+ stream.lastSbn = frame.meta.fecSbn;
18946
+ }
18947
+ if (frame.meta.recovered !== true) stream.sourced = true;
18195
18948
  this.#gate.onReleased?.(frame.data, frame.meta);
18196
- this.#emit(frame.data, frame.meta);
18949
+ if (!stale) this.#emit(frame.data, frame.meta);
18197
18950
  }
18198
18951
  };
18199
18952
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18234,6 +18987,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18234
18987
  this.#reclaimedTimingSamples = 0;
18235
18988
  this.#evictedTimingSamples = 0;
18236
18989
  this.#rejectedMovieFragments = 0;
18990
+ this.#recoveredHeadsAwaitingTiming = {
18991
+ held: 0,
18992
+ expired: 0,
18993
+ discarded: 0,
18994
+ superseded: 0
18995
+ };
18237
18996
  this.#sequencers = /* @__PURE__ */ new Map();
18238
18997
  this.#fecGate = null;
18239
18998
  this.#fecSequencer = null;
@@ -18342,6 +19101,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18342
19101
  #reclaimedTimingSamples;
18343
19102
  #evictedTimingSamples;
18344
19103
  #rejectedMovieFragments;
19104
+ #recoveredHeadsAwaitingTiming;
18345
19105
  #sequencers;
18346
19106
  #fecGate;
18347
19107
  #fecSequencer;
@@ -18447,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18447
19207
  /**
18448
19208
  * Typed view of the ISO-BMFF timing-sample budget.
18449
19209
  *
18450
- * This is the acceptance signal for the budget ratchet: `pending` walking
18451
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18452
- * and it is observable here rather than only as media silently stopping.
18453
- * Per repo policy this is a producer-owned counter -- never infer budget
18454
- * state by parsing logs.
19210
+ * Each stream admits timing against its own window of `capacity` slots
19211
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
19212
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
19213
+ * timing a stream declared and never delivered shows up there. `pending`
19214
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
19215
+ * a producer-owned counter -- never infer budget state by parsing logs.
18455
19216
  */
18456
19217
  get timingBudgetStats() {
18457
19218
  return {
@@ -18462,6 +19223,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18462
19223
  rejectedMovieFragments: this.#rejectedMovieFragments
18463
19224
  };
18464
19225
  }
19226
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
19227
+ get recoveredHeadsAwaitingTiming() {
19228
+ let pending = 0;
19229
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19230
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
19231
+ }
19232
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
19233
+ }
18465
19234
  get reorderStats() {
18466
19235
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18467
19236
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18503,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18503
19272
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18504
19273
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18505
19274
  }
19275
+ /** See FecReleaseSequencer.unsource. */
19276
+ unsourceFecRelease(packetId, catalogTrackName) {
19277
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
19278
+ }
19279
+ /** See FecReleaseSequencer.holdsRebuilt. */
19280
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
19281
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
19282
+ }
18506
19283
  /** Re-evaluate held gaps after FEC block state changed. */
18507
19284
  pokeFecRelease(packetId, catalogTrackName) {
18508
19285
  if (!this.#fecSequencer) return;
@@ -18590,10 +19367,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18590
19367
  get reassemblerDataUnitsReturned() {
18591
19368
  return this.#reassemblerDataUnitsReturned;
18592
19369
  }
18593
- #countOutcome(token) {
18594
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18595
- this.#feedOutcomes.set(token, count);
18596
- this.#lastOutcome = token;
19370
+ #countOutcome(token2) {
19371
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
19372
+ this.#feedOutcomes.set(token2, count);
19373
+ this.#lastOutcome = token2;
18597
19374
  this.#lastOutcomeCount = count;
18598
19375
  }
18599
19376
  #countDeferredOutcome(reason) {
@@ -18635,7 +19412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18635
19412
  const now = Date.now();
18636
19413
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18637
19414
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18638
- awaitingTiming.shift();
19415
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18639
19416
  }
18640
19417
  }
18641
19418
  }
@@ -18753,11 +19530,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
18753
19530
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
18754
19531
  return this.#drop("fragment-unexpected-fragmentation");
18755
19532
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
18756
- if (!resolver2) return this.#drop("fragment-no-resolver");
19533
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
18757
19534
  const pendingBefore = resolver2.pendingSampleCount;
18758
19535
  const evictedBefore = resolver2.evictedSampleCount;
18759
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
18760
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19536
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19537
+ const admitted = resolver2.addMovieFragment(
19538
+ mpu.mpuSequenceNumber,
19539
+ data,
19540
+ remaining,
19541
+ options?.recovered === true
19542
+ );
18761
19543
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
18762
19544
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
18763
19545
  if (!admitted) {
@@ -18777,7 +19559,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18777
19559
  };
18778
19560
  }
18779
19561
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
18780
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19562
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
18781
19563
  if (!resolver) return this.#drop("mfu-no-resolver");
18782
19564
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
18783
19565
  return this.#drop("mfu-empty-payload");
@@ -18798,6 +19580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18798
19580
  mpuSequenceNumber: mpu.mpuSequenceNumber,
18799
19581
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
18800
19582
  sampleNumber: mfuDuHeader.sampleNumber,
19583
+ sequenceNumber: info.sequenceNumber,
18801
19584
  arrivedAt: Date.now()
18802
19585
  });
18803
19586
  return this.#drop("mfu-sample-timing-missing");
@@ -18823,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
18823
19606
  info,
18824
19607
  resolver,
18825
19608
  options?.recovered === true,
18826
- options?.fecSbn
19609
+ options?.fecSbn,
19610
+ options?.fecSbnFirst
18827
19611
  );
18828
19612
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
18829
19613
  }
@@ -18916,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
18916
19700
  info,
18917
19701
  resolver,
18918
19702
  options?.recovered === true,
18919
- options?.fecSbn
19703
+ options?.fecSbn,
19704
+ options?.fecSbnFirst
18920
19705
  );
18921
19706
  if (outcome.record !== void 0) return outcome.record;
18922
19707
  return this.#drop(outcome.reason);
@@ -18936,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18936
19721
  * attribution exists to keep readable. Leaving the choice at the call site
18937
19722
  * is what keeps both true.
18938
19723
  */
18939
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19724
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
18940
19725
  const timing = resolver.sampleTiming(
18941
19726
  combined.mpuSequenceNumber,
18942
19727
  combined.movieFragmentSequenceNumber,
@@ -18953,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18953
19738
  );
18954
19739
  if (!meta) return { reason: "combined-meta-build-failed" };
18955
19740
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19741
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
18956
19742
  if (!resolver.consumeSample(
18957
19743
  combined.mpuSequenceNumber,
18958
19744
  combined.movieFragmentSequenceNumber,
@@ -19121,6 +19907,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19121
19907
  }
19122
19908
  this.#emitFrame(data, meta);
19123
19909
  }
19910
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19911
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19912
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19913
+ }
19124
19914
  /**
19125
19915
  * Claim and return the authored timing for a recovered sample. Recovery does
19126
19916
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19182,6 +19972,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19182
19972
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19183
19973
  this.#sequencers.clear();
19184
19974
  this.#fecSequencer?.discard();
19975
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19976
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19977
+ }
19185
19978
  this.#timingStreams.clear();
19186
19979
  this.#pendingTimingSamples = 0;
19187
19980
  this.#reliableMoqStartupGate.clear();
@@ -19200,10 +19993,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19200
19993
  return resolver;
19201
19994
  }
19202
19995
  #awaitTiming(streamKey, head) {
19203
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19204
- if (!awaitingTiming) return;
19996
+ const state = this.#timingStreams.get(streamKey);
19997
+ if (!state) return;
19998
+ if (!head.options?.recovered) {
19999
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
20000
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
20001
+ state.awaitingTiming = kept;
20002
+ }
20003
+ const awaitingTiming = state.awaitingTiming;
19205
20004
  awaitingTiming.push(head);
19206
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
20005
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
20006
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
20007
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
20008
+ }
20009
+ }
20010
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
20011
+ #leaveAwaitingTiming(head, outcome) {
20012
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19207
20013
  }
19208
20014
  /**
19209
20015
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19231,6 +20037,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19231
20037
  const state = this.#timingStreams.get(streamKey);
19232
20038
  if (!state) return;
19233
20039
  this.#timingStreams.delete(streamKey);
20040
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19234
20041
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19235
20042
  if (this.#pendingTimingSamples < 0) {
19236
20043
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22437,7 +23244,18 @@ function keyframeOverlaySsId(header) {
22437
23244
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22438
23245
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22439
23246
  }
23247
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23248
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23249
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23250
+ }
23251
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23252
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23253
+ return headerPsn + offset >>> 0;
23254
+ }
23255
+ var OVERLAY_ROUTED_LIMIT = 1024;
23256
+ var OVERLAY_PENDING_LIMIT = 256;
22440
23257
  var OVERLAY_OFFSET_MPUS = 16;
23258
+ var OVERLAY_REBUILT_LIMIT = 256;
22441
23259
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22442
23260
  function assertStreamIdentity(role, stream) {
22443
23261
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -22548,6 +23366,7 @@ var MmtpRouter = class {
22548
23366
  #provenance = /* @__PURE__ */ new Map();
22549
23367
  #onFlowProtected = null;
22550
23368
  #unadmittedListedDrops = 0;
23369
+ #admittedSourceDatagrams = 0;
22551
23370
  #unlistedRepairSetAside = 0;
22552
23371
  #assistBlocks = /* @__PURE__ */ new Map();
22553
23372
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22604,6 +23423,26 @@ var MmtpRouter = class {
22604
23423
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22605
23424
  */
22606
23425
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23426
+ /**
23427
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23428
+ * sender's own packet_sequence_number: a recovered packet's header psn
23429
+ * equal to its SS_ID minus that offset. While it equals the stream's
23430
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23431
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23432
+ * an MPU no member of which arrived).
23433
+ */
23434
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23435
+ /**
23436
+ * The same, shown by the overlay alone, before any base recovery did: a
23437
+ * packet it rebuilt whose base copy carries its protected psn
23438
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
23439
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
23440
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
23441
+ * make the base drop a late base copy of a keyframe the overlay never
23442
+ * finished, and lift a block's late-source cut, which nothing before
23443
+ * this offset did.
23444
+ */
23445
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22607
23446
  /**
22608
23447
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22609
23448
  * not parsed, typically because its overlap copy was parsed first
@@ -22673,7 +23512,9 @@ var MmtpRouter = class {
22673
23512
  recovered: 0,
22674
23513
  restarts: 0,
22675
23514
  unsequencedDropped: 0,
22676
- protectedDropped: 0
23515
+ protectedDropped: 0,
23516
+ baseFed: 0,
23517
+ baseUndetermined: 0
22677
23518
  };
22678
23519
  /** Callback fired when a delta-track FEC block completes or fails. */
22679
23520
  #onDeltaBlockCompleteCb = null;
@@ -22846,7 +23687,7 @@ var MmtpRouter = class {
22846
23687
  const cb = this.#getFrameCallback(packetId);
22847
23688
  if (isVideo && !assembler) {
22848
23689
  if (frameMeta.recovered) {
22849
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23690
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
22850
23691
  } else {
22851
23692
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
22852
23693
  }
@@ -22879,13 +23720,18 @@ var MmtpRouter = class {
22879
23720
  this.#onDeltaBlockCompleteCb?.(packetId, false);
22880
23721
  }
22881
23722
  };
22882
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23723
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
22883
23724
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
22884
23725
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
22885
23726
  this.#onDeltaBlockCompleteCb?.(packetId, true);
22886
23727
  }
22887
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
22888
- this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23728
+ if (span === void 0) {
23729
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23730
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23731
+ } else {
23732
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
23733
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
23734
+ }
22889
23735
  };
22890
23736
  }
22891
23737
  }
@@ -22930,8 +23776,9 @@ var MmtpRouter = class {
22930
23776
  }
22931
23777
  /**
22932
23778
  * FEC-recovered source symbols whose reconstructed header named a
22933
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
22934
- * a hop re-stamped the sequence after the sender protected the packet.
23779
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
23780
+ * or, rebuilt by the overlay, than their base copy carries: a hop
23781
+ * re-stamped the sequence after the sender protected the packet.
22935
23782
  */
22936
23783
  get restampedRecoveredSequenceCount() {
22937
23784
  return this.#restampedRecoveredSeqs;
@@ -23110,6 +23957,7 @@ var MmtpRouter = class {
23110
23957
  enableMseAssembler(packetId, config, catalogTrackName) {
23111
23958
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23112
23959
  const previous = this.#mseAssemblers.get(assemblerKey);
23960
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23113
23961
  if (previous) {
23114
23962
  previous.flush();
23115
23963
  previous.dispose();
@@ -23236,6 +24084,10 @@ var MmtpRouter = class {
23236
24084
  get keyframeOverlayStats() {
23237
24085
  return { ...this.#overlayStats };
23238
24086
  }
24087
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
24088
+ get recoveredHeadsAwaitingTiming() {
24089
+ return this.#parser.recoveredHeadsAwaitingTiming;
24090
+ }
23239
24091
  #isOverlayRepair(repairPacketId, repairTrackName) {
23240
24092
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23241
24093
  }
@@ -23256,7 +24108,8 @@ var MmtpRouter = class {
23256
24108
  fec,
23257
24109
  k,
23258
24110
  offsets: /* @__PURE__ */ new Map(),
23259
- moqBlocks: /* @__PURE__ */ new Set()
24111
+ moqBlocks: /* @__PURE__ */ new Set(),
24112
+ rebuilt: /* @__PURE__ */ new Map()
23260
24113
  };
23261
24114
  this.#overlays.set(repairPacketId, inst);
23262
24115
  return () => {
@@ -23286,12 +24139,18 @@ var MmtpRouter = class {
23286
24139
  */
23287
24140
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23288
24141
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
24142
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
24143
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
24144
+ stream.overlayToBase.routed.clear();
24145
+ stream.overlayToBase.pending.clear();
24146
+ }
23289
24147
  }
23290
24148
  #resetOverlay(inst) {
23291
24149
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23292
24150
  inst.last = void 0;
23293
24151
  inst.offsets.clear();
23294
24152
  inst.moqBlocks.clear();
24153
+ inst.rebuilt.clear();
23295
24154
  }
23296
24155
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23297
24156
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23342,18 +24201,33 @@ var MmtpRouter = class {
23342
24201
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23343
24202
  * every overlay installed for its source, at that SS_ID, with the bytes the
23344
24203
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23345
- * included (7.1, 7.4.3).
23346
- */
23347
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
24204
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
24205
+ * FEC reads it.
24206
+ *
24207
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
24208
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
24209
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
24210
+ * the stream unstamped at the SS_ID offset it states, for the release gate
24211
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
24212
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
24213
+ */
24214
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23348
24215
  if (this.#overlays.size === 0) return;
23349
24216
  const ssId = keyframeOverlaySsId(header);
23350
24217
  if (ssId === void 0) return;
24218
+ let rebuiltPsn;
23351
24219
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23352
24220
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24221
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24222
+ inst.rebuilt.delete(ssId);
23353
24223
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23354
24224
  if (identity !== null) {
23355
24225
  inst.offsets.delete(identity.mpuSequenceNumber);
23356
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
24226
+ inst.offsets.set(identity.mpuSequenceNumber, {
24227
+ psnOffset: ssId - sequenceNumber >>> 0,
24228
+ overlaySsId: ssId,
24229
+ ...baseSsId === void 0 ? {} : { baseSsId }
24230
+ });
23357
24231
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23358
24232
  }
23359
24233
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23375,6 +24249,14 @@ var MmtpRouter = class {
23375
24249
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23376
24250
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23377
24251
  }
24252
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
24253
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
24254
+ if (rebuiltPsn !== sequenceNumber) {
24255
+ this.#restampedRecoveredSeqs++;
24256
+ } else if (baseSsId !== void 0) {
24257
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24258
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24259
+ }
23378
24260
  }
23379
24261
  #noteOverlayMoqBlock(inst, blockStart) {
23380
24262
  inst.moqBlocks.add(blockStart);
@@ -23408,11 +24290,16 @@ var MmtpRouter = class {
23408
24290
  }
23409
24291
  /**
23410
24292
  * Route an overlay-recovered block's packets as source media of the
23411
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23412
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23413
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23414
- * from its keyframe's own offset; without one, a stream the base never
23415
- * re-stamped keeps the header's sequence and any other drops the packet.
24293
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
24294
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
24295
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
24296
+ * base FEC recovered does, so it waits behind base frames still in flight,
24297
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
24298
+ * longer than unanchored; one whose base SS_ID is undetermined has no
24299
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
24300
+ * advance. Each packet's wire sequence is restored from its keyframe's own
24301
+ * offset; without one, a stream the base never re-stamped keeps the
24302
+ * header's sequence and any other drops the packet.
23416
24303
  */
23417
24304
  #routeOverlayRecovered(inst, blockStart, recovered) {
23418
24305
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23432,22 +24319,139 @@ var MmtpRouter = class {
23432
24319
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23433
24320
  const restamped = this.#restampedStreams.has(streamKey);
23434
24321
  const restored = /* @__PURE__ */ new Set();
24322
+ const baseSsIds = /* @__PURE__ */ new Map();
24323
+ const gateOnly = /* @__PURE__ */ new Set();
23435
24324
  const symbols = result.symbols.flatMap((sym) => {
23436
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23437
- if (offset !== void 0) {
23438
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24325
+ const overlaySsId = serial32Add(blockStart, sym.index);
24326
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24327
+ if (anchor !== void 0) {
24328
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23439
24329
  restored.add(packetSequenceNumber);
23440
- return [{ ...sym, packetSequenceNumber }];
24330
+ const out = { ...sym, packetSequenceNumber };
24331
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24332
+ if (baseSsId !== void 0) {
24333
+ baseSsIds.set(sym.index, baseSsId);
24334
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24335
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
24336
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24337
+ }
24338
+ }
24339
+ return [out];
24340
+ }
24341
+ if (!restamped) {
24342
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24343
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24344
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24345
+ if (baseSsId !== void 0) {
24346
+ baseSsIds.set(sym.index, baseSsId);
24347
+ if (fedSsId === void 0) gateOnly.add(sym.index);
24348
+ } else {
24349
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
24350
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
24351
+ }
24352
+ return [sym];
23441
24353
  }
23442
- if (!restamped) return [sym];
23443
24354
  this.#overlayStats.unsequencedDropped++;
23444
24355
  return [];
23445
24356
  });
23446
24357
  this.#overlayStats.recovered++;
24358
+ const mpuFirst = /* @__PURE__ */ new Map();
24359
+ for (const sym of symbols) {
24360
+ const ssId = baseSsIds.get(sym.index);
24361
+ const mpu = sym.fragment.mpuSequenceNumber;
24362
+ const first = mpuFirst.get(mpu);
24363
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
24364
+ }
24365
+ const firstSsIds = /* @__PURE__ */ new Map();
24366
+ for (const sym of symbols) {
24367
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
24368
+ }
23447
24369
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23448
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24370
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24371
+ baseSsIds,
24372
+ firstSsIds
23449
24373
  });
23450
24374
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24375
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24376
+ }
24377
+ /**
24378
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24379
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24380
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24381
+ * base recovery showed; `baseSsIds` holds no other),
24382
+ * so it is fed to the base decoder as a rebuilt source
24383
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24384
+ * fewer to solve, and its repair can recover the block's other packets.
24385
+ * A base block no repair has stated yet takes it when one does
24386
+ * (#feedPendingRebuilt).
24387
+ *
24388
+ * The overlay route already handled each packet fed (delivered it, or
24389
+ * held an untimed head for its FT=1), so a base recovery routes none of
24390
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24391
+ * held head would wait in the parser for timing its first copy consumed.
24392
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24393
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24394
+ * that crossed no multicast socket, which the provenance counted from the
24395
+ * block's own arrivals cannot show.
24396
+ */
24397
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24398
+ const fec = this.#fecManager;
24399
+ const owner = inst.leg.sourcePacketId;
24400
+ const name = inst.leg.sourceCatalogTrackName;
24401
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24402
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24403
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24404
+ const feeds = [];
24405
+ for (const sym of symbols) {
24406
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24407
+ const ssId = baseSsIds.get(sym.index);
24408
+ if (ssId === void 0) {
24409
+ this.#overlayStats.baseUndetermined++;
24410
+ continue;
24411
+ }
24412
+ routed.delete(ssId);
24413
+ routed.add(ssId);
24414
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24415
+ feeds.push([ssId, sym.rawSymbol]);
24416
+ }
24417
+ for (const [ssId, symbolData] of feeds) {
24418
+ if (this.#classifyPacketId(owner) === null) return;
24419
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24420
+ }
24421
+ }
24422
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24423
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24424
+ const fec = this.#fecManager;
24425
+ if (!fec?.feedRebuiltSourceSymbol) return;
24426
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24427
+ if (coords == null) {
24428
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24429
+ pending.delete(ssId);
24430
+ pending.set(ssId, symbolData.slice());
24431
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24432
+ return;
24433
+ }
24434
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24435
+ if (!fed.taken) return;
24436
+ this.#overlayStats.baseFed++;
24437
+ if (fed.recovered === null) return;
24438
+ const sbn = coords.sbn;
24439
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24440
+ fec.cancelBlockTimeout(sbn, owner, name);
24441
+ fec.countRecovery(owner, sbn, name);
24442
+ fec.markBlockCompleted(sbn, owner, name);
24443
+ });
24444
+ }
24445
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24446
+ #feedPendingRebuilt(owner, name, block) {
24447
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24448
+ if (pending === void 0 || pending.size === 0) return;
24449
+ for (const [ssId, symbolData] of [...pending]) {
24450
+ if (this.#classifyPacketId(owner) === null) return;
24451
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24452
+ pending.delete(ssId);
24453
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24454
+ }
23451
24455
  }
23452
24456
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23453
24457
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23579,13 +24583,18 @@ var MmtpRouter = class {
23579
24583
  if (!entry) return;
23580
24584
  entry.receiver.release();
23581
24585
  entry.protected = false;
24586
+ entry.userAgent = false;
23582
24587
  this.#assistBlocks.clear();
23583
24588
  this.#assistFailures.clear();
23584
24589
  }
23585
- /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
24590
+ /** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
23586
24591
  get unadmittedListedDropCount() {
23587
24592
  return this.#unadmittedListedDrops;
23588
24593
  }
24594
+ /** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
24595
+ get admittedSourceDatagramCount() {
24596
+ return this.#admittedSourceDatagrams;
24597
+ }
23589
24598
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23590
24599
  get unlistedRepairSetAsideCount() {
23591
24600
  return this.#unlistedRepairSetAside;
@@ -23623,6 +24632,13 @@ var MmtpRouter = class {
23623
24632
  }
23624
24633
  return false;
23625
24634
  }
24635
+ /** Whether a protected flow the user agent admitted lists `packetId`. */
24636
+ #userAgentGoverns(packetId) {
24637
+ for (const entry of this.#provenance.values()) {
24638
+ if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
24639
+ }
24640
+ return false;
24641
+ }
23626
24642
  /**
23627
24643
  * A MoQ repair symbol no protected flow lists, owned by a source track one
23628
24644
  * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
@@ -23713,7 +24729,7 @@ var MmtpRouter = class {
23713
24729
  fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
23714
24730
  fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
23715
24731
  fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
23716
- })) {
24732
+ }, true)) {
23717
24733
  this.#unlistedAssistedRecoveries++;
23718
24734
  this.#assistBlocks.delete(key);
23719
24735
  return;
@@ -24065,6 +25081,8 @@ var MmtpRouter = class {
24065
25081
  )) return;
24066
25082
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24067
25083
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
25084
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
25085
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24068
25086
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24069
25087
  const recovered = fec.feedRepairSymbol(
24070
25088
  ownerPacketId,
@@ -24115,7 +25133,7 @@ var MmtpRouter = class {
24115
25133
  catalogTrackName
24116
25134
  )) continue;
24117
25135
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24118
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25136
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24119
25137
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24120
25138
  }
24121
25139
  if (payload.oversized) continue;
@@ -24202,6 +25220,8 @@ var MmtpRouter = class {
24202
25220
  this.#droppedPreSyncVideo.delete(streamKey);
24203
25221
  this.#latestVideoSeqByStream.delete(streamKey);
24204
25222
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25223
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25224
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24205
25225
  this.#multicastRapTwinByStream.delete(streamKey);
24206
25226
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24207
25227
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24328,6 +25348,12 @@ var MmtpRouter = class {
24328
25348
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24329
25349
  * own RAP cadence, so no timer is involved.
24330
25350
  *
25351
+ * While the switch is staged, the FEC release gate lets a keyframe the
25352
+ * overlay rebuilt reach it, on either side, when the keyframe did before
25353
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
25354
+ * decides on what each side delivered, and the commit discards the gate's
25355
+ * frames of the outgoing stream.
25356
+ *
24331
25357
  * The commit is one synchronous transition inside the frame path: the
24332
25358
  * outgoing stream's already-admitted frames are released (its queued FEC
24333
25359
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24378,6 +25404,9 @@ var MmtpRouter = class {
24378
25404
  droppedIncomingFrames: 0
24379
25405
  };
24380
25406
  this.#staged = state;
25407
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25408
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25409
+ }
24381
25410
  return {
24382
25411
  cancel: () => {
24383
25412
  if (this.#staged !== state) return false;
@@ -24460,7 +25489,17 @@ var MmtpRouter = class {
24460
25489
  */
24461
25490
  #routeParsedFrame(data, meta) {
24462
25491
  if (this.#classifyPacketId(meta.packetId) === null) return;
24463
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
25492
+ if (this.#staged !== null) {
25493
+ let consumed = false;
25494
+ this.#fecPokeDepth++;
25495
+ try {
25496
+ consumed = this.#interceptSwitchFrame(data, meta);
25497
+ } finally {
25498
+ this.#fecPokeDepth--;
25499
+ this.#flushPendingFecPokes();
25500
+ }
25501
+ if (consumed) return;
25502
+ }
24464
25503
  this.#deliverParsedFrame(data, meta);
24465
25504
  }
24466
25505
  #commitStreamSwitch(state, rap, reason) {
@@ -24612,12 +25651,16 @@ var MmtpRouter = class {
24612
25651
  this.#parser.deferFecRelease(work);
24613
25652
  } finally {
24614
25653
  this.#fecPokeDepth--;
24615
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
24616
- const pokes = [...this.#pendingFecPokes.values()];
24617
- this.#pendingFecPokes.clear();
24618
- for (const poke of pokes) {
24619
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
24620
- }
25654
+ this.#flushPendingFecPokes();
25655
+ }
25656
+ }
25657
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
25658
+ #flushPendingFecPokes() {
25659
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25660
+ const pokes = [...this.#pendingFecPokes.values()];
25661
+ this.#pendingFecPokes.clear();
25662
+ for (const poke of pokes) {
25663
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
24621
25664
  }
24622
25665
  }
24623
25666
  }
@@ -24695,6 +25738,14 @@ var MmtpRouter = class {
24695
25738
  onRap: (packetId, catalogTrackName, ssId) => {
24696
25739
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
24697
25740
  },
25741
+ // A staged switch commits on when each side's frames reach it
25742
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
25743
+ // they did unanchored.
25744
+ releaseUnanchored: (packetId, catalogTrackName) => {
25745
+ const staged = this.#staged;
25746
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
25747
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
25748
+ },
24698
25749
  onReleased: (data, meta) => {
24699
25750
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
24700
25751
  const timing = meta.authoredTiming;
@@ -24836,6 +25887,7 @@ var MmtpRouter = class {
24836
25887
  const parsed = this.#parser.parsePacket(packet);
24837
25888
  if (!parsed) return;
24838
25889
  const info = parsed.info;
25890
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
24839
25891
  if (source === "multicast" && !manifestAdmitted) {
24840
25892
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
24841
25893
  }
@@ -24869,6 +25921,12 @@ var MmtpRouter = class {
24869
25921
  debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
24870
25922
  return;
24871
25923
  }
25924
+ if (ingress.admittedBy === USER_AGENT_VERIFIED) {
25925
+ this.#noteFlowAdmitted(ingress.flow);
25926
+ protectedFlow.userAgent = true;
25927
+ this.#processMMTP(packet, source, true, ingress, overlapCopy);
25928
+ return;
25929
+ }
24872
25930
  const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
24873
25931
  if (verdict === "admitted") {
24874
25932
  this.#noteFlowAdmitted(ingress.flow);
@@ -24886,6 +25944,11 @@ var MmtpRouter = class {
24886
25944
  return;
24887
25945
  }
24888
25946
  }
25947
+ const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
25948
+ if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
25949
+ this.#unadmittedListedDrops++;
25950
+ return;
25951
+ }
24889
25952
  if (info.isRepair) {
24890
25953
  const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
24891
25954
  if (overlay !== void 0) {
@@ -24903,7 +25966,7 @@ var MmtpRouter = class {
24903
25966
  );
24904
25967
  if (!repair) return;
24905
25968
  if (ownerPacketId != null && this.#fecManager) {
24906
- if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
25969
+ if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
24907
25970
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
24908
25971
  const count = this.#repairPidCounts[info.packetId];
24909
25972
  if (count <= 2 || count % 500 === 0) {
@@ -25106,7 +26169,8 @@ var MmtpRouter = class {
25106
26169
  parsed.header,
25107
26170
  info.sequenceNumber,
25108
26171
  info.rapFlag,
25109
- source
26172
+ source,
26173
+ fecPayloadId?.ssId
25110
26174
  );
25111
26175
  }
25112
26176
  }
@@ -25132,7 +26196,8 @@ var MmtpRouter = class {
25132
26196
  source: parserOptions?.source,
25133
26197
  deliveryProvenance: parserOptions?.deliveryProvenance,
25134
26198
  catalogTrackName: parserOptions?.catalogTrackName,
25135
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26199
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26200
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25136
26201
  };
25137
26202
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25138
26203
  else this.#parser.feed(packet, feedOptions);
@@ -25256,7 +26321,8 @@ var MmtpRouter = class {
25256
26321
  if (stream === void 0) {
25257
26322
  stream = {
25258
26323
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25259
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26324
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26325
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25260
26326
  };
25261
26327
  byName.set(catalogTrackName, stream);
25262
26328
  }
@@ -25371,8 +26437,16 @@ var MmtpRouter = class {
25371
26437
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25372
26438
  this.#emitDirectVideo(packetId, data, meta, cb);
25373
26439
  }
25374
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26440
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25375
26441
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26442
+ if (this.#isFecTrack(packetId)) {
26443
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26444
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26445
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26446
+ this.#emitDirectVideo(packetId, data, meta, cb);
26447
+ return;
26448
+ }
26449
+ }
25376
26450
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25377
26451
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25378
26452
  this.#drainDirectRecoveredVideo(
@@ -25482,7 +26556,7 @@ var MmtpRouter = class {
25482
26556
  }
25483
26557
  // ── Private: FEC recovery routing ───────────────────────────────
25484
26558
  /** Route WASM-recovered block through appropriate path based on container kind. */
25485
- #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
26559
+ #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
25486
26560
  const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
25487
26561
  if (ownerPacketId === void 0) {
25488
26562
  throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
@@ -25499,7 +26573,7 @@ var MmtpRouter = class {
25499
26573
  ownerPacketId,
25500
26574
  blockProfile
25501
26575
  });
25502
- return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
26576
+ return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
25503
26577
  }
25504
26578
  /**
25505
26579
  * A recovered source packet of a protected flow is admitted on its own
@@ -25511,9 +26585,31 @@ var MmtpRouter = class {
25511
26585
  * recovered packets is refused is unrecovered (7.2.7): its caller leaves it
25512
26586
  * open, so it reaches its FEC deadline and fails by class. Always true
25513
26587
  * outside a protected flow.
25514
- */
25515
- #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
25516
- const receiver = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
26588
+ *
26589
+ * On a flow the user agent admitted, the live decoder's recoveries are kept
26590
+ * without a record: while the flow is protected that decoder takes only
26591
+ * symbols the user agent or a record verified (a flow-less multicast
26592
+ * datagram of a listed track is dropped or set aside in #processMMTP), so
26593
+ * what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
26594
+ * unlisted MoQ repair and still needs a record.
26595
+ * ponytail: per-flow flag; per-block provenance only if a protected flow
26596
+ * ever mixes verified and unverified live inputs.
26597
+ * ponytail: a worker frame computed before the protection reset can still
26598
+ * arrive after it (frames carry no epoch); its inputs are MoQ copies read
26599
+ * and played before protection, so it admits nothing new. Drop pre-reset
26600
+ * frames by epoch only if pre-protection input ever matters.
26601
+ */
26602
+ #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26603
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26604
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26605
+ result = {
26606
+ ...result,
26607
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26608
+ };
26609
+ }
26610
+ const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
26611
+ const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
26612
+ const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
25517
26613
  let recovered = true;
25518
26614
  if (receiver) {
25519
26615
  const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
@@ -25525,6 +26621,8 @@ var MmtpRouter = class {
25525
26621
  let admitted = false;
25526
26622
  if (receiver === "ambiguous") {
25527
26623
  this.#ambiguousRecoveredDrops++;
26624
+ } else if (userAgent) {
26625
+ admitted = true;
25528
26626
  } else {
25529
26627
  const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
25530
26628
  wire.set(sym.rawSymbol);
@@ -25552,12 +26650,12 @@ var MmtpRouter = class {
25552
26650
  catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
25553
26651
  );
25554
26652
  if (flows.size > 1) return "ambiguous";
25555
- for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
26653
+ for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
25556
26654
  let match;
25557
- for (const { receiver, ingress } of this.#provenance.values()) {
25558
- if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
26655
+ for (const entry of this.#provenance.values()) {
26656
+ if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
25559
26657
  if (match) return "ambiguous";
25560
- match = receiver;
26658
+ match = entry;
25561
26659
  }
25562
26660
  }
25563
26661
  return match;
@@ -25565,7 +26663,12 @@ var MmtpRouter = class {
25565
26663
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25566
26664
  if (ownerPacketId == null) return;
25567
26665
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25568
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26666
+ const fecSbnOf = (index) => {
26667
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26668
+ const fecSbn = overlay.baseSsIds.get(index);
26669
+ const fecSbnFirst = overlay.firstSsIds.get(index);
26670
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
26671
+ };
25569
26672
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25570
26673
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25571
26674
  if (ssIdSeqOffset !== void 0) {
@@ -25573,7 +26676,10 @@ var MmtpRouter = class {
25573
26676
  ...result,
25574
26677
  symbols: result.symbols.map((sym) => {
25575
26678
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25576
- if (wire === sym.packetSequenceNumber) return sym;
26679
+ if (wire === sym.packetSequenceNumber) {
26680
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26681
+ return sym;
26682
+ }
25577
26683
  this.#restampedRecoveredSeqs++;
25578
26684
  this.#restampedStreams.add(streamKey);
25579
26685
  return { ...sym, packetSequenceNumber: wire };
@@ -25609,6 +26715,29 @@ var MmtpRouter = class {
25609
26715
  }
25610
26716
  );
25611
26717
  }
26718
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26719
+ ownerPacketId,
26720
+ sym.fragment.mpuSequenceNumber,
26721
+ sym.fragment.movieFragmentSequenceNumber,
26722
+ sym.fragment.sampleNumber,
26723
+ catalogTrackName
26724
+ );
26725
+ for (const sym of result.symbols) {
26726
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26727
+ this.#feedViaParser(
26728
+ sym.rawSymbol,
26729
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26730
+ void 0,
26731
+ {
26732
+ skipFecPayloadId: true,
26733
+ recovered: true,
26734
+ deliveryProvenance,
26735
+ ...fecSbnOf(sym.index),
26736
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26737
+ }
26738
+ );
26739
+ }
26740
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25612
26741
  const gated = this.#isFecTrack(ownerPacketId);
25613
26742
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25614
26743
  if (assembler) {
@@ -26339,11 +27468,8 @@ function providerErrorOf(error) {
26339
27468
  return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
26340
27469
  }
26341
27470
  var MAX_TRACKED_DROPPED_PIDS = 16;
26342
- function getMulticastGlobals() {
26343
- return globalThis;
26344
- }
26345
27471
  function hasMulticastGateway() {
26346
- return typeof getMulticastGlobals().multicast?.subscribe === "function";
27472
+ return typeof resolveMulticastGateway()?.subscribe === "function";
26347
27473
  }
26348
27474
  function senderOf(event) {
26349
27475
  const sourceAddress = event.sourceAddress;
@@ -26435,6 +27561,9 @@ var MulticastTransport = class {
26435
27561
  // listener closure, so an in-place admission change (setContext) applies
26436
27562
  // from the next packet on without rejoining.
26437
27563
  #context = void 0;
27564
+ #deliveredContext = void 0;
27565
+ /** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
27566
+ #userAgentGateway = void 0;
26438
27567
  // The live join's provider-side filter. An in-place widening must reach it,
26439
27568
  // or the provider never delivers the new ID; null without a live join.
26440
27569
  #providerFilter = null;
@@ -26471,7 +27600,7 @@ var MulticastTransport = class {
26471
27600
  this.#dataPlaneLive = false;
26472
27601
  this.#sender = void 0;
26473
27602
  if (this.#disposed) return this.#declined(generation, "disposed");
26474
- const gateway = this.#gateway ?? getMulticastGlobals().multicast;
27603
+ const gateway = this.#gateway ?? resolveMulticastGateway();
26475
27604
  if (typeof gateway?.subscribe !== "function") {
26476
27605
  console.warn("[MulticastTransport] Multicast API not available");
26477
27606
  return this.#declined(generation, "unavailable");
@@ -26487,15 +27616,16 @@ var MulticastTransport = class {
26487
27616
  console.log(
26488
27617
  `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
26489
27618
  );
27619
+ const userAgent = isUserAgentVerifiedGateway(gateway);
26490
27620
  const subscribeConfig = toWindowMulticastSubscribeConfig({
26491
27621
  source: config.sourceAddress,
26492
27622
  senderAddress: config.senderAddress,
26493
27623
  group: config.groupAddress,
26494
27624
  port: config.port,
26495
- mode: config.mode,
26496
- amtRelay: config.amtRelay,
26497
- amtRelayPort: config.amtRelayPort,
26498
- amtDiscovery: config.amtDiscovery,
27625
+ mode: userAgent ? "native" : config.mode,
27626
+ amtRelay: userAgent ? void 0 : config.amtRelay,
27627
+ amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
27628
+ amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
26499
27629
  packetIds: config.packetIds,
26500
27630
  tracks: config.tracks,
26501
27631
  atsc3: config.atsc3
@@ -26541,7 +27671,8 @@ var MulticastTransport = class {
26541
27671
  acknowledgedUpdate: 0,
26542
27672
  inFlight: 0
26543
27673
  };
26544
- this.#context = context;
27674
+ this.#userAgentGateway = userAgent ? gateway : void 0;
27675
+ this.#setContext(context);
26545
27676
  if (packetIds) {
26546
27677
  for (const packetId of packetIds) {
26547
27678
  if (!this.#acceptedBytesByPacketId.has(packetId)) {
@@ -26549,7 +27680,7 @@ var MulticastTransport = class {
26549
27680
  }
26550
27681
  }
26551
27682
  }
26552
- this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
27683
+ this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
26553
27684
  const delivery = subscription.delivery ?? "datagram";
26554
27685
  if (delivery !== "datagram" && delivery !== "ordered-relay") {
26555
27686
  throw new Error(
@@ -26630,7 +27761,7 @@ var MulticastTransport = class {
26630
27761
  }
26631
27762
  if (!this.#acceptPacket(packet, sourceAddress)) return false;
26632
27763
  const liveness = !this.#isRecordCandidate(packet);
26633
- this.#onPacket(packet, this.#context);
27764
+ this.#onPacket(packet, this.#deliveredContext);
26634
27765
  return liveness;
26635
27766
  }
26636
27767
  #isRecordCandidate(packet) {
@@ -26841,8 +27972,10 @@ var MulticastTransport = class {
26841
27972
  return this.#subscription !== null;
26842
27973
  }
26843
27974
  /**
26844
- * What the live join delivers with every accepted packet (the join's
26845
- * `context`, or the last setContext), or undefined without a live join.
27975
+ * The live join's context as the caller gave it (the join's `context`, or
27976
+ * the last setContext), or undefined without a live join. On a native
27977
+ * provider each accepted packet is delivered with a marked copy of it
27978
+ * (`userAgentVerifiedIngress`); this getter never carries the mark.
26846
27979
  */
26847
27980
  get context() {
26848
27981
  return this.#context;
@@ -26866,7 +27999,16 @@ var MulticastTransport = class {
26866
27999
  "[MulticastTransport] setContext needs a live join; join() carries the first context"
26867
28000
  );
26868
28001
  }
28002
+ this.#setContext(context);
28003
+ }
28004
+ /**
28005
+ * The join's context, and what the subscription delivers with each packet:
28006
+ * marked admitted by the user agent when the gateway is its native
28007
+ * MulticastReceiver provider. feedPacket never marks.
28008
+ */
28009
+ #setContext(context) {
26869
28010
  this.#context = context;
28011
+ this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
26870
28012
  }
26871
28013
  /**
26872
28014
  * Replace the live join's packet-ID allow-list in place, without rejoining.
@@ -26998,6 +28140,8 @@ var MulticastTransport = class {
26998
28140
  this.#matchedSource = void 0;
26999
28141
  this.#providerFilter = null;
27000
28142
  this.#context = void 0;
28143
+ this.#deliveredContext = void 0;
28144
+ this.#userAgentGateway = void 0;
27001
28145
  this.#withdrawnPacketIds.clear();
27002
28146
  }
27003
28147
  /** The ingress filter; an accepted packet is counted here, once. */
@@ -27320,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
27320
28464
  function isSymbolCounts(value) {
27321
28465
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
27322
28466
  }
28467
+ function isSpanFailure(value) {
28468
+ if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
28469
+ const length = value["length"];
28470
+ const sources = value["sources"];
28471
+ return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
28472
+ }
27323
28473
  function isNullableNonNegative(value) {
27324
28474
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
27325
28475
  }
@@ -27339,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
27339
28489
  case "recovered":
27340
28490
  return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
27341
28491
  case "failed":
27342
- return hasExactKeys(value, ["version", "type", "block", "blockClass", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]);
28492
+ return hasExactKeys(
28493
+ value,
28494
+ "span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
28495
+ ) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
27343
28496
  case "repair-preempted":
27344
28497
  case "repair-deferred":
27345
28498
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -27605,7 +28758,7 @@ var MulticastManager = class {
27605
28758
  onPublisherRestart: (packetId, catalogTrackName) => {
27606
28759
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
27607
28760
  },
27608
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28761
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
27609
28762
  const pathCount = this.#fec.blockPathCount(
27610
28763
  sbn,
27611
28764
  packetId,
@@ -27623,7 +28776,8 @@ var MulticastManager = class {
27623
28776
  symbols: {
27624
28777
  multicast: pathCount?.multicastSymbols ?? 0,
27625
28778
  moq: pathCount?.moqSymbols ?? 0
27626
- }
28779
+ },
28780
+ ...span === void 0 ? {} : { span }
27627
28781
  });
27628
28782
  },
27629
28783
  onFrameGap: (decision) => {
@@ -28350,8 +29504,17 @@ var MulticastManager = class {
28350
29504
  * repair roles and owners are registered here; in-band owners stay the
28351
29505
  * caller's. Joining is best effort: a group that fails to join is logged
28352
29506
  * and leaves the media join as it was.
28353
- */
28354
- joinRepairEndpoints(endpoints, provider) {
29507
+ *
29508
+ * `options.gateway` names the gateway a subscribe callback reads through.
29509
+ * When it is the browser's own MulticastReceiver provider, the callback's
29510
+ * packets are fed as admitted by the user agent
29511
+ * (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
29512
+ * The mark rests on this statement, not on the data path: every packet the
29513
+ * callback delivers must come from a subscription opened on
29514
+ * `options.gateway`. A callback that also merges another source (a MoQ or
29515
+ * WebSocket repair feed) must not name a native provider here.
29516
+ */
29517
+ joinRepairEndpoints(endpoints, provider, options) {
28355
29518
  const releases = [];
28356
29519
  let released = false;
28357
29520
  const safely = (each) => {
@@ -28373,17 +29536,19 @@ var MulticastManager = class {
28373
29536
  ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
28374
29537
  };
28375
29538
  let sender;
28376
- const onPacket = (packet, sourceAddress) => {
29539
+ const feed = (packet, sourceAddress, ingressToFeed) => {
28377
29540
  if (released) return;
28378
29541
  if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
28379
- this.#router.feedPacket(packet, "multicast", ingress);
29542
+ this.#router.feedPacket(packet, "multicast", ingressToFeed);
28380
29543
  };
28381
29544
  const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
28382
29545
  if (typeof provider === "function") {
29546
+ const marked = userAgentVerifiedIngress(ingress, options?.gateway);
29547
+ const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
28383
29548
  Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
28384
29549
  continue;
28385
29550
  }
28386
- const transport = new MulticastTransport((packet) => onPacket(packet), {
29551
+ const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
28387
29552
  gateway: provider,
28388
29553
  lockedTo: () => this.#transport.sender
28389
29554
  });
@@ -28395,11 +29560,12 @@ var MulticastManager = class {
28395
29560
  mode: endpoint.mode,
28396
29561
  amtRelay: endpoint.amtRelay,
28397
29562
  amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
29563
+ amtDiscovery: endpoint.amtDiscovery,
28398
29564
  packetIds: endpoint.packetIds,
28399
29565
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
28400
29566
  // A protected repair group's records ride in band on its port (7.2.3).
28401
29567
  ...endpoint.flow === void 0 ? {} : { protected: true }
28402
- }).then((joined) => {
29568
+ }, ingress).then((joined) => {
28403
29569
  if (!joined && !released) fail(transport.lastJoinOutcome);
28404
29570
  });
28405
29571
  }
@@ -28991,10 +30157,12 @@ var MulticastManager = class {
28991
30157
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
28992
30158
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
28993
30159
  * pending hole and reported here, and `closeIsoHoles` must be called for it
28994
- * once its range FETCH ends (or none is sent).
30160
+ * once its range FETCH ends (or none is sent). `onExpired` is told of a
30161
+ * pending hole the stream runs past the locator's horizon: returning true,
30162
+ * the caller closes it (`closeIsoHoles`); otherwise it is discarded.
28995
30163
  */
28996
- onUndelimitedLoss(cb) {
28997
- this.#fec.setUndelimitedLossHandler(cb);
30164
+ onUndelimitedLoss(cb, onExpired) {
30165
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
28998
30166
  }
28999
30167
  /**
29000
30168
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -29230,7 +30398,8 @@ var MulticastManager = class {
29230
30398
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
29231
30399
  ...event,
29232
30400
  block: Object.freeze({ ...event.block }),
29233
- ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
30401
+ ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
30402
+ ..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
29234
30403
  }
29235
30404
  );
29236
30405
  if (this.#logRepairLifecycle) {
@@ -29343,7 +30512,7 @@ async function setupFecWorkerForTrack(opts) {
29343
30512
  blockProfile,
29344
30513
  k: opts.fecK,
29345
30514
  // P as this receiver joined it, not the catalog total: the worker snapshot
29346
- // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
30515
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29347
30516
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29348
30517
  symbolSize: opts.fecSymbolSize,
29349
30518
  interleaveDepth: interleaveDepthBlocks,
@@ -29885,24 +31054,24 @@ var MediaRouter = class {
29885
31054
  if (!this.#multicastVideoKeyed) return;
29886
31055
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
29887
31056
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
29888
- const token = ++this.#sourceSwitchToken;
31057
+ const token2 = ++this.#sourceSwitchToken;
29889
31058
  const maybeReady = this.#onSourceSwitch?.("multicast");
29890
31059
  if (!maybeReady || typeof maybeReady.then !== "function") {
29891
- this.#commitMulticastActivation(token);
31060
+ this.#commitMulticastActivation(token2);
29892
31061
  return;
29893
31062
  }
29894
31063
  const ready = Promise.resolve(maybeReady);
29895
- this.#pendingMulticastToken = token;
31064
+ this.#pendingMulticastToken = token2;
29896
31065
  this.#sourceSwitchReady = ready;
29897
31066
  void ready.then(
29898
- () => this.#commitMulticastActivation(token),
29899
- (err) => this.#rejectMulticastActivation(token, err)
31067
+ () => this.#commitMulticastActivation(token2),
31068
+ (err) => this.#rejectMulticastActivation(token2, err)
29900
31069
  ).finally(() => {
29901
31070
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
29902
31071
  });
29903
31072
  }
29904
- #commitMulticastActivation(token) {
29905
- if (token !== this.#sourceSwitchToken) return;
31073
+ #commitMulticastActivation(token2) {
31074
+ if (token2 !== this.#sourceSwitchToken) return;
29906
31075
  this.#active = "multicast";
29907
31076
  this.#suppressed = true;
29908
31077
  this.#pendingMulticastToken = null;
@@ -29913,9 +31082,9 @@ var MediaRouter = class {
29913
31082
  if (video) this.#onVideoFrame?.(video.data, video.meta);
29914
31083
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
29915
31084
  }
29916
- #rejectMulticastActivation(token, err) {
29917
- if (token !== this.#sourceSwitchToken) return;
29918
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
31085
+ #rejectMulticastActivation(token2, err) {
31086
+ if (token2 !== this.#sourceSwitchToken) return;
31087
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
29919
31088
  this.#pendingMulticastVideo = null;
29920
31089
  this.#pendingMulticastAudio = null;
29921
31090
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -30884,63 +32053,65 @@ var FecStarvationDetector = class {
30884
32053
 
30885
32054
  // src/cmcd-producer.ts
30886
32055
  var PLAY_MODES = ["full", "base-only", "single-track"];
30887
- function sanitize(value) {
30888
- return value.replace(/[,\r\n]/g, "");
30889
- }
30890
- function buildFecParams(stats) {
30891
- const params = {};
30892
- const cfg = stats.config;
30893
- params["x-bc-fec-algo"] = cfg.algorithm;
30894
- params["x-bc-fec-k"] = cfg.k;
30895
- params["x-bc-fec-p"] = cfg.repairCount;
30896
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
30897
- return params;
30898
- }
30899
- function buildTransportParams(snap) {
30900
- if (!snap) return {};
30901
- const params = {};
30902
- params["x-bc-transport"] = snap.transport;
30903
- if (snap.multicastGroup != null) {
30904
- params["x-bc-multicast-group"] = snap.multicastGroup;
30905
- }
30906
- if (snap.multicastSource != null) {
30907
- params["x-bc-multicast-source"] = snap.multicastSource;
30908
- }
30909
- if (snap.amtRelay != null) {
30910
- params["x-bc-amt-relay"] = snap.amtRelay;
30911
- }
30912
- return params;
30913
- }
30914
- function formatQueryString(params) {
30915
- return Object.keys(params).sort().map((key) => {
30916
- const v = params[key];
30917
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
30918
- return `${key}=${v}`;
32056
+ var DEFAULT_INTERVAL_MS = 3e4;
32057
+ var CUSTOM_PREFIX = "com.blockcast-";
32058
+ var MAX_CUSTOM_VALUE = 64;
32059
+ var TRANSPORT_TOKEN = {
32060
+ native: "ssm",
32061
+ amt: "amt",
32062
+ moq: "moq"
32063
+ };
32064
+ function sfString(value) {
32065
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
32066
+ }
32067
+ function encodeCmcdRecord(record) {
32068
+ return Object.keys(record).sort().map((key) => {
32069
+ const value = record[key];
32070
+ if (typeof value === "number") return `${key}=${value}`;
32071
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
32072
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
32073
+ return `${key}=${value.token}`;
30919
32074
  }).join(",");
30920
32075
  }
32076
+ var token = (value) => ({ token: value });
32077
+ function putCustom(record, name, value) {
32078
+ if (value === void 0) return;
32079
+ if (typeof value === "number") value = String(value);
32080
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
32081
+ record[CUSTOM_PREFIX + name] = value;
32082
+ }
30921
32083
  var CmcdProducer = class {
30922
- #onCmcdParams;
30923
32084
  #onCmcdPost;
32085
+ #intervalMs;
32086
+ #sessionId;
32087
+ #now;
32088
+ #sn = 0;
32089
+ #lastHeartbeat = null;
30924
32090
  #transport;
30925
32091
  #lastStats;
30926
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
30927
32092
  #playMode = null;
30928
32093
  #deltaFailRate = null;
30929
32094
  #switchMiscount = 0;
30930
32095
  #manifestStats = null;
30931
32096
  #fecFailClosed = null;
32097
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
32098
+ #lifecycle = null;
30932
32099
  constructor(options) {
30933
- this.#onCmcdParams = options.onCmcdParams;
30934
32100
  this.#onCmcdPost = options.onCmcdPost;
32101
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
32102
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
32103
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
32104
+ }
32105
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
32106
+ this.#now = options.now ?? Date.now;
30935
32107
  }
30936
32108
  /** Update the stored transport state. Called by player wiring layer. */
30937
32109
  updateTransport(snapshot) {
30938
32110
  this.#transport = snapshot;
30939
32111
  }
30940
32112
  /**
30941
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
30942
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
30943
- * (CLAUDE.md: error on bad init).
32113
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
32114
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
30944
32115
  */
30945
32116
  updatePlayMode(mode) {
30946
32117
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -30951,9 +32122,8 @@ var CmcdProducer = class {
30951
32122
  this.#playMode = mode;
30952
32123
  }
30953
32124
  /**
30954
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
30955
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
30956
- * Omitted on single-track streams.
32125
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
32126
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
30957
32127
  */
30958
32128
  updateDeltaFailRate(rate) {
30959
32129
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -30963,112 +32133,139 @@ var CmcdProducer = class {
30963
32133
  }
30964
32134
  this.#deltaFailRate = rate;
30965
32135
  }
30966
- /**
30967
- * Bump the cumulative switching-set misalignment counter. Emitted as
30968
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
30969
- */
32136
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
30970
32137
  incrementSwitchMiscount() {
30971
32138
  this.#switchMiscount += 1;
30972
32139
  }
30973
32140
  /**
30974
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
30975
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
30976
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
30977
- * stream playing without FEC is told apart from one with no loss.
32141
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
32142
+ * as `e=e` with the reason code in `ec`, and on every later counter report
32143
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
32144
+ * B_i was undercut), so a stream playing without FEC is told apart from one
32145
+ * with no loss.
30978
32146
  */
30979
32147
  updateFecFailClosed(reason) {
32148
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
30980
32149
  this.#fecFailClosed = reason;
32150
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
30981
32151
  }
30982
32152
  updateManifestVerification(stats) {
30983
32153
  this.#manifestStats = stats;
30984
32154
  }
30985
32155
  /**
30986
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
30987
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
30988
- * include the drift key until the next emitSwitchDrift(). Mirrors the
30989
- * existing onTransition() pattern.
32156
+ * Count a video block's resolution from the repair lifecycle: recovered in
32157
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
32158
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
32159
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
30990
32160
  */
32161
+ onRepairLifecycle(event) {
32162
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
32163
+ if (event.type === "recovered" && event.blockClass !== "audio") {
32164
+ if (event.via === "inband") this.#lifecycle.inband++;
32165
+ else this.#lifecycle.fetch++;
32166
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
32167
+ this.#lifecycle.failedKeyframe++;
32168
+ }
32169
+ }
32170
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
30991
32171
  emitSwitchDrift(driftUs) {
30992
32172
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
30993
32173
  throw new Error(
30994
32174
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
30995
32175
  );
30996
32176
  }
30997
- const params = {};
30998
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
30999
- Object.assign(params, buildTransportParams(this.#transport));
31000
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
31001
- params["x-bc-switch-drift-us"] = driftUs;
31002
- this.#emit(params);
32177
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
32178
+ putCustom(record, "sdu", Math.round(driftUs));
32179
+ this.#post([record]);
31003
32180
  }
31004
- /**
31005
- * Process a FecTrackStats snapshot. Finds the video track, builds
31006
- * x-bc-* vendor keys, and fires both delivery callbacks.
31007
- */
32181
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31008
32182
  onSnapshot(data) {
31009
32183
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31010
32184
  if (!videoTrack) return;
31011
32185
  this.#lastStats = videoTrack;
31012
- const params = {
31013
- ...buildFecParams(videoTrack),
31014
- ...buildTransportParams(this.#transport)
31015
- };
31016
- if (this.#playMode !== null) {
31017
- params["x-bc-play-mode"] = this.#playMode;
31018
- }
31019
- if (this.#fecFailClosed) {
31020
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
31021
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
31022
- }
31023
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31024
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
31025
- }
31026
- if (this.#switchMiscount > 0) {
31027
- params["x-bc-switch-miscount"] = this.#switchMiscount;
31028
- }
31029
- if (this.#manifestStats) {
31030
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
31031
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
31032
- }
31033
- this.#emit(params);
32186
+ this.#heartbeat();
31034
32187
  }
31035
32188
  /**
31036
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31037
- * the caller because only the transport integration knows when state changed.
32189
+ * Take transport state for receivers that do not own FEC stats, and send the
32190
+ * heartbeat when due.
31038
32191
  */
31039
32192
  onTransportSnapshot(data) {
31040
32193
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31041
32194
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31042
32195
  }
31043
32196
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
31044
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
31045
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
32197
+ if (data.switchMiscount !== void 0) {
32198
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
32199
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
32200
+ }
32201
+ this.#switchMiscount = data.switchMiscount;
31046
32202
  }
31047
32203
  this.#transport = data.transport;
31048
- const params = buildTransportParams(data.transport);
31049
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
31050
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
31051
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
31052
- this.#emit(params);
32204
+ this.#heartbeat();
31053
32205
  }
31054
- /**
31055
- * Signal a transport transition event. Fires an immediate out-of-band
31056
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
31057
- * x-bc-ct (one-shot).
31058
- */
32206
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31059
32207
  onTransition(event) {
31060
- const params = {};
31061
- if (this.#lastStats) {
31062
- Object.assign(params, buildFecParams(this.#lastStats));
32208
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
32209
+ putCustom(record, "ct", token(event));
32210
+ this.#putTransport(record);
32211
+ this.#post([record]);
32212
+ }
32213
+ #base(event) {
32214
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
32215
+ }
32216
+ #heartbeat() {
32217
+ const now = this.#now();
32218
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
32219
+ this.#lastHeartbeat = now;
32220
+ this.#post([this.#base("t"), this.#counters()]);
32221
+ }
32222
+ #putTransport(record) {
32223
+ const snap = this.#transport;
32224
+ if (!snap) return;
32225
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
32226
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
32227
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
32228
+ }
32229
+ putCustom(record, "ar", snap.amtRelay);
32230
+ }
32231
+ /** The `e=ce` counter report. */
32232
+ #counters() {
32233
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
32234
+ const stats = this.#lastStats;
32235
+ if (stats) {
32236
+ putCustom(record, "fa", stats.config.algorithm);
32237
+ putCustom(record, "fk", stats.config.k);
32238
+ putCustom(record, "fp", stats.config.repairCount);
32239
+ putCustom(record, "pr", stats.sourceSymbols);
32240
+ putCustom(record, "pl", stats.sourcePacketsMissing);
32241
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
32242
+ putCustom(record, "fbf", stats.blocksFailed);
32243
+ }
32244
+ if (this.#lifecycle) {
32245
+ putCustom(record, "fbr", this.#lifecycle.inband);
32246
+ putCustom(record, "fbq", this.#lifecycle.fetch);
32247
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
32248
+ }
32249
+ this.#putTransport(record);
32250
+ if (this.#playMode !== null) {
32251
+ putCustom(record, "pm", token(this.#playMode));
32252
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
32253
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
32254
+ }
31063
32255
  }
31064
- Object.assign(params, buildTransportParams(this.#transport));
31065
- params["x-bc-ct"] = event;
31066
- this.#emit(params);
32256
+ if (this.#fecFailClosed) {
32257
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
32258
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
32259
+ }
32260
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
32261
+ if (this.#manifestStats) {
32262
+ putCustom(record, "mv", this.#manifestStats.verified);
32263
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
32264
+ }
32265
+ return record;
31067
32266
  }
31068
- /** Fire both delivery callbacks with the given params. */
31069
- #emit(params) {
31070
- this.#onCmcdParams?.(params);
31071
- this.#onCmcdPost?.(formatQueryString(params));
32267
+ #post(records) {
32268
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31072
32269
  }
31073
32270
  };
31074
32271
 
@@ -31164,18 +32361,31 @@ function resolveRepairLayers(catalog, source) {
31164
32361
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31165
32362
  };
31166
32363
  }
32364
+ function valueAt(catalog, pointer) {
32365
+ let node = catalog;
32366
+ for (const segment of pointer.split("/").slice(1)) {
32367
+ if (typeof node !== "object" || node === null) return void 0;
32368
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
32369
+ }
32370
+ return node;
32371
+ }
32372
+ function catalogTrackDeliveryMode(catalog, source) {
32373
+ const binding = bindingOf(catalog, source);
32374
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
32375
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
32376
+ }
31167
32377
  function moqtPriority2(value) {
31168
32378
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31169
32379
  }
31170
32380
  function catalogRepairTarget(catalog, source) {
31171
32381
  const binding = bindingOf(catalog, source);
31172
32382
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31173
- const layerZero = binding.layers[0];
32383
+ const layerZero2 = binding.layers[0];
31174
32384
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31175
32385
  const tracks = catalog.tracks;
31176
32386
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31177
32387
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31178
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
32388
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31179
32389
  }
31180
32390
  function numericField(value) {
31181
32391
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31207,6 +32417,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31207
32417
  repairTrackName: base.repairTrack,
31208
32418
  repairLayers: repair.baseLayers,
31209
32419
  repairOverlays: repair.overlays,
32420
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31210
32421
  ...mediaRate
31211
32422
  };
31212
32423
  }
@@ -31257,7 +32468,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31257
32468
  var PATH_BITS = { moq: 1, multicast: 2 };
31258
32469
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31259
32470
  var MmtpFecSession = class {
32471
+ /** P_0 = B_0, the held total every repair packet is checked against. */
32472
+ get repairSymbols() {
32473
+ return this.#repairSymbols;
32474
+ }
31260
32475
  #decoder;
32476
+ #repairSymbols;
32477
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
32478
+ #message;
31261
32479
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31262
32480
  #blocks = /* @__PURE__ */ new Map();
31263
32481
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31295,6 +32513,24 @@ var MmtpFecSession = class {
31295
32513
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31296
32514
  */
31297
32515
  constructor(config) {
32516
+ if (config && "alFecFlow" in config) {
32517
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
32518
+ if (!message) {
32519
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
32520
+ }
32521
+ this.#message = message;
32522
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
32523
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
32524
+ this.sourceSymbols = message.maximumK;
32525
+ this.#repairSymbols = message.maximumP;
32526
+ this.symbolSize = message.repairSymbolSize;
32527
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
32528
+ this.#repairPacketId = message.repairFlowId;
32529
+ this.#heldLayers = layerZero(message.maximumP);
32530
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
32531
+ this.#decoder = buildDecoder(config.Decoder, this);
32532
+ return;
32533
+ }
31298
32534
  const fec = config?.fec;
31299
32535
  if (!fec || typeof fec !== "object") {
31300
32536
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31305,7 +32541,7 @@ var MmtpFecSession = class {
31305
32541
  }
31306
32542
  this.algorithm = fec.scheme;
31307
32543
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31308
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
32544
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31309
32545
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31310
32546
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31311
32547
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31325,17 +32561,30 @@ var MmtpFecSession = class {
31325
32561
  config.heldLayers
31326
32562
  );
31327
32563
  this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31328
- if (typeof config.Decoder !== "function") {
31329
- throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
31330
- }
31331
- const decoder = new config.Decoder(this.retentionBlocks, this.blockProfile, this.algorithm);
31332
- try {
31333
- decoder.configure_params(this.sourceSymbols, this.symbolSize);
31334
- } catch (error) {
31335
- decoder.free();
31336
- throw error;
31337
- }
31338
- this.#decoder = decoder;
32564
+ this.#decoder = buildDecoder(config.Decoder, this);
32565
+ }
32566
+ /**
32567
+ * A later version of the AL-FEC message, catalog-less sessions only. A
32568
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
32569
+ * anchored check restarts at the new total, so a block generated under the
32570
+ * previous one stays valid before the first block carrying the new total
32571
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
32572
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
32573
+ * does. An entry with another asset, repair flow, code point, K or T, or
32574
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
32575
+ * a new session. A latched fail-closed stays latched either way.
32576
+ */
32577
+ updateAlFecFlow(flow) {
32578
+ const held = this.#message;
32579
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
32580
+ const next = alFecRaptorQFlowConfig(flow);
32581
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
32582
+ return "refused";
32583
+ }
32584
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
32585
+ this.#repairSymbols = next.maximumP;
32586
+ this.#heldLayers = layerZero(next.maximumP);
32587
+ return "raised";
31339
32588
  }
31340
32589
  /** True once release() ran. */
31341
32590
  get released() {
@@ -31430,7 +32679,7 @@ var MmtpFecSession = class {
31430
32679
  }
31431
32680
  if (admission.reject) {
31432
32681
  const verdict2 = this.#heldLayers.failsClosed(
31433
- repairTrackName,
32682
+ this.#trackFor(repairTrackName),
31434
32683
  frame.ssStart,
31435
32684
  frame.rsbLength,
31436
32685
  this.#anchorHorizon,
@@ -31445,7 +32694,7 @@ var MmtpFecSession = class {
31445
32694
  this.#repairPacketId ??= frame.packetId;
31446
32695
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31447
32696
  const verdict = this.#heldLayers.admit(
31448
- repairTrackName,
32697
+ this.#trackFor(repairTrackName),
31449
32698
  frame.ssStart,
31450
32699
  frame.rsbLength,
31451
32700
  this.#anchorHorizon,
@@ -31504,6 +32753,10 @@ var MmtpFecSession = class {
31504
32753
  decoder.free();
31505
32754
  return true;
31506
32755
  }
32756
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
32757
+ #trackFor(repairTrackName) {
32758
+ return this.#message ? void 0 : repairTrackName;
32759
+ }
31507
32760
  #drop(reason) {
31508
32761
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31509
32762
  return [];
@@ -31625,6 +32878,22 @@ var MmtpFecSession = class {
31625
32878
  }
31626
32879
  }
31627
32880
  };
32881
+ function layerZero(total) {
32882
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32883
+ }
32884
+ function buildDecoder(Decoder, session) {
32885
+ if (typeof Decoder !== "function") {
32886
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32887
+ }
32888
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32889
+ try {
32890
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32891
+ } catch (error) {
32892
+ decoder.free();
32893
+ throw error;
32894
+ }
32895
+ return decoder;
32896
+ }
31628
32897
  function requirePositiveInteger(value, path) {
31629
32898
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31630
32899
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
@@ -31638,8 +32907,171 @@ function requireNumber(value, path) {
31638
32907
  return value;
31639
32908
  }
31640
32909
 
32910
+ // src/repair-layer-subscriptions.ts
32911
+ var SERIAL32_HALF2 = 2147483648;
32912
+ var RepairLayerSubscriptions = class {
32913
+ #opts;
32914
+ #windowPackets;
32915
+ #margin;
32916
+ #dropAfterWindows;
32917
+ #source;
32918
+ #layerCount = 0;
32919
+ #subs = /* @__PURE__ */ new Map();
32920
+ #disposed = false;
32921
+ #lastSeq;
32922
+ #expected = 0;
32923
+ #lost = 0;
32924
+ #wantFewer = 0;
32925
+ #lossRate = null;
32926
+ #repairPackets = 0;
32927
+ #repairBytes = 0;
32928
+ #staleDropped = 0;
32929
+ constructor(opts) {
32930
+ this.#opts = opts;
32931
+ this.#source = opts.source;
32932
+ this.#windowPackets = opts.windowPackets ?? 500;
32933
+ this.#margin = opts.margin ?? 2;
32934
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
32935
+ }
32936
+ get layerCount() {
32937
+ return this.#layerCount;
32938
+ }
32939
+ /**
32940
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
32941
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
32942
+ * or interleaved sequences read as loss.
32943
+ */
32944
+ noteSourceSequence(seq) {
32945
+ if (this.#disposed || this.#delivery() === "stream") return;
32946
+ seq >>>= 0;
32947
+ if (this.#lastSeq === void 0) {
32948
+ this.#lastSeq = seq;
32949
+ return;
32950
+ }
32951
+ const d = serial32Distance(this.#lastSeq, seq);
32952
+ if (d === 0) return;
32953
+ const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
32954
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
32955
+ this.#lastSeq = seq;
32956
+ this.#expected = 0;
32957
+ this.#lost = 0;
32958
+ return;
32959
+ }
32960
+ if (back === 0) {
32961
+ this.#expected += d;
32962
+ this.#lost += d - 1;
32963
+ this.#lastSeq = seq;
32964
+ } else if (this.#lost > 0) {
32965
+ this.#lost -= 1;
32966
+ }
32967
+ if (this.#expected >= this.#windowPackets) this.#closeWindow();
32968
+ }
32969
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
32970
+ switchSource(next) {
32971
+ if (this.#disposed) return;
32972
+ this.#closeAll();
32973
+ this.#source = next;
32974
+ this.#lastSeq = void 0;
32975
+ this.#expected = 0;
32976
+ this.#lost = 0;
32977
+ this.#wantFewer = 0;
32978
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32979
+ this.#reconcile();
32980
+ }
32981
+ snapshot() {
32982
+ return {
32983
+ delivery: this.#delivery(),
32984
+ layerCount: this.#layerCount,
32985
+ subscribedTracks: [...this.#subs.keys()],
32986
+ repairPackets: this.#repairPackets,
32987
+ repairBytes: this.#repairBytes,
32988
+ staleDropped: this.#staleDropped,
32989
+ lossRate: this.#lossRate
32990
+ };
32991
+ }
32992
+ dispose() {
32993
+ if (this.#disposed) return;
32994
+ this.#closeAll();
32995
+ this.#disposed = true;
32996
+ }
32997
+ #delivery() {
32998
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32999
+ }
33000
+ #layers() {
33001
+ return this.#source.repairLayers ?? [];
33002
+ }
33003
+ #closeWindow() {
33004
+ const rate = this.#lost / this.#expected;
33005
+ this.#lossRate = rate;
33006
+ this.#expected = 0;
33007
+ this.#lost = 0;
33008
+ const target = this.#targetLayers(rate);
33009
+ if (target > this.#layerCount) {
33010
+ this.#wantFewer = 0;
33011
+ this.#layerCount = target;
33012
+ } else if (target < this.#layerCount) {
33013
+ this.#wantFewer += 1;
33014
+ if (this.#wantFewer >= this.#dropAfterWindows) {
33015
+ this.#wantFewer = 0;
33016
+ this.#layerCount -= 1;
33017
+ }
33018
+ } else {
33019
+ this.#wantFewer = 0;
33020
+ }
33021
+ this.#reconcile();
33022
+ }
33023
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
33024
+ #targetLayers(rate) {
33025
+ if (rate <= 0) return 0;
33026
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
33027
+ const layers = this.#layers();
33028
+ let held = 0;
33029
+ for (let n = 0; n < layers.length; n++) {
33030
+ held += layers[n].repairSymbols;
33031
+ if (held >= needed) return n + 1;
33032
+ }
33033
+ return layers.length;
33034
+ }
33035
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
33036
+ #reconcile() {
33037
+ const want = /* @__PURE__ */ new Map();
33038
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
33039
+ want.set(layer.repairTrackName, layer.priority);
33040
+ }
33041
+ const overlay = this.#source.repairOverlays?.[0];
33042
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
33043
+ for (const [name, entry] of this.#subs) {
33044
+ if (!want.has(name)) {
33045
+ entry.sub.close();
33046
+ this.#subs.delete(name);
33047
+ }
33048
+ }
33049
+ for (const [name, priority] of want) {
33050
+ if (this.#subs.has(name)) continue;
33051
+ const entry = {};
33052
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
33053
+ this.#subs.set(name, entry);
33054
+ }
33055
+ }
33056
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
33057
+ #onPacket(name, entry, packet) {
33058
+ if (this.#disposed || this.#subs.get(name) !== entry) {
33059
+ this.#staleDropped += 1;
33060
+ return;
33061
+ }
33062
+ this.#repairPackets += 1;
33063
+ this.#repairBytes += packet.byteLength;
33064
+ this.#opts.feed(packet, name);
33065
+ }
33066
+ #closeAll() {
33067
+ for (const entry of this.#subs.values()) entry.sub.close();
33068
+ this.#subs.clear();
33069
+ }
33070
+ };
33071
+
31641
33072
  // src/repair-fetch.ts
31642
33073
  var MOQT_PRIORITY_MAX = 255;
33074
+ var WALK_LEAD_MS = 3;
31643
33075
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
31644
33076
  function isMoqtPriority(value) {
31645
33077
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -31690,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
31690
33122
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
31691
33123
  };
31692
33124
  const clearEntry = (entry) => {
31693
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
31694
- else clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33125
+ const { range } = entry;
33126
+ if (range) {
33127
+ atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
33128
+ } else {
33129
+ clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33130
+ }
33131
+ };
33132
+ const atReport = (range, work) => {
33133
+ const fec = mc.fecManager;
33134
+ if (range.groups !== null || fec.withReceiptTime === void 0) work();
33135
+ else fec.withReceiptTime(range.reportedAt, work);
31695
33136
  };
31696
33137
  const releaseHold = (entry) => {
31697
33138
  if (entry.range) return;
@@ -31758,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
31758
33199
  const admit = (entry, now) => {
31759
33200
  const budgetMs = entry.deadlineAt - now;
31760
33201
  const rttMs = readRtt();
31761
- const reason = !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < budgetMs ? null : "rtt-exceeds-budget";
33202
+ const reason = refusal(entry, now, rttMs);
31762
33203
  if (reason === null) return rttMs;
31763
33204
  skip(entry, reason, rttMs, budgetMs);
31764
33205
  return null;
31765
33206
  };
33207
+ const refusal = (entry, now, rttMs) => !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < entry.deadlineAt - now ? null : "rtt-exceeds-budget";
31766
33208
  const retire = (entry) => {
31767
33209
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
31768
33210
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
31769
33211
  };
31770
- const outstandingBelow = (priority) => {
33212
+ const holdsBack = (entry, priority, videoWalks = true) => {
33213
+ if (entry.priority >= priority) return false;
33214
+ const { walk } = entry;
33215
+ if (walk === void 0) return true;
33216
+ if (entry.priority !== 0 || !videoWalks) return false;
33217
+ return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
33218
+ };
33219
+ const outstandingBelow = (priority, videoWalks = true) => {
31771
33220
  for (const entry of entries.values()) {
31772
- if (entry.priority < priority) return true;
33221
+ if (holdsBack(entry, priority, videoWalks)) return true;
31773
33222
  }
31774
33223
  return false;
31775
33224
  };
33225
+ const yieldVideoWalks = (queued) => {
33226
+ const now = performance.now();
33227
+ const rttMs = readRtt();
33228
+ const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
33229
+ if (!due) return;
33230
+ for (const entry of entries.values()) {
33231
+ const walk = entry.walk;
33232
+ if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
33233
+ if (walk.group.controller.signal.aborted) continue;
33234
+ walk.yielded = true;
33235
+ walk.group.controller.abort();
33236
+ }
33237
+ };
31776
33238
  const preemptBestEffort = () => {
31777
33239
  for (const entry of entries.values()) {
31778
33240
  if (entry.priority === 0 || !entry.running) continue;
@@ -31801,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
31801
33263
  }
31802
33264
  const rttMs = admit(entry, now);
31803
33265
  if (rttMs === null) return;
31804
- if (entry.priority === 0) preemptBestEffort();
33266
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33267
+ if (entry.range === void 0) leaveWalkedGroup(entry);
31805
33268
  if (counted) entry.attempts++;
31806
33269
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
31807
33270
  rttMs,
@@ -31823,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
31823
33286
  }
31824
33287
  void execute(entry, run);
31825
33288
  };
33289
+ const leaveWalkedGroup = (block) => {
33290
+ for (const entry of entries.values()) {
33291
+ const group = entry.walk?.group;
33292
+ if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
33293
+ group.controller.abort();
33294
+ }
33295
+ }
33296
+ };
31826
33297
  const dispatch = () => {
31827
33298
  if (disposed) return;
31828
33299
  const now = performance.now();
@@ -31847,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
31847
33318
  for (const entry of queued) {
31848
33319
  if (entry.priority === 0) start(entry);
31849
33320
  }
33321
+ yieldVideoWalks(queued);
31850
33322
  if (outstandingBelow(1)) return;
31851
33323
  for (const entry of queued) {
31852
33324
  if (entry.priority === 1) start(entry);
@@ -31873,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
31873
33345
  }
31874
33346
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
31875
33347
  };
33348
+ const groupServed = (sbn, packetId, catalogTrackName) => {
33349
+ if (mc.fecManager.blockSymbolCounts === void 0) return false;
33350
+ const counts = symbolCounts(sbn, packetId, catalogTrackName);
33351
+ if (counts === void 0 || counts.authoredLength === void 0) return true;
33352
+ return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
33353
+ };
33354
+ const statedEnd = (sbn, packetId, catalogTrackName) => {
33355
+ const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
33356
+ return length === void 0 ? void 0 : serial32Add(sbn, length);
33357
+ };
31876
33358
  const finish = (entry, run, fed, error) => {
31877
33359
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
31878
33360
  return;
@@ -31917,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
31917
33399
  signal: run.controller.signal,
31918
33400
  subscriberPriority: entry.subscriberPriority
31919
33401
  };
33402
+ const { range } = entry;
33403
+ const feed = (packet) => {
33404
+ if (entry.catalogTrackName === void 0) {
33405
+ mc.processRepairFromMoQ(packet, entry.packetId);
33406
+ } else {
33407
+ mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
33408
+ }
33409
+ };
31920
33410
  try {
31921
- const packets = entry.range ? opts.rangeTransport({
33411
+ const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
31922
33412
  ...common,
31923
- startGroup: entry.range.startGroup,
31924
- endGroup: entry.range.endGroup,
31925
- lostSsId: entry.range.lostSsId
31926
- }) : opts.transport({ ...common, sbn: entry.sbn });
33413
+ startGroup: range.groups.startGroup,
33414
+ endGroup: range.groups.endGroup,
33415
+ lostSsId: range.lostSsId
33416
+ });
31927
33417
  for await (const packet of packets) {
31928
33418
  if (run.stopped !== null || entry.running !== run) break;
31929
33419
  fed++;
31930
- if (entry.catalogTrackName === void 0) {
31931
- mc.processRepairFromMoQ(packet, entry.packetId);
31932
- } else {
31933
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
31934
- }
33420
+ if (range === void 0) feed(packet);
33421
+ else atReport(range, () => feed(packet));
31935
33422
  }
31936
33423
  finish(entry, run, fed, void 0);
31937
33424
  } catch (error) {
31938
33425
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
31939
33426
  }
31940
33427
  };
33428
+ async function* walkHole(entry, walk, hole, common) {
33429
+ const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
33430
+ for (let first = true; !common.signal.aborted; first = false) {
33431
+ if (!first && entry.priority !== 0 && outstandingBelow(1)) {
33432
+ stop(entry, "preempted");
33433
+ note("repair-preempted", entry);
33434
+ return;
33435
+ }
33436
+ if (!first && entry.priority === 0) {
33437
+ dispatch();
33438
+ if (common.signal.aborted) return;
33439
+ }
33440
+ const sbn = walk.next;
33441
+ let next;
33442
+ if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
33443
+ const rsIds = /* @__PURE__ */ new Set();
33444
+ const group = new AbortController();
33445
+ const cancel = () => group.abort();
33446
+ common.signal.addEventListener("abort", cancel, { once: true });
33447
+ walk.group = { sbn, controller: group };
33448
+ try {
33449
+ for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
33450
+ yield packet;
33451
+ const id = extractRepairFecPayloadId(packet);
33452
+ if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
33453
+ next ??= serial32Add(sbn, id.ssbLength);
33454
+ rsIds.add(id.rsId);
33455
+ if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
33456
+ group.abort();
33457
+ break;
33458
+ }
33459
+ }
33460
+ } catch (error) {
33461
+ if (!group.signal.aborted || common.signal.aborted) throw error;
33462
+ } finally {
33463
+ common.signal.removeEventListener("abort", cancel);
33464
+ walk.group = null;
33465
+ }
33466
+ if (walk.yielded) {
33467
+ walk.yielded = false;
33468
+ continue;
33469
+ }
33470
+ }
33471
+ next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
33472
+ if (next === void 0) return;
33473
+ if (serial32Distance(entry.sbn, next) > lostAt) return;
33474
+ walk.next = next;
33475
+ }
33476
+ }
31941
33477
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
31942
33478
  for (const entry of [...entries.values()]) {
31943
33479
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -32133,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
32133
33669
  throw error;
32134
33670
  }
32135
33671
  };
32136
- if (opts.rangeTransport !== void 0) {
33672
+ const trackKind = (packetId) => {
33673
+ try {
33674
+ return mc.fecManager.trackKindFor?.(packetId);
33675
+ } catch {
33676
+ return void 0;
33677
+ }
33678
+ };
33679
+ const walkDeadlineAt = (packetId, now, deadlineAt) => {
33680
+ let repairDeadlineMs;
33681
+ try {
33682
+ repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
33683
+ } catch {
33684
+ repairDeadlineMs = void 0;
33685
+ }
33686
+ if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
33687
+ return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
33688
+ };
33689
+ const onHoleExpired = (hole) => {
33690
+ if (disposed) return false;
33691
+ const entry = [...entries.values()].find((candidate) => candidate.walk !== void 0 && candidate.packetId === hole.packetId && candidate.catalogTrackName === hole.catalogTrackName && serial32Distance(candidate.sbn, hole.holeStart) < candidate.range.holeLength);
33692
+ if (entry === void 0) return false;
33693
+ queueMicrotask(() => {
33694
+ if (entries.get(entry.key) !== entry) return;
33695
+ retire(entry);
33696
+ stop(entry, "expired");
33697
+ clearEntry(entry);
33698
+ dispatch();
33699
+ });
33700
+ return true;
33701
+ };
33702
+ if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
32137
33703
  mc.onUndelimitedLoss((loss) => {
32138
33704
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
32139
33705
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -32141,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
32141
33707
  const blockProfile = blockProfileOf(packetId);
32142
33708
  const target = opts.resolveTarget(packetId, catalogTrackName);
32143
33709
  if (!target) return close();
32144
- const priority = repairPriorityForClass("unknown");
32145
- const range = rangeFor(loss, target.repairTrack);
32146
- if (range === null) {
33710
+ const walked = opts.rangeTransport === void 0;
33711
+ const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
33712
+ const priority = repairPriorityForClass(holeClass);
33713
+ const groups = walked ? null : rangeFor(loss, target.repairTrack);
33714
+ if (!walked && groups === null) {
32147
33715
  close();
32148
33716
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
32149
33717
  return;
@@ -32154,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
32154
33722
  packetId,
32155
33723
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
32156
33724
  };
32157
- const wire = wirePriority(identity, "unknown");
32158
- const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, performance.now(), "unknown");
33725
+ const wire = wirePriority(identity, holeClass);
33726
+ if (wire === null) return close();
33727
+ const now = performance.now();
33728
+ const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
32159
33729
  if (deadline === null) {
32160
33730
  close();
32161
33731
  mc.noteRepairSkipped(
@@ -32167,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
32167
33737
  );
32168
33738
  return;
32169
33739
  }
33740
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
32170
33741
  const entry = {
32171
33742
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
32172
33743
  ...identity,
@@ -32174,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
32174
33745
  blockProfile,
32175
33746
  priority,
32176
33747
  subscriberPriority: wire.subscriberPriority,
32177
- deadlineAt: deadline.deadlineAt,
32178
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33748
+ deadlineAt,
33749
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
32179
33750
  anchorAt: deadline.anchorAt,
32180
33751
  delayMs: deadline.delayMs,
32181
33752
  bound: deadline.bound,
32182
33753
  order: enqueueOrder++,
32183
33754
  attempts: 0,
32184
33755
  running: null,
32185
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33756
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33757
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
32186
33758
  };
32187
33759
  const previous = entries.get(entry.key);
32188
33760
  if (previous) {
@@ -32192,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
32192
33764
  entries.set(entry.key, entry);
32193
33765
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
32194
33766
  dispatch();
32195
- });
33767
+ }, onHoleExpired);
32196
33768
  }
32197
33769
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
32198
33770
  queueMicrotask(() => {
@@ -32283,6 +33855,7 @@ export {
32283
33855
  REPAIR_LIFECYCLE_LOG_PREFIX,
32284
33856
  REPAIR_PRIORITY_MAX,
32285
33857
  REPAIR_PRIORITY_MIN,
33858
+ RepairLayerSubscriptions,
32286
33859
  SOURCE_MEDIA_PRIORITY_MAX,
32287
33860
  SampleCombineError,
32288
33861
  SampleCombiner,
@@ -32307,6 +33880,7 @@ export {
32307
33880
  catalogGroupDurationMs,
32308
33881
  catalogKeyframeCadenceMs,
32309
33882
  catalogRepairTarget,
33883
+ catalogTrackDeliveryMode,
32310
33884
  catalogTrackFecUnavailable,
32311
33885
  classifyH264Keyframe,
32312
33886
  classifyHevcKeyframe,
@@ -32327,6 +33901,7 @@ export {
32327
33901
  detectH264Keyframe,
32328
33902
  detectHevcKeyframe,
32329
33903
  driftCheck,
33904
+ encodeCmcdRecord,
32330
33905
  encodeDataSegment,
32331
33906
  encodeFecSourceInfo,
32332
33907
  encodeLocTimestamp,