@blockcast/mmt-container 2.1.0 → 2.2.0-main.079cd7664c0b

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 (99) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1271 -247
  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 +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +213 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  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 +37 -2
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +98 -28
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +222 -45
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +97 -28
  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 +25 -2
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +1718 -54
  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 +15 -2
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +15652 -14939
  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 +215 -42
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.js.map +1 -1
  72. package/dist/repair-layer-subscriptions.d.ts +82 -0
  73. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  74. package/dist/repair-layer-subscriptions.js +192 -0
  75. package/dist/repair-layer-subscriptions.js.map +7 -0
  76. package/dist/signaling.js.map +2 -2
  77. package/dist/timestamp-codec.js +12 -19
  78. package/dist/timestamp-codec.js.map +2 -2
  79. package/dist/track-session.d.ts +1 -1
  80. package/dist/track-session.js.map +1 -1
  81. package/dist/wasm-mfu-reassembler.js.map +1 -1
  82. package/dist/wasm-mmtp-container-parser.js +97 -28
  83. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  84. package/package.json +8 -5
  85. package/schemas/catalog.schema.json +4 -0
  86. package/src/catalog-fec-params.ts +36 -1
  87. package/src/cmcd-producer.ts +194 -179
  88. package/src/fec-manager.ts +256 -6
  89. package/src/fec-worker-setup.ts +2 -2
  90. package/src/index.ts +18 -2
  91. package/src/iso-block-locator.ts +19 -0
  92. package/src/iso-bmff-timing.ts +40 -1
  93. package/src/mmtp-container-parser.ts +96 -8
  94. package/src/mmtp-fec-session.ts +115 -15
  95. package/src/mmtp-router.ts +344 -27
  96. package/src/multicast-manager.ts +26 -5
  97. package/src/multicast-transport.ts +44 -20
  98. package/src/repair-layer-subscriptions.ts +261 -0
  99. package/src/track-session.ts +1 -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);
@@ -903,6 +898,50 @@ var NTP_SHORT_BITS = 32;
903
898
  var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
904
899
  var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
905
900
 
901
+ // ../mmt-base/dist/signalling.js
902
+ var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
903
+ var SSBG_MODE_1 = 1;
904
+ var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
905
+ var REPAIR_SYMBOL_ID_MAX = 16777215;
906
+ var MIN_REPAIR_SYMBOL_SIZE = 14;
907
+ var REED_SOLOMON_MAX_BLOCK = 255;
908
+ function alFecRaptorQFlowConfig(flow) {
909
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
910
+ return null;
911
+ if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
912
+ return null;
913
+ if (flow.packetIds.length !== 1)
914
+ return null;
915
+ const repair = flow.repairFlows[0];
916
+ const { fecCodeId, maximumK: k, maximumP: p } = repair;
917
+ const t = flow.repairSymbolSize;
918
+ if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
919
+ return null;
920
+ if (fecCodeId === FEC_CODE_RAPTORQ) {
921
+ if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
922
+ return null;
923
+ } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
924
+ if (k + p > REED_SOLOMON_MAX_BLOCK)
925
+ return null;
926
+ } else {
927
+ return null;
928
+ }
929
+ if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
930
+ return null;
931
+ return {
932
+ sourcePacketIds: [...flow.packetIds],
933
+ repairFlowId: repair.repairFlowId,
934
+ fecCodeId,
935
+ ssbgMode: flow.ssbgMode,
936
+ fecPayloadIdMode: flow.fecPayloadIdMode,
937
+ repairSymbolSize: t,
938
+ maximumK: k,
939
+ maximumP: p,
940
+ protectionWindowTime: repair.protectionWindowTime,
941
+ protectionWindowSize: repair.protectionWindowSize
942
+ };
943
+ }
944
+
906
945
  // ../mmt-base/dist/serial-number.js
907
946
  var SERIAL32_SPAN = 4294967296;
908
947
  var SERIAL32_HALF = 2147483648;
@@ -7457,11 +7496,33 @@ var AdaptiveBitrateController = class {
7457
7496
  };
7458
7497
 
7459
7498
  // node_modules/@blockcast/mmt-base/dist/mmtp.js
7499
+ var MMTP_HEADER_SIZE2 = 12;
7460
7500
  var MPU_HEADER_SIZE2 = 8;
7461
7501
  var MPU_LENGTH_FIELD_SIZE2 = 2;
7462
7502
  var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
7463
7503
  var MMTP_SIGNALING_PACKET_ID2 = 0;
7464
7504
  var MMTP_PACKET_ID_MAX2 = 65535;
7505
+ function readMMTPPacketId2(buffer) {
7506
+ if (buffer.length < MMTP_HEADER_SIZE2)
7507
+ return null;
7508
+ const flags = buffer[0];
7509
+ if ((flags >> 6 & 3) !== 0)
7510
+ return null;
7511
+ let offset = MMTP_HEADER_SIZE2;
7512
+ if ((flags & 32) !== 0) {
7513
+ if (offset + 4 > buffer.length)
7514
+ return null;
7515
+ offset += 4;
7516
+ }
7517
+ if ((flags & 2) !== 0) {
7518
+ if (offset + 4 > buffer.length)
7519
+ return null;
7520
+ const length = buffer[offset + 2] << 8 | buffer[offset + 3];
7521
+ if (offset + 4 + length > buffer.length)
7522
+ return null;
7523
+ }
7524
+ return buffer[2] << 8 | buffer[3];
7525
+ }
7465
7526
 
7466
7527
  // node_modules/@blockcast/mmt-base/dist/net-constants.js
7467
7528
  var DEFAULT_AMT_RELAY_PORT2 = 2268;
@@ -7927,8 +7988,8 @@ var MulticastSubscriptionPool = class {
7927
7988
  record.claims--;
7928
7989
  return this.acquire(gateway, config, consumer);
7929
7990
  }
7930
- const token = Symbol(key);
7931
- entry.consumers.set(token, consumer);
7991
+ const token2 = Symbol(key);
7992
+ entry.consumers.set(token2, consumer);
7932
7993
  let released = false;
7933
7994
  return {
7934
7995
  key,
@@ -7936,7 +7997,7 @@ var MulticastSubscriptionPool = class {
7936
7997
  if (released)
7937
7998
  return;
7938
7999
  released = true;
7939
- entry.consumers.delete(token);
8000
+ entry.consumers.delete(token2);
7940
8001
  record.claims--;
7941
8002
  if (record.claims > 0)
7942
8003
  return;
@@ -8447,6 +8508,205 @@ var TransportManager = class {
8447
8508
  };
8448
8509
  var transportManager = new TransportManager();
8449
8510
 
8511
+ // node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
8512
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
8513
+ var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
8514
+ var INTEGRITY_SCHEME = "manifest-ed25519-2";
8515
+ var MulticastError = class extends Error {
8516
+ code;
8517
+ constructor(code, message, cause) {
8518
+ super(message, cause === void 0 ? void 0 : { cause });
8519
+ this.name = "MulticastError";
8520
+ this.code = code;
8521
+ }
8522
+ };
8523
+ function datagramsOf(chunk) {
8524
+ const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
8525
+ const out = [];
8526
+ for (const d of list) {
8527
+ const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
8528
+ if (data instanceof Uint8Array)
8529
+ out.push(data);
8530
+ else if (data instanceof ArrayBuffer)
8531
+ out.push(new Uint8Array(data));
8532
+ }
8533
+ return out;
8534
+ }
8535
+ function reasonOf(value) {
8536
+ return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
8537
+ }
8538
+ function registry() {
8539
+ const g = globalThis;
8540
+ if (g[REGISTRY] === void 0) {
8541
+ const providers = /* @__PURE__ */ new WeakSet();
8542
+ const byScope = /* @__PURE__ */ new WeakMap();
8543
+ Object.defineProperty(g, REGISTRY, {
8544
+ value: Object.freeze({
8545
+ isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
8546
+ providerFor: (scope) => {
8547
+ let provider = byScope.get(scope);
8548
+ if (provider === void 0) {
8549
+ provider = createProvider(scope);
8550
+ providers.add(provider);
8551
+ byScope.set(scope, provider);
8552
+ }
8553
+ return provider;
8554
+ }
8555
+ }),
8556
+ writable: false,
8557
+ configurable: false,
8558
+ enumerable: false
8559
+ });
8560
+ }
8561
+ return g[REGISTRY];
8562
+ }
8563
+ function resolveMulticastGateway(scope = globalThis) {
8564
+ const s = scope;
8565
+ if (typeof s.MulticastReceiver === "function")
8566
+ return registry().providerFor(scope);
8567
+ if (typeof s.multicast?.subscribe === "function")
8568
+ return s.multicast;
8569
+ return void 0;
8570
+ }
8571
+ function isUserAgentVerifiedGateway(gateway) {
8572
+ return registry().isProvider(gateway);
8573
+ }
8574
+ function userAgentVerifiedIngress(ingress, gateway) {
8575
+ if (isUserAgentVerifiedGateway(gateway))
8576
+ return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
8577
+ if (!("admittedBy" in ingress))
8578
+ return ingress;
8579
+ const { admittedBy: _, ...rest } = ingress;
8580
+ return rest;
8581
+ }
8582
+ function unsupported(message, cause) {
8583
+ return new MulticastError("capability-unsupported", message, cause);
8584
+ }
8585
+ function createProvider(scope) {
8586
+ const Receiver = scope.MulticastReceiver;
8587
+ let gate = null;
8588
+ return {
8589
+ capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
8590
+ subscribe: async (config) => {
8591
+ const c = config;
8592
+ if (c.consume !== "bytes")
8593
+ throw unsupported(`consume mode ${String(c.consume)} is not supported`);
8594
+ const origin = c.origin ?? "ip-multicast";
8595
+ if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
8596
+ throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
8597
+ }
8598
+ if (c.relay !== void 0)
8599
+ throw unsupported("MulticastReceiver picks its own AMT relay");
8600
+ if (!c.source)
8601
+ throw unsupported("MulticastReceiver needs an SSM source");
8602
+ if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
8603
+ throw unsupported("MulticastReceiver filters by source only");
8604
+ }
8605
+ const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
8606
+ if (gate !== null)
8607
+ await gate;
8608
+ let rx;
8609
+ try {
8610
+ rx = new Receiver(options);
8611
+ } catch (error) {
8612
+ throw unsupported("MulticastReceiver refused the request", error);
8613
+ }
8614
+ const opened = rx.opened;
8615
+ if (gate === null)
8616
+ gate = opened.then(() => void 0, () => void 0);
8617
+ return openSubscription(rx, opened, options, c.packetIds);
8618
+ }
8619
+ };
8620
+ }
8621
+ async function openSubscription(rx, openedPromise, options, packetIds) {
8622
+ let opened;
8623
+ try {
8624
+ opened = await openedPromise;
8625
+ } catch (error) {
8626
+ throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8627
+ }
8628
+ if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8629
+ await rx.close();
8630
+ throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8631
+ }
8632
+ const events = new EventTarget();
8633
+ let state = "connected";
8634
+ let filter = packetIds?.length ? new Set(packetIds) : null;
8635
+ let closing = false;
8636
+ const setState = (next) => {
8637
+ if (state === next)
8638
+ return;
8639
+ state = next;
8640
+ events.dispatchEvent(new CustomEvent("state", { detail: next }));
8641
+ };
8642
+ const sourceAddress = options.sourceAddress;
8643
+ const subscription = {
8644
+ id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
8645
+ get state() {
8646
+ return state;
8647
+ },
8648
+ delivery: "datagram",
8649
+ eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
8650
+ setPacketIds(ids) {
8651
+ filter = ids.length ? new Set(ids) : null;
8652
+ },
8653
+ addEventListener: events.addEventListener.bind(events),
8654
+ removeEventListener: events.removeEventListener.bind(events),
8655
+ async unsubscribe() {
8656
+ closing = true;
8657
+ await rx.close();
8658
+ setState("closed");
8659
+ }
8660
+ };
8661
+ void (async () => {
8662
+ const reader = opened.readable.getReader();
8663
+ try {
8664
+ for (; ; ) {
8665
+ const { value, done } = await reader.read();
8666
+ if (done)
8667
+ break;
8668
+ const packets = [];
8669
+ for (const data of datagramsOf(value)) {
8670
+ if (filter !== null) {
8671
+ const id = readMMTPPacketId2(data);
8672
+ if (id === null || !filter.has(id))
8673
+ continue;
8674
+ }
8675
+ const copy = data.slice().buffer;
8676
+ packets.push(copy);
8677
+ events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
8678
+ }
8679
+ if (packets.length > 0)
8680
+ events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
8681
+ }
8682
+ } catch {
8683
+ }
8684
+ })();
8685
+ void rx.closed.then((value) => {
8686
+ const reason = reasonOf(value);
8687
+ if (!closing) {
8688
+ if (reason === "consent-withdrawn") {
8689
+ events.dispatchEvent(new CustomEvent("error", {
8690
+ detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
8691
+ }));
8692
+ } else if (reason === "oversubscribed" || reason === "network") {
8693
+ events.dispatchEvent(new CustomEvent("error", {
8694
+ detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
8695
+ }));
8696
+ }
8697
+ }
8698
+ setState("closed");
8699
+ }, (error) => {
8700
+ if (!closing) {
8701
+ events.dispatchEvent(new CustomEvent("error", {
8702
+ detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
8703
+ }));
8704
+ }
8705
+ setState("closed");
8706
+ });
8707
+ return subscription;
8708
+ }
8709
+
8450
8710
  // node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
