@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.
- package/README.md +2 -2
- package/dist/browser-runtime.js +3458 -356
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -1
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +18 -2
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +21 -16
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +177 -80
- package/dist/fec-manager.js.map +3 -3
- package/dist/held-layer-admission.d.ts +58 -0
- package/dist/held-layer-admission.d.ts.map +1 -0
- package/dist/held-layer-admission.js +105 -0
- package/dist/held-layer-admission.js.map +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2682 -1023
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-fec-session.d.ts +36 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +114 -5
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +72 -28
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +321 -203
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.d.ts +10 -2
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +422 -22
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-manager.d.ts +48 -9
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3406 -340
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +4 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +20 -3
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.d.ts +1 -2
- package/dist/no-modules.d.ts.map +1 -1
- package/dist/no-modules.js +2668 -1007
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +10 -1
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +5 -2
- package/dist/repair-fetch.js.map +2 -2
- package/package.json +5 -14
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +121 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +5 -9
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +390 -272
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +189 -48
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +24 -4
- package/dist/manifest-auth-plan.d.ts +0 -31
- package/dist/manifest-auth-plan.d.ts.map +0 -1
- package/dist/manifest-auth-plan.js +0 -426
- package/dist/manifest-auth-plan.js.map +0 -7
- package/dist/source-auth.d.ts +0 -2
- package/dist/source-auth.d.ts.map +0 -1
- package/dist/source-auth.js +0 -3912
- package/dist/source-auth.js.map +0 -7
- package/src/manifest-auth-plan.ts +0 -55
- package/src/source-auth.ts +0 -1
- package/test-vectors/generate-source-auth-fixture.mjs +0 -114
- package/test-vectors/source-auth-v1.json +0 -61
package/src/mmtp-router.ts
CHANGED
|
@@ -67,10 +67,8 @@ import {
|
|
|
67
67
|
type DuplicateHorizon,
|
|
68
68
|
} from "./duplicate-ledger.js";
|
|
69
69
|
import type {
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
864
|
-
readonly #
|
|
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
|
-
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
1835
|
-
this.#
|
|
1836
|
-
if (
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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,
|
|
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",
|
|
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
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
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
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
2662
|
-
this.#
|
|
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
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
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
|
|
3207
|
-
//
|
|
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
|
-
|
|
3437
|
-
|
|
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
|
-
|
|
3610
|
+
if (tracked) fec.cancelBlockTimeout(sbn, packetId);
|
|
3611
|
+
fec.countRecovery(packetId, sbn, catalogTrackName);
|
|
3612
|
+
fec.markBlockCompleted(sbn, packetId);
|
|
3440
3613
|
} else {
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
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
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
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
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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
|
-
|
|
4144
|
-
|
|
4145
|
-
const
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
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
|
-
|
|
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(
|