@blockcast/mmt-container 2.1.0 → 2.2.0-main.51d1358fdf4c

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.
@@ -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`);
@@ -903,6 +905,50 @@ var NTP_SHORT_BITS = 32;
903
905
  var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
904
906
  var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
905
907
 
908
+ // ../mmt-base/dist/signalling.js
909
+ var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
910
+ var SSBG_MODE_1 = 1;
911
+ var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
912
+ var REPAIR_SYMBOL_ID_MAX = 16777215;
913
+ var MIN_REPAIR_SYMBOL_SIZE = 14;
914
+ var REED_SOLOMON_MAX_BLOCK = 255;
915
+ function alFecRaptorQFlowConfig(flow) {
916
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
917
+ return null;
918
+ if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
919
+ return null;
920
+ if (flow.packetIds.length !== 1)
921
+ return null;
922
+ const repair = flow.repairFlows[0];
923
+ const { fecCodeId, maximumK: k, maximumP: p } = repair;
924
+ const t = flow.repairSymbolSize;
925
+ if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
926
+ return null;
927
+ if (fecCodeId === FEC_CODE_RAPTORQ) {
928
+ if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
929
+ return null;
930
+ } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
931
+ if (k + p > REED_SOLOMON_MAX_BLOCK)
932
+ return null;
933
+ } else {
934
+ return null;
935
+ }
936
+ if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
937
+ return null;
938
+ return {
939
+ sourcePacketIds: [...flow.packetIds],
940
+ repairFlowId: repair.repairFlowId,
941
+ fecCodeId,
942
+ ssbgMode: flow.ssbgMode,
943
+ fecPayloadIdMode: flow.fecPayloadIdMode,
944
+ repairSymbolSize: t,
945
+ maximumK: k,
946
+ maximumP: p,
947
+ protectionWindowTime: repair.protectionWindowTime,
948
+ protectionWindowSize: repair.protectionWindowSize
949
+ };
950
+ }
951
+
906
952
  // ../mmt-base/dist/serial-number.js
907
953
  var SERIAL32_SPAN = 4294967296;
908
954
  var SERIAL32_HALF = 2147483648;
@@ -7457,11 +7503,33 @@ var AdaptiveBitrateController = class {
7457
7503
  };
7458
7504
 
7459
7505
  // node_modules/@blockcast/mmt-base/dist/mmtp.js
7506
+ var MMTP_HEADER_SIZE2 = 12;
7460
7507
  var MPU_HEADER_SIZE2 = 8;
7461
7508
  var MPU_LENGTH_FIELD_SIZE2 = 2;
7462
7509
  var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
7463
7510
  var MMTP_SIGNALING_PACKET_ID2 = 0;
7464
7511
  var MMTP_PACKET_ID_MAX2 = 65535;
7512
+ function readMMTPPacketId2(buffer) {
7513
+ if (buffer.length < MMTP_HEADER_SIZE2)
7514
+ return null;
7515
+ const flags = buffer[0];
7516
+ if ((flags >> 6 & 3) !== 0)
7517
+ return null;
7518
+ let offset = MMTP_HEADER_SIZE2;
7519
+ if ((flags & 32) !== 0) {
7520
+ if (offset + 4 > buffer.length)
7521
+ return null;
7522
+ offset += 4;
7523
+ }
7524
+ if ((flags & 2) !== 0) {
7525
+ if (offset + 4 > buffer.length)
7526
+ return null;
7527
+ const length = buffer[offset + 2] << 8 | buffer[offset + 3];
7528
+ if (offset + 4 + length > buffer.length)
7529
+ return null;
7530
+ }
7531
+ return buffer[2] << 8 | buffer[3];
7532
+ }
7465
7533
 
7466
7534
  // node_modules/@blockcast/mmt-base/dist/net-constants.js
7467
7535
  var DEFAULT_AMT_RELAY_PORT2 = 2268;
@@ -8447,6 +8515,205 @@ var TransportManager = class {
8447
8515
  };
8448
8516
  var transportManager = new TransportManager();
8449
8517
 
8518
+ // node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
8519
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
8520
+ var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
8521
+ var INTEGRITY_SCHEME = "manifest-ed25519-2";
8522
+ var MulticastError = class extends Error {
8523
+ code;
8524
+ constructor(code, message, cause) {
8525
+ super(message, cause === void 0 ? void 0 : { cause });
8526
+ this.name = "MulticastError";
8527
+ this.code = code;
8528
+ }
8529
+ };
8530
+ function datagramsOf(chunk) {
8531
+ const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
8532
+ const out = [];
8533
+ for (const d of list) {
8534
+ const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
8535
+ if (data instanceof Uint8Array)
8536
+ out.push(data);
8537
+ else if (data instanceof ArrayBuffer)
8538
+ out.push(new Uint8Array(data));
8539
+ }
8540
+ return out;
8541
+ }
8542
+ function reasonOf(value) {
8543
+ return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
8544
+ }
8545
+ function registry() {
8546
+ const g = globalThis;
8547
+ if (g[REGISTRY] === void 0) {
8548
+ const providers = /* @__PURE__ */ new WeakSet();
8549
+ const byScope = /* @__PURE__ */ new WeakMap();
8550
+ Object.defineProperty(g, REGISTRY, {
8551
+ value: Object.freeze({
8552
+ isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
8553
+ providerFor: (scope) => {
8554
+ let provider = byScope.get(scope);
8555
+ if (provider === void 0) {
8556
+ provider = createProvider(scope);
8557
+ providers.add(provider);
8558
+ byScope.set(scope, provider);
8559
+ }
8560
+ return provider;
8561
+ }
8562
+ }),
8563
+ writable: false,
8564
+ configurable: false,
8565
+ enumerable: false
8566
+ });
8567
+ }
8568
+ return g[REGISTRY];
8569
+ }
8570
+ function resolveMulticastGateway(scope = globalThis) {
8571
+ const s = scope;
8572
+ if (typeof s.MulticastReceiver === "function")
8573
+ return registry().providerFor(scope);
8574
+ if (typeof s.multicast?.subscribe === "function")
8575
+ return s.multicast;
8576
+ return void 0;
8577
+ }
8578
+ function isUserAgentVerifiedGateway(gateway) {
8579
+ return registry().isProvider(gateway);
8580
+ }
8581
+ function userAgentVerifiedIngress(ingress, gateway) {
8582
+ if (isUserAgentVerifiedGateway(gateway))
8583
+ return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
8584
+ if (!("admittedBy" in ingress))
8585
+ return ingress;
8586
+ const { admittedBy: _, ...rest } = ingress;
8587
+ return rest;
8588
+ }
8589
+ function unsupported(message, cause) {
8590
+ return new MulticastError("capability-unsupported", message, cause);
8591
+ }
8592
+ function createProvider(scope) {
8593
+ const Receiver = scope.MulticastReceiver;
8594
+ let gate = null;
8595
+ return {
8596
+ capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
8597
+ subscribe: async (config) => {
8598
+ const c = config;
8599
+ if (c.consume !== "bytes")
8600
+ throw unsupported(`consume mode ${String(c.consume)} is not supported`);
8601
+ const origin = c.origin ?? "ip-multicast";
8602
+ if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
8603
+ throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
8604
+ }
8605
+ if (c.relay !== void 0)
8606
+ throw unsupported("MulticastReceiver picks its own AMT relay");
8607
+ if (!c.source)
8608
+ throw unsupported("MulticastReceiver needs an SSM source");
8609
+ if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
8610
+ throw unsupported("MulticastReceiver filters by source only");
8611
+ }
8612
+ const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
8613
+ if (gate !== null)
8614
+ await gate;
8615
+ let rx;
8616
+ try {
8617
+ rx = new Receiver(options);
8618
+ } catch (error) {
8619
+ throw unsupported("MulticastReceiver refused the request", error);
8620
+ }
8621
+ const opened = rx.opened;
8622
+ if (gate === null)
8623
+ gate = opened.then(() => void 0, () => void 0);
8624
+ return openSubscription(rx, opened, options, c.packetIds);
8625
+ }
8626
+ };
8627
+ }
8628
+ async function openSubscription(rx, openedPromise, options, packetIds) {
8629
+ let opened;
8630
+ try {
8631
+ opened = await openedPromise;
8632
+ } catch (error) {
8633
+ throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8634
+ }
8635
+ if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8636
+ await rx.close();
8637
+ throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8638
+ }
8639
+ const events = new EventTarget();
8640
+ let state = "connected";
8641
+ let filter = packetIds?.length ? new Set(packetIds) : null;
8642
+ let closing = false;
8643
+ const setState = (next) => {
8644
+ if (state === next)
8645
+ return;
8646
+ state = next;
8647
+ events.dispatchEvent(new CustomEvent("state", { detail: next }));
8648
+ };
8649
+ const sourceAddress = options.sourceAddress;
8650
+ const subscription = {
8651
+ id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
8652
+ get state() {
8653
+ return state;
8654
+ },
8655
+ delivery: "datagram",
8656
+ eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
8657
+ setPacketIds(ids) {
8658
+ filter = ids.length ? new Set(ids) : null;
8659
+ },
8660
+ addEventListener: events.addEventListener.bind(events),
8661
+ removeEventListener: events.removeEventListener.bind(events),
8662
+ async unsubscribe() {
8663
+ closing = true;
8664
+ await rx.close();
8665
+ setState("closed");
8666
+ }
8667
+ };
8668
+ void (async () => {
8669
+ const reader = opened.readable.getReader();
8670
+ try {
8671
+ for (; ; ) {
8672
+ const { value, done } = await reader.read();
8673
+ if (done)
8674
+ break;
8675
+ const packets = [];
8676
+ for (const data of datagramsOf(value)) {
8677
+ if (filter !== null) {
8678
+ const id = readMMTPPacketId2(data);
8679
+ if (id === null || !filter.has(id))
8680
+ continue;
8681
+ }
8682
+ const copy = data.slice().buffer;
8683
+ packets.push(copy);
8684
+ events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
8685
+ }
8686
+ if (packets.length > 0)
8687
+ events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
8688
+ }
8689
+ } catch {
8690
+ }
8691
+ })();
8692
+ void rx.closed.then((value) => {
8693
+ const reason = reasonOf(value);
8694
+ if (!closing) {
8695
+ if (reason === "consent-withdrawn") {
8696
+ events.dispatchEvent(new CustomEvent("error", {
8697
+ detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
8698
+ }));
8699
+ } else if (reason === "oversubscribed" || reason === "network") {
8700
+ events.dispatchEvent(new CustomEvent("error", {
8701
+ detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
8702
+ }));
8703
+ }
8704
+ }
8705
+ setState("closed");
8706
+ }, (error) => {
8707
+ if (!closing) {
8708
+ events.dispatchEvent(new CustomEvent("error", {
8709
+ detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
8710
+ }));
8711
+ }
8712
+ setState("closed");
8713
+ });
8714
+ return subscription;
8715
+ }
8716
+
8450
8717
  // node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
8451
8718
  var MAX_MOQT_PRIORITY = 255;
8452
8719
  function moqtPriority(value, field) {
@@ -8733,9 +9000,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
8733
9000
  }
8734
9001
  function validateMmtpDashTemplate(template, path) {
8735
9002
  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}$`);