8451
8711
  var MAX_MOQT_PRIORITY = 255;
8452
8712
  function moqtPriority(value, field) {
@@ -8486,14 +8746,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8486
8746
  });
8487
8747
 
8488
8748
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8749
+ var MAX_PENDING_PACKETS = 256;
8489
8750
  var AMTGatewayManager = class {
8490
8751
  subscriptions = /* @__PURE__ */ new Map();
8491
8752
  messageHandler;
8753
+ messageListenerRegistered = false;
8492
8754
  pendingPackets = /* @__PURE__ */ new Map();
8493
8755
  constructor() {
8494
8756
  this.messageHandler = (event) => {
8495
8757
  const data = event.data;
8496
- console.log(`[AMT] SW message received:`, data.type);
8497
8758
  if (data.type === "UDP_PACKET") {
8498
8759
  this.handlePacket(data);
8499
8760
  } else if (data.type === "SOCKET_ERROR") {
@@ -8502,12 +8763,22 @@ var AMTGatewayManager = class {
8502
8763
  console.log(`[AMT SW Debug] ${data.message}`);
8503
8764
  }
8504
8765
  };
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");
8766
+ }
8767
+ ensureMessageListener() {
8768
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8769
+ return;
8510
8770
  }
8771
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8772
+ this.messageListenerRegistered = true;
8773
+ console.log("[AMT] Registered service worker message handler");
8774
+ }
8775
+ removeMessageListener() {
8776
+ if (!this.messageListenerRegistered)
8777
+ return;
8778
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8779
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8780
+ }
8781
+ this.messageListenerRegistered = false;
8511
8782
  }
8512
8783
  /**
8513
8784
  * Subscribe via AMT tunnel
@@ -8517,6 +8788,7 @@ var AMTGatewayManager = class {
8517
8788
  if (config.protected) {
8518
8789
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8519
8790
  }
8791
+ this.ensureMessageListener();
8520
8792
  const keyStr = this.getKeyString(key);
8521
8793
  if (this.subscriptions.has(keyStr)) {
8522
8794
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8552,6 +8824,8 @@ var AMTGatewayManager = class {
8552
8824
  this.subscriptions.delete(keyStr);
8553
8825
  this.pendingPackets.delete(subscription.subscriptionId);
8554
8826
  }
8827
+ if (this.subscriptions.size === 0)
8828
+ this.removeMessageListener();
8555
8829
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8556
8830
  }
8557
8831
  }
@@ -8573,6 +8847,8 @@ var AMTGatewayManager = class {
8573
8847
  } finally {
8574
8848
  this.subscriptions.delete(handle);
8575
8849
  this.pendingPackets.delete(subscription.subscriptionId);
8850
+ if (this.subscriptions.size === 0)
8851
+ this.removeMessageListener();
8576
8852
  }
8577
8853
  }
8578
8854
  /**
@@ -8589,14 +8865,12 @@ var AMTGatewayManager = class {
8589
8865
  if (!subscriptionId)
8590
8866
  return;
8591
8867
  const subscription = this.subscriptions.get(subscriptionId);
8592
- if (!subscription) {
8593
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8868
+ if (!subscription)
8594
8869
  return;
8595
- }
8596
8870
  if (!subscription.handshakeComplete) {
8597
8871
  const queue = this.pendingPackets.get(subscriptionId);
8598
8872
  const packetData2 = data;
8599
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8600
8874
  queue.push({
8601
8875
  data: packetData2.data,
8602
8876
  remoteAddress: packetData2.remoteAddress,
@@ -8622,19 +8896,15 @@ var AMTGatewayManager = class {
8622
8896
  if (!subscriptionId)
8623
8897
  return;
8624
8898
  const subscription = this.subscriptions.get(subscriptionId);
8625
- if (!subscription) {
8626
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8899
+ if (!subscription)
8627
8900
  return;
8628
- }
8629
8901
  subscription.config.onError(new Error(error || "Unknown error"));
8630
8902
  }
8631
8903
  /**
8632
8904
  * Cleanup on destroy
8633
8905
  */
8634
8906
  destroy() {
8635
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8636
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8637
- }
8907
+ this.removeMessageListener();
8638
8908
  const handles = Array.from(this.subscriptions.keys());
