@blockcast/mmt-container 2.0.0 → 2.0.1-main.0f2de6af487b

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.
package/README.md CHANGED
@@ -277,7 +277,7 @@ per-commit `0.1.0-main.<sha>` prerelease from `stamp-prerelease.mjs`), and a
277
277
  > Superseded by BLO-40665. Every package is now bumped in lockstep by a `v*`
278
278
  > tag and the sibling ranges move with it, so the objection above no longer
279
279
  > applies -- it was an argument against bumping *one* package. The set is at
280
- > `2.0.0`; see `RELEASES.md`.
280
+ > `2.0.1`; see `RELEASES.md`.
281
281
 
282
282
  #### Track-role convention (Phase 14)
283
283
 
@@ -8514,6 +8514,9 @@ var AMTGatewayManager = class {
8514
8514
  */
8515
8515
  async subscribe(config) {
8516
8516
  const { key } = config;
8517
+ if (config.protected) {
8518
+ throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8519
+ }
8517
8520
  const keyStr = this.getKeyString(key);
8518
8521
  if (this.subscriptions.has(keyStr)) {
8519
8522
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -21977,7 +21980,6 @@ _RistrettoPoint.Fn = /* @__PURE__ */ (() => Fn)();
21977
21980
  var BCPV2_LEAD_BOUND_MS = 5e3;
21978
21981
  var REPLAY_WINDOW = 64n;
21979
21982
  var BUCKET_DEPTH = 64;
21980
- var BUCKET_RATE_PER_MS = 1;
21981
21983
  var hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
21982
21984
  function sameBytes(a, b) {
21983
21985
  if (a.byteLength !== b.byteLength)
@@ -22006,6 +22008,10 @@ var BcProvenanceV2Receiver = class {
22006
22008
  verifySig;
22007
22009
  accepted;
22008
22010
  queueLimit;
22011
+ /** Octets a held datagram is charged beyond its payload: the IPv4 or IPv6 plus UDP headers (step 2). */
22012
+ headerOctets;
22013
+ /** Budget refill, tokens per ms: V / 1000 (step 1). */
22014
+ bucketRatePerMs;
22009
22015
  current = null;
22010
22016
  previous = null;
22011
22017
  graceStart = {};
@@ -22051,6 +22057,8 @@ var BcProvenanceV2Receiver = class {
22051
22057
  multicast: this.maxAuthDelayMs * bandwidth / 8e3,
22052
22058
  unicast: (this.maxAuthDelayMs + this.widenMs) * bandwidth / 8e3
22053
22059
  };
22060
+ this.headerOctets = parseBcProvenanceV2Address(cfg.endpoint.groupAddress).byteLength === 16 ? 48 : 28;
22061
+ this.bucketRatePerMs = Math.max(1e3, 1.25 * bandwidth / (8 * 512 * 37)) / 1e3;
22054
22062
  const t = this.now();
22055
22063
  this.buckets = {
22056
22064
  multicast: { tokens: BUCKET_DEPTH, last: t, dropped: 0 },
@@ -22149,7 +22157,7 @@ var BcProvenanceV2Receiver = class {
22149
22157
  arrival: now,
22150
22158
  sent,
22151
22159
  digest: hex(sha2562(bytes)),
22152
- pairs: record.digests.map((d, i) => ({ digest: hex(d), seq: record.firstSeq + BigInt(i), admitted: false })),
22160
+ pairs: record.digests.map((d, i) => ({ digest: hex(d), epoch, seq: record.firstSeq + BigInt(i), admitted: false })),
22153
22161
  awaitingMulticast: path === "mirror"
22154
22162
  };
22155
22163
  this.verified.set(id, entry);
@@ -22216,12 +22224,12 @@ var BcProvenanceV2Receiver = class {
22216
22224
  digest(datagram) {
22217
22225
  return hex(bcProvenanceV2DatagramDigest(this.cfg.endpoint, datagram));
22218
22226
  }
22219
- /** Consume the lowest-seq pending entry for `digest`, admitting it. */
22227
+ /** Consume the pending entry for `digest` lowest in (epoch, sequence) order, admitting it. */
22220
22228
  consumePending(digest, now) {
22221
22229
  const list = this.pending.get(digest);
22222
22230
  if (!list?.length)
22223
22231
  return false;
22224
- list.sort((a, b) => a.pair.seq < b.pair.seq ? -1 : a.pair.seq > b.pair.seq ? 1 : 0);
22232
+ list.sort((a, b) => a.pair.epoch - b.pair.epoch || (a.pair.seq < b.pair.seq ? -1 : a.pair.seq > b.pair.seq ? 1 : 0));
22225
22233
  const hit = list.shift();
22226
22234
  if (list.length === 0)
22227
22235
  this.pending.delete(digest);
@@ -22230,14 +22238,18 @@ var BcProvenanceV2Receiver = class {
22230
22238
  this.admittedUntil.set(digest, now + this.maxAuthDelayMs + this.leadMs);
22231
22239
  return true;
22232
22240
  }
22241
+ /** A held datagram's charge against its queue: payload plus IP and UDP headers. */
22242
+ charge(held) {
22243
+ return held.datagram.byteLength + this.headerOctets;
22244
+ }
22233
22245
  enqueue(held, path, now) {
22234
- if (this.holdBytes[path] + held.datagram.byteLength > this.queueLimit[path]) {
22246
+ if (this.holdBytes[path] + this.charge(held) > this.queueLimit[path]) {
22235
22247
  return { verdict: "overflow", admitted: [] };
22236
22248
  }
22237
22249
  held.expiry = now + this.maxAuthDelayMs + (path === "unicast" ? this.widenMs : 0);
22238
22250
  held.order = this.order++;
22239
22251
  this.hold[path].push(held);
22240
- this.holdBytes[path] += held.datagram.byteLength;
22252
+ this.holdBytes[path] += this.charge(held);
22241
22253
  return { verdict: "held", admitted: [] };
22242
22254
  }
22243
22255
  /** Remove and return the oldest held datagram with `digest`, across both queues. */
@@ -22252,7 +22264,7 @@ var BcProvenanceV2Receiver = class {
22252
22264
  if (!best)
22253
22265
  return void 0;
22254
22266
  const [held] = this.hold[best.path].splice(best.index, 1);
22255
- this.holdBytes[best.path] -= held.datagram.byteLength;
22267
+ this.holdBytes[best.path] -= this.charge(held);
22256
22268
  return held.datagram;
22257
22269
  }
22258
22270
  dropAdmittedHeld() {
@@ -22260,7 +22272,7 @@ var BcProvenanceV2Receiver = class {
22260
22272
  this.hold[path] = this.hold[path].filter((h) => {
22261
22273
  const keep = h.digest === void 0 || !this.admittedUntil.has(h.digest);
22262
22274
  if (!keep)
22263
- this.holdBytes[path] -= h.datagram.byteLength;
22275
+ this.holdBytes[path] -= this.charge(h);
22264
22276
  return keep;
22265
22277
  });
22266
22278
  }
@@ -22344,7 +22356,7 @@ var BcProvenanceV2Receiver = class {
22344
22356
  }
22345
22357
  take(path, now) {
22346
22358
  const b = this.buckets[path];
22347
- b.tokens = Math.min(BUCKET_DEPTH, b.tokens + (now - b.last) * BUCKET_RATE_PER_MS);
22359
+ b.tokens = Math.min(BUCKET_DEPTH, b.tokens + (now - b.last) * this.bucketRatePerMs);
22348
22360
  b.last = now;
22349
22361
  if (b.tokens < 1) {
22350
22362
  b.dropped++;
@@ -22358,7 +22370,7 @@ var BcProvenanceV2Receiver = class {
22358
22370
  this.hold[path] = this.hold[path].filter((h) => {
22359
22371
  if (h.expiry > now)
22360
22372
  return true;
22361
- this.holdBytes[path] -= h.datagram.byteLength;
22373
+ this.holdBytes[path] -= this.charge(h);
22362
22374
  this.expired[path]++;
22363
22375
  return false;
22364
22376
  });
@@ -22635,6 +22647,14 @@ var MmtpRouter = class {
22635
22647
  #packetIdRoles = /* @__PURE__ */ new Map();
22636
22648
  /** Explicit catalog ownership for dedicated repair-track packet IDs. */
22637
22649
  #repairOwnersByPacketId = /* @__PURE__ */ new Map();
22650
+ /** Keyframe-overlay repair PID → the source its FEC instance protects
22651
+ * (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
22652
+ * #repairOwnersByPacketId: the overlay is decoded as its own instance. */
22653
+ #overlayLegs = /* @__PURE__ */ new Map();
22654
+ /** Keyframe-overlay repair track names, which name a leg too: a catalog
22655
+ * may state no PID for it (learned at subscribe time). */
22656
+ #overlayRepairNames = /* @__PURE__ */ new Map();
22657
+ #overlayStats = { repairDropped: 0, ownerRefused: 0 };
22638
22658
  /** Callback fired when a delta-track FEC block completes or fails. */
22639
22659
  #onDeltaBlockCompleteCb = null;
22640
22660
  // Debug counters
@@ -23153,13 +23173,53 @@ var MmtpRouter = class {
23153
23173
  /** Replace catalog-derived repair ownership before a catalog reconfiguration. */
23154
23174
  clearRepairPacketIdOwners() {
23155
23175
  this.#repairOwnersByPacketId.clear();
23176
+ this.#overlayLegs.clear();
23177
+ this.#overlayRepairNames.clear();
23156
23178
  }
23157
- /** Bind one dedicated repair-track PID to its authoritative media PID. */
23179
+ /** Bind one dedicated repair-track PID to its authoritative media PID. A
23180
+ * known keyframe-overlay PID is refused, and counted: its symbols belong to
23181
+ * the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
23158
23182
  setRepairPacketIdOwner(repairPacketId, ownerPacketId) {
23183
+ if (this.#overlayLegs.has(repairPacketId)) {
23184
+ this.#overlayStats.ownerRefused++;
23185
+ return;
23186
+ }
23159
23187
  const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? /* @__PURE__ */ new Set();
23160
23188
  owners.add(ownerPacketId);
23161
23189
  this.#repairOwnersByPacketId.set(repairPacketId, owners);
23162
23190
  }
23191
+ /**
23192
+ * Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
23193
+ * PID, the source it protects, and its repair track name. Either one names
23194
+ * the leg, so a catalog that states no PID (learned at subscribe time) is
23195
+ * covered by the name. Overlay repair is then never resolved to a base
23196
+ * owner, on multicast or MoQ. Cleared with the catalog owners
23197
+ * (`clearRepairPacketIdOwners`).
23198
+ */
23199
+ setKeyframeOverlayLeg(repairPacketId, leg) {
23200
+ if (leg.repairTrackName !== void 0) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
23201
+ if (repairPacketId === void 0) return;
23202
+ this.#repairOwnersByPacketId.delete(repairPacketId);
23203
+ this.#overlayLegs.set(repairPacketId, { ...leg });
23204
+ }
23205
+ /** The keyframe-overlay leg registered on `repairPacketId`, if any. */
23206
+ keyframeOverlayLeg(repairPacketId) {
23207
+ return this.#overlayLegs.get(repairPacketId);
23208
+ }
23209
+ /** Overlay repair dropped for want of an overlay instance, and base-owner
23210
+ * registrations refused on an overlay PID. */
23211
+ get keyframeOverlayStats() {
23212
+ return { ...this.#overlayStats };
23213
+ }
23214
+ #isOverlayRepair(repairPacketId, repairTrackName) {
23215
+ return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23216
+ }
23217
+ /** Overlay repair has no decoder until the overlay instance lands
23218
+ * (BLO-52077 PR 2): drop it before any base owner, locator or decoder. */
23219
+ #dropOverlayRepair(repairPacketId) {
23220
+ this.#overlayStats.repairDropped++;
23221
+ debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
23222
+ }
23163
23223
  /**
23164
23224
  * Register callback fired when a delta-track FEC block completes or fails.
23165
23225
  *
@@ -23583,6 +23643,10 @@ var MmtpRouter = class {
23583
23643
  return;
23584
23644
  }
23585
23645
  const repairPacketId = header.packetId;
23646
+ if (this.#isOverlayRepair(repairPacketId, repairTrackName)) {
23647
+ this.#dropOverlayRepair(repairPacketId);
23648
+ return;
23649
+ }
23586
23650
  const hintedOwner = ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null ? ownerPacketIdHint : null;
23587
23651
  const ownerPacketId = hintedOwner ?? (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
23588
23652
  if (ownerPacketId === null) {
@@ -23958,6 +24022,13 @@ var MmtpRouter = class {
23958
24022
  this.#packetIdRoles.delete(packetId);
23959
24023
  this.#catalogTrackNamesByPacketId.delete(packetId);
23960
24024
  this.#repairOwnersByPacketId.delete(packetId);
24025
+ this.#overlayLegs.delete(packetId);
24026
+ for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
24027
+ if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
24028
+ }
24029
+ for (const [name, leg] of [...this.#overlayRepairNames]) {
24030
+ if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
24031
+ }
23961
24032
  delete this.#pidCounts[packetId];
23962
24033
  delete this.#repairPidCounts[packetId];
23963
24034
  delete this.#moqRepairPidCounts[packetId];
@@ -24196,6 +24267,8 @@ var MmtpRouter = class {
24196
24267
  this.#parserDiagnostics.clear();
24197
24268
  this.#catalogTrackNamesByPacketId.clear();
24198
24269
  this.#repairOwnersByPacketId.clear();
24270
+ this.#overlayLegs.clear();
24271
+ this.#overlayRepairNames.clear();
24199
24272
  for (const { receiver } of this.#provenance.values()) receiver.release();
24200
24273
  this.#provenance.clear();
24201
24274
  this.#provenanceStreamFlow.clear();
@@ -24551,6 +24624,10 @@ var MmtpRouter = class {
24551
24624
  }
24552
24625
  }
24553
24626
  if (info.isRepair) {
24627
+ if (this.#isOverlayRepair(info.packetId, catalogTrackNameForPacket(ingress, info.packetId))) {
24628
+ this.#dropOverlayRepair(info.packetId);
24629
+ return;
24630
+ }
24554
24631
  const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
24555
24632
  if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
24556
24633
  const repair = this.#readRepairObject(
@@ -26069,6 +26146,10 @@ var MulticastTransport = class {
26069
26146
  #acceptedBytesByPacketId = /* @__PURE__ */ new Map();
26070
26147
  #packetIds = null;
26071
26148
  #protected = false;
26149
+ // A protected join's S, and the last sender spelling that matched it, so a
26150
+ // matching sender parses once per spelling rather than per datagram.
26151
+ #protectedSource = void 0;
26152
+ #matchedSource = void 0;
26072
26153
  // The sender S this join's accepted packets came from, once known; see
26073
26154
  // packetsDroppedForeignSender. Cleared by every join and by a silence
26074
26155
  // failback, so a replacement publisher is not locked out.
@@ -26120,6 +26201,11 @@ var MulticastTransport = class {
26120
26201
  return this.#declined(generation, "unavailable");
26121
26202
  }
26122
26203
  let failure = "invalid-request";
26204
+ const protectedSource = config.protected === true ? config.sourceAddress ?? config.senderAddress : void 0;
26205
+ if (config.protected === true && (protectedSource === void 0 || config.atsc3)) {
26206
+ console.warn("[MulticastTransport] protected join refused: the path reports no per-datagram sender to check against S");
26207
+ return this.#declined(generation, "invalid-request");
26208
+ }
26123
26209
  try {
26124
26210
  const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null;
26125
26211
  console.log(
@@ -26155,7 +26241,7 @@ var MulticastTransport = class {
26155
26241
  this.#subscription = subscription;
26156
26242
  const live = () => !this.#disposed && generation === this.#joinGeneration;
26157
26243
  const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3;
26158
- const packetListener = !anySource && subscriptionDeliversPacketBatches(subscription) ? {
26244
+ const packetListener = !anySource && protectedSource === void 0 && subscriptionDeliversPacketBatches(subscription) ? {
26159
26245
  type: "packets",
26160
26246
  listener: (event) => {
26161
26247
  if (!live()) return;
@@ -26171,6 +26257,8 @@ var MulticastTransport = class {
26171
26257
  this.#packetListener = packetListener;
26172
26258
  this.#packetIds = packetIds;
26173
26259
  this.#protected = config.protected === true;
26260
+ this.#protectedSource = protectedSource;
26261
+ this.#matchedSource = void 0;
26174
26262
  this.#providerFilter = {
26175
26263
  acknowledged: packetIds,
26176
26264
  sent: 0,
@@ -26630,13 +26718,23 @@ var MulticastTransport = class {
26630
26718
  this.#delivery = void 0;
26631
26719
  this.#packetIds = null;
26632
26720
  this.#protected = false;
26721
+ this.#protectedSource = void 0;
26722
+ this.#matchedSource = void 0;
26633
26723
  this.#providerFilter = null;
26634
26724
  this.#context = void 0;
26635
26725
  this.#withdrawnPacketIds.clear();
26636
26726
  }
26637
26727
  /** The ingress filter; an accepted packet is counted here, once. */
26638
26728
  #acceptPacket(packet, sourceAddress) {
26639
- const owner = this.#lockedTo?.() ?? this.#sender;
26729
+ const source = this.#protectedSource;
26730
+ if (source !== void 0 && (sourceAddress === void 0 || sourceAddress !== this.#matchedSource)) {
26731
+ if (sourceAddress === void 0 || !sameIpAddress(sourceAddress, source)) {
26732
+ this.#stats.packetsDroppedForeignSender++;
26733
+ return false;
26734
+ }
26735
+ this.#matchedSource = sourceAddress;
26736
+ }
26737
+ const owner = source === void 0 ? this.#lockedTo?.() ?? this.#sender : void 0;
26640
26738
  if (sourceAddress !== void 0 && owner !== void 0 && sourceAddress !== owner) {
26641
26739
  this.#stats.packetsDroppedForeignSender++;
26642
26740
  return false;
@@ -27160,9 +27258,7 @@ function repairTrackNamesBySource(tracks) {
27160
27258
  }
27161
27259
  function boundRepairNames(binding) {
27162
27260
  if (binding?.status !== "ok") return [];
27163
- const names = binding.layers.map((layer) => layer.repairTrackName);
27164
- if (binding.overlay) names.push(binding.overlay.repairTrackName);
27165
- return names;
27261
+ return binding.layers.map((layer) => layer.repairTrackName);
27166
27262
  }
27167
27263
  var MulticastManager = class {
27168
27264
  #fec;
@@ -27928,7 +28024,11 @@ var MulticastManager = class {
27928
28024
  console.warn("[MulticastManager] repair group release failed", error);
27929
28025
  }
27930
28026
  };
27931
- const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners);
28027
+ const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
28028
+ ...owner,
28029
+ ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
28030
+ repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
28031
+ }));
27932
28032
  this.#applyRepairOwners(repairOwners);
27933
28033
  for (const endpoint of endpoints) {
27934
28034
  const ingress = {
@@ -27976,9 +28076,17 @@ var MulticastManager = class {
27976
28076
  return release;
27977
28077
  }
27978
28078
  #applyRepairOwners(owners) {
27979
- for (const { packetId, ownerPacketId } of owners) {
28079
+ for (const { packetId, ownerPacketId, instance, ownerTrackName, repairTrackName } of owners) {
27980
28080
  this.#router.setPacketIdRole(packetId, "repair");
27981
- this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
28081
+ if (instance === "keyframe") {
28082
+ this.#router.setKeyframeOverlayLeg(packetId, {
28083
+ sourcePacketId: ownerPacketId,
28084
+ ...ownerTrackName === void 0 ? {} : { sourceCatalogTrackName: ownerTrackName },
28085
+ ...repairTrackName === void 0 ? {} : { repairTrackName }
28086
+ });
28087
+ } else {
28088
+ this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
28089
+ }
27982
28090
  }
27983
28091
  }
27984
28092
  // ---------------------------------------------------------------------------
@@ -28242,8 +28350,8 @@ var MulticastManager = class {
28242
28350
  this.#fec.setMulticastSymbolDelivery(delivery);
28243
28351
  }
28244
28352
  /** Process a repair packet from MoQ (reliable QUIC). */
28245
- processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
28246
- this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
28353
+ processRepairFromMoQ(packet, ownerPacketId, catalogTrackName, repairTrackName) {
28354
+ this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName, repairTrackName);
28247
28355
  }
28248
28356
  /** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
28249
28357
  routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
@@ -28295,7 +28403,10 @@ var MulticastManager = class {
28295
28403
  const ownedRepairNames = /* @__PURE__ */ new Set();
28296
28404
  const claimed = /* @__PURE__ */ new Map();
28297
28405
  for (const binding of bindings.values()) {
28298
- for (const name of boundRepairNames(binding)) claimed.set(name, (claimed.get(name) ?? 0) + 1);
28406
+ const overlay = binding.status === "ok" ? binding.overlay?.repairTrackName : void 0;
28407
+ for (const name of [...boundRepairNames(binding), ...overlay === void 0 ? [] : [overlay]]) {
28408
+ claimed.set(name, (claimed.get(name) ?? 0) + 1);
28409
+ }
28299
28410
  }
28300
28411
  for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
28301
28412
  const repairOwnerGroup = /* @__PURE__ */ new Map();
@@ -28426,6 +28537,14 @@ var MulticastManager = class {
28426
28537
  this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
28427
28538
  }
28428
28539
  }
28540
+ const overlayName = binding.overlay?.repairTrackName;
28541
+ if (overlayName !== void 0 && ownedRepairNames.has(overlayName)) {
28542
+ this.#router.setKeyframeOverlayLeg(catalogTracksByName.get(overlayName)?.packetId, {
28543
+ sourcePacketId: track.packetId,
28544
+ sourceCatalogTrackName: track.name,
28545
+ repairTrackName: overlayName
28546
+ });
28547
+ }
28429
28548
  }
28430
28549
  const wasmDecoder = fecDecoders.get(i);
28431
28550
  if (track.packetId !== void 0 && wasmDecoder !== void 0) {