@blockcast/mmt-container 0.4.1 → 0.5.0-main.2c1f81817995

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 (72) hide show
  1. package/README.md +2 -2
  2. package/dist/browser-runtime.js +3458 -356
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/cmcd-producer.d.ts +5 -1
  5. package/dist/cmcd-producer.d.ts.map +1 -1
  6. package/dist/cmcd-producer.js.map +2 -2
  7. package/dist/delivery-catalog.js +18 -2
  8. package/dist/delivery-catalog.js.map +2 -2
  9. package/dist/fec-manager.d.ts +21 -16
  10. package/dist/fec-manager.d.ts.map +1 -1
  11. package/dist/fec-manager.js +177 -80
  12. package/dist/fec-manager.js.map +3 -3
  13. package/dist/held-layer-admission.d.ts +58 -0
  14. package/dist/held-layer-admission.d.ts.map +1 -0
  15. package/dist/held-layer-admission.js +105 -0
  16. package/dist/held-layer-admission.js.map +7 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +2 -2
  19. package/dist/mmtp-container-parser.no-modules.js +2682 -1023
  20. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  21. package/dist/mmtp-fec-session.d.ts +36 -3
  22. package/dist/mmtp-fec-session.d.ts.map +1 -1
  23. package/dist/mmtp-fec-session.js +114 -5
  24. package/dist/mmtp-fec-session.js.map +3 -3
  25. package/dist/mmtp-router.d.ts +72 -28
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +321 -203
  28. package/dist/mmtp-router.js.map +2 -2
  29. package/dist/multicast-auth-catalog.d.ts +10 -2
  30. package/dist/multicast-auth-catalog.d.ts.map +1 -1
  31. package/dist/multicast-auth-catalog.js +422 -22
  32. package/dist/multicast-auth-catalog.js.map +4 -4
  33. package/dist/multicast-manager.d.ts +48 -9
  34. package/dist/multicast-manager.d.ts.map +1 -1
  35. package/dist/multicast-manager.js +3406 -340
  36. package/dist/multicast-manager.js.map +4 -4
  37. package/dist/multicast-transport.d.ts +4 -1
  38. package/dist/multicast-transport.d.ts.map +1 -1
  39. package/dist/multicast-transport.js +20 -3
  40. package/dist/multicast-transport.js.map +2 -2
  41. package/dist/no-modules.d.ts +1 -2
  42. package/dist/no-modules.d.ts.map +1 -1
  43. package/dist/no-modules.js +2668 -1007
  44. package/dist/no-modules.js.map +4 -4
  45. package/dist/repair-fetch.d.ts +10 -1
  46. package/dist/repair-fetch.d.ts.map +1 -1
  47. package/dist/repair-fetch.js +5 -2
  48. package/dist/repair-fetch.js.map +2 -2
  49. package/package.json +5 -14
  50. package/src/cmcd-producer.ts +6 -1
  51. package/src/fec-manager.ts +121 -131
  52. package/src/held-layer-admission.ts +161 -0
  53. package/src/index.ts +5 -9
  54. package/src/mmtp-fec-session.ts +76 -3
  55. package/src/mmtp-router.ts +390 -272
  56. package/src/multicast-auth-catalog.ts +81 -8
  57. package/src/multicast-manager.ts +189 -48
  58. package/src/multicast-transport.ts +6 -2
  59. package/src/no-modules.ts +1 -6
  60. package/src/repair-fetch.ts +24 -4
  61. package/dist/manifest-auth-plan.d.ts +0 -31
  62. package/dist/manifest-auth-plan.d.ts.map +0 -1
  63. package/dist/manifest-auth-plan.js +0 -426
  64. package/dist/manifest-auth-plan.js.map +0 -7
  65. package/dist/source-auth.d.ts +0 -2
  66. package/dist/source-auth.d.ts.map +0 -1
  67. package/dist/source-auth.js +0 -3912
  68. package/dist/source-auth.js.map +0 -7
  69. package/src/manifest-auth-plan.ts +0 -55
  70. package/src/source-auth.ts +0 -1
  71. package/test-vectors/generate-source-auth-fixture.mjs +0 -114
  72. package/test-vectors/source-auth-v1.json +0 -61
@@ -67,10 +67,8 @@ import {
67
67
  type DuplicateHorizon,
68
68
  } from "./duplicate-ledger.js";
69
69
  import type {
70
- ManifestAuthMode,
71
- ManifestObjectIdentity,
72
- ManifestTrackContext,
73
- ManifestVerifier,
70
+ BcProvenanceV2Receiver,
71
+ BcpV2RecordPath,
74
72
  } from "@blockcast/mmt-manifest-verify";
75
73
 
76
74
  // ─── Constants ─────────────────────────────────────────────────────