8639
8909
  for (const handle of handles) {
8640
8910
  this.unsubscribe(handle).catch((err) => {
@@ -8733,9 +9003,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
8733
9003
  }
8734
9004
  function validateMmtpDashTemplate(template, path) {
8735
9005
  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}$`);
9006
+ const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
9007
+ if (unsupported2 !== void 0) {
9008
+ throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
8739
9009
  }
8740
9010
  if (variables.filter((variable) => variable === "Number").length !== 1) {
8741
9011
  throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
@@ -12679,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12679
12949
  #retentionBlocks;
12680
12950
  #heldEvicted;
12681
12951
  #newest;
12952
+ #newestSource;
12682
12953
  #holes;
12683
12954
  static #checkedRetention(retentionBlocks) {
12684
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12716,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12716
12987
  observe(position) {
12717
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12718
12989
  }
12990
+ /**
12991
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
12992
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
12993
+ * a source cannot run ahead of the sources sent before it except by
12994
+ * reordering.
12995
+ */
12996
+ get newestSource() {
12997
+ return this.#newestSource;
12998
+ }
12999
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13000
+ noteSource(ssId) {
13001
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13002
+ this.#newestSource = ssId;
13003
+ return true;
13004
+ }
12719
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12720
13006
  get heldEvicted() {
12721
13007
  return this.#heldEvicted;
@@ -12948,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12948
13234
  this.#held.clear();
12949
13235
  this.#coverageEnd = void 0;
12950
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
12951
13238
  }
12952
13239
  #claim(block) {
12953
13240
  const placed = [];
@@ -13567,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13567
13854
  var WORKER_ERROR_KIND_CAP = 32;
13568
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13569
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13570
13858
  function isMulticastSymbolDelivery(value) {
13571
13859
  return value === "datagram" || value === "ordered-relay";
13572
13860
  }
@@ -13664,6 +13952,10 @@ var FECManager = class _FECManager {
13664
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13665
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13666
13954
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
13955
+ // Source ESIs per block that another FEC instance rebuilt and fed in
13956
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
13957
+ // not network arrivals and never make a block clean.
13958
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13667
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13668
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13669
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -13792,6 +14084,10 @@ var FECManager = class _FECManager {
13792
14084
  // ── Keyframe-protected repair policy (block-policy.ts) ──
13793
14085
  // One classifier per physical stream, built from that stream's geometry.
13794
14086
  this.#classifiers = /* @__PURE__ */ new Map();
14087
+ // A stream without catalog timing has no classifier, so the last RAP it
14088
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14089
+ // a second RAP is released inside one MPU (BLO-52085).
14090
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
13795
14091
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
13796
14092
  // rungs can share a packetId with a different framerate or GOP, so the
13797
14093
  // packet-level geometry is only the geometry of a packet whose streams
@@ -13949,6 +14245,7 @@ var FECManager = class _FECManager {
13949
14245
  #pendingWorkerCleanupBounds;
13950
14246
  #blockSourceCounts;
13951
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
13952
14249
  #blockAuthoredLengths;
13953
14250
  #ssbLengthConflictCount;
13954
14251
  #blockRepairRsIds;
@@ -13991,6 +14288,7 @@ var FECManager = class _FECManager {
13991
14288
  #orderedFrontiers;
13992
14289
  #orderedPendingBlocks;
13993
14290
  #classifiers;
14291
+ #noSpanRaps;
13994
14292
  #streamGeometries;
13995
14293
  #failedBlocks;
13996
14294
  #resolvedBlockClasses;
@@ -14652,6 +14950,7 @@ var FECManager = class _FECManager {
14652
14950
  };
14653
14951
  const discontinuity = () => {
14654
14952
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14953
+ this.#noSpanRaps.delete(streamKey);
14655
14954
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14656
14955
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14657
14956
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15064,6 +15363,9 @@ var FECManager = class _FECManager {
15064
15363
  }
15065
15364
  if (policyChanged) {
15066
15365
  this.#bumpEpoch(packetId);
15366
+ for (const key of [...this.#noSpanRaps.keys()]) {
15367
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15368
+ }
15067
15369
  for (const [key, classifier] of [...this.#classifiers]) {
15068
15370
  if (!this.#belongsToPacket(key, packetId)) continue;
15069
15371
  const stream = this.#parseStreamKey(key);
@@ -15266,6 +15568,7 @@ var FECManager = class _FECManager {
15266
15568
  }
15267
15569
  resetMap(this.#blockSourceCounts);
15268
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15269
15572
  resetMap(this.#blockAuthoredLengths);
15270
15573
  resetMap(this.#blockRepairRsIds);
15271
15574
  resetMap(this.#blockRsbLengths);
@@ -15299,6 +15602,7 @@ var FECManager = class _FECManager {
15299
15602
  resetMap(this.#isoLocators);
15300
15603
  resetMap(this.#sourceAnchors);
15301
15604
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15605
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15302
15606
  this.#bumpEpoch(packetId);
15303
15607
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15304
15608
  for (const key of keys.keys()) {
@@ -15389,6 +15693,7 @@ var FECManager = class _FECManager {
15389
15693
  dropMap(this.#pendingWorkerCleanupClients);
15390
15694
  dropMap(this.#blockSourceCounts);
15391
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15392
15697
  dropMap(this.#blockAuthoredLengths);
15393
15698
  dropMap(this.#blockRepairRsIds);
15394
15699
  dropMap(this.#blockRsbLengths);
@@ -15405,6 +15710,7 @@ var FECManager = class _FECManager {
15405
15710
  dropMap(this.#isoLocators);
15406
15711
  dropMap(this.#sourceAnchors);
15407
15712
  dropMap(this.#classifiers);
15713
+ dropMap(this.#noSpanRaps);
15408
15714
  dropMap(this.#streamGeometries);
15409
15715
  dropSet(this.#pendingWorkerCleanups);
15410
15716
  dropSet(this.#completedBlocks);
@@ -15550,6 +15856,9 @@ var FECManager = class _FECManager {
15550
15856
  this.#unindexBlockKey(key);
15551
15857
  }
15552
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15553
15862
  return true;
15554
15863
  }
15555
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -15867,6 +16176,7 @@ var FECManager = class _FECManager {
15867
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
15868
16177
  const evictedBefore = locator.heldEvicted;
15869
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
15870
16180
  const evicted = locator.heldEvicted - evictedBefore;
15871
16181
  if (evicted > 0) {
15872
16182
  const before = this.#isoHeldEvictedCount;
@@ -16035,11 +16345,14 @@ var FECManager = class _FECManager {
16035
16345
  /**
16036
16346
  * Whether an MPU boundary still bounds this stream's RAPs (see
16037
16347
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16038
- * it does not. A stream with no classifier yet has shown nothing against it.
16348
+ * it does not. A stream with no classifier yet has shown nothing against it,
16349
+ * and neither has a stream without catalog timing until it releases two
16350
+ * RAPs inside one MPU.
16039
16351
  */
16040
16352
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16041
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16042
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16353
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16354
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16355
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16043
16356
  }
16044
16357
  /** A block's class now, or frozen at its resolution. */
16045
16358
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16065,7 +16378,14 @@ var FECManager = class _FECManager {
16065
16378
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16066
16379
  */
16067
16380
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16068
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16381
+ if (!this.#trackConfigs.has(packetId)) return;
16382
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16383
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16384
+ const last = this.#noSpanRaps.get(streamKey);
16385
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16386
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16387
+ return;
16388
+ }
16069
16389
  this.#applyClassifierChanges(
16070
16390
  packetId,
16071
16391
  catalogTrackName,
@@ -16452,7 +16772,15 @@ var FECManager = class _FECManager {
16452
16772
  this.#isoSourceOversizeCount++;
16453
16773
  return null;
16454
16774
  }
16455
- const sbn = located.block.start;
16775
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16776
+ }
16777
+ /**
16778
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16779
+ * answers through onFrame (null here), or its synchronous decoder, whose
16780
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16781
+ * stated for `ssId`.
16782
+ */
16783
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16456
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16457
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16458
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16481,6 +16809,68 @@ var FECManager = class _FECManager {
16481
16809
  }
16482
16810
  return recovered;
16483
16811
  }
16812
+ /**
16813
+ * Feed a source symbol of this stream that another FEC instance over the
16814
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16815
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16816
+ * BLO-52085). It joins its block's decoder as a received source would, so
16817
+ * it reduces the erasures the block still has to solve, but it is not a
16818
+ * network arrival: no transport accounting, no classification evidence,
16819
+ * no deadline refresh, and it never makes a block clean.
16820
+ *
16821
+ * Only a block a repair stated takes one (a repair-less span has no
16822
+ * decoder block). A block that is terminal, or already holds that source,
16823
+ * does not. The symbol is the exact protected packet, unpadded, as
16824
+ * feedSourceSymbol takes it.
16825
+ *
16826
+ * `recovered` is the block the synchronous decoder completed with it; the
16827
+ * caller routes it and completes the block, as for any recovery. When
16828
+ * every source of the block is then present, received or rebuilt, and no
16829
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16830
+ * block as clean on its own side), the block is completed here as
16831
+ * recovered, with nothing left to route.
16832
+ */
16833
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16834
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16835
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16836
+ if (located === null || located.block.span) return NOT_TAKEN;
16837
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16838
+ const sbn = located.block.start;
16839
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16840
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16841
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16842
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16843
+ if (rebuilt === void 0) {
16844
+ rebuilt = /* @__PURE__ */ new Set();
16845
+ this.#blockRebuiltEsis.set(key, rebuilt);
16846
+ }
16847
+ rebuilt.add(located.esi);
16848
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16849
+ if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName);
16850
+ return { taken: true, recovered };
16851
+ }
16852
+ /**
16853
+ * A block that holds every one of its `length` sources, received or
16854
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16855
+ * every packet of it was delivered, by its own arrival or by the instance
16856
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16857
+ */
16858
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16859
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16860
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16861
+ let present = rebuilt.size;
16862
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16863
+ if (present < length) return false;
16864
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16865
+ this.countRecovery(packetId, sbn, catalogTrackName);
16866
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16867
+ if (catalogTrackName === void 0) {
16868
+ this.onBlockComplete?.(sbn, packetId);
16869
+ } else {
16870
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16871
+ }
16872
+ return true;
16873
+ }
16484
16874
  /**
16485
16875
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16486
16876
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16683,8 +17073,72 @@ var FECManager = class _FECManager {
16683
17073
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16684
17074
  return null;
16685
17075
  }
17076
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16686
17077
  return recovered;
16687
17078
  }
17079
+ /**
17080
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17081
+ * its sources in send order, so one that moves the stream's newest source
17082
+ * position past a block leaves the sources that block still misses
17083
+ * reordered behind it at best: it shortens the wait of every block it
17084
+ * passed that only a late source can complete. MoQ is left out: its streams
17085
+ * can overtake one another, so a MoQ source shows nothing about the sources
17086
+ * sent before it.
17087
+ */
17088
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17089
+ const previous = locator.newestSource;
17090
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17091
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17092
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17093
+ this.#shortenUnrecoverableWait(
17094
+ this.#blockKey(block.start, packetId, catalogTrackName),
17095
+ block.start,
17096
+ packetId,
17097
+ catalogTrackName
17098
+ );
17099
+ }
17100
+ }
17101
+ /**
17102
+ * The wait of a block that only a late source can complete (BLO-52085): a
17103
+ * repair stated it, its received sources plus every symbol its repair group
17104
+ * carries (RSB_length) are short of K' (the repair scheduler's
17105
+ * group-cannot-complete), and a later source of the stream has arrived, so
17106
+ * a source it misses is lost or reordered behind that one. The span, which
17107
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17108
+ * the late-symbol allowance alone, the multicast wait less the span (the
17109
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17110
+ * block.
17111
+ */
17112
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17113
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17114
+ const rsbLength = this.#blockRsbLengths.get(key);
17115
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17116
+ if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
17117
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17118
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17119
+ return void 0;
17120
+ }
17121
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17122
+ }
17123
+ /**
17124
+ * Bring an open block's wait in to its late-source wait, when it has one.
17125
+ * Never later than it is armed: only a source can complete such a block,
17126
+ * so a repair of its group (paced across the span) must not keep it open.
17127
+ */
17128
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17129
+ if (!this.#activeBlocks.has(key)) return;
17130
+ const config = this.#trackConfigs.get(packetId);
17131
+ if (config === void 0) return;
17132
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17133
+ if (waitMs === void 0) return;
17134
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17135
+ this.#armBlockTimeout(
17136
+ sbn,
17137
+ packetId,
17138
+ catalogTrackName,
17139
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17140
+ );
17141
+ }
16688
17142
  /**
16689
17143
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16690
17144
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -16839,6 +17293,15 @@ var FECManager = class _FECManager {
16839
17293
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
16840
17294
  return;
16841
17295
  }
17296
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17297
+ key,
17298
+ sbn,
17299
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17300
+ packetId,
17301
+ catalogTrackName
17302
+ )) {
17303
+ return;
17304
+ }
16842
17305
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
16843
17306
  }
16844
17307
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -16847,6 +17310,7 @@ var FECManager = class _FECManager {
16847
17310
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16848
17311
  this.#blockSourceCounts.delete(key);
16849
17312
  this.#blockSourceEsis.delete(key);
17313
+ this.#blockRebuiltEsis.delete(key);
16850
17314
  this.#blockAuthoredLengths.delete(key);
16851
17315
  this.#blockRepairRsIds.delete(key);
16852
17316
  this.#blockRsbLengths.delete(key);
@@ -16913,7 +17377,16 @@ var FECManager = class _FECManager {
16913
17377
  this.#repairDeadlineMs(trackConfig)
16914
17378
  );
16915
17379
  }
16916
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17380
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17381
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17382
+ return;
17383
+ }
17384
+ this.#armBlockTimeout(
17385
+ sbn,
17386
+ packetId,
17387
+ catalogTrackName,
17388
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17389
+ );
16917
17390
  }
16918
17391
  #failBlock(sbn, packetId, catalogTrackName) {
16919
17392
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -16926,6 +17399,7 @@ var FECManager = class _FECManager {
16926
17399
  }
16927
17400
  this.#blockSourceCounts.delete(key);
16928
17401
  this.#blockSourceEsis.delete(key);
17402
+ this.#blockRebuiltEsis.delete(key);
16929
17403
  this.#blockAuthoredLengths.delete(key);
16930
17404
  this.#blockRepairRsIds.delete(key);
16931
17405
  this.#blockRsbLengths.delete(key);
@@ -16964,6 +17438,7 @@ var FECManager = class _FECManager {
16964
17438
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
16965
17439
  this.#blockSourceCounts.delete(key);
16966
17440
  this.#blockSourceEsis.delete(key);
17441
+ this.#blockRebuiltEsis.delete(key);
16967
17442
  this.#blockAuthoredLengths.delete(key);
16968
17443
  this.#blockRepairRsIds.delete(key);
16969
17444
  this.#blockRsbLengths.delete(key);
@@ -17055,6 +17530,7 @@ var FECManager = class _FECManager {
17055
17530
  this.#blockKeysBySbn.clear();
17056
17531
  this.#blockSourceCounts.clear();
17057
17532
  this.#blockSourceEsis.clear();
17533
+ this.#blockRebuiltEsis.clear();
17058
17534
  this.#blockAuthoredLengths.clear();
17059
17535
  this.#blockRepairRsIds.clear();
17060
17536
  this.#blockRsbLengths.clear();
@@ -17080,6 +17556,7 @@ var FECManager = class _FECManager {
17080
17556
  this.#blockAnchorsMs.clear();
17081
17557
  this.#blockCatalogTrackNames.clear();
17082
17558
  this.#classifiers.clear();
17559
+ this.#noSpanRaps.clear();
17083
17560
  this.#streamGeometries.clear();
17084
17561
  this.#failedBlocks.clear();
17085
17562
  this.#resolvedBlockClasses.clear();
@@ -17098,17 +17575,17 @@ var FECManager = class _FECManager {
17098
17575
 
17099
17576
  // src/mfu-reassembly-policy.ts
17100
17577
  function validateReliableMoqOptionShape(value) {
17101
- const unsupported = Object.keys(value).filter(
17578
+ const unsupported2 = Object.keys(value).filter(
17102
17579
  (key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
17103
17580
  );
17104
- if (unsupported.length > 0) {
17105
- if (unsupported.includes("maxRacingGroups")) {
17581
+ if (unsupported2.length > 0) {
17582
+ if (unsupported2.includes("maxRacingGroups")) {
17106
17583
  throw new Error(
17107
17584
  "[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
17108
17585
  );
17109
17586
  }
17110
17587
  throw new Error(
17111
- `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported.join(", ")}`
17588
+ `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
17112
17589
  );