9003
+ const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
9004
+ if (unsupported2 !== void 0) {
9005
+ throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
8739
9006
  }
8740
9007
  if (variables.filter((variable) => variable === "Number").length !== 1) {
8741
9008
  throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
@@ -17098,17 +17365,17 @@ var FECManager = class _FECManager {
17098
17365
 
17099
17366
  // src/mfu-reassembly-policy.ts
17100
17367
  function validateReliableMoqOptionShape(value) {
17101
- const unsupported = Object.keys(value).filter(
17368
+ const unsupported2 = Object.keys(value).filter(
17102
17369
  (key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
17103
17370
  );
17104
- if (unsupported.length > 0) {
17105
- if (unsupported.includes("maxRacingGroups")) {
17371
+ if (unsupported2.length > 0) {
17372
+ if (unsupported2.includes("maxRacingGroups")) {
17106
17373
  throw new Error(
17107
17374
  "[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
17108
17375
  );
17109
17376
  }
17110
17377
  throw new Error(
17111
- `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported.join(", ")}`
17378
+ `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
17112
17379
  );
17113
17380
  }
17114
17381
  const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
@@ -17152,12 +17419,12 @@ function assertValidMfuReassemblyPolicy(policy) {
17152
17419
  return;
17153
17420
  }
17154
17421
  if (value["transport"] === "datagram") {
17155
- const unsupported = Object.keys(value).filter(
17422
+ const unsupported2 = Object.keys(value).filter(
17156
17423
  (key) => key !== "transport" && key !== "deadlineMs"
17157
17424
  );
17158
- if (unsupported.length > 0) {
17425
+ if (unsupported2.length > 0) {
17159
17426
  throw new Error(
17160
- `[MFU reassembly] unsupported datagram option(s): ${unsupported.join(", ")}`
17427
+ `[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
17161
17428
  );
17162
17429
  }
17163
17430
  assertValidDatagramDeadline(value["deadlineMs"]);
@@ -23579,10 +23846,11 @@ var MmtpRouter = class {
23579
23846
  if (!entry) return;
23580
23847
  entry.receiver.release();
23581
23848
  entry.protected = false;
23849
+ entry.userAgent = false;
23582
23850
  this.#assistBlocks.clear();
23583
23851
  this.#assistFailures.clear();
23584
23852
  }
23585
- /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
23853
+ /** 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
23854
  get unadmittedListedDropCount() {
23587
23855
  return this.#unadmittedListedDrops;
23588
23856
  }
@@ -23623,6 +23891,13 @@ var MmtpRouter = class {
23623
23891
  }
23624
23892
  return false;
23625
23893
  }
23894
+ /** Whether a protected flow the user agent admitted lists `packetId`. */
23895
+ #userAgentGoverns(packetId) {
23896
+ for (const entry of this.#provenance.values()) {
23897
+ if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
23898
+ }
23899
+ return false;
23900
+ }
23626
23901
  /**
23627
23902
  * A MoQ repair symbol no protected flow lists, owned by a source track one
23628
23903
  * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
@@ -23713,7 +23988,7 @@ var MmtpRouter = class {
23713
23988
  fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
23714
23989
  fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
23715
23990
  fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
23716
- })) {
23991
+ }, true)) {
23717
23992
  this.#unlistedAssistedRecoveries++;
23718
23993
  this.#assistBlocks.delete(key);
23719
23994
  return;
@@ -24869,6 +25144,12 @@ var MmtpRouter = class {
24869
25144
  debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
24870
25145
  return;
24871
25146
  }
25147
+ if (ingress.admittedBy === USER_AGENT_VERIFIED) {
25148
+ this.#noteFlowAdmitted(ingress.flow);
25149
+ protectedFlow.userAgent = true;
25150
+ this.#processMMTP(packet, source, true, ingress, overlapCopy);
25151
+ return;
25152
+ }
24872
25153
  const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
24873
25154
  if (verdict === "admitted") {
24874
25155
  this.#noteFlowAdmitted(ingress.flow);
@@ -24886,6 +25167,11 @@ var MmtpRouter = class {
24886
25167
  return;
24887
25168
  }
24888
25169
  }
25170
+ const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
25171
+ if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
25172
+ this.#unadmittedListedDrops++;
25173
+ return;
25174
+ }
24889
25175
  if (info.isRepair) {
24890
25176
  const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
24891
25177
  if (overlay !== void 0) {
@@ -24903,7 +25189,7 @@ var MmtpRouter = class {
24903
25189
  );
24904
25190
  if (!repair) return;
24905
25191
  if (ownerPacketId != null && this.#fecManager) {
24906
- if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
25192
+ if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
24907
25193
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
24908
25194
  const count = this.#repairPidCounts[info.packetId];
24909
25195
  if (count <= 2 || count % 500 === 0) {
@@ -25482,7 +25768,7 @@ var MmtpRouter = class {
25482
25768
  }
25483
25769
  // ── Private: FEC recovery routing ───────────────────────────────
25484
25770
  /** Route WASM-recovered block through appropriate path based on container kind. */
25485
- #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
25771
+ #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
25486
25772
  const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
25487
25773
  if (ownerPacketId === void 0) {
25488
25774
  throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
@@ -25499,7 +25785,7 @@ var MmtpRouter = class {
25499
25785
  ownerPacketId,
25500
25786
  blockProfile
25501
25787
  });
25502
- return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
25788
+ return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
25503
25789
  }
25504
25790
  /**
25505
25791
  * A recovered source packet of a protected flow is admitted on its own
@@ -25511,9 +25797,24 @@ var MmtpRouter = class {
25511
25797
  * recovered packets is refused is unrecovered (7.2.7): its caller leaves it
25512
25798
  * open, so it reaches its FEC deadline and fails by class. Always true
25513
25799
  * outside a protected flow.
25514
- */
25515
- #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
25516
- const receiver = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25800
+ *
25801
+ * On a flow the user agent admitted, the live decoder's recoveries are kept
25802
+ * without a record: while the flow is protected that decoder takes only
25803
+ * symbols the user agent or a record verified (a flow-less multicast
25804
+ * datagram of a listed track is dropped or set aside in #processMMTP), so
25805
+ * what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
25806
+ * unlisted MoQ repair and still needs a record.
25807
+ * ponytail: per-flow flag; per-block provenance only if a protected flow
25808
+ * ever mixes verified and unverified live inputs.
25809
+ * ponytail: a worker frame computed before the protection reset can still
25810
+ * arrive after it (frames carry no epoch); its inputs are MoQ copies read
25811
+ * and played before protection, so it admits nothing new. Drop pre-reset
25812
+ * frames by epoch only if pre-protection input ever matters.
25813
+ */
25814
+ #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
25815
+ const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25816
+ const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25817
+ const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
25517
25818
  let recovered = true;
25518
25819
  if (receiver) {
25519
25820
  const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
@@ -25525,6 +25826,8 @@ var MmtpRouter = class {
25525
25826
  let admitted = false;
25526
25827
  if (receiver === "ambiguous") {
25527
25828
  this.#ambiguousRecoveredDrops++;
25829
+ } else if (userAgent) {
25830
+ admitted = true;
25528
25831
  } else {
25529
25832
  const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
25530
25833
  wire.set(sym.rawSymbol);
@@ -25552,12 +25855,12 @@ var MmtpRouter = class {
25552
25855
  catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
25553
25856
  );
25554
25857
  if (flows.size > 1) return "ambiguous";
25555
- for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
25858
+ for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
25556
25859
  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))) {
25860
+ for (const entry of this.#provenance.values()) {
25861
+ if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
25559
25862
  if (match) return "ambiguous";
25560
- match = receiver;
25863
+ match = entry;
25561
25864
  }
25562
25865
  }
25563
25866
  return match;
@@ -26339,11 +26642,8 @@ function providerErrorOf(error) {
26339
26642
  return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
26340
26643
  }
26341
26644
  var MAX_TRACKED_DROPPED_PIDS = 16;
26342
- function getMulticastGlobals() {
26343
- return globalThis;
26344
- }
26345
26645
  function hasMulticastGateway() {
26346
- return typeof getMulticastGlobals().multicast?.subscribe === "function";
26646
+ return typeof resolveMulticastGateway()?.subscribe === "function";
26347
26647
  }
26348
26648
  function senderOf(event) {
26349
26649
  const sourceAddress = event.sourceAddress;
@@ -26435,6 +26735,9 @@ var MulticastTransport = class {
26435
26735
  // listener closure, so an in-place admission change (setContext) applies
26436
26736
  // from the next packet on without rejoining.
26437
26737
  #context = void 0;
26738
+ #deliveredContext = void 0;
26739
+ /** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
26740
+ #userAgentGateway = void 0;
26438
26741
  // The live join's provider-side filter. An in-place widening must reach it,
26439
26742
  // or the provider never delivers the new ID; null without a live join.
26440
26743
  #providerFilter = null;
@@ -26471,7 +26774,7 @@ var MulticastTransport = class {
26471
26774
  this.#dataPlaneLive = false;
26472
26775
  this.#sender = void 0;
26473
26776
  if (this.#disposed) return this.#declined(generation, "disposed");
26474
- const gateway = this.#gateway ?? getMulticastGlobals().multicast;
26777
+ const gateway = this.#gateway ?? resolveMulticastGateway();
26475
26778
  if (typeof gateway?.subscribe !== "function") {
26476
26779
  console.warn("[MulticastTransport] Multicast API not available");
26477
26780
  return this.#declined(generation, "unavailable");
@@ -26487,15 +26790,16 @@ var MulticastTransport = class {
26487
26790
  console.log(
26488
26791
  `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
26489
26792
  );
26793
+ const userAgent = isUserAgentVerifiedGateway(gateway);
26490
26794
  const subscribeConfig = toWindowMulticastSubscribeConfig({
26491
26795
  source: config.sourceAddress,
26492
26796
  senderAddress: config.senderAddress,
26493
26797
  group: config.groupAddress,
26494
26798
  port: config.port,
26495
- mode: config.mode,
26496
- amtRelay: config.amtRelay,
26497
- amtRelayPort: config.amtRelayPort,
26498
- amtDiscovery: config.amtDiscovery,
26799
+ mode: userAgent ? "native" : config.mode,
26800
+ amtRelay: userAgent ? void 0 : config.amtRelay,
26801
+ amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
26802
+ amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
26499
26803
  packetIds: config.packetIds,
26500
26804
  tracks: config.tracks,
26501
26805
  atsc3: config.atsc3
@@ -26541,7 +26845,8 @@ var MulticastTransport = class {
26541
26845
  acknowledgedUpdate: 0,
26542
26846
  inFlight: 0
26543
26847
  };
26544
- this.#context = context;
26848
+ this.#userAgentGateway = userAgent ? gateway : void 0;
26849
+ this.#setContext(context);
26545
26850
  if (packetIds) {
26546
26851
  for (const packetId of packetIds) {
26547
26852
  if (!this.#acceptedBytesByPacketId.has(packetId)) {
@@ -26549,7 +26854,7 @@ var MulticastTransport = class {
26549
26854
  }
26550
26855
  }
26551
26856
  }
26552
- this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
26857
+ this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
26553
26858
  const delivery = subscription.delivery ?? "datagram";
26554
26859
  if (delivery !== "datagram" && delivery !== "ordered-relay") {
26555
26860
  throw new Error(
@@ -26630,7 +26935,7 @@ var MulticastTransport = class {
26630
26935
  }
26631
26936
  if (!this.#acceptPacket(packet, sourceAddress)) return false;
26632
26937
  const liveness = !this.#isRecordCandidate(packet);
26633
- this.#onPacket(packet, this.#context);
26938
+ this.#onPacket(packet, this.#deliveredContext);
26634
26939
  return liveness;
26635
26940
  }
26636
26941
  #isRecordCandidate(packet) {
@@ -26841,8 +27146,10 @@ var MulticastTransport = class {
26841
27146
  return this.#subscription !== null;
26842
27147
  }
26843
27148
  /**
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.
27149
+ * The live join's context as the caller gave it (the join's `context`, or
27150
+ * the last setContext), or undefined without a live join. On a native
27151
+ * provider each accepted packet is delivered with a marked copy of it
27152
+ * (`userAgentVerifiedIngress`); this getter never carries the mark.
26846
27153
  */
26847
27154
  get context() {
26848
27155
  return this.#context;
@@ -26866,7 +27173,16 @@ var MulticastTransport = class {
26866
27173
  "[MulticastTransport] setContext needs a live join; join() carries the first context"
26867
27174
  );
26868
27175
  }
27176
+ this.#setContext(context);
27177
+ }
27178
+ /**
27179
+ * The join's context, and what the subscription delivers with each packet:
27180
+ * marked admitted by the user agent when the gateway is its native
27181
+ * MulticastReceiver provider. feedPacket never marks.
27182
+ */
27183
+ #setContext(context) {
26869
27184
  this.#context = context;
27185
+ this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
26870
27186
  }
26871
27187
  /**
26872
27188
  * Replace the live join's packet-ID allow-list in place, without rejoining.
@@ -26998,6 +27314,8 @@ var MulticastTransport = class {
26998
27314
  this.#matchedSource = void 0;
26999
27315
  this.#providerFilter = null;
27000
27316
  this.#context = void 0;
27317
+ this.#deliveredContext = void 0;
27318
+ this.#userAgentGateway = void 0;
27001
27319
  this.#withdrawnPacketIds.clear();
27002
27320
  }
27003
27321
  /** The ingress filter; an accepted packet is counted here, once. */
@@ -28350,8 +28668,17 @@ var MulticastManager = class {
28350
28668
  * repair roles and owners are registered here; in-band owners stay the
28351
28669
  * caller's. Joining is best effort: a group that fails to join is logged
28352
28670
  * and leaves the media join as it was.
28353
- */
28354
- joinRepairEndpoints(endpoints, provider) {
28671
+ *
28672
+ * `options.gateway` names the gateway a subscribe callback reads through.
28673
+ * When it is the browser's own MulticastReceiver provider, the callback's
28674
+ * packets are fed as admitted by the user agent
28675
+ * (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
28676
+ * The mark rests on this statement, not on the data path: every packet the
28677
+ * callback delivers must come from a subscription opened on
28678
+ * `options.gateway`. A callback that also merges another source (a MoQ or
28679
+ * WebSocket repair feed) must not name a native provider here.
28680
+ */
28681
+ joinRepairEndpoints(endpoints, provider, options) {
28355
28682
  const releases = [];
28356
28683
  let released = false;
28357
28684
  const safely = (each) => {
@@ -28373,17 +28700,19 @@ var MulticastManager = class {
28373
28700
  ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
28374
28701
  };
28375
28702
  let sender;
28376
- const onPacket = (packet, sourceAddress) => {
28703
+ const feed = (packet, sourceAddress, ingressToFeed) => {
28377
28704
  if (released) return;
28378
28705
  if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
28379
- this.#router.feedPacket(packet, "multicast", ingress);
28706
+ this.#router.feedPacket(packet, "multicast", ingressToFeed);
28380
28707
  };
28381
28708
  const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
28382
28709
  if (typeof provider === "function") {
28710
+ const marked = userAgentVerifiedIngress(ingress, options?.gateway);
28711
+ const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
28383
28712
  Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
28384
28713
  continue;
28385
28714
  }
28386
- const transport = new MulticastTransport((packet) => onPacket(packet), {
28715
+ const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
28387
28716
  gateway: provider,
28388
28717
  lockedTo: () => this.#transport.sender
28389
28718
  });
@@ -28395,11 +28724,12 @@ var MulticastManager = class {
28395
28724
  mode: endpoint.mode,
28396
28725
  amtRelay: endpoint.amtRelay,
28397
28726
  amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
28727
+ amtDiscovery: endpoint.amtDiscovery,
28398
28728
  packetIds: endpoint.packetIds,
28399
28729
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
28400
28730
  // A protected repair group's records ride in band on its port (7.2.3).
28401
28731
  ...endpoint.flow === void 0 ? {} : { protected: true }
28402
- }).then((joined) => {
28732
+ }, ingress).then((joined) => {
28403
28733
  if (!joined && !released) fail(transport.lastJoinOutcome);
28404
28734
  });
28405
28735
  }
@@ -31170,12 +31500,12 @@ function moqtPriority2(value) {
31170
31500
  function catalogRepairTarget(catalog, source) {
31171
31501
  const binding = bindingOf(catalog, source);
31172
31502
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31173
- const layerZero = binding.layers[0];
31503
+ const layerZero2 = binding.layers[0];
31174
31504
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31175
31505
  const tracks = catalog.tracks;
31176
31506
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31177
31507
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31178
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
31508
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31179
31509
  }
31180
31510
  function numericField(value) {
31181
31511
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31257,7 +31587,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31257
31587
  var PATH_BITS = { moq: 1, multicast: 2 };
31258
31588
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31259
31589
  var MmtpFecSession = class {
31590
+ /** P_0 = B_0, the held total every repair packet is checked against. */
31591
+ get repairSymbols() {
31592
+ return this.#repairSymbols;
31593
+ }
31260
31594
  #decoder;
31595
+ #repairSymbols;
31596
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
31597
+ #message;
31261
31598
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31262
31599
  #blocks = /* @__PURE__ */ new Map();
31263
31600
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31295,6 +31632,24 @@ var MmtpFecSession = class {
31295
31632
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31296
31633
  */
31297
31634
  constructor(config) {
31635
+ if (config && "alFecFlow" in config) {
31636
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
31637
+ if (!message) {
31638
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
31639
+ }
31640
+ this.#message = message;
31641
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
31642
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
31643
+ this.sourceSymbols = message.maximumK;
31644
+ this.#repairSymbols = message.maximumP;
31645
+ this.symbolSize = message.repairSymbolSize;
31646
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
31647
+ this.#repairPacketId = message.repairFlowId;
31648
+ this.#heldLayers = layerZero(message.maximumP);
31649
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31650
+ this.#decoder = buildDecoder(config.Decoder, this);
31651
+ return;
31652
+ }
31298
31653
  const fec = config?.fec;
31299
31654
  if (!fec || typeof fec !== "object") {
31300
31655
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31305,7 +31660,7 @@ var MmtpFecSession = class {
31305
31660
  }
31306
31661
  this.algorithm = fec.scheme;
31307
31662
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31308
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31663
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31309
31664
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31310
31665
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31311
31666
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31325,17 +31680,30 @@ var MmtpFecSession = class {
31325
31680
  config.heldLayers
31326
31681
  );
31327
31682
  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;
31683
+ this.#decoder = buildDecoder(config.Decoder, this);
31684
+ }
31685
+ /**
31686
+ * A later version of the AL-FEC message, catalog-less sessions only. A
31687
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
31688
+ * anchored check restarts at the new total, so a block generated under the
31689
+ * previous one stays valid before the first block carrying the new total
31690
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
31691
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
31692
+ * does. An entry with another asset, repair flow, code point, K or T, or
31693
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
31694
+ * a new session. A latched fail-closed stays latched either way.
31695
+ */
31696
+ updateAlFecFlow(flow) {
31697
+ const held = this.#message;
31698
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
31699
+ const next = alFecRaptorQFlowConfig(flow);
31700
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
31701
+ return "refused";
31702
+ }
31703
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
31704
+ this.#repairSymbols = next.maximumP;
31705
+ this.#heldLayers = layerZero(next.maximumP);
31706
+ return "raised";
31339
31707
  }
31340
31708
  /** True once release() ran. */
31341
31709
  get released() {
@@ -31430,7 +31798,7 @@ var MmtpFecSession = class {
31430
31798
  }
31431
31799
  if (admission.reject) {
31432
31800
  const verdict2 = this.#heldLayers.failsClosed(
31433
- repairTrackName,
31801
+ this.#trackFor(repairTrackName),
31434
31802
  frame.ssStart,
31435
31803
  frame.rsbLength,
31436
31804
  this.#anchorHorizon,
@@ -31445,7 +31813,7 @@ var MmtpFecSession = class {
31445
31813
  this.#repairPacketId ??= frame.packetId;
31446
31814
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31447
31815
  const verdict = this.#heldLayers.admit(
31448
- repairTrackName,
31816
+ this.#trackFor(repairTrackName),
31449
31817
  frame.ssStart,
31450
31818
  frame.rsbLength,
31451
31819
  this.#anchorHorizon,
@@ -31504,6 +31872,10 @@ var MmtpFecSession = class {
31504
31872
  decoder.free();
31505
31873
  return true;
31506
31874
  }
31875
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
31876
+ #trackFor(repairTrackName) {
31877
+ return this.#message ? void 0 : repairTrackName;
31878
+ }
31507
31879
  #drop(reason) {
31508
31880
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31509
31881
  return [];
@@ -31625,6 +31997,22 @@ var MmtpFecSession = class {
31625
31997
  }
31626
31998
  }
31627
31999
  };
32000
+ function layerZero(total) {
32001
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32002
+ }
32003
+ function buildDecoder(Decoder, session) {
32004
+ if (typeof Decoder !== "function") {
32005
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32006
+ }
32007
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32008
+ try {
32009
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32010
+ } catch (error) {
32011
+ decoder.free();
32012
+ throw error;
32013
+ }
32014
+ return decoder;
32015
+ }
31628
32016
  function requirePositiveInteger(value, path) {
31629
32017
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31630
32018
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);