@@ -177,6 +175,12 @@ export type RoutedFrameMeta = {
177
175
 
178
176
  export interface MmtpIngressContext {
179
177
  tracks: readonly Readonly<{ name: string; packetId: number }>[];
178
+ /**
179
+ * The bc-provenance v2 flow (draft-ramadan-moq-multicast-01 7.2.7) that
180
+ * delivered the packet, when its endpoint is protected: the key a receiver
181
+ * was registered under with `registerProvenanceFlow`.
182
+ */
183
+ flow?: string;
180
184
  }
181
185
 
182
186
  /** One physical stream: an MMTP packet ID and the catalog track it carries. */
@@ -558,6 +562,9 @@ export interface FECManagerLike {
558
562
  catalogTrackName?: string,
559
563
  ): Uint8Array | null;
560
564
 
565
+ /** How long ago the stream's oldest open block with an FEC deadline took its first symbol (optional). */
566
+ oldestOpenBlockAgeMs?(packetId: number, catalogTrackName: string): number | undefined;
567
+
561
568
  /** A block's RSB_length, from one of its repair symbols (optional). */
562
569
  noteRepairGroupLength?(sbn: number, packetId: number, rsbLength: number, catalogTrackName?: string): void;
563
570
 
@@ -703,6 +710,14 @@ export interface FECManagerLike {
703
710
  catalogTrackName: string | undefined,
704
711
  ssId: number,
705
712
  ): { sbn: number; esi: number } | null;
713
+ /** Begin a fresh decode epoch for one stream, dropping what it holds (FECManager.resetTrack). */
714
+ resetTrack?(packetId: number, catalogTrackName: string): void;
715
+ /**
716
+ * Run `fn` with `receivedAtMs` (performance.now() clock) as the receipt
717
+ * time of the packet it feeds: a block it opens takes that ts, and its
718
+ * deadlines run from it (fec-01 9, a datagram held and admitted later).
719
+ */
720
+ withReceiptTime?(receivedAtMs: number, fn: () => void): void;
706
721
  /** iso-ssbg1-v1: hold a first-arrival source until a repair states its block. */
707
722
  holdIsoSource?(
708
723
  packetId: number,
@@ -804,26 +819,6 @@ export interface FECManagerLike {
804
819
 
805
820
  // ─── MmtpRouter ─────────────────────────────────────────────────────
806
821
 
807
- export interface ManifestIdentityResolutionInput {
808
- packetId: number;
809
- sbn: number;
810
- esi: number;
811
- /**
812
- * The packet's SS_ID and its MPU's mpu_sequence_number. Under
813
- * iso-ssbg1-v1 blocks vary in length, so a packet's media group comes from
814
- * its own MPU header, never from SBN arithmetic (BLO-35597 D2a).
815
- */
816
- ssId?: number;
817
- mpuSequenceNumber?: number;
818
- catalogTrackName?: string;
819
- recovered: boolean;
820
- sourceSymbolFormat: "full-mmtp-packet" | "fec-padded-recovery";
821
- }
822
-
823
- export type ManifestIdentityResolver = (
824
- input: ManifestIdentityResolutionInput,
825
- ) => ManifestObjectIdentity | null;
826
-
827
822
  /**
828
823
  * Classification evidence carried by one first-arrival network source
829
824
  * packet: the MMTP header R bit and the MPU / data-unit identity it states
@@ -860,26 +855,25 @@ export class MmtpRouter {
860
855
  readonly #fecManager: FECManagerLike | null;
861
856
  #defaultFecMapper: FecBlockMapper | null;
862
857
  #fecMappersByPacketId = new Map<number, FecBlockMapper>();
863
- readonly #manifestVerifier: ManifestVerifier | null;
864
- readonly #ownsManifestVerifier: boolean;
865
- readonly #resolveManifestIdentity:
866
- | ((
867
- packetId: number,
868
- sbn: number,
869
- esi: number,
870
- catalogTrackName?: string,
871
- ) => ManifestObjectIdentity | null)
872
- | null;
873
- readonly #resolveManifestIdentityForRoute: ManifestIdentityResolver | null;
874
- #manifestRecoveryQueues = new Map<
858
+ /** Protected flows: one 7.2.7 receiver each, and the ingress its datagrams arrive with. */
859
+ readonly #provenance = new Map<
875
860
  string,
876
- Array<{
877
- blockId: number;
878
- result: ProcessFecBlockResult;
879
- ownerPacketId: number;
880
- catalogTrackName?: string;
881
- }>
861
+ { receiver: BcProvenanceV2Receiver; ingress: MmtpIngressContext; protected?: boolean }
882
862
  >();
863
+ #onFlowProtected: ((flow: string) => void) | null = null;
864
+ #unadmittedListedDrops = 0;
865
+ #unlistedRepairSetAside = 0;
866
+ /** The ingress a held datagram arrived with; the receiver returns the same bytes on release. */
867
+ readonly #heldIngress = new WeakMap<Uint8Array, MmtpIngressContext>();
868
+ #unicastRepair = false;
869
+ /** How to route a held unicast (MoQ) datagram once a record admits it. */
870
+ readonly #heldReplay = new WeakMap<Uint8Array, () => void>();
871
+ /** When a held datagram was received: admitted later, it keeps that receipt time (fec-01 9). */
872
+ readonly #heldAt = new WeakMap<Uint8Array, number>();
873
+ /** The flow whose receiver admitted a stream's source datagrams, by packetId then catalog track name: recovered packets answer to it. */
874
+ readonly #provenanceStreamFlow = new Map<number, Map<string | undefined, string>>();
875
+ #unregisteredFlowDrops = 0;
876
+ #ambiguousRecoveredDrops = 0;
883
877
 
884
878
  // Temp state set by #processMMTP before calling parser.feed(),
885
879
  // read by the parser's onFrame callback for CMAF assembly + dedup.
@@ -1056,16 +1050,6 @@ export class MmtpRouter {
1056
1050
  * Pass a reader when its inputs are measured later; see
1057
1051
  * setKeyframeLostTimeoutMs. */
1058
1052
  keyframeLostTimeoutMs?: number | (() => number);
1059
- manifestVerifier?: ManifestVerifier;
1060
- /** Existing behavior is router ownership; shared browser plans opt into caller ownership. */
1061
- manifestVerifierOwnership?: "caller" | "router";
1062
- resolveManifestIdentity?: (
1063
- packetId: number,
1064
- sbn: number,
1065
- esi: number,
1066
- catalogTrackName?: string,
1067
- ) => ManifestObjectIdentity | null;
1068
- resolveManifestIdentityForRoute?: ManifestIdentityResolver;
1069
1053
  /** Internal terminal-failure sink owned by the composing transport. */
1070
1054
  onBlockFailed?: (
1071
1055
  sbn: number,
@@ -1088,10 +1072,6 @@ export class MmtpRouter {
1088
1072
  this.#fecManager = config?.fecManager ?? null;
1089
1073
  this.#onFrameGap = config?.onFrameGap ?? null;
1090
1074
  this.#defaultFecMapper = config?.fecMapper ?? null;
1091
- this.#manifestVerifier = config?.manifestVerifier ?? null;
1092
- this.#ownsManifestVerifier = config?.manifestVerifierOwnership !== "caller";
1093
- this.#resolveManifestIdentity = config?.resolveManifestIdentity ?? null;
1094
- this.#resolveManifestIdentityForRoute = config?.resolveManifestIdentityForRoute ?? null;
1095
1075
 
1096
1076
  // Create MMTP parser — handles MFU extraction, reassembly, keyframe detection.
1097
1077
  // Its MFUReassembler is the single reassembler for BOTH the source path
@@ -1822,32 +1802,216 @@ export class MmtpRouter {
1822
1802
  });
1823
1803
  }
1824
1804
 
1825
- registerManifestTrack(context: ManifestTrackContext): void {
1826
- if (!this.#manifestVerifier) throw new Error("manifest verifier is not configured");
1827
- this.#manifestVerifier.registerTrack(context);
1805
+ /**
1806
+ * Protect a flow (draft-ramadan-moq-multicast-01 7.2.7): every multicast
1807
+ * datagram whose ingress names `ingress.flow`, source and repair alike, is
1808
+ * admitted by `receiver` before anything reads it, and an unadmitted one is
1809
+ * held or dropped. The caller owns the receiver's configuration; the router
1810
+ * releases it on `releaseProvenanceFlow` and on dispose.
1811
+ */
1812
+ registerProvenanceFlow(
1813
+ ingress: MmtpIngressContext & { flow: string },
1814
+ receiver: BcProvenanceV2Receiver,
1815
+ ): void {
1816
+ this.#provenance.get(ingress.flow)?.receiver.release();
1817
+ this.#provenance.set(ingress.flow, { receiver, ingress });
1818
+ receiver.keepPendingAgeMs = () => this.#oldestOpenBlockAgeMs(ingress.flow);
1828
1819
  }
1829
1820
 
1830
- feedManifestFrame(trackName: string, frame: Uint8Array): void {
1831
- this.#manifestVerifier?.ingestManifestFrame(trackName, frame);
1821
+ /**
1822
+ * Declare that this receiver can request unicast repair (a repair FETCH
1823
+ * scheduler is wired): a protected flow then keeps the pending entries of
1824
+ * its open blocks for the repair to consume (7.2.7 step 2).
1825
+ */
1826
+ setUnicastRepair(enabled: boolean): void {
1827
+ this.#unicastRepair = enabled;
1832
1828
  }
1833
1829
 
1834
- setManifestAuthMode(mode: ManifestAuthMode): void {
1835
- this.#manifestVerifier?.setMode(mode);
1836
- if (mode !== "enforce") {
1837
- for (const queue of this.#manifestRecoveryQueues.values()) {
1838
- for (const item of queue) {
1839
- this.#routeProcessedFecSymbols(
1840
- item.blockId,
1841
- item.result,
1842
- item.ownerPacketId,
1843
- item.catalogTrackName,
1844
- );
1830
+ #oldestOpenBlockAgeMs(flow: string): number | undefined {
1831
+ const entry = this.#provenance.get(flow);
1832
+ if (!this.#unicastRepair || !entry) return undefined;
1833
+ let age: number | undefined;
1834
+ for (const { name, packetId } of entry.ingress.tracks) {
1835
+ const a = name ? this.#fecManager?.oldestOpenBlockAgeMs?.(packetId, name) : undefined;
1836
+ if (a !== undefined && (age === undefined || a > age)) age = a;
1837
+ }
1838
+ return age;
1839
+ }
1840
+
1841
+ /**
1842
+ * Replace a registered flow's ingress (a rendition switch staged, committed
1843
+ * or cancelled) while keeping its receiver; a no-op for an unregistered flow.
1844
+ */
1845
+ updateProvenanceFlowIngress(ingress: MmtpIngressContext & { flow: string }): void {
1846
+ const entry = this.#provenance.get(ingress.flow);
1847
+ if (!entry) return;
1848
+ this.#provenance.set(ingress.flow, { ...entry, ingress });
1849
+ // A protected flow newly listing a track drops its MoQ copies from now
1850
+ // on, so discard what its FEC instance holds of them first (7.2.7).
1851
+ if (entry.protected) {
1852
+ for (const { name, packetId } of ingress.tracks) {
1853
+ if (name && !entry.ingress.tracks.some((t) => t.name === name && t.packetId === packetId)) {
1854
+ this.#fecManager?.resetTrack?.(packetId, name);
1845
1855
  }
1846
1856
  }
1847
- this.#manifestRecoveryQueues.clear();
1848
1857
  }
1849
1858
  }
1850
1859
 
1860
+ /**
1861
+ * Called once per protection of a flow: at the admission that makes this
1862
+ * router's consumers protected under it (7.2.7), after their unadmitted
1863
+ * holdings of its listed tracks are discarded. Again after an unprotect.
1864
+ */
1865
+ onProvenanceFlowProtected(cb: ((flow: string) => void) | null): void {
1866
+ this.#onFlowProtected = cb;
1867
+ }
1868
+
1869
+ /**
1870
+ * The receiver ceased to hold the flow's endpoint on failover (7.2.7): drop
1871
+ * its state and hold queues (no expiry counted) and un-protect its tracks,
1872
+ * which then take MoQ again. The registration stays, so multicast resuming
1873
+ * protects the flow anew at its next admission.
1874
+ */
1875
+ unprotectProvenanceFlow(flow: string): void {
1876
+ const entry = this.#provenance.get(flow);
1877
+ if (!entry) return;
1878
+ entry.receiver.release();
1879
+ entry.protected = false;
1880
+ }
1881
+
1882
+ /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
1883
+ get unadmittedListedDropCount(): number {
1884
+ return this.#unadmittedListedDrops;
1885
+ }
1886
+
1887
+ /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
1888
+ get unlistedRepairSetAsideCount(): number {
1889
+ return this.#unlistedRepairSetAside;
1890
+ }
1891
+
1892
+ /**
1893
+ * A MoQ repair symbol no protected flow lists, owned by a source track one
1894
+ * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
1895
+ * symbols, so the symbol is set aside. Un-listing needs no hook: an
1896
+ * unprotected or released flow lists nothing.
1897
+ * ponytail: set aside = dropped. 7.2.7 lets it enter a decode (MAY), only
1898
+ * for a shortfall; the unlisted-symbol pool (BLO-49773 item 2) adds that.
1899
+ */
1900
+ #setAsideUnlistedRepair(ownerPacketId: number): boolean {
1901
+ for (const entry of this.#provenance.values()) {
1902
+ if (entry.protected && ingressHasPacketId(entry.ingress, ownerPacketId)) {
1903
+ this.#unlistedRepairSetAside++;
1904
+ return true;
1905
+ }
1906
+ }
1907
+ return false;
1908
+ }
1909
+
1910
+ /**
1911
+ * The admission that makes this router protected under `flow` (7.2.7):
1912
+ * its FEC instances discard what they hold of the flow's listed tracks so
1913
+ * no unadmitted symbol stands for an admitted one, then notify. Before it
1914
+ * nothing of the flow was admitted, so every such symbol is unadmitted.
1915
+ * The playback parse ledger is kept: it records MoQ copies already played,
1916
+ * not holdings, and clearing it re-emits that media (audio twice).
1917
+ * The FEC reset is the whole 7.2.7 discard here: no keyframe-overlay
1918
+ * instance exists to hold anything (repair-layers.ts is the catalog
1919
+ * contract only, no decoder), and packet_id 0 is never a listed track
1920
+ * (`ingressHasPacketId`), so nothing of it is held for the flow.
1921
+ */
1922
+ #noteFlowAdmitted(flow: string): void {
1923
+ const entry = this.#provenance.get(flow);
1924
+ if (!entry || entry.protected) return;
1925
+ entry.protected = true;
1926
+ for (const { name, packetId } of entry.ingress.tracks) {
1927
+ if (name) this.#fecManager?.resetTrack?.(packetId, name);
1928
+ }
1929
+ this.#onFlowProtected?.(flow);
1930
+ }
1931
+
1932
+ /**
1933
+ * Admit a MoQ datagram of `packetId` on the unicast path of the protected
1934
+ * flow that lists it (7.2.7). `unlisted`: no protected flow lists it, so it
1935
+ * is read as before. A held datagram is routed by `replay` when a record
1936
+ * admits it; one more than one protected flow lists is dropped. So is an
1937
+ * overlap copy: it never enters FEC accounting, so admitting it would leave
1938
+ * its multicast twin a duplicate and the block one symbol short.
1939
+ */
1940
+ #admitUnicast(
1941
+ packetId: number,
1942
+ packet: Uint8Array,
1943
+ replay: () => void,
1944
+ overlapCopy = false,
1945
+ ): "unlisted" | "admitted" | "held" | "dropped" {
1946
+ let receiver: BcProvenanceV2Receiver | undefined;
1947
+ for (const entry of this.#provenance.values()) {
1948
+ if (!entry.protected || !ingressHasPacketId(entry.ingress, packetId)) continue;
1949
+ if (receiver || overlapCopy) {
1950
+ this.#unadmittedListedDrops++;
1951
+ return "dropped";
1952
+ }
1953
+ receiver = entry.receiver;
1954
+ }
1955
+ if (!receiver) return "unlisted";
1956
+ const { verdict } = receiver.onDatagram(packet, "unicast");
1957
+ if (verdict === "admitted") return "admitted";
1958
+ if (verdict === "held") {
1959
+ this.#heldReplay.set(packet, replay);
1960
+ this.#heldAt.set(packet, performance.now());
1961
+ return "held";
1962
+ }
1963
+ this.#unadmittedListedDrops++;
1964
+ return "dropped";
1965
+ }
1966
+
1967
+ /**
1968
+ * Feed one bc-provenance record of `flow`: a datagram from `manifestPort`
1969
+ * (`multicast`) or an object of the manifest track (`mirror`). Held
1970
+ * datagrams the record admits are routed before this returns.
1971
+ */
1972
+ feedProvenanceRecord(flow: string, record: Uint8Array, path: BcpV2RecordPath): string {
1973
+ const entry = this.#provenance.get(flow);
1974
+ if (!entry || this.#disposed) return "unregistered";
1975
+ const result = entry.receiver.onRecord(record, path);
1976
+ if (result.admitted.length > 0) {
1977
+ this.#noteFlowAdmitted(flow);
1978
+ this.#withDeferredFecPokes(() => {
1979
+ for (const datagram of result.admitted) {
1980
+ const route = () => {
1981
+ const replay = this.#heldReplay.get(datagram);
1982
+ if (replay) replay();
1983
+ else this.#processMMTP(datagram, "multicast", true, this.#heldIngress.get(datagram) ?? entry.ingress, false, true);
1984
+ };
1985
+ // A datagram held on a miss and admitted now keeps its receipt time
1986
+ // as the block's ts (fec-01 9): its FEC deadline runs from then.
1987
+ const heldAt = this.#heldAt.get(datagram);
1988
+ if (heldAt !== undefined && this.#fecManager?.withReceiptTime) this.#fecManager.withReceiptTime(heldAt, route);
1989
+ else route();
1990
+ }
1991
+ });
1992
+ }
1993
+ return result.verdict;
1994
+ }
1995
+
1996
+ /** The receiver ceased to hold the flow: its hold queues are dropped, not expired. */
1997
+ releaseProvenanceFlow(flow: string): void {
1998
+ this.#provenance.get(flow)?.receiver.release();
1999
+ this.#provenance.delete(flow);
2000
+ for (const byName of this.#provenanceStreamFlow.values()) {
2001
+ for (const [name, bound] of byName) if (bound === flow) byName.delete(name);
2002
+ }
2003
+ }
2004
+
2005
+ /** Multicast datagrams dropped because their ingress names a flow with no registered receiver. */
2006
+ get unregisteredFlowDropCount(): number {
2007
+ return this.#unregisteredFlowDrops;
2008
+ }
2009
+
2010
+ /** Recovered packets dropped because more than one protected flow could own their stream. */
2011
+ get ambiguousRecoveredDropCount(): number {
2012
+ return this.#ambiguousRecoveredDrops;
2013
+ }
2014
+
1851
2015
  /**
1852
2016
  * Main entry point: feed a raw MMTP packet for processing.
1853
2017
  *
@@ -1865,12 +2029,11 @@ export class MmtpRouter {
1865
2029
  feedPacket(
1866
2030
  packet: Uint8Array,
1867
2031
  source: "multicast" | "moq",
1868
- manifestIdentity?: ManifestObjectIdentity,
1869
2032
  ingress?: MmtpIngressContext,
1870
2033
  ): void {
1871
2034
  if (this.#disposed) return;
1872
2035
  this.#withDeferredFecPokes(() => {
1873
- this.#processMMTP(packet, source, manifestIdentity, false, ingress);
2036
+ this.#processMMTP(packet, source, false, ingress);
1874
2037
  });
1875
2038
  }
1876
2039
 
@@ -1906,7 +2069,7 @@ export class MmtpRouter {
1906
2069
  feedOverlapCopy(packet: Uint8Array, ingress?: MmtpIngressContext): void {
1907
2070
  if (this.#disposed) return;
1908
2071
  this.#withDeferredFecPokes(() => {
1909
- this.#processMMTP(packet, "moq", undefined, false, ingress, true);
2072
+ this.#processMMTP(packet, "moq", false, ingress, true);
1910
2073
  });
1911
2074
  }
1912
2075
 
@@ -1931,6 +2094,7 @@ export class MmtpRouter {
1931
2094
  ownerPacketIdHint: number | undefined,
1932
2095
  catalogTrackName: string | undefined,
1933
2096
  repairTrackName: string | undefined,
2097
+ admitted = false,
1934
2098
  ): void {
1935
2099
  // One header parse per packet, as on the multicast repair branch: mmt-fec's
1936
2100
  // parseRepairPacket() parsed it once more before this parse (BLO-36599).
@@ -1961,6 +2125,15 @@ export class MmtpRouter {
1961
2125
  );
1962
2126
  }
1963
2127
 
2128
+ // Admission keys on the repair track's own packet id: a repair layer no
2129
+ // protected flow lists is an unlisted track (7.2.7), not a datagram of the
2130
+ // flow that lists its owner, where it would be held until expiry.
2131
+ if (ownerPacketId != null && !admitted) {
2132
+ const admit = this.#admitUnicast(repairPacketId, packet, () =>
2133
+ this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, true));
2134
+ if (admit === "held" || admit === "dropped") return;
2135
+ if (admit === "unlisted" && this.#setAsideUnlistedRepair(ownerPacketId)) return;
2136
+ }
1964
2137
  if (ownerPacketId != null && this.#fecManager) {
1965
2138
  this.#moqRepairPidCounts[repairPacketId] =
1966
2139
  (this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
@@ -2147,10 +2320,11 @@ export class MmtpRouter {
2147
2320
  catalogTrackName,
2148
2321
  );
2149
2322
  if (recovered && !alreadyCompleted) {
2150
- fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
2151
- fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
2152
- fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
2153
- this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName);
2323
+ this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
2324
+ fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
2325
+ fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
2326
+ fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
2327
+ });
2154
2328
  }
2155
2329
  }
2156
2330
 
@@ -2203,10 +2377,11 @@ export class MmtpRouter {
2203
2377
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
2204
2378
  const recovered = fec.feedSourceSymbol(ownerPacketId, ssId, payload.symbolData, catalogTrackName);
2205
2379
  if (!recovered || alreadyCompleted) continue;
2206
- fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
2207
- fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
2208
- fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
2209
- this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName);
2380
+ this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
2381
+ fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
2382
+ fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
2383
+ fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
2384
+ });
2210
2385
  }
2211
2386
  }
2212
2387
 
@@ -2244,10 +2419,14 @@ export class MmtpRouter {
2244
2419
  data: Uint8Array,
2245
2420
  packetId: number,
2246
2421
  catalogTrackName?: string,
2247
- ): void {
2422
+ ): boolean {
2248
2423
  // A Worker-recovered block arrives here whole: defer the gate's drains
2249
2424
  // until all its frames are in, as the in-band recovery path does.
2250
- this.#withDeferredFecPokes(() => this.#routeRecoveredBlock(blockId, data, packetId, catalogTrackName));
2425
+ let recovered = false;
2426
+ this.#withDeferredFecPokes(() => {
2427
+ recovered = this.#routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
2428
+ });
2429
+ return recovered;
2251
2430
  }
2252
2431
 
2253
2432
  /** Route a single recovered source symbol through the parser + dedup pipeline.
@@ -2258,10 +2437,10 @@ export class MmtpRouter {
2258
2437
  packetId: number,
2259
2438
  esi = 0,
2260
2439
  catalogTrackName?: string,
2261
- ): void {
2440
+ ): boolean {
2262
2441
  const blockProfile = this.#blockProfileFor(packetId);
2263
2442
  const result = processFecSymbol(data, { ownerPacketId: packetId, index: esi, blockProfile });
2264
- this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
2443
+ return this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
2265
2444
  }
2266
2445
 
2267
2446
  // ── Keyframe timer cancellation ─────────────────────────────────
@@ -2310,7 +2489,6 @@ export class MmtpRouter {
2310
2489
  this.#lastDirectSourceAudioCursorByStream.delete(streamKey);
2311
2490
  this.#lastDirectEmittedAudioCursorByStream.delete(streamKey);
2312
2491
  this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
2313
- this.#manifestRecoveryQueues.delete(streamKey);
2314
2492
  this.#deliveredVideoHighWaterByStream.delete(streamKey);
2315
2493
  this.#stream(packetId, catalogTrackName).duplicates.clear();
2316
2494
  const assemblersToReset = new Set<CmafBlockAssembler>();
@@ -2362,7 +2540,6 @@ export class MmtpRouter {
2362
2540
  this.#lastDirectSourceAudioCursorByStream.delete(key);
2363
2541
  this.#lastDirectEmittedAudioCursorByStream.delete(key);
2364
2542
  this.#pendingDirectRecoveredAudioByStream.delete(key);
2365
- this.#manifestRecoveryQueues.delete(key);
2366
2543
  this.#trackInitDelivered.delete(key);
2367
2544
  this.#parserDiagnostics.delete(key);
2368
2545
  this.#pendingFecPokes.delete(key);
@@ -2658,8 +2835,9 @@ export class MmtpRouter {
2658
2835
  this.#parserDiagnostics.clear();
2659
2836
  this.#catalogTrackNamesByPacketId.clear();
2660
2837
  this.#repairOwnersByPacketId.clear();
2661
- if (this.#ownsManifestVerifier) this.#manifestVerifier?.dispose();
2662
- this.#manifestRecoveryQueues.clear();
2838
+ for (const { receiver } of this.#provenance.values()) receiver.release();
2839
+ this.#provenance.clear();
2840
+ this.#provenanceStreamFlow.clear();
2663
2841
  for (const assembler of this.#mseAssemblers.values()) assembler.dispose();
2664
2842
  this.#mseAssemblers.clear();
2665
2843
  }
@@ -3013,7 +3191,6 @@ export class MmtpRouter {
3013
3191
  #processMMTP(
3014
3192
  packet: Uint8Array,
3015
3193
  source: "multicast" | "moq",
3016
- manifestIdentity?: ManifestObjectIdentity,
3017
3194
  manifestAdmitted = false,
3018
3195
  ingress?: MmtpIngressContext,
3019
3196
  overlapCopy = false,
@@ -3062,57 +3239,43 @@ export class MmtpRouter {
3062
3239
  this.#classifyPacketId(info.packetId) !== null &&
3063
3240
  this.#configuredBlockProfile(info.packetId) === undefined;
3064
3241
  if (fecUnreadable && !manifestAdmitted) this.#noteUnreadableFecPayloadId(info.packetId, "source");
3242
+ if (source === "multicast" && manifestAdmitted && !info.isRepair && ingress?.flow !== undefined) {
3243
+ let byName = this.#provenanceStreamFlow.get(info.packetId);
3244
+ if (!byName) this.#provenanceStreamFlow.set(info.packetId, (byName = new Map()));
3245
+ byName.set(catalogTrackNameForPacket(ingress, info.packetId), ingress.flow);
3246
+ }
3065
3247
 
3066
- if (!info.isRepair && this.#manifestVerifier && !manifestAdmitted) {
3067
- let identity = manifestIdentity;
3068
- if (!identity && info.fecPayloadId && !fecUnreadable && (this.#resolveManifestIdentityForRoute || this.#resolveManifestIdentity)) {
3069
- // A source resolves with its block coordinates (SS_Start, ESI), the
3070
- // pair a recovered symbol of the same block resolves with. SS_ID alone
3071
- // locates no block (C.5.3): until a repair states this source's block
3072
- // it has none, its identity stays absent, and the verifier's
3073
- // missing-identity policy below applies.
3074
- const catalogTrackName = catalogTrackNameForPacket(ingress, info.packetId);
3075
- const coordinates = this.#locateFecSource(info.packetId, catalogTrackName, info.fecPayloadId.ssId);
3076
- identity = coordinates === null ? undefined : (this.#resolveManifestIdentityForRoute
3077
- ? this.#resolveManifestIdentityForRoute({
3078
- packetId: info.packetId,
3079
- sbn: coordinates.sbn,
3080
- esi: coordinates.esi,
3081
- ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
3082
- recovered: false,
3083
- sourceSymbolFormat: "full-mmtp-packet",
3084
- })
3085
- : this.#resolveManifestIdentity!(
3086
- info.packetId,
3087
- coordinates.sbn,
3088
- coordinates.esi,
3089
- catalogTrackName,
3090
- )) ?? undefined;
3091
- }
3092
- if (identity) {
3093
- identity = {
3094
- ...identity,
3095
- packetId: info.packetId,
3096
- ...(info.fecPayloadId ? { ssId: info.fecPayloadId.ssId } : {}),
3097
- };
3098
- const authenticated = info.fecPayloadId ? packet.subarray(0, packet.length - 4) : packet;
3099
- // A `stall` admission quarantines the packet and releases it later,
3100
- // after the stream has moved on; a synchronous release happens inside
3101
- // admitObject, before this flag is set.
3102
- let quarantined = false;
3103
- const admission = this.#manifestVerifier.admitObject(identity, authenticated, () => {
3104
- if (!this.#disposed) {
3105
- this.#processMMTP(packet, source, identity, true, ingress, overlapCopy, quarantined);
3106
- }
3107
- });
3108
- quarantined = admission.action === "stall";
3248
+ // A protected flow admits every datagram, repair included, before it is
3249
+ // read (7.2.7): an unadmitted repair symbol never reaches the decoder.
3250
+ if (source === "multicast" && !manifestAdmitted && ingress?.flow !== undefined) {
3251
+ const protectedFlow = this.#provenance.get(ingress.flow);
3252
+ // Named but not registered (before registration, after release): the
3253
+ // flow is protected, so nothing admits this datagram. Fail closed.
3254
+ if (!protectedFlow) {
3255
+ this.#unregisteredFlowDrops++;
3256
+ debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
3109
3257
  return;
3110
3258
  }
3111
- if (
3112
- this.#manifestVerifier.config.mode === "enforce" &&
3113
- this.#manifestVerifier.config.onMissingManifest === "stall"
3114
- ) {
3115
- console.warn(`[MmtpRouter] manifest identity absent; stalling packetId=${info.packetId}`);
3259
+ // Held: released by a record (feedProvenanceRecord) or expired. Duplicate / overflow: dropped.
3260
+ const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
3261
+ if (verdict === "admitted") {
3262
+ this.#noteFlowAdmitted(ingress.flow);
3263
+ this.#processMMTP(packet, source, true, ingress, overlapCopy);
3264
+ }
3265
+ else if (verdict === "held") {
3266
+ this.#heldIngress.set(packet, ingress!);
3267
+ this.#heldAt.set(packet, performance.now());
3268
+ }
3269
+ return;
3270
+ }
3271
+
3272
+ // A MoQ packet of a track a protected flow lists is a datagram of that
3273
+ // flow on its unicast path (7.2.7): admitted before it is read, or held.
3274
+ if (source === "moq" && !manifestAdmitted) {
3275
+ const admit = this.#admitUnicast(info.packetId, packet, () =>
3276
+ this.#processMMTP(packet, source, true, ingress, overlapCopy, true), overlapCopy);
3277
+ if (admit !== "unlisted") {
3278
+ if (admit === "admitted") this.#processMMTP(packet, source, true, ingress, overlapCopy);
3116
3279
  return;
3117
3280
  }
3118
3281
  }
@@ -3127,6 +3290,8 @@ export class MmtpRouter {
3127
3290
  const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
3128
3291
  if (ownerPacketId != null && this.#fecManager) {
3129
3292
  if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
3293
+ // An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast).
3294
+ if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId)) return;
3130
3295
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
3131
3296
  const count = this.#repairPidCounts[info.packetId]!;
3132
3297
  if (count <= 2 || count % 500 === 0) {
@@ -3203,8 +3368,8 @@ export class MmtpRouter {
3203
3368
  // Start a fresh epoch for the stream before this packet is looked up.
3204
3369
  // The reset needs the catalog identity, so a stream without one is not
3205
3370
  // checked; an overlap copy is MoQ bytes and says nothing about the
3206
- // multicast publisher, and neither does a packet the manifest verifier
3207
- // quarantined: it predates the horizon because it waited, not because the
3371
+ // multicast publisher, and neither does a packet a provenance hold queue
3372
+ // released: it predates the horizon because it waited, not because the
3208
3373
  // publisher restarted.
3209
3374
  if (
3210
3375
  source === "multicast" &&
@@ -3433,37 +3598,24 @@ export class MmtpRouter {
3433
3598
  sourceFec.symbolData,
3434
3599
  catalogTrackName,
3435
3600
  );
3436
- if (recovered) {
3437
- if (!alreadyCompleted && this.#classifyPacketId(info.packetId) !== null) {
3601
+ // Only route recovered blocks that needed actual repair; complete
3602
+ // one only once admission has released its recovered packets.
3603
+ if (recovered && !alreadyCompleted) {
3604
+ const fec = this.#fecManager;
3605
+ const sbn = sourceFec.sbn;
3606
+ const packetId = info.packetId;
3607
+ const tracked = this.#classifyPacketId(packetId) !== null;
3608
+ this.#routeRecoveredBlock(sbn, recovered, packetId, catalogTrackName, () => {
3438
3609
  if (catalogTrackName === undefined) {
3439
- this.#fecManager.cancelBlockTimeout(sourceFec.sbn, info.packetId);
3610
+ if (tracked) fec.cancelBlockTimeout(sbn, packetId);
3611
+ fec.countRecovery(packetId, sbn, catalogTrackName);
3612
+ fec.markBlockCompleted(sbn, packetId);
3440
3613
  } else {
3441
- this.#fecManager.cancelBlockTimeout(
3442
- sourceFec.sbn,
3443
- info.packetId,
3444
- catalogTrackName,
3445
- );
3614
+ if (tracked) fec.cancelBlockTimeout(sbn, packetId, catalogTrackName);
3615
+ fec.countRecovery(packetId, sbn, catalogTrackName);
3616
+ fec.markBlockCompleted(sbn, packetId, catalogTrackName);
3446
3617
  }
3447
- }
3448
- // Only route recovered blocks that needed actual repair.
3449
- if (!alreadyCompleted) {
3450
- this.#fecManager.countRecovery(info.packetId, sourceFec.sbn, catalogTrackName);
3451
- if (catalogTrackName === undefined) {
3452
- this.#fecManager.markBlockCompleted(sourceFec.sbn, info.packetId);
3453
- } else {
3454
- this.#fecManager.markBlockCompleted(
3455
- sourceFec.sbn,
3456
- info.packetId,
3457
- catalogTrackName,
3458
- );
3459
- }
3460
- this.#routeRecoveredBlock(
3461
- sourceFec.sbn,
3462
- recovered,
3463
- info.packetId,
3464
- catalogTrackName,
3465
- );
3466
- }
3618
+ });
3467
3619
  }
3468
3620
  }
3469
3621
  }
@@ -4068,7 +4220,8 @@ export class MmtpRouter {
4068
4220
  recovered: Uint8Array,
4069
4221
  ownerPacketId?: number,
4070
4222
  catalogTrackName?: string,
4071
- ): void {
4223
+ complete?: () => void,
4224
+ ): boolean {
4072
4225
  // MMTP path: use processFecBlock for MMTP-wrapped recovery.
4073
4226
  // Recovered symbols are individual MMTP packets that may be fragments
4074
4227
  // (FI=1/2/3) of a larger MFU. Route through the reassembler so
@@ -4092,118 +4245,83 @@ export class MmtpRouter {
4092
4245
  ownerPacketId,
4093
4246
  blockProfile,
4094
4247
  });
4095
- this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
4248
+ return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
4096
4249
  }
4097
4250
 
4251
+ /**
4252
+ * A recovered source packet of a protected flow is admitted on its own
4253
+ * digest: its bytes with the SS_ID it was sent with (SS_Start + ESI)
4254
+ * appended are the datagram the record listed (7.2.7). One that no record
4255
+ * lists is dropped; holding it would let it out later as a multicast arrival.
4256
+ * Returns whether the decode recovered the block: every packet it yields
4257
+ * that this router has not parsed already is admitted. A block one of whose
4258
+ * recovered packets is refused is unrecovered (7.2.7): its caller leaves it
4259
+ * open, so it reaches its FEC deadline and fails by class. Always true
4260
+ * outside a protected flow.
4261
+ */
4098
4262
  #routeVerifiedFecSymbols(
4099
4263
  blockId: number,
4100
4264
  result: ProcessFecBlockResult,
4101
4265
  ownerPacketId?: number,
4102
4266
  catalogTrackName?: string,
4103
- ): void {
4104
- if (!this.#manifestVerifier || ownerPacketId == null) {
4105
- this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
4106
- return;
4107
- }
4108
- if (!this.#resolveManifestIdentity && !this.#resolveManifestIdentityForRoute) {
4109
- console.warn(`[MmtpRouter] manifest identity resolver unavailable; recovered media is fail-open for packetId=${ownerPacketId} block=${blockId}`);
4110
- this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
4111
- return;
4267
+ complete?: () => void,
4268
+ ): boolean {
4269
+ const receiver = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
4270
+ let recovered = true;
4271
+ if (receiver) {
4272
+ const parsed = this.#stream(ownerPacketId!, catalogTrackName).duplicates.parsedSymbols;
4273
+ result = {
4274
+ ...result,
4275
+ symbols: result.symbols.filter((sym) => {
4276
+ const ssId = this.#fecAnchorForRecovered(blockId, sym.index);
4277
+ // A received copy was admitted on arrival; its recovered twin is a duplicate.
4278
+ if (parsed.has(ssId)) return false;
4279
+ let admitted = false;
4280
+ if (receiver === "ambiguous") {
4281
+ this.#ambiguousRecoveredDrops++;
4282
+ } else {
4283
+ const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
4284
+ wire.set(sym.rawSymbol);
4285
+ new DataView(wire.buffer).setUint32(sym.rawSymbol.byteLength, ssId);
4286
+ admitted = receiver.admitRecovered(wire);
4287
+ }
4288
+ if (!admitted) recovered = false;
4289
+ return admitted;
4290
+ }),
4291
+ };
4112
4292
  }
4113
- const queueKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
4114
- const queue = this.#manifestRecoveryQueues.get(queueKey) ?? [];
4115
- queue.push({ blockId, result, ownerPacketId, catalogTrackName });
4116
- this.#manifestRecoveryQueues.set(queueKey, queue);
4117
- if (queue.length > 1) return;
4118
- const drain = (): void => {
4119
- const next = queue[0];
4120
- if (!next) {
4121
- this.#manifestRecoveryQueues.delete(queueKey);
4122
- return;
4123
- }
4124
- this.#admitRecoveredResult(
4125
- next.blockId,
4126
- next.result,
4127
- next.ownerPacketId,
4128
- next.catalogTrackName,
4129
- () => {
4130
- queue.shift();
4131
- drain();
4132
- },
4133
- );
4134
- };
4135
- drain();
4293
+ if (recovered) complete?.();
4294
+ this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
4295
+ return recovered;
4136
4296
  }
4137
4297
 
4138
- #admitRecoveredResult(
4139
- blockId: number,
4140
- result: ProcessFecBlockResult,
4141
- ownerPacketId: number,
4298
+ /**
4299
+ * The receiver a recovered packet of `packetId` answers to: the flow that
4300
+ * admitted the stream's source datagrams. Before any was admitted, the one
4301
+ * flow whose registration lists the stream; more than one (a repair flow
4302
+ * also lists its owners' source tracks) cannot be told apart, so none.
4303
+ */
4304
+ #provenanceFor(
4305
+ packetId: number,
4142
4306
  catalogTrackName: string | undefined,
4143
- done: () => void,
4144
- ): void {
4145
- const verifier = this.#manifestVerifier;
4146
- const resolveIdentity = this.#resolveManifestIdentity;
4147
- const resolveIdentityForRoute = this.#resolveManifestIdentityForRoute;
4148
- if (!verifier) {
4149
- this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
4150
- done();
4151
- return;
4152
- }
4153
- if (!resolveIdentity && !resolveIdentityForRoute) throw new Error("manifest identity resolver disappeared during admission");
4154
- const symbols = result.symbols;
4155
- const admitNext = (index: number): void => {
4156
- const symbol = symbols[index];
4157
- if (!symbol) {
4158
- done();
4159
- return;
4160
- }
4161
- const identity = resolveIdentityForRoute
4162
- ? resolveIdentityForRoute({
4163
- packetId: ownerPacketId,
4164
- sbn: blockId,
4165
- esi: symbol.index,
4166
- ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
4167
- recovered: true,
4168
- sourceSymbolFormat: "fec-padded-recovery",
4169
- })
4170
- : resolveIdentity!(ownerPacketId, blockId, symbol.index, catalogTrackName);
4171
- if (!identity) {
4172
- console.warn(`[MmtpRouter] manifest identity unresolved; recovered symbol is fail-open for packetId=${ownerPacketId} block=${blockId} esi=${symbol.index}`);
4173
- this.#routeProcessedFecSymbols(
4174
- blockId,
4175
- { ...result, symbols: [symbol] },
4176
- ownerPacketId,
4177
- catalogTrackName,
4178
- );
4179
- admitNext(index + 1);
4180
- return;
4307
+ ): BcProvenanceV2Receiver | "ambiguous" | undefined {
4308
+ // A caller that names no track matches every name the packet id was admitted under.
4309
+ const byName = this.#provenanceStreamFlow.get(packetId);
4310
+ const flows = new Set(
4311
+ catalogTrackName === undefined || !byName
4312
+ ? byName?.values()
4313
+ : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)!] : [],
4314
+ );
4315
+ if (flows.size > 1) return "ambiguous";
4316
+ for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
4317
+ let match: BcProvenanceV2Receiver | undefined;
4318
+ for (const { receiver, ingress } of this.#provenance.values()) {
4319
+ if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
4320
+ if (match) return "ambiguous";
4321
+ match = receiver;
4181
4322
  }
4182
- const admission = verifier.admitObject(
4183
- {
4184
- ...identity,
4185
- packetId: ownerPacketId,
4186
- },
4187
- symbol.rawSymbol,
4188
- () => {
4189
- if (!this.#disposed) {
4190
- this.#routeProcessedFecSymbols(
4191
- blockId,
4192
- { ...result, symbols: [symbol] },
4193
- ownerPacketId,
4194
- catalogTrackName,
4195
- );
4196
- }
4197
- admitNext(index + 1);
4198
- },
4199
- // A stalled symbol that is later discarded instead of played must still
4200
- // advance this loop; enforce mode never calls the release callback for
4201
- // it (BLO-30095).
4202
- () => admitNext(index + 1),
4203
- );
4204
- if (admission.action === "drop") admitNext(index + 1);
4205
- };
4206
- admitNext(0);
4323
+ }
4324
+ return match;
4207
4325
  }
4208
4326
 
4209
4327
  #routeProcessedFecSymbols(