17113
17590
  }
17114
17591
  const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
@@ -17152,12 +17629,12 @@ function assertValidMfuReassemblyPolicy(policy) {
17152
17629
  return;
17153
17630
  }
17154
17631
  if (value["transport"] === "datagram") {
17155
- const unsupported = Object.keys(value).filter(
17632
+ const unsupported2 = Object.keys(value).filter(
17156
17633
  (key) => key !== "transport" && key !== "deadlineMs"
17157
17634
  );
17158
- if (unsupported.length > 0) {
17635
+ if (unsupported2.length > 0) {
17159
17636
  throw new Error(
17160
- `[MFU reassembly] unsupported datagram option(s): ${unsupported.join(", ")}`
17637
+ `[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
17161
17638
  );
17162
17639
  }
17163
17640
  assertValidDatagramDeadline(value["deadlineMs"]);
@@ -17610,9 +18087,21 @@ var IsoBmffTimingResolver = class {
17610
18087
  }
17611
18088
  #track = null;
17612
18089
  #fragments = /* @__PURE__ */ new Map();
18090
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18091
+ #firstSamples = /* @__PURE__ */ new Map();
18092
+ /**
18093
+ * First sample and sample count of the newest fragments whose every slot was
18094
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18095
+ * matches the combiner, since it retires an abandoned sample as well.
18096
+ */
18097
+ #delivered = /* @__PURE__ */ new Map();
17613
18098
  #pendingSamples = 0;
17614
18099
  #evictedSamples = 0;
17615
18100
  #lastMovieFragmentRejection = null;
18101
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18102
+ get initialized() {
18103
+ return this.#track !== null;
18104
+ }
17616
18105
  get pendingSampleCount() {
17617
18106
  return this.#pendingSamples;
17618
18107
  }
@@ -17636,7 +18125,14 @@ var IsoBmffTimingResolver = class {
17636
18125
  this.#track = timing;
17637
18126
  return true;
17638
18127
  }
17639
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18128
+ /**
18129
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18130
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18131
+ * fragment whose samples are all done is a duplicate: admitting it again
18132
+ * would charge stale timing and could displace the live MPU's. A source
18133
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18134
+ */
18135
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17640
18136
  this.#lastMovieFragmentRejection = null;
17641
18137
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17642
18138
  this.#lastMovieFragmentRejection = "invalid";
@@ -17662,6 +18158,8 @@ var IsoBmffTimingResolver = class {
17662
18158
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17663
18159
  );
17664
18160
  }
18161
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18162
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17665
18163
  let freed = 0;
17666
18164
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17667
18165
  freed += this.#evictOldestFragment();
@@ -17675,6 +18173,7 @@ var IsoBmffTimingResolver = class {
17675
18173
  return false;
17676
18174
  }
17677
18175
  this.#fragments.set(key, [...parsed.samples]);
18176
+ this.#firstSamples.set(key, parsed.samples[0]);
17678
18177
  this.#pendingSamples += parsed.samples.length;
17679
18178
  return true;
17680
18179
  }
@@ -17732,6 +18231,7 @@ var IsoBmffTimingResolver = class {
17732
18231
  let live = 0;
17733
18232
  for (const sample of samples) if (sample !== null) live++;
17734
18233
  this.#fragments.delete(oldest);
18234
+ this.#firstSamples.delete(oldest);
17735
18235
  this.#pendingSamples -= live;
17736
18236
  this.#evictedSamples += live;
17737
18237
  return live;
@@ -17747,12 +18247,23 @@ var IsoBmffTimingResolver = class {
17747
18247
  if (!samples || !samples[index]) return false;
17748
18248
  samples[index] = null;
17749
18249
  this.#pendingSamples--;
17750
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18250
+ if (samples.every((value) => value === null)) {
18251
+ this.#fragments.delete(key);
18252
+ const first = this.#firstSamples.get(key);
18253
+ this.#firstSamples.delete(key);
18254
+ this.#delivered.delete(key);
18255
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18256
+ if (this.#delivered.size > this.maxPendingSamples) {
18257
+ this.#delivered.delete(this.#delivered.keys().next().value);
18258
+ }
18259
+ }
17751
18260
  return true;
17752
18261
  }
17753
18262
  reset() {
17754
18263
  this.#track = null;
17755
18264
  this.#fragments.clear();
18265
+ this.#firstSamples.clear();
18266
+ this.#delivered.clear();
17756
18267
  this.#pendingSamples = 0;
17757
18268
  }
17758
18269
  };
@@ -18234,6 +18745,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18234
18745
  this.#reclaimedTimingSamples = 0;
18235
18746
  this.#evictedTimingSamples = 0;
18236
18747
  this.#rejectedMovieFragments = 0;
18748
+ this.#recoveredHeadsAwaitingTiming = {
18749
+ held: 0,
18750
+ expired: 0,
18751
+ discarded: 0,
18752
+ superseded: 0
18753
+ };
18237
18754
  this.#sequencers = /* @__PURE__ */ new Map();
18238
18755
  this.#fecGate = null;
18239
18756
  this.#fecSequencer = null;
@@ -18342,6 +18859,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18342
18859
  #reclaimedTimingSamples;
18343
18860
  #evictedTimingSamples;
18344
18861
  #rejectedMovieFragments;
18862
+ #recoveredHeadsAwaitingTiming;
18345
18863
  #sequencers;
18346
18864
  #fecGate;
18347
18865
  #fecSequencer;
@@ -18462,6 +18980,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18462
18980
  rejectedMovieFragments: this.#rejectedMovieFragments
18463
18981
  };
18464
18982
  }
18983
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
18984
+ get recoveredHeadsAwaitingTiming() {
18985
+ let pending = 0;
18986
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
18987
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
18988
+ }
18989
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
18990
+ }
18465
18991
  get reorderStats() {
18466
18992
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18467
18993
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18590,10 +19116,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18590
19116
  get reassemblerDataUnitsReturned() {
18591
19117
  return this.#reassemblerDataUnitsReturned;
18592
19118
  }
18593
- #countOutcome(token) {
18594
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18595
- this.#feedOutcomes.set(token, count);
18596
- this.#lastOutcome = token;
19119
+ #countOutcome(token2) {
19120
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
19121
+ this.#feedOutcomes.set(token2, count);
19122
+ this.#lastOutcome = token2;
18597
19123
  this.#lastOutcomeCount = count;
18598
19124
  }
18599
19125
  #countDeferredOutcome(reason) {
@@ -18635,7 +19161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18635
19161
  const now = Date.now();
18636
19162
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18637
19163
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18638
- awaitingTiming.shift();
19164
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18639
19165
  }
18640
19166
  }
18641
19167
  }
@@ -18753,11 +19279,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
18753
19279
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
18754
19280
  return this.#drop("fragment-unexpected-fragmentation");
18755
19281
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
18756
- if (!resolver2) return this.#drop("fragment-no-resolver");
19282
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
18757
19283
  const pendingBefore = resolver2.pendingSampleCount;
18758
19284
  const evictedBefore = resolver2.evictedSampleCount;
18759
19285
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
18760
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19286
+ const admitted = resolver2.addMovieFragment(
19287
+ mpu.mpuSequenceNumber,
19288
+ data,
19289
+ remaining,
19290
+ options?.recovered === true
19291
+ );
18761
19292
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
18762
19293
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
18763
19294
  if (!admitted) {
@@ -18777,7 +19308,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18777
19308
  };
18778
19309
  }
18779
19310
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
18780
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19311
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
18781
19312
  if (!resolver) return this.#drop("mfu-no-resolver");
18782
19313
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
18783
19314
  return this.#drop("mfu-empty-payload");
@@ -18798,6 +19329,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18798
19329
  mpuSequenceNumber: mpu.mpuSequenceNumber,
18799
19330
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
18800
19331
  sampleNumber: mfuDuHeader.sampleNumber,
19332
+ sequenceNumber: info.sequenceNumber,
18801
19333
  arrivedAt: Date.now()
18802
19334
  });
18803
19335
  return this.#drop("mfu-sample-timing-missing");
@@ -19121,6 +19653,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19121
19653
  }
19122
19654
  this.#emitFrame(data, meta);
19123
19655
  }
19656
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19657
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19658
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19659
+ }
19124
19660
  /**
19125
19661
  * Claim and return the authored timing for a recovered sample. Recovery does
19126
19662
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19182,6 +19718,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19182
19718
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19183
19719
  this.#sequencers.clear();
19184
19720
  this.#fecSequencer?.discard();
19721
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19722
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19723
+ }
19185
19724
  this.#timingStreams.clear();
19186
19725
  this.#pendingTimingSamples = 0;
19187
19726
  this.#reliableMoqStartupGate.clear();
@@ -19200,10 +19739,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19200
19739
  return resolver;
19201
19740
  }
19202
19741
  #awaitTiming(streamKey, head) {
19203
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19204
- if (!awaitingTiming) return;
19742
+ const state = this.#timingStreams.get(streamKey);
19743
+ if (!state) return;
19744
+ if (!head.options?.recovered) {
19745
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19746
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19747
+ state.awaitingTiming = kept;
19748
+ }
19749
+ const awaitingTiming = state.awaitingTiming;
19205
19750
  awaitingTiming.push(head);
19206
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19751
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19752
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19753
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19754
+ }
19755
+ }
19756
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19757
+ #leaveAwaitingTiming(head, outcome) {
19758
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19207
19759
  }
19208
19760
  /**
19209
19761
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19231,6 +19783,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19231
19783
  const state = this.#timingStreams.get(streamKey);
19232
19784
  if (!state) return;
19233
19785
  this.#timingStreams.delete(streamKey);
19786
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19234
19787
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19235
19788
  if (this.#pendingTimingSamples < 0) {
19236
19789
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22437,6 +22990,12 @@ function keyframeOverlaySsId(header) {
22437
22990
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22438
22991
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22439
22992
  }
22993
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
22994
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
22995
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
22996
+ }
22997
+ var OVERLAY_ROUTED_LIMIT = 1024;
22998
+ var OVERLAY_PENDING_LIMIT = 256;
22440
22999
  var OVERLAY_OFFSET_MPUS = 16;
22441
23000
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22442
23001
  function assertStreamIdentity(role, stream) {
@@ -22673,7 +23232,9 @@ var MmtpRouter = class {
22673
23232
  recovered: 0,
22674
23233
  restarts: 0,
22675
23234
  unsequencedDropped: 0,
22676
- protectedDropped: 0
23235
+ protectedDropped: 0,
23236
+ baseFed: 0,
23237
+ baseUndetermined: 0
22677
23238
  };
22678
23239
  /** Callback fired when a delta-track FEC block completes or fails. */
22679
23240
  #onDeltaBlockCompleteCb = null;
@@ -22846,7 +23407,7 @@ var MmtpRouter = class {
22846
23407
  const cb = this.#getFrameCallback(packetId);
22847
23408
  if (isVideo && !assembler) {
22848
23409
  if (frameMeta.recovered) {
22849
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23410
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
22850
23411
  } else {
22851
23412
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
22852
23413
  }
@@ -23236,6 +23797,10 @@ var MmtpRouter = class {
23236
23797
  get keyframeOverlayStats() {
23237
23798
  return { ...this.#overlayStats };
23238
23799
  }
23800
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23801
+ get recoveredHeadsAwaitingTiming() {
23802
+ return this.#parser.recoveredHeadsAwaitingTiming;
23803
+ }
23239
23804
  #isOverlayRepair(repairPacketId, repairTrackName) {
23240
23805
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23241
23806
  }
@@ -23286,6 +23851,11 @@ var MmtpRouter = class {
23286
23851
  */
23287
23852
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23288
23853
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23854
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
23855
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
23856
+ stream.overlayToBase.routed.clear();
23857
+ stream.overlayToBase.pending.clear();
23858
+ }
23289
23859
  }
23290
23860
  #resetOverlay(inst) {
23291
23861
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -23342,9 +23912,10 @@ var MmtpRouter = class {
23342
23912
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23343
23913
  * every overlay installed for its source, at that SS_ID, with the bytes the
23344
23914
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23345
- * included (7.1, 7.4.3).
23915
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23916
+ * FEC reads it.
23346
23917
  */
23347
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
23918
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23348
23919
  if (this.#overlays.size === 0) return;
23349
23920
  const ssId = keyframeOverlaySsId(header);
23350
23921
  if (ssId === void 0) return;
@@ -23353,7 +23924,11 @@ var MmtpRouter = class {
23353
23924
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23354
23925
  if (identity !== null) {
23355
23926
  inst.offsets.delete(identity.mpuSequenceNumber);
23356
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
23927
+ inst.offsets.set(identity.mpuSequenceNumber, {
23928
+ psnOffset: ssId - sequenceNumber >>> 0,
23929
+ overlaySsId: ssId,
23930
+ ...baseSsId === void 0 ? {} : { baseSsId }
23931
+ });
23357
23932
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23358
23933
  }
23359
23934
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23432,12 +24007,17 @@ var MmtpRouter = class {
23432
24007
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23433
24008
  const restamped = this.#restampedStreams.has(streamKey);
23434
24009
  const restored = /* @__PURE__ */ new Set();
24010
+ const baseSsIds = /* @__PURE__ */ new Map();
23435
24011
  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;
24012
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24013
+ if (anchor !== void 0) {
24014
+ const overlaySsId = serial32Add(blockStart, sym.index);
24015
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23439
24016
  restored.add(packetSequenceNumber);
23440
- return [{ ...sym, packetSequenceNumber }];
24017
+ const out = { ...sym, packetSequenceNumber };
24018
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24019
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
24020
+ return [out];
23441
24021
  }
23442
24022
  if (!restamped) return [sym];
23443
24023
  this.#overlayStats.unsequencedDropped++;
@@ -23448,6 +24028,80 @@ var MmtpRouter = class {
23448
24028
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
23449
24029
  });
23450
24030
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24031
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24032
+ }
24033
+ /**
24034
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24035
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId),
24036
+ * so it is fed to the base decoder as a rebuilt source
24037
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24038
+ * fewer to solve, and its repair can recover the block's other packets.
24039
+ * A base block no repair has stated yet takes it when one does
24040
+ * (#feedPendingRebuilt).
24041
+ *
24042
+ * The overlay route already handled each packet fed (delivered it, or
24043
+ * held an untimed head for its FT=1), so a base recovery routes none of
24044
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24045
+ * held head would wait in the parser for timing its first copy consumed.
24046
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24047
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24048
+ * that crossed no multicast socket, which the provenance counted from the
24049
+ * block's own arrivals cannot show.
24050
+ */
24051
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24052
+ const fec = this.#fecManager;
24053
+ const owner = inst.leg.sourcePacketId;
24054
+ const name = inst.leg.sourceCatalogTrackName;
24055
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24056
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24057
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24058
+ const feeds = [];
24059
+ for (const sym of symbols) {
24060
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24061
+ const ssId = baseSsIds.get(sym);
24062
+ if (ssId === void 0) {
24063
+ this.#overlayStats.baseUndetermined++;
24064
+ continue;
24065
+ }
24066
+ routed.delete(ssId);
24067
+ routed.add(ssId);
24068
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24069
+ feeds.push([ssId, sym.rawSymbol]);
24070
+ }
24071
+ for (const [ssId, symbolData] of feeds) this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24072
+ }
24073
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24074
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24075
+ const fec = this.#fecManager;
24076
+ if (!fec?.feedRebuiltSourceSymbol) return;
24077
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24078
+ if (coords == null) {
24079
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24080
+ pending.delete(ssId);
24081
+ pending.set(ssId, symbolData);
24082
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24083
+ return;
24084
+ }
24085
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24086
+ if (!fed.taken) return;
24087
+ this.#overlayStats.baseFed++;
24088
+ if (fed.recovered === null) return;
24089
+ const sbn = coords.sbn;
24090
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24091
+ fec.cancelBlockTimeout(sbn, owner, name);
24092
+ fec.countRecovery(owner, sbn, name);
24093
+ fec.markBlockCompleted(sbn, owner, name);
24094
+ });
24095
+ }
24096
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24097
+ #feedPendingRebuilt(owner, name, block) {
24098
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24099
+ if (pending === void 0 || pending.size === 0) return;
24100
+ for (const [ssId, symbolData] of [...pending]) {
24101
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24102
+ pending.delete(ssId);
24103
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24104
+ }
23451
24105
  }
23452
24106
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23453
24107
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23579,10 +24233,11 @@ var MmtpRouter = class {
23579
24233
  if (!entry) return;
23580
24234
  entry.receiver.release();
23581
24235
  entry.protected = false;
24236
+ entry.userAgent = false;
23582
24237
  this.#assistBlocks.clear();
23583
24238
  this.#assistFailures.clear();
23584
24239
  }
23585
- /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
24240
+ /** 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
24241
  get unadmittedListedDropCount() {
23587
24242
  return this.#unadmittedListedDrops;
23588
24243
  }
@@ -23623,6 +24278,13 @@ var MmtpRouter = class {
23623
24278
  }
23624
24279
  return false;
23625
24280
  }
24281
+ /** Whether a protected flow the user agent admitted lists `packetId`. */
24282
+ #userAgentGoverns(packetId) {
24283
+ for (const entry of this.#provenance.values()) {
24284
+ if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
24285
+ }
24286
+ return false;
24287
+ }
23626
24288
  /**
23627
24289
  * A MoQ repair symbol no protected flow lists, owned by a source track one
23628
24290
  * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
@@ -23713,7 +24375,7 @@ var MmtpRouter = class {
23713
24375
  fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
23714
24376
  fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
23715
24377
  fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
23716
- })) {
24378
+ }, true)) {
23717
24379
  this.#unlistedAssistedRecoveries++;
23718
24380
  this.#assistBlocks.delete(key);
23719
24381
  return;
@@ -24055,6 +24717,8 @@ var MmtpRouter = class {
24055
24717
  }
24056
24718
  this.#replayIsoSources(ownerPacketId, catalogTrackName, blockStart, placement.placed);
24057
24719
  if (this.#classifyPacketId(ownerPacketId) === null) return;
24720
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24721
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24058
24722
  if (!this.#feedFecTransportSymbol(
24059
24723
  ownerPacketId,
24060
24724
  blockStart,
@@ -24869,6 +25533,12 @@ var MmtpRouter = class {
24869
25533
  debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
24870
25534
  return;
24871
25535
  }
25536
+ if (ingress.admittedBy === USER_AGENT_VERIFIED) {
25537
+ this.#noteFlowAdmitted(ingress.flow);
25538
+ protectedFlow.userAgent = true;
25539
+ this.#processMMTP(packet, source, true, ingress, overlapCopy);
25540
+ return;
25541
+ }
24872
25542
  const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
24873
25543
  if (verdict === "admitted") {
24874
25544
  this.#noteFlowAdmitted(ingress.flow);
@@ -24886,6 +25556,11 @@ var MmtpRouter = class {
24886
25556
  return;
24887
25557
  }
24888
25558
  }
25559
+ const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
25560
+ if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
25561
+ this.#unadmittedListedDrops++;
25562
+ return;
25563
+ }
24889
25564
  if (info.isRepair) {
24890
25565
  const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
24891
25566
  if (overlay !== void 0) {
@@ -24903,7 +25578,7 @@ var MmtpRouter = class {
24903
25578
  );
24904
25579
  if (!repair) return;
24905
25580
  if (ownerPacketId != null && this.#fecManager) {
24906
- if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
25581
+ if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
24907
25582
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
24908
25583
  const count = this.#repairPidCounts[info.packetId];
24909
25584
  if (count <= 2 || count % 500 === 0) {
@@ -25106,7 +25781,8 @@ var MmtpRouter = class {
25106
25781
  parsed.header,
25107
25782
  info.sequenceNumber,
25108
25783
  info.rapFlag,
25109
- source
25784
+ source,
25785
+ fecPayloadId?.ssId
25110
25786
  );
25111
25787
  }
25112
25788
  }
@@ -25256,7 +25932,8 @@ var MmtpRouter = class {
25256
25932
  if (stream === void 0) {
25257
25933
  stream = {
25258
25934
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25259
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
25935
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
25936
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25260
25937
  };
25261
25938
  byName.set(catalogTrackName, stream);
25262
25939
  }
@@ -25371,8 +26048,16 @@ var MmtpRouter = class {
25371
26048
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25372
26049
  this.#emitDirectVideo(packetId, data, meta, cb);
25373
26050
  }
25374
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26051
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25375
26052
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26053
+ if (this.#isFecTrack(packetId)) {
26054
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26055
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26056
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26057
+ this.#emitDirectVideo(packetId, data, meta, cb);
26058
+ return;
26059
+ }
26060
+ }
25376
26061
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25377
26062
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25378
26063
  this.#drainDirectRecoveredVideo(
@@ -25482,7 +26167,7 @@ var MmtpRouter = class {
25482
26167
  }
25483
26168
  // ── Private: FEC recovery routing ───────────────────────────────
25484
26169
  /** Route WASM-recovered block through appropriate path based on container kind. */
25485
- #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
26170
+ #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
25486
26171
  const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
25487
26172
  if (ownerPacketId === void 0) {
25488
26173
  throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
@@ -25499,7 +26184,7 @@ var MmtpRouter = class {
25499
26184
  ownerPacketId,
25500
26185
  blockProfile
25501
26186
  });
25502
- return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
26187
+ return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
25503
26188
  }
25504
26189
  /**
25505
26190
  * A recovered source packet of a protected flow is admitted on its own
@@ -25511,9 +26196,31 @@ var MmtpRouter = class {
25511
26196
  * recovered packets is refused is unrecovered (7.2.7): its caller leaves it
25512
26197
  * open, so it reaches its FEC deadline and fails by class. Always true
25513
26198
  * outside a protected flow.
25514
- */
25515
- #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
25516
- const receiver = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
26199
+ *
26200
+ * On a flow the user agent admitted, the live decoder's recoveries are kept
26201
+ * without a record: while the flow is protected that decoder takes only
26202
+ * symbols the user agent or a record verified (a flow-less multicast
26203
+ * datagram of a listed track is dropped or set aside in #processMMTP), so
26204
+ * what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
26205
+ * unlisted MoQ repair and still needs a record.
26206
+ * ponytail: per-flow flag; per-block provenance only if a protected flow
26207
+ * ever mixes verified and unverified live inputs.
26208
+ * ponytail: a worker frame computed before the protection reset can still
26209
+ * arrive after it (frames carry no epoch); its inputs are MoQ copies read
26210
+ * and played before protection, so it admits nothing new. Drop pre-reset
26211
+ * frames by epoch only if pre-protection input ever matters.
26212
+ */
26213
+ #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26214
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26215
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26216
+ result = {
26217
+ ...result,
26218
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26219
+ };
26220
+ }
26221
+ const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
26222
+ const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
26223
+ const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
25517
26224
  let recovered = true;
25518
26225
  if (receiver) {
25519
26226
  const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
@@ -25525,6 +26232,8 @@ var MmtpRouter = class {
25525
26232
  let admitted = false;
25526
26233
  if (receiver === "ambiguous") {
25527
26234
  this.#ambiguousRecoveredDrops++;
26235
+ } else if (userAgent) {
26236
+ admitted = true;
25528
26237
  } else {
25529
26238
  const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
25530
26239
  wire.set(sym.rawSymbol);
@@ -25552,12 +26261,12 @@ var MmtpRouter = class {
25552
26261
  catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
25553
26262
  );
25554
26263
  if (flows.size > 1) return "ambiguous";
25555
- for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
26264
+ for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
25556
26265
  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))) {
26266
+ for (const entry of this.#provenance.values()) {
26267
+ if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
25559
26268
  if (match) return "ambiguous";
25560
- match = receiver;
26269
+ match = entry;
25561
26270
  }
25562
26271
  }
25563
26272
  return match;
@@ -25609,6 +26318,29 @@ var MmtpRouter = class {
25609
26318
  }
25610
26319
  );
25611
26320
  }
26321
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26322
+ ownerPacketId,
26323
+ sym.fragment.mpuSequenceNumber,
26324
+ sym.fragment.movieFragmentSequenceNumber,
26325
+ sym.fragment.sampleNumber,
26326
+ catalogTrackName
26327
+ );
26328
+ for (const sym of result.symbols) {
26329
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26330
+ this.#feedViaParser(
26331
+ sym.rawSymbol,
26332
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26333
+ void 0,
26334
+ {
26335
+ skipFecPayloadId: true,
26336
+ recovered: true,
26337
+ deliveryProvenance,
26338
+ ...fecSbnOf(sym.index),
26339
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26340
+ }
26341
+ );
26342
+ }
26343
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25612
26344
  const gated = this.#isFecTrack(ownerPacketId);
25613
26345
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25614
26346
  if (assembler) {
@@ -26339,11 +27071,8 @@ function providerErrorOf(error) {
26339
27071
  return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
26340
27072
  }
26341
27073
  var MAX_TRACKED_DROPPED_PIDS = 16;
26342
- function getMulticastGlobals() {
26343
- return globalThis;
26344
- }
26345
27074
  function hasMulticastGateway() {
26346
- return typeof getMulticastGlobals().multicast?.subscribe === "function";
27075
+ return typeof resolveMulticastGateway()?.subscribe === "function";
26347
27076
  }
26348
27077
  function senderOf(event) {
26349
27078
  const sourceAddress = event.sourceAddress;
@@ -26435,6 +27164,9 @@ var MulticastTransport = class {
26435
27164
  // listener closure, so an in-place admission change (setContext) applies
26436
27165
  // from the next packet on without rejoining.
26437
27166
  #context = void 0;
27167
+ #deliveredContext = void 0;
27168
+ /** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
27169
+ #userAgentGateway = void 0;
26438
27170
  // The live join's provider-side filter. An in-place widening must reach it,
26439
27171
  // or the provider never delivers the new ID; null without a live join.
26440
27172
  #providerFilter = null;
@@ -26471,7 +27203,7 @@ var MulticastTransport = class {
26471
27203
  this.#dataPlaneLive = false;
26472
27204
  this.#sender = void 0;
26473
27205
  if (this.#disposed) return this.#declined(generation, "disposed");
26474
- const gateway = this.#gateway ?? getMulticastGlobals().multicast;
27206
+ const gateway = this.#gateway ?? resolveMulticastGateway();
26475
27207
  if (typeof gateway?.subscribe !== "function") {
26476
27208
  console.warn("[MulticastTransport] Multicast API not available");
26477
27209
  return this.#declined(generation, "unavailable");
@@ -26487,15 +27219,16 @@ var MulticastTransport = class {
26487
27219
  console.log(
26488
27220
  `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
26489
27221
  );
27222
+ const userAgent = isUserAgentVerifiedGateway(gateway);
26490
27223
  const subscribeConfig = toWindowMulticastSubscribeConfig({
26491
27224
  source: config.sourceAddress,
26492
27225
  senderAddress: config.senderAddress,
26493
27226
  group: config.groupAddress,
26494
27227
  port: config.port,
26495
- mode: config.mode,
26496
- amtRelay: config.amtRelay,
26497
- amtRelayPort: config.amtRelayPort,
26498
- amtDiscovery: config.amtDiscovery,
27228
+ mode: userAgent ? "native" : config.mode,
27229
+ amtRelay: userAgent ? void 0 : config.amtRelay,
27230
+ amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
27231
+ amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
26499
27232
  packetIds: config.packetIds,
26500
27233
  tracks: config.tracks,
26501
27234
  atsc3: config.atsc3
@@ -26541,7 +27274,8 @@ var MulticastTransport = class {
26541
27274
  acknowledgedUpdate: 0,
26542
27275
  inFlight: 0
26543
27276
  };
26544
- this.#context = context;
27277
+ this.#userAgentGateway = userAgent ? gateway : void 0;
27278
+ this.#setContext(context);
26545
27279
  if (packetIds) {
26546
27280
  for (const packetId of packetIds) {
26547
27281
  if (!this.#acceptedBytesByPacketId.has(packetId)) {
@@ -26549,7 +27283,7 @@ var MulticastTransport = class {
26549
27283
  }
26550
27284
  }
26551
27285
  }
26552
- this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
27286
+ this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
26553
27287
  const delivery = subscription.delivery ?? "datagram";
26554
27288
  if (delivery !== "datagram" && delivery !== "ordered-relay") {
26555
27289
  throw new Error(
@@ -26630,7 +27364,7 @@ var MulticastTransport = class {
26630
27364
  }
26631
27365
  if (!this.#acceptPacket(packet, sourceAddress)) return false;
26632
27366
  const liveness = !this.#isRecordCandidate(packet);
26633
- this.#onPacket(packet, this.#context);
27367
+ this.#onPacket(packet, this.#deliveredContext);
26634
27368
  return liveness;
26635
27369
  }
26636
27370
  #isRecordCandidate(packet) {
@@ -26841,8 +27575,10 @@ var MulticastTransport = class {
26841
27575
  return this.#subscription !== null;
26842
27576
  }
26843
27577
  /**
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.
27578
+ * The live join's context as the caller gave it (the join's `context`, or
27579
+ * the last setContext), or undefined without a live join. On a native
27580
+ * provider each accepted packet is delivered with a marked copy of it
27581
+ * (`userAgentVerifiedIngress`); this getter never carries the mark.
26846
27582
  */
26847
27583
  get context() {
26848
27584
  return this.#context;
@@ -26866,7 +27602,16 @@ var MulticastTransport = class {
26866
27602
  "[MulticastTransport] setContext needs a live join; join() carries the first context"
26867
27603
  );
26868
27604
  }
27605
+ this.#setContext(context);
27606
+ }
27607
+ /**
27608
+ * The join's context, and what the subscription delivers with each packet:
27609
+ * marked admitted by the user agent when the gateway is its native
27610
+ * MulticastReceiver provider. feedPacket never marks.
27611
+ */
27612
+ #setContext(context) {
26869
27613
  this.#context = context;
27614
+ this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
26870
27615
  }
26871
27616
  /**
26872
27617
  * Replace the live join's packet-ID allow-list in place, without rejoining.
@@ -26998,6 +27743,8 @@ var MulticastTransport = class {
26998
27743
  this.#matchedSource = void 0;
26999
27744
  this.#providerFilter = null;
27000
27745
  this.#context = void 0;
27746
+ this.#deliveredContext = void 0;
27747
+ this.#userAgentGateway = void 0;
27001
27748
  this.#withdrawnPacketIds.clear();
27002
27749
  }
27003
27750
  /** The ingress filter; an accepted packet is counted here, once. */
@@ -28350,8 +29097,17 @@ var MulticastManager = class {
28350
29097
  * repair roles and owners are registered here; in-band owners stay the
28351
29098
  * caller's. Joining is best effort: a group that fails to join is logged
28352
29099
  * and leaves the media join as it was.
28353
- */
28354
- joinRepairEndpoints(endpoints, provider) {
29100
+ *
29101
+ * `options.gateway` names the gateway a subscribe callback reads through.
29102
+ * When it is the browser's own MulticastReceiver provider, the callback's
29103
+ * packets are fed as admitted by the user agent
29104
+ * (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
29105
+ * The mark rests on this statement, not on the data path: every packet the
29106
+ * callback delivers must come from a subscription opened on
29107
+ * `options.gateway`. A callback that also merges another source (a MoQ or
29108
+ * WebSocket repair feed) must not name a native provider here.
29109
+ */
29110
+ joinRepairEndpoints(endpoints, provider, options) {
28355
29111
  const releases = [];
28356
29112
  let released = false;
28357
29113
  const safely = (each) => {
@@ -28373,17 +29129,19 @@ var MulticastManager = class {
28373
29129
  ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
28374
29130
  };
28375
29131
  let sender;
28376
- const onPacket = (packet, sourceAddress) => {
29132
+ const feed = (packet, sourceAddress, ingressToFeed) => {
28377
29133
  if (released) return;
28378
29134
  if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
28379
- this.#router.feedPacket(packet, "multicast", ingress);
29135
+ this.#router.feedPacket(packet, "multicast", ingressToFeed);
28380
29136
  };
28381
29137
  const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
28382
29138
  if (typeof provider === "function") {
29139
+ const marked = userAgentVerifiedIngress(ingress, options?.gateway);
29140
+ const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
28383
29141
  Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
28384
29142
  continue;
28385
29143
  }
28386
- const transport = new MulticastTransport((packet) => onPacket(packet), {
29144
+ const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
28387
29145
  gateway: provider,
28388
29146
  lockedTo: () => this.#transport.sender
28389
29147
  });
@@ -28395,11 +29153,12 @@ var MulticastManager = class {
28395
29153
  mode: endpoint.mode,
28396
29154
  amtRelay: endpoint.amtRelay,
28397
29155
  amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
29156
+ amtDiscovery: endpoint.amtDiscovery,
28398
29157
  packetIds: endpoint.packetIds,
28399
29158
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
28400
29159
  // A protected repair group's records ride in band on its port (7.2.3).
28401
29160
  ...endpoint.flow === void 0 ? {} : { protected: true }
28402
- }).then((joined) => {
29161
+ }, ingress).then((joined) => {
28403
29162
  if (!joined && !released) fail(transport.lastJoinOutcome);
28404
29163
  });
28405
29164
  }
@@ -29343,7 +30102,7 @@ async function setupFecWorkerForTrack(opts) {
29343
30102
  blockProfile,
29344
30103
  k: opts.fecK,
29345
30104
  // 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.
30105
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29347
30106
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29348
30107
  symbolSize: opts.fecSymbolSize,
29349
30108
  interleaveDepth: interleaveDepthBlocks,
@@ -29885,24 +30644,24 @@ var MediaRouter = class {
29885
30644
  if (!this.#multicastVideoKeyed) return;
29886
30645
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
29887
30646
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
29888
- const token = ++this.#sourceSwitchToken;
30647
+ const token2 = ++this.#sourceSwitchToken;
29889
30648
  const maybeReady = this.#onSourceSwitch?.("multicast");
29890
30649
  if (!maybeReady || typeof maybeReady.then !== "function") {
29891
- this.#commitMulticastActivation(token);
30650
+ this.#commitMulticastActivation(token2);
29892
30651
  return;
29893
30652
  }
29894
30653
  const ready = Promise.resolve(maybeReady);
29895
- this.#pendingMulticastToken = token;
30654
+ this.#pendingMulticastToken = token2;
29896
30655
  this.#sourceSwitchReady = ready;
29897
30656
  void ready.then(
29898
- () => this.#commitMulticastActivation(token),
29899
- (err) => this.#rejectMulticastActivation(token, err)
30657
+ () => this.#commitMulticastActivation(token2),
30658
+ (err) => this.#rejectMulticastActivation(token2, err)
29900
30659
  ).finally(() => {
29901
30660
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
29902
30661
  });
29903
30662
  }
29904
- #commitMulticastActivation(token) {
29905
- if (token !== this.#sourceSwitchToken) return;
30663
+ #commitMulticastActivation(token2) {
30664
+ if (token2 !== this.#sourceSwitchToken) return;
29906
30665
  this.#active = "multicast";
29907
30666
  this.#suppressed = true;
29908
30667
  this.#pendingMulticastToken = null;
@@ -29913,9 +30672,9 @@ var MediaRouter = class {
29913
30672
  if (video) this.#onVideoFrame?.(video.data, video.meta);
29914
30673
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
29915
30674
  }
29916
- #rejectMulticastActivation(token, err) {
29917
- if (token !== this.#sourceSwitchToken) return;
29918
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30675
+ #rejectMulticastActivation(token2, err) {
30676
+ if (token2 !== this.#sourceSwitchToken) return;
30677
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
29919
30678
  this.#pendingMulticastVideo = null;
29920
30679
  this.#pendingMulticastAudio = null;
29921
30680
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -30884,63 +31643,65 @@ var FecStarvationDetector = class {
30884
31643
 
30885
31644
  // src/cmcd-producer.ts
30886
31645
  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}`;
31646
+ var DEFAULT_INTERVAL_MS = 3e4;
31647
+ var CUSTOM_PREFIX = "com.blockcast-";
31648
+ var MAX_CUSTOM_VALUE = 64;
31649
+ var TRANSPORT_TOKEN = {
31650
+ native: "ssm",
31651
+ amt: "amt",
31652
+ moq: "moq"
31653
+ };
31654
+ function sfString(value) {
31655
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31656
+ }
31657
+ function encodeCmcdRecord(record) {
31658
+ return Object.keys(record).sort().map((key) => {
31659
+ const value = record[key];
31660
+ if (typeof value === "number") return `${key}=${value}`;
31661
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31662
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31663
+ return `${key}=${value.token}`;
30919
31664
  }).join(",");
30920
31665
  }
31666
+ var token = (value) => ({ token: value });
31667
+ function putCustom(record, name, value) {
31668
+ if (value === void 0) return;
31669
+ if (typeof value === "number") value = String(value);
31670
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31671
+ record[CUSTOM_PREFIX + name] = value;
31672
+ }
30921
31673
  var CmcdProducer = class {
30922
- #onCmcdParams;
30923
31674
  #onCmcdPost;
31675
+ #intervalMs;
31676
+ #sessionId;
31677
+ #now;
31678
+ #sn = 0;
31679
+ #lastHeartbeat = null;
30924
31680
  #transport;
30925
31681
  #lastStats;
30926
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
30927
31682
  #playMode = null;
30928
31683
  #deltaFailRate = null;
30929
31684
  #switchMiscount = 0;
30930
31685
  #manifestStats = null;
30931
31686
  #fecFailClosed = null;
31687
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31688
+ #lifecycle = null;
30932
31689
  constructor(options) {
30933
- this.#onCmcdParams = options.onCmcdParams;
30934
31690
  this.#onCmcdPost = options.onCmcdPost;
31691
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31692
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31693
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31694
+ }
31695
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31696
+ this.#now = options.now ?? Date.now;
30935
31697
  }
30936
31698
  /** Update the stored transport state. Called by player wiring layer. */
30937
31699
  updateTransport(snapshot) {
30938
31700
  this.#transport = snapshot;
30939
31701
  }
30940
31702
  /**
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).
31703
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31704
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
30944
31705
  */
30945
31706
  updatePlayMode(mode) {
30946
31707
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -30951,9 +31712,8 @@ var CmcdProducer = class {
30951
31712
  this.#playMode = mode;
30952
31713
  }
30953
31714
  /**
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.
31715
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31716
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
30957
31717
  */
30958
31718
  updateDeltaFailRate(rate) {
30959
31719
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -30963,112 +31723,139 @@ var CmcdProducer = class {
30963
31723
  }
30964
31724
  this.#deltaFailRate = rate;
30965
31725
  }
30966
- /**
30967
- * Bump the cumulative switching-set misalignment counter. Emitted as
30968
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
30969
- */
31726
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
30970
31727
  incrementSwitchMiscount() {
30971
31728
  this.#switchMiscount += 1;
30972
31729
  }
30973
31730
  /**
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.
31731
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31732
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31733
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31734
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31735
+ * with no loss.
30978
31736
  */
30979
31737
  updateFecFailClosed(reason) {
31738
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
30980
31739
  this.#fecFailClosed = reason;
31740
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
30981
31741
  }
30982
31742
  updateManifestVerification(stats) {
30983
31743
  this.#manifestStats = stats;
30984
31744
  }
30985
31745
  /**
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.
31746
+ * Count a video block's resolution from the repair lifecycle: recovered in
31747
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31748
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31749
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
30990
31750
  */
31751
+ onRepairLifecycle(event) {
31752
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31753
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31754
+ if (event.via === "inband") this.#lifecycle.inband++;
31755
+ else this.#lifecycle.fetch++;
31756
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31757
+ this.#lifecycle.failedKeyframe++;
31758
+ }
31759
+ }
31760
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
30991
31761
  emitSwitchDrift(driftUs) {
30992
31762
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
30993
31763
  throw new Error(
30994
31764
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
30995
31765
  );
30996
31766
  }
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);
31767
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31768
+ putCustom(record, "sdu", Math.round(driftUs));
31769
+ this.#post([record]);
31003
31770
  }
31004
- /**
31005
- * Process a FecTrackStats snapshot. Finds the video track, builds
31006
- * x-bc-* vendor keys, and fires both delivery callbacks.
31007
- */
31771
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31008
31772
  onSnapshot(data) {
31009
31773
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31010
31774
  if (!videoTrack) return;
31011
31775
  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);
31776
+ this.#heartbeat();
31034
31777
  }
31035
31778
  /**
31036
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31037
- * the caller because only the transport integration knows when state changed.
31779
+ * Take transport state for receivers that do not own FEC stats, and send the
31780
+ * heartbeat when due.
31038
31781
  */
31039
31782
  onTransportSnapshot(data) {
31040
31783
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31041
31784
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31042
31785
  }
31043
31786
  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");
31787
+ if (data.switchMiscount !== void 0) {
31788
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31789
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31790
+ }
31791
+ this.#switchMiscount = data.switchMiscount;
31046
31792
  }
31047
31793
  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);
31794
+ this.#heartbeat();
31053
31795
  }
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
- */
31796
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31059
31797
  onTransition(event) {
31060
- const params = {};
31061
- if (this.#lastStats) {
31062
- Object.assign(params, buildFecParams(this.#lastStats));
31798
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31799
+ putCustom(record, "ct", token(event));
31800
+ this.#putTransport(record);
31801
+ this.#post([record]);
31802
+ }
31803
+ #base(event) {
31804
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31805
+ }
31806
+ #heartbeat() {
31807
+ const now = this.#now();
31808
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31809
+ this.#lastHeartbeat = now;
31810
+ this.#post([this.#base("t"), this.#counters()]);
31811
+ }
31812
+ #putTransport(record) {
31813
+ const snap = this.#transport;
31814
+ if (!snap) return;
31815
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31816
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31817
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31818
+ }
31819
+ putCustom(record, "ar", snap.amtRelay);
31820
+ }
31821
+ /** The `e=ce` counter report. */
31822
+ #counters() {
31823
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31824
+ const stats = this.#lastStats;
31825
+ if (stats) {
31826
+ putCustom(record, "fa", stats.config.algorithm);
31827
+ putCustom(record, "fk", stats.config.k);
31828
+ putCustom(record, "fp", stats.config.repairCount);
31829
+ putCustom(record, "pr", stats.sourceSymbols);
31830
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31831
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31832
+ putCustom(record, "fbf", stats.blocksFailed);
31833
+ }
31834
+ if (this.#lifecycle) {
31835
+ putCustom(record, "fbr", this.#lifecycle.inband);
31836
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31837
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31838
+ }
31839
+ this.#putTransport(record);
31840
+ if (this.#playMode !== null) {
31841
+ putCustom(record, "pm", token(this.#playMode));
31842
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31843
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31844
+ }
31845
+ }
31846
+ if (this.#fecFailClosed) {
31847
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31848
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31849
+ }
31850
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31851
+ if (this.#manifestStats) {
31852
+ putCustom(record, "mv", this.#manifestStats.verified);
31853
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31063
31854
  }
31064
- Object.assign(params, buildTransportParams(this.#transport));
31065
- params["x-bc-ct"] = event;
31066
- this.#emit(params);
31855
+ return record;
31067
31856
  }
31068
- /** Fire both delivery callbacks with the given params. */
31069
- #emit(params) {
31070
- this.#onCmcdParams?.(params);
31071
- this.#onCmcdPost?.(formatQueryString(params));
31857
+ #post(records) {
31858
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31072
31859
  }
31073
31860
  };
31074
31861
 
@@ -31164,18 +31951,31 @@ function resolveRepairLayers(catalog, source) {
31164
31951
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31165
31952
  };
31166
31953
  }
31954
+ function valueAt(catalog, pointer) {
31955
+ let node = catalog;
31956
+ for (const segment of pointer.split("/").slice(1)) {
31957
+ if (typeof node !== "object" || node === null) return void 0;
31958
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
31959
+ }
31960
+ return node;
31961
+ }
31962
+ function catalogTrackDeliveryMode(catalog, source) {
31963
+ const binding = bindingOf(catalog, source);
31964
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
31965
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
31966
+ }
31167
31967
  function moqtPriority2(value) {
31168
31968
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31169
31969
  }
31170
31970
  function catalogRepairTarget(catalog, source) {
31171
31971
  const binding = bindingOf(catalog, source);
31172
31972
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31173
- const layerZero = binding.layers[0];
31973
+ const layerZero2 = binding.layers[0];
31174
31974
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31175
31975
  const tracks = catalog.tracks;
31176
31976
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31177
31977
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31178
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
31978
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31179
31979
  }
31180
31980
  function numericField(value) {
31181
31981
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31207,6 +32007,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31207
32007
  repairTrackName: base.repairTrack,
31208
32008
  repairLayers: repair.baseLayers,
31209
32009
  repairOverlays: repair.overlays,
32010
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31210
32011
  ...mediaRate
31211
32012
  };
31212
32013
  }
@@ -31257,7 +32058,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31257
32058
  var PATH_BITS = { moq: 1, multicast: 2 };
31258
32059
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31259
32060
  var MmtpFecSession = class {
32061
+ /** P_0 = B_0, the held total every repair packet is checked against. */
32062
+ get repairSymbols() {
32063
+ return this.#repairSymbols;
32064
+ }
31260
32065
  #decoder;
32066
+ #repairSymbols;
32067
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
32068
+ #message;
31261
32069
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31262
32070
  #blocks = /* @__PURE__ */ new Map();
31263
32071
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31295,6 +32103,24 @@ var MmtpFecSession = class {
31295
32103
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31296
32104
  */
31297
32105
  constructor(config) {
32106
+ if (config && "alFecFlow" in config) {
32107
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
32108
+ if (!message) {
32109
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
32110
+ }
32111
+ this.#message = message;
32112
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
32113
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
32114
+ this.sourceSymbols = message.maximumK;
32115
+ this.#repairSymbols = message.maximumP;
32116
+ this.symbolSize = message.repairSymbolSize;
32117
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
32118
+ this.#repairPacketId = message.repairFlowId;
32119
+ this.#heldLayers = layerZero(message.maximumP);
32120
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
32121
+ this.#decoder = buildDecoder(config.Decoder, this);
32122
+ return;
32123
+ }
31298
32124
  const fec = config?.fec;
31299
32125
  if (!fec || typeof fec !== "object") {
31300
32126
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31305,7 +32131,7 @@ var MmtpFecSession = class {
31305
32131
  }
31306
32132
  this.algorithm = fec.scheme;
31307
32133
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31308
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
32134
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31309
32135
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31310
32136
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31311
32137
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31325,17 +32151,30 @@ var MmtpFecSession = class {
31325
32151
  config.heldLayers
31326
32152
  );
31327
32153
  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;
32154
+ this.#decoder = buildDecoder(config.Decoder, this);
32155
+ }
32156
+ /**
32157
+ * A later version of the AL-FEC message, catalog-less sessions only. A
32158
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
32159
+ * anchored check restarts at the new total, so a block generated under the
32160
+ * previous one stays valid before the first block carrying the new total
32161
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
32162
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
32163
+ * does. An entry with another asset, repair flow, code point, K or T, or
32164
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
32165
+ * a new session. A latched fail-closed stays latched either way.
32166
+ */
32167
+ updateAlFecFlow(flow) {
32168
+ const held = this.#message;
32169
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
32170
+ const next = alFecRaptorQFlowConfig(flow);
32171
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
32172
+ return "refused";
32173
+ }
32174
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
32175
+ this.#repairSymbols = next.maximumP;
32176
+ this.#heldLayers = layerZero(next.maximumP);
32177
+ return "raised";
31339
32178
  }
31340
32179
  /** True once release() ran. */
31341
32180
  get released() {
@@ -31430,7 +32269,7 @@ var MmtpFecSession = class {
31430
32269
  }
31431
32270
  if (admission.reject) {
31432
32271
  const verdict2 = this.#heldLayers.failsClosed(
31433
- repairTrackName,
32272
+ this.#trackFor(repairTrackName),
31434
32273
  frame.ssStart,
31435
32274
  frame.rsbLength,
31436
32275
  this.#anchorHorizon,
@@ -31445,7 +32284,7 @@ var MmtpFecSession = class {
31445
32284
  this.#repairPacketId ??= frame.packetId;
31446
32285
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31447
32286
  const verdict = this.#heldLayers.admit(
31448
- repairTrackName,
32287
+ this.#trackFor(repairTrackName),
31449
32288
  frame.ssStart,
31450
32289
  frame.rsbLength,
31451
32290
  this.#anchorHorizon,
@@ -31504,6 +32343,10 @@ var MmtpFecSession = class {
31504
32343
  decoder.free();
31505
32344
  return true;
31506
32345
  }
32346
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
32347
+ #trackFor(repairTrackName) {
32348
+ return this.#message ? void 0 : repairTrackName;
32349
+ }
31507
32350
  #drop(reason) {
31508
32351
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31509
32352
  return [];
@@ -31625,6 +32468,22 @@ var MmtpFecSession = class {
31625
32468
  }
31626
32469
  }
31627
32470
  };
32471
+ function layerZero(total) {
32472
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32473
+ }
32474
+ function buildDecoder(Decoder, session) {
32475
+ if (typeof Decoder !== "function") {
32476
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32477
+ }
32478
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32479
+ try {
32480
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32481
+ } catch (error) {
32482
+ decoder.free();
32483
+ throw error;
32484
+ }
32485
+ return decoder;
32486
+ }
31628
32487
  function requirePositiveInteger(value, path) {
31629
32488
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31630
32489
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
@@ -31638,6 +32497,168 @@ function requireNumber(value, path) {
31638
32497
  return value;
31639
32498
  }
31640
32499
 
32500
+ // src/repair-layer-subscriptions.ts
32501
+ var SERIAL32_HALF2 = 2147483648;
32502
+ var RepairLayerSubscriptions = class {
32503
+ #opts;
32504
+ #windowPackets;
32505
+ #margin;
32506
+ #dropAfterWindows;
32507
+ #source;
32508
+ #layerCount = 0;
32509
+ #subs = /* @__PURE__ */ new Map();
32510
+ #disposed = false;
32511
+ #lastSeq;
32512
+ #expected = 0;
32513
+ #lost = 0;
32514
+ #wantFewer = 0;
32515
+ #lossRate = null;
32516
+ #repairPackets = 0;
32517
+ #repairBytes = 0;
32518
+ #staleDropped = 0;
32519
+ constructor(opts) {
32520
+ this.#opts = opts;
32521
+ this.#source = opts.source;
32522
+ this.#windowPackets = opts.windowPackets ?? 500;
32523
+ this.#margin = opts.margin ?? 2;
32524
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
32525
+ }
32526
+ get layerCount() {
32527
+ return this.#layerCount;
32528
+ }
32529
+ /**
32530
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
32531
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
32532
+ * or interleaved sequences read as loss.
32533
+ */
32534
+ noteSourceSequence(seq) {
32535
+ if (this.#disposed || this.#delivery() === "stream") return;
32536
+ seq >>>= 0;
32537
+ if (this.#lastSeq === void 0) {
32538
+ this.#lastSeq = seq;
32539
+ return;
32540
+ }
32541
+ const d = serial32Distance(this.#lastSeq, seq);
32542
+ if (d === 0) return;
32543
+ const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
32544
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
32545
+ this.#lastSeq = seq;
32546
+ this.#expected = 0;
32547
+ this.#lost = 0;
32548
+ return;
32549
+ }
32550
+ if (back === 0) {
32551
+ this.#expected += d;
32552
+ this.#lost += d - 1;
32553
+ this.#lastSeq = seq;
32554
+ } else if (this.#lost > 0) {
32555
+ this.#lost -= 1;
32556
+ }
32557
+ if (this.#expected >= this.#windowPackets) this.#closeWindow();
32558
+ }
32559
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
32560
+ switchSource(next) {
32561
+ if (this.#disposed) return;
32562
+ this.#closeAll();
32563
+ this.#source = next;
32564
+ this.#lastSeq = void 0;
32565
+ this.#expected = 0;
32566
+ this.#lost = 0;
32567
+ this.#wantFewer = 0;
32568
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32569
+ this.#reconcile();
32570
+ }
32571
+ snapshot() {
32572
+ return {
32573
+ delivery: this.#delivery(),
32574
+ layerCount: this.#layerCount,
32575
+ subscribedTracks: [...this.#subs.keys()],
32576
+ repairPackets: this.#repairPackets,
32577
+ repairBytes: this.#repairBytes,
32578
+ staleDropped: this.#staleDropped,
32579
+ lossRate: this.#lossRate
32580
+ };
32581
+ }
32582
+ dispose() {
32583
+ if (this.#disposed) return;
32584
+ this.#closeAll();
32585
+ this.#disposed = true;
32586
+ }
32587
+ #delivery() {
32588
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32589
+ }
32590
+ #layers() {
32591
+ return this.#source.repairLayers ?? [];
32592
+ }
32593
+ #closeWindow() {
32594
+ const rate = this.#lost / this.#expected;
32595
+ this.#lossRate = rate;
32596
+ this.#expected = 0;
32597
+ this.#lost = 0;
32598
+ const target = this.#targetLayers(rate);
32599
+ if (target > this.#layerCount) {
32600
+ this.#wantFewer = 0;
32601
+ this.#layerCount = target;
32602
+ } else if (target < this.#layerCount) {
32603
+ this.#wantFewer += 1;
32604
+ if (this.#wantFewer >= this.#dropAfterWindows) {
32605
+ this.#wantFewer = 0;
32606
+ this.#layerCount -= 1;
32607
+ }
32608
+ } else {
32609
+ this.#wantFewer = 0;
32610
+ }
32611
+ this.#reconcile();
32612
+ }
32613
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
32614
+ #targetLayers(rate) {
32615
+ if (rate <= 0) return 0;
32616
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
32617
+ const layers = this.#layers();
32618
+ let held = 0;
32619
+ for (let n = 0; n < layers.length; n++) {
32620
+ held += layers[n].repairSymbols;
32621
+ if (held >= needed) return n + 1;
32622
+ }
32623
+ return layers.length;
32624
+ }
32625
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
32626
+ #reconcile() {
32627
+ const want = /* @__PURE__ */ new Map();
32628
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
32629
+ want.set(layer.repairTrackName, layer.priority);
32630
+ }
32631
+ const overlay = this.#source.repairOverlays?.[0];
32632
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
32633
+ for (const [name, entry] of this.#subs) {
32634
+ if (!want.has(name)) {
32635
+ entry.sub.close();
32636
+ this.#subs.delete(name);
32637
+ }
32638
+ }
32639
+ for (const [name, priority] of want) {
32640
+ if (this.#subs.has(name)) continue;
32641
+ const entry = {};
32642
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
32643
+ this.#subs.set(name, entry);
32644
+ }
32645
+ }
32646
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
32647
+ #onPacket(name, entry, packet) {
32648
+ if (this.#disposed || this.#subs.get(name) !== entry) {
32649
+ this.#staleDropped += 1;
32650
+ return;
32651
+ }
32652
+ this.#repairPackets += 1;
32653
+ this.#repairBytes += packet.byteLength;
32654
+ this.#opts.feed(packet, name);
32655
+ }
32656
+ #closeAll() {
32657
+ for (const entry of this.#subs.values()) entry.sub.close();
32658
+ this.#subs.clear();
32659
+ }
32660
+ };
32661
+
31641
32662
  // src/repair-fetch.ts
31642
32663
  var MOQT_PRIORITY_MAX = 255;
31643
32664
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
@@ -32283,6 +33304,7 @@ export {
32283
33304
  REPAIR_LIFECYCLE_LOG_PREFIX,
32284
33305
  REPAIR_PRIORITY_MAX,
32285
33306
  REPAIR_PRIORITY_MIN,
33307
+ RepairLayerSubscriptions,
32286
33308
  SOURCE_MEDIA_PRIORITY_MAX,
32287
33309
  SampleCombineError,
32288
33310
  SampleCombiner,
@@ -32307,6 +33329,7 @@ export {
32307
33329
  catalogGroupDurationMs,
32308
33330
  catalogKeyframeCadenceMs,
32309
33331
  catalogRepairTarget,
33332
+ catalogTrackDeliveryMode,
32310
33333
  catalogTrackFecUnavailable,
32311
33334
  classifyH264Keyframe,
32312
33335
  classifyHevcKeyframe,
@@ -32327,6 +33350,7 @@ export {
32327
33350
  detectH264Keyframe,
32328
33351
  detectHevcKeyframe,
32329
33352
  driftCheck,
33353
+ encodeCmcdRecord,
32330
33354
  encodeDataSegment,
32331
33355
  encodeFecSourceInfo,
32332
33356
  encodeLocTimestamp,