@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0
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 +3769 -364
- 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 +197 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +10 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +31 -1
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2713 -1024
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +31 -1
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- 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 +96 -29
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +522 -205
- 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 +78 -11
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3663 -333
- 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 +2699 -1008
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +50 -10
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +62 -20
- package/dist/repair-fetch.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +31 -1
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +7 -15
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +147 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +6 -9
- package/src/mmtp-container-parser.ts +48 -0
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +637 -274
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +274 -49
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +154 -34
- package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
- package/test-vectors/repair-policy-v1.json +113 -0
- 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
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type FecBlockProfile,
|
|
23
23
|
type MMTPHeader,
|
|
24
24
|
} from "@blockcast/mmt-base";
|
|
25
|
-
import type { IsoHeldSource } from "./fec-manager.js";
|
|
25
|
+
import type { IsoHeldSource, WasmFecDecoder } from "./fec-manager.js";
|
|
26
26
|
import type { IsoPlacedSource, IsoRepairPlacement } from "./iso-block-locator.js";
|
|
27
27
|
import { isMfuReassemblyError } from "./mfu-reassembler.js";
|
|
28
28
|
import type {
|
|
@@ -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. */
|
|
@@ -513,6 +517,32 @@ const NO_BLOCK_SPAN_HORIZON: DuplicateHorizon = Object.freeze({
|
|
|
513
517
|
});
|
|
514
518
|
|
|
515
519
|
const DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT = 512;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A governed block's admitted symbols, as fed to its live decoder, and the
|
|
523
|
+
* unlisted repair pooled for it: what an unlisted-assisted decode runs on
|
|
524
|
+
* (7.2.7 step 2). Keyed by ESI / RS_ID.
|
|
525
|
+
*/
|
|
526
|
+
interface AssistBlock {
|
|
527
|
+
readonly ssbLength: number;
|
|
528
|
+
readonly sources: Map<number, Uint8Array>;
|
|
529
|
+
readonly repairs: Map<number, Uint8Array>;
|
|
530
|
+
readonly pooled: Map<number, Uint8Array>;
|
|
531
|
+
}
|
|
532
|
+
// ponytail: FIFO over blocks (they arrive in order); an LRU if blocks interleave.
|
|
533
|
+
const ASSIST_BLOCK_LIMIT = 64;
|
|
534
|
+
/**
|
|
535
|
+
* Failed unlisted-assisted decodes per block, kept apart from the block record
|
|
536
|
+
* so evicting and reopening a record never resets the cap.
|
|
537
|
+
* ponytail: one LRU shared by every governed owner/track, refreshed only on a
|
|
538
|
+
* failure: a block may fail twice more once 256 other failing blocks (any
|
|
539
|
+
* track) have aged it out. Per-track ledgers if a multi-track flood matters.
|
|
540
|
+
*/
|
|
541
|
+
const ASSIST_FAILURE_LEDGER_LIMIT = 4 * ASSIST_BLOCK_LIMIT;
|
|
542
|
+
/** 7.2.7 step 2: at most two unlisted-assisted decodes may fail per block. */
|
|
543
|
+
const ASSIST_FAILED_DECODES_MAX = 2;
|
|
544
|
+
const assistKey = (ownerPacketId: number, catalogTrackName: string | undefined, blockStart: number) =>
|
|
545
|
+
`${ownerPacketId}/${catalogTrackName ?? ""}/${blockStart}`;
|
|
516
546
|
const DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT = 512;
|
|
517
547
|
|
|
518
548
|
type ParserDiagnostics = {
|
|
@@ -558,6 +588,9 @@ export interface FECManagerLike {
|
|
|
558
588
|
catalogTrackName?: string,
|
|
559
589
|
): Uint8Array | null;
|
|
560
590
|
|
|
591
|
+
/** How long ago the stream's oldest open block with an FEC deadline took its first symbol (optional). */
|
|
592
|
+
oldestOpenBlockAgeMs?(packetId: number, catalogTrackName: string): number | undefined;
|
|
593
|
+
|
|
561
594
|
/** A block's RSB_length, from one of its repair symbols (optional). */
|
|
562
595
|
noteRepairGroupLength?(sbn: number, packetId: number, rsbLength: number, catalogTrackName?: string): void;
|
|
563
596
|
|
|
@@ -703,6 +736,14 @@ export interface FECManagerLike {
|
|
|
703
736
|
catalogTrackName: string | undefined,
|
|
704
737
|
ssId: number,
|
|
705
738
|
): { sbn: number; esi: number } | null;
|
|
739
|
+
/** Begin a fresh decode epoch for one stream, dropping what it holds (FECManager.resetTrack). */
|
|
740
|
+
resetTrack?(packetId: number, catalogTrackName: string): void;
|
|
741
|
+
/**
|
|
742
|
+
* Run `fn` with `receivedAtMs` (performance.now() clock) as the receipt
|
|
743
|
+
* time of the packet it feeds: a block it opens takes that ts, and its
|
|
744
|
+
* deadlines run from it (fec-01 9, a datagram held and admitted later).
|
|
745
|
+
*/
|
|
746
|
+
withReceiptTime?(receivedAtMs: number, fn: () => void): void;
|
|
706
747
|
/** iso-ssbg1-v1: hold a first-arrival source until a repair states its block. */
|
|
707
748
|
holdIsoSource?(
|
|
708
749
|
packetId: number,
|
|
@@ -804,26 +845,6 @@ export interface FECManagerLike {
|
|
|
804
845
|
|
|
805
846
|
// ─── MmtpRouter ─────────────────────────────────────────────────────
|
|
806
847
|
|
|
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
848
|
/**
|
|
828
849
|
* Classification evidence carried by one first-arrival network source
|
|
829
850
|
* packet: the MMTP header R bit and the MPU / data-unit identity it states
|
|
@@ -860,26 +881,30 @@ export class MmtpRouter {
|
|
|
860
881
|
readonly #fecManager: FECManagerLike | null;
|
|
861
882
|
#defaultFecMapper: FecBlockMapper | null;
|
|
862
883
|
#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<
|
|
884
|
+
/** Protected flows: one 7.2.7 receiver each, and the ingress its datagrams arrive with. */
|
|
885
|
+
readonly #provenance = new Map<
|
|
875
886
|
string,
|
|
876
|
-
|
|
877
|
-
blockId: number;
|
|
878
|
-
result: ProcessFecBlockResult;
|
|
879
|
-
ownerPacketId: number;
|
|
880
|
-
catalogTrackName?: string;
|
|
881
|
-
}>
|
|
887
|
+
{ receiver: BcProvenanceV2Receiver; ingress: MmtpIngressContext; protected?: boolean }
|
|
882
888
|
>();
|
|
889
|
+
#onFlowProtected: ((flow: string) => void) | null = null;
|
|
890
|
+
#unadmittedListedDrops = 0;
|
|
891
|
+
#unlistedRepairSetAside = 0;
|
|
892
|
+
readonly #assistBlocks = new Map<string, AssistBlock>();
|
|
893
|
+
readonly #assistFailures = new Map<string, number>();
|
|
894
|
+
#scratchDecoderFactory: ((ownerPacketId: number, catalogTrackName: string | undefined) => WasmFecDecoder | null) | null = null;
|
|
895
|
+
#unlistedAssistedRecoveries = 0;
|
|
896
|
+
#unlistedAssistedRefusals = 0;
|
|
897
|
+
/** The ingress a held datagram arrived with; the receiver returns the same bytes on release. */
|
|
898
|
+
readonly #heldIngress = new WeakMap<Uint8Array, MmtpIngressContext>();
|
|
899
|
+
#unicastRepair = false;
|
|
900
|
+
/** How to route a held unicast (MoQ) datagram once a record admits it. */
|
|
901
|
+
readonly #heldReplay = new WeakMap<Uint8Array, () => void>();
|
|
902
|
+
/** When a held datagram was received: admitted later, it keeps that receipt time (fec-01 9). */
|
|
903
|
+
readonly #heldAt = new WeakMap<Uint8Array, number>();
|
|
904
|
+
/** The flow whose receiver admitted a stream's source datagrams, by packetId then catalog track name: recovered packets answer to it. */
|
|
905
|
+
readonly #provenanceStreamFlow = new Map<number, Map<string | undefined, string>>();
|
|
906
|
+
#unregisteredFlowDrops = 0;
|
|
907
|
+
#ambiguousRecoveredDrops = 0;
|
|
883
908
|
|
|
884
909
|
// Temp state set by #processMMTP before calling parser.feed(),
|
|
885
910
|
// read by the parser's onFrame callback for CMAF assembly + dedup.
|
|
@@ -986,6 +1011,7 @@ export class MmtpRouter {
|
|
|
986
1011
|
| null = null;
|
|
987
1012
|
/** Typed observer of every gap decision the release gate makes. */
|
|
988
1013
|
readonly #onFrameGap: ((decision: FrameGapDecision) => void) | null;
|
|
1014
|
+
readonly #fecRapObservers = new Set<(packetId: number, catalogTrackName: string | undefined, ssId: number) => void>();
|
|
989
1015
|
// Pokes requested while a packet is being processed are deferred to its
|
|
990
1016
|
// end: a synchronous recovery completes its block BEFORE routing the
|
|
991
1017
|
// recovered frames, and releasing a held gap in between would strand them.
|
|
@@ -1056,16 +1082,6 @@ export class MmtpRouter {
|
|
|
1056
1082
|
* Pass a reader when its inputs are measured later; see
|
|
1057
1083
|
* setKeyframeLostTimeoutMs. */
|
|
1058
1084
|
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
1085
|
/** Internal terminal-failure sink owned by the composing transport. */
|
|
1070
1086
|
onBlockFailed?: (
|
|
1071
1087
|
sbn: number,
|
|
@@ -1088,10 +1104,6 @@ export class MmtpRouter {
|
|
|
1088
1104
|
this.#fecManager = config?.fecManager ?? null;
|
|
1089
1105
|
this.#onFrameGap = config?.onFrameGap ?? null;
|
|
1090
1106
|
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
1107
|
|
|
1096
1108
|
// Create MMTP parser — handles MFU extraction, reassembly, keyframe detection.
|
|
1097
1109
|
// Its MFUReassembler is the single reassembler for BOTH the source path
|
|
@@ -1822,32 +1834,385 @@ export class MmtpRouter {
|
|
|
1822
1834
|
});
|
|
1823
1835
|
}
|
|
1824
1836
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1837
|
+
/**
|
|
1838
|
+
* Protect a flow (draft-ramadan-moq-multicast-01 7.2.7): every multicast
|
|
1839
|
+
* datagram whose ingress names `ingress.flow`, source and repair alike, is
|
|
1840
|
+
* admitted by `receiver` before anything reads it, and an unadmitted one is
|
|
1841
|
+
* held or dropped. The caller owns the receiver's configuration; the router
|
|
1842
|
+
* releases it on `releaseProvenanceFlow` and on dispose.
|
|
1843
|
+
*/
|
|
1844
|
+
registerProvenanceFlow(
|
|
1845
|
+
ingress: MmtpIngressContext & { flow: string },
|
|
1846
|
+
receiver: BcProvenanceV2Receiver,
|
|
1847
|
+
): void {
|
|
1848
|
+
this.#provenance.get(ingress.flow)?.receiver.release();
|
|
1849
|
+
this.#provenance.set(ingress.flow, { receiver, ingress });
|
|
1850
|
+
receiver.keepPendingAgeMs = () => this.#oldestOpenBlockAgeMs(ingress.flow);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Declare that this receiver can request unicast repair (a repair FETCH
|
|
1855
|
+
* scheduler is wired): a protected flow then keeps the pending entries of
|
|
1856
|
+
* its open blocks for the repair to consume (7.2.7 step 2).
|
|
1857
|
+
*/
|
|
1858
|
+
setUnicastRepair(enabled: boolean): void {
|
|
1859
|
+
this.#unicastRepair = enabled;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
#oldestOpenBlockAgeMs(flow: string): number | undefined {
|
|
1863
|
+
const entry = this.#provenance.get(flow);
|
|
1864
|
+
if (!this.#unicastRepair || !entry) return undefined;
|
|
1865
|
+
let age: number | undefined;
|
|
1866
|
+
for (const { name, packetId } of entry.ingress.tracks) {
|
|
1867
|
+
const a = name ? this.#fecManager?.oldestOpenBlockAgeMs?.(packetId, name) : undefined;
|
|
1868
|
+
if (a !== undefined && (age === undefined || a > age)) age = a;
|
|
1869
|
+
}
|
|
1870
|
+
return age;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
* Replace a registered flow's ingress (a rendition switch staged, committed
|
|
1875
|
+
* or cancelled) while keeping its receiver; a no-op for an unregistered flow.
|
|
1876
|
+
*/
|
|
1877
|
+
updateProvenanceFlowIngress(ingress: MmtpIngressContext & { flow: string }): void {
|
|
1878
|
+
const entry = this.#provenance.get(ingress.flow);
|
|
1879
|
+
if (!entry) return;
|
|
1880
|
+
// A protected flow newly listing a track drops its MoQ copies from now
|
|
1881
|
+
// on (7.2.7). An instance already governed holds only admitted symbols,
|
|
1882
|
+
// which stay admitted (multicast 7.2.7, 1720-1729): only its pooled
|
|
1883
|
+
// unlisted repair is discarded. An instance governed from now on holds
|
|
1884
|
+
// unadmitted symbols, so it is discarded whole.
|
|
1885
|
+
const added = entry.protected
|
|
1886
|
+
? ingress.tracks.filter(({ name, packetId }) =>
|
|
1887
|
+
name && !entry.ingress.tracks.some((t) => t.name === name && t.packetId === packetId))
|
|
1888
|
+
: [];
|
|
1889
|
+
const governed = added.map(({ packetId }) =>
|
|
1890
|
+
[...(this.#repairOwnersByPacketId.get(packetId) ?? [packetId])].some((owner) => this.#governs(owner)));
|
|
1891
|
+
this.#provenance.set(ingress.flow, { ...entry, ingress });
|
|
1892
|
+
added.forEach(({ name, packetId }, i) => {
|
|
1893
|
+
// ponytail: discards every block's pool, not only the newly listed track's symbols.
|
|
1894
|
+
if (governed[i]) for (const block of this.#assistBlocks.values()) block.pooled.clear();
|
|
1895
|
+
else this.#fecManager?.resetTrack?.(packetId, name!);
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* Called once per protection of a flow: at the admission that makes this
|
|
1901
|
+
* router's consumers protected under it (7.2.7), after their unadmitted
|
|
1902
|
+
* holdings of its listed tracks are discarded. Again after an unprotect.
|
|
1903
|
+
*/
|
|
1904
|
+
onProvenanceFlowProtected(cb: ((flow: string) => void) | null): void {
|
|
1905
|
+
this.#onFlowProtected = cb;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* The receiver ceased to hold the flow's endpoint on failover (7.2.7): drop
|
|
1910
|
+
* its state and hold queues (no expiry counted) and un-protect its tracks,
|
|
1911
|
+
* which then take MoQ again. The registration stays, so multicast resuming
|
|
1912
|
+
* protects the flow anew at its next admission.
|
|
1913
|
+
*/
|
|
1914
|
+
unprotectProvenanceFlow(flow: string): void {
|
|
1915
|
+
const entry = this.#provenance.get(flow);
|
|
1916
|
+
if (!entry) return;
|
|
1917
|
+
entry.receiver.release();
|
|
1918
|
+
entry.protected = false;
|
|
1919
|
+
// ponytail: drops every governed block's record, not only this flow's; per-flow if joins multiply.
|
|
1920
|
+
this.#assistBlocks.clear();
|
|
1921
|
+
this.#assistFailures.clear();
|
|
1828
1922
|
}
|
|
1829
1923
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1924
|
+
/** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
|
|
1925
|
+
get unadmittedListedDropCount(): number {
|
|
1926
|
+
return this.#unadmittedListedDrops;
|
|
1832
1927
|
}
|
|
1833
1928
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1929
|
+
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
1930
|
+
get unlistedRepairSetAsideCount(): number {
|
|
1931
|
+
return this.#unlistedRepairSetAside;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/** Governed blocks recovered with the help of unlisted repair (7.2.7 step 2). */
|
|
1935
|
+
get unlistedAssistedRecoveryCount(): number {
|
|
1936
|
+
return this.#unlistedAssistedRecoveries;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/** Unlisted-assisted decodes refused because two already failed for the block (7.2.7 step 2). */
|
|
1940
|
+
get unlistedAssistedRefusalCount(): number {
|
|
1941
|
+
return this.#unlistedAssistedRefusals;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/** Unlisted repair symbols pooled now, across every governed block (7.2.7 step 2). */
|
|
1945
|
+
get unlistedPoolSymbolCount(): number {
|
|
1946
|
+
let n = 0;
|
|
1947
|
+
for (const block of this.#assistBlocks.values()) n += block.pooled.size;
|
|
1948
|
+
return n;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* Wire the decoder an unlisted-assisted decode runs in: a fresh, configured
|
|
1953
|
+
* decoder of the owner's track, used once and discarded, never the live one.
|
|
1954
|
+
* Without it unlisted repair on a governed instance is set aside and dropped.
|
|
1955
|
+
*/
|
|
1956
|
+
setScratchFecDecoderFactory(
|
|
1957
|
+
factory: ((ownerPacketId: number, catalogTrackName: string | undefined) => WasmFecDecoder | null) | null,
|
|
1958
|
+
): void {
|
|
1959
|
+
this.#scratchDecoderFactory = factory;
|
|
1960
|
+
if (!factory) {
|
|
1961
|
+
this.#assistBlocks.clear();
|
|
1962
|
+
this.#assistFailures.clear();
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/** Whether a protected flow lists `ownerPacketId`: its FEC instance is governed (7.2.7). */
|
|
1967
|
+
#governs(ownerPacketId: number): boolean {
|
|
1968
|
+
for (const entry of this.#provenance.values()) {
|
|
1969
|
+
if (entry.protected && ingressHasPacketId(entry.ingress, ownerPacketId)) return true;
|
|
1970
|
+
}
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
1976
|
+
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
1977
|
+
* symbols, so the symbol is set aside: pooled for an unlisted-assisted
|
|
1978
|
+
* decode of a block admitted repair already delimits, else dropped. It
|
|
1979
|
+
* never states a block, so the live locator and decoder are unchanged.
|
|
1980
|
+
* Un-listing needs no hook: an unprotected or released flow lists nothing.
|
|
1981
|
+
*/
|
|
1982
|
+
#setAsideUnlistedRepair(
|
|
1983
|
+
ownerPacketId: number,
|
|
1984
|
+
catalogTrackName: string | undefined,
|
|
1985
|
+
repair: { ssStart: number; ssbLength: number; rsbLength: number; rsId: number; symbolData: Uint8Array },
|
|
1986
|
+
): boolean {
|
|
1987
|
+
if (!this.#governs(ownerPacketId)) return false;
|
|
1988
|
+
this.#unlistedRepairSetAside++;
|
|
1989
|
+
const key = assistKey(ownerPacketId, catalogTrackName, repair.ssStart);
|
|
1990
|
+
const block = this.#assistBlocks.get(key);
|
|
1991
|
+
// Pooled only up to the block's length: more never closes a shortfall.
|
|
1992
|
+
if (
|
|
1993
|
+
block !== undefined &&
|
|
1994
|
+
block.ssbLength === repair.ssbLength &&
|
|
1995
|
+
!block.repairs.has(repair.rsId) &&
|
|
1996
|
+
block.pooled.size < block.ssbLength &&
|
|
1997
|
+
repair.symbolData.byteLength === this.#symbolSizeForPacket(ownerPacketId) &&
|
|
1998
|
+
this.#fecManager?.admitRepairBlockLength?.(ownerPacketId, repair.ssbLength, repair.rsbLength, repair.rsId) !== false
|
|
1999
|
+
) {
|
|
2000
|
+
block.pooled.set(repair.rsId, repair.symbolData);
|
|
2001
|
+
this.#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, repair.ssStart);
|
|
2002
|
+
}
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* An admitted repair placed a governed block (its SSB_length delimits it):
|
|
2008
|
+
* open the block's record, which the symbols its live decoder takes join.
|
|
2009
|
+
*/
|
|
2010
|
+
#openAssistBlock(ownerPacketId: number, catalogTrackName: string | undefined, blockStart: number, ssbLength: number): void {
|
|
2011
|
+
const key = assistKey(ownerPacketId, catalogTrackName, blockStart);
|
|
2012
|
+
if (!this.#scratchDecoderFactory || this.#assistBlocks.has(key) || !this.#governs(ownerPacketId)) return;
|
|
2013
|
+
this.#assistBlocks.set(key, { ssbLength, sources: new Map(), repairs: new Map(), pooled: new Map() });
|
|
2014
|
+
for (const oldest of this.#assistBlocks.keys()) {
|
|
2015
|
+
if (this.#assistBlocks.size <= ASSIST_BLOCK_LIMIT) break;
|
|
2016
|
+
this.#assistBlocks.delete(oldest);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/** A symbol fed to an open governed block's live decoder that did not recover it. */
|
|
2021
|
+
#noteAssistAdmitted(
|
|
2022
|
+
ownerPacketId: number,
|
|
2023
|
+
catalogTrackName: string | undefined,
|
|
2024
|
+
blockStart: number,
|
|
2025
|
+
symbol: { esi: number } | { rsId: number },
|
|
2026
|
+
data: Uint8Array,
|
|
2027
|
+
): void {
|
|
2028
|
+
const key = assistKey(ownerPacketId, catalogTrackName, blockStart);
|
|
2029
|
+
const block = this.#assistBlocks.get(key);
|
|
2030
|
+
if (block === undefined) return;
|
|
2031
|
+
if ("rsId" in symbol) {
|
|
2032
|
+
block.repairs.set(symbol.rsId, data);
|
|
2033
|
+
block.pooled.delete(symbol.rsId); // an admitted copy wins over an unlisted one
|
|
2034
|
+
} else {
|
|
2035
|
+
block.sources.set(symbol.esi, data);
|
|
2036
|
+
}
|
|
2037
|
+
this.#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, blockStart);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* 7.2.7 step 2, phase two: a governed block its admitted symbols cannot
|
|
2042
|
+
* decode (the live decoder already had them all) is decoded in a scratch
|
|
2043
|
+
* decoder from its admitted symbols plus pooled unlisted repair, only as
|
|
2044
|
+
* many as close the shortfall. What it recovers is released only through
|
|
2045
|
+
* admission (#routeRecoveredBlock), so the live decoder is never touched and
|
|
2046
|
+
* a refused recovery leaves the block to fail at its FEC deadline. At most
|
|
2047
|
+
* two such decodes may fail per block; a third is refused.
|
|
2048
|
+
*/
|
|
2049
|
+
#tryAssistedDecode(
|
|
2050
|
+
key: string,
|
|
2051
|
+
block: AssistBlock,
|
|
2052
|
+
ownerPacketId: number,
|
|
2053
|
+
catalogTrackName: string | undefined,
|
|
2054
|
+
blockStart: number,
|
|
2055
|
+
): void {
|
|
2056
|
+
const fec = this.#fecManager;
|
|
2057
|
+
const admitted = block.sources.size + block.repairs.size;
|
|
2058
|
+
if (!fec || block.pooled.size === 0) return;
|
|
2059
|
+
if (admitted >= block.ssbLength || admitted + block.pooled.size < block.ssbLength) return;
|
|
2060
|
+
if (fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName)) {
|
|
2061
|
+
this.#assistBlocks.delete(key);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
if ((this.#assistFailures.get(key) ?? 0) >= ASSIST_FAILED_DECODES_MAX) {
|
|
2065
|
+
this.#unlistedAssistedRefusals++;
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const decoder = this.#scratchDecoderFactory?.(ownerPacketId, catalogTrackName);
|
|
2069
|
+
if (!decoder) return;
|
|
2070
|
+
let recovered: Uint8Array | null = null;
|
|
2071
|
+
const fed: number[] = [];
|
|
2072
|
+
try {
|
|
2073
|
+
for (const [esi, data] of block.sources) decoder.add_source(serial32Add(blockStart, esi), data, 0);
|
|
2074
|
+
for (const [rsId, data] of block.repairs) {
|
|
2075
|
+
recovered ??= decoder.add_repair(blockStart, block.ssbLength, rsId, data, 0);
|
|
2076
|
+
}
|
|
2077
|
+
// ponytail: exactly the shortfall; a RaptorQ decoder short of K+overhead
|
|
2078
|
+
// fails here, burning one of the two decodes on fresh pooled symbols.
|
|
2079
|
+
for (const [rsId, data] of block.pooled) {
|
|
2080
|
+
if (recovered || fed.length >= block.ssbLength - admitted) break;
|
|
2081
|
+
fed.push(rsId);
|
|
2082
|
+
recovered = decoder.add_repair(blockStart, block.ssbLength, rsId, data, 0);
|
|
2083
|
+
}
|
|
2084
|
+
} catch {
|
|
2085
|
+
recovered = null;
|
|
2086
|
+
} finally {
|
|
2087
|
+
(decoder as { free?(): void }).free?.();
|
|
2088
|
+
}
|
|
2089
|
+
if (recovered && this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
|
|
2090
|
+
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2091
|
+
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2092
|
+
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2093
|
+
})) {
|
|
2094
|
+
this.#unlistedAssistedRecoveries++;
|
|
2095
|
+
this.#assistBlocks.delete(key);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
// Any failed decode (null, throw, or refused recovery) drops the pooled
|
|
2099
|
+
// symbols it fed: the shortfall cap would otherwise re-feed the same
|
|
2100
|
+
// prefix and the retry fails the same way.
|
|
2101
|
+
for (const rsId of fed) block.pooled.delete(rsId);
|
|
2102
|
+
const failed = (this.#assistFailures.get(key) ?? 0) + 1;
|
|
2103
|
+
this.#assistFailures.delete(key);
|
|
2104
|
+
this.#assistFailures.set(key, failed);
|
|
2105
|
+
for (const oldest of this.#assistFailures.keys()) {
|
|
2106
|
+
if (this.#assistFailures.size <= ASSIST_FAILURE_LEDGER_LIMIT) break;
|
|
2107
|
+
this.#assistFailures.delete(oldest);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* The admission that makes this router protected under `flow` (7.2.7):
|
|
2113
|
+
* its FEC instances discard what they hold of the flow's listed tracks so
|
|
2114
|
+
* no unadmitted symbol stands for an admitted one, then notify. Before it
|
|
2115
|
+
* nothing of the flow was admitted, so every such symbol is unadmitted.
|
|
2116
|
+
* The playback parse ledger is kept: it records MoQ copies already played,
|
|
2117
|
+
* not holdings, and clearing it re-emits that media (audio twice).
|
|
2118
|
+
* The FEC reset is the whole 7.2.7 discard here: no keyframe-overlay
|
|
2119
|
+
* instance exists to hold anything (repair-layers.ts is the catalog
|
|
2120
|
+
* contract only, no decoder), and packet_id 0 is never a listed track
|
|
2121
|
+
* (`ingressHasPacketId`), so nothing of it is held for the flow.
|
|
2122
|
+
*/
|
|
2123
|
+
#noteFlowAdmitted(flow: string): void {
|
|
2124
|
+
const entry = this.#provenance.get(flow);
|
|
2125
|
+
if (!entry || entry.protected) return;
|
|
2126
|
+
entry.protected = true;
|
|
2127
|
+
for (const { name, packetId } of entry.ingress.tracks) {
|
|
2128
|
+
if (name) this.#fecManager?.resetTrack?.(packetId, name);
|
|
2129
|
+
}
|
|
2130
|
+
this.#onFlowProtected?.(flow);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Admit a MoQ datagram of `packetId` on the unicast path of the protected
|
|
2135
|
+
* flow that lists it (7.2.7). `unlisted`: no protected flow lists it, so it
|
|
2136
|
+
* is read as before. A held datagram is routed by `replay` when a record
|
|
2137
|
+
* admits it; one more than one protected flow lists is dropped. So is an
|
|
2138
|
+
* overlap copy: it never enters FEC accounting, so admitting it would leave
|
|
2139
|
+
* its multicast twin a duplicate and the block one symbol short.
|
|
2140
|
+
*/
|
|
2141
|
+
#admitUnicast(
|
|
2142
|
+
packetId: number,
|
|
2143
|
+
packet: Uint8Array,
|
|
2144
|
+
replay: () => void,
|
|
2145
|
+
overlapCopy = false,
|
|
2146
|
+
): "unlisted" | "admitted" | "held" | "dropped" {
|
|
2147
|
+
let receiver: BcProvenanceV2Receiver | undefined;
|
|
2148
|
+
for (const entry of this.#provenance.values()) {
|
|
2149
|
+
if (!entry.protected || !ingressHasPacketId(entry.ingress, packetId)) continue;
|
|
2150
|
+
if (receiver || overlapCopy) {
|
|
2151
|
+
this.#unadmittedListedDrops++;
|
|
2152
|
+
return "dropped";
|
|
1846
2153
|
}
|
|
1847
|
-
|
|
2154
|
+
receiver = entry.receiver;
|
|
2155
|
+
}
|
|
2156
|
+
if (!receiver) return "unlisted";
|
|
2157
|
+
const { verdict } = receiver.onDatagram(packet, "unicast");
|
|
2158
|
+
if (verdict === "admitted") return "admitted";
|
|
2159
|
+
if (verdict === "held") {
|
|
2160
|
+
this.#heldReplay.set(packet, replay);
|
|
2161
|
+
this.#heldAt.set(packet, performance.now());
|
|
2162
|
+
return "held";
|
|
2163
|
+
}
|
|
2164
|
+
this.#unadmittedListedDrops++;
|
|
2165
|
+
return "dropped";
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* Feed one bc-provenance record of `flow`: a datagram from `manifestPort`
|
|
2170
|
+
* (`multicast`) or an object of the manifest track (`mirror`). Held
|
|
2171
|
+
* datagrams the record admits are routed before this returns.
|
|
2172
|
+
*/
|
|
2173
|
+
feedProvenanceRecord(flow: string, record: Uint8Array, path: BcpV2RecordPath): string {
|
|
2174
|
+
const entry = this.#provenance.get(flow);
|
|
2175
|
+
if (!entry || this.#disposed) return "unregistered";
|
|
2176
|
+
const result = entry.receiver.onRecord(record, path);
|
|
2177
|
+
if (result.admitted.length > 0) {
|
|
2178
|
+
this.#noteFlowAdmitted(flow);
|
|
2179
|
+
this.#withDeferredFecPokes(() => {
|
|
2180
|
+
for (const datagram of result.admitted) {
|
|
2181
|
+
const route = () => {
|
|
2182
|
+
const replay = this.#heldReplay.get(datagram);
|
|
2183
|
+
if (replay) replay();
|
|
2184
|
+
else this.#processMMTP(datagram, "multicast", true, this.#heldIngress.get(datagram) ?? entry.ingress, false, true);
|
|
2185
|
+
};
|
|
2186
|
+
// A datagram held on a miss and admitted now keeps its receipt time
|
|
2187
|
+
// as the block's ts (fec-01 9): its FEC deadline runs from then.
|
|
2188
|
+
const heldAt = this.#heldAt.get(datagram);
|
|
2189
|
+
if (heldAt !== undefined && this.#fecManager?.withReceiptTime) this.#fecManager.withReceiptTime(heldAt, route);
|
|
2190
|
+
else route();
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
return result.verdict;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/** The receiver ceased to hold the flow: its hold queues are dropped, not expired. */
|
|
2198
|
+
releaseProvenanceFlow(flow: string): void {
|
|
2199
|
+
this.#provenance.get(flow)?.receiver.release();
|
|
2200
|
+
this.#provenance.delete(flow);
|
|
2201
|
+
for (const byName of this.#provenanceStreamFlow.values()) {
|
|
2202
|
+
for (const [name, bound] of byName) if (bound === flow) byName.delete(name);
|
|
1848
2203
|
}
|
|
1849
2204
|
}
|
|
1850
2205
|
|
|
2206
|
+
/** Multicast datagrams dropped because their ingress names a flow with no registered receiver. */
|
|
2207
|
+
get unregisteredFlowDropCount(): number {
|
|
2208
|
+
return this.#unregisteredFlowDrops;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/** Recovered packets dropped because more than one protected flow could own their stream. */
|
|
2212
|
+
get ambiguousRecoveredDropCount(): number {
|
|
2213
|
+
return this.#ambiguousRecoveredDrops;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
1851
2216
|
/**
|
|
1852
2217
|
* Main entry point: feed a raw MMTP packet for processing.
|
|
1853
2218
|
*
|
|
@@ -1865,12 +2230,11 @@ export class MmtpRouter {
|
|
|
1865
2230
|
feedPacket(
|
|
1866
2231
|
packet: Uint8Array,
|
|
1867
2232
|
source: "multicast" | "moq",
|
|
1868
|
-
manifestIdentity?: ManifestObjectIdentity,
|
|
1869
2233
|
ingress?: MmtpIngressContext,
|
|
1870
2234
|
): void {
|
|
1871
2235
|
if (this.#disposed) return;
|
|
1872
2236
|
this.#withDeferredFecPokes(() => {
|
|
1873
|
-
this.#processMMTP(packet, source,
|
|
2237
|
+
this.#processMMTP(packet, source, false, ingress);
|
|
1874
2238
|
});
|
|
1875
2239
|
}
|
|
1876
2240
|
|
|
@@ -1906,7 +2270,7 @@ export class MmtpRouter {
|
|
|
1906
2270
|
feedOverlapCopy(packet: Uint8Array, ingress?: MmtpIngressContext): void {
|
|
1907
2271
|
if (this.#disposed) return;
|
|
1908
2272
|
this.#withDeferredFecPokes(() => {
|
|
1909
|
-
this.#processMMTP(packet, "moq",
|
|
2273
|
+
this.#processMMTP(packet, "moq", false, ingress, true);
|
|
1910
2274
|
});
|
|
1911
2275
|
}
|
|
1912
2276
|
|
|
@@ -1931,6 +2295,7 @@ export class MmtpRouter {
|
|
|
1931
2295
|
ownerPacketIdHint: number | undefined,
|
|
1932
2296
|
catalogTrackName: string | undefined,
|
|
1933
2297
|
repairTrackName: string | undefined,
|
|
2298
|
+
admitted = false,
|
|
1934
2299
|
): void {
|
|
1935
2300
|
// One header parse per packet, as on the multicast repair branch: mmt-fec's
|
|
1936
2301
|
// parseRepairPacket() parsed it once more before this parse (BLO-36599).
|
|
@@ -1961,6 +2326,17 @@ export class MmtpRouter {
|
|
|
1961
2326
|
);
|
|
1962
2327
|
}
|
|
1963
2328
|
|
|
2329
|
+
// Admission keys on the repair track's own packet id: a repair layer no
|
|
2330
|
+
// protected flow lists is an unlisted track (7.2.7), not a datagram of the
|
|
2331
|
+
// flow that lists its owner, where it would be held until expiry. The
|
|
2332
|
+
// flow listing it governs the owner too: a repair flow also lists its
|
|
2333
|
+
// owners' source tracks (#provenanceFor).
|
|
2334
|
+
if (ownerPacketId != null && !admitted) {
|
|
2335
|
+
const admit = this.#admitUnicast(repairPacketId, packet, () =>
|
|
2336
|
+
this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, true));
|
|
2337
|
+
if (admit === "held" || admit === "dropped") return;
|
|
2338
|
+
if (admit === "unlisted" && this.#fecManager && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackName, parsed)) return;
|
|
2339
|
+
}
|
|
1964
2340
|
if (ownerPacketId != null && this.#fecManager) {
|
|
1965
2341
|
this.#moqRepairPidCounts[repairPacketId] =
|
|
1966
2342
|
(this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
|
|
@@ -2122,6 +2498,7 @@ export class MmtpRouter {
|
|
|
2122
2498
|
const placement = fec.placeIsoRepair(ownerPacketId, catalogTrackName, repair.ssStart, repair.ssbLength);
|
|
2123
2499
|
if (placement.status !== "placed") return;
|
|
2124
2500
|
const blockStart = placement.block.start;
|
|
2501
|
+
this.#openAssistBlock(ownerPacketId, catalogTrackName, blockStart, repair.ssbLength);
|
|
2125
2502
|
if (placement.span !== undefined) {
|
|
2126
2503
|
this.#replayIsoSources(ownerPacketId, catalogTrackName, placement.span.block.start, placement.span.placed);
|
|
2127
2504
|
}
|
|
@@ -2147,10 +2524,13 @@ export class MmtpRouter {
|
|
|
2147
2524
|
catalogTrackName,
|
|
2148
2525
|
);
|
|
2149
2526
|
if (recovered && !alreadyCompleted) {
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2527
|
+
this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
|
|
2528
|
+
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2529
|
+
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2530
|
+
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2531
|
+
});
|
|
2532
|
+
} else if (!recovered) {
|
|
2533
|
+
this.#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, { rsId: repair.rsId }, repair.symbolData);
|
|
2154
2534
|
}
|
|
2155
2535
|
}
|
|
2156
2536
|
|
|
@@ -2202,11 +2582,13 @@ export class MmtpRouter {
|
|
|
2202
2582
|
if (payload.oversized) continue;
|
|
2203
2583
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2204
2584
|
const recovered = fec.feedSourceSymbol(ownerPacketId, ssId, payload.symbolData, catalogTrackName);
|
|
2585
|
+
if (!recovered) this.#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, { esi }, payload.symbolData);
|
|
2205
2586
|
if (!recovered || alreadyCompleted) continue;
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2587
|
+
this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
|
|
2588
|
+
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2589
|
+
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2590
|
+
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2591
|
+
});
|
|
2210
2592
|
}
|
|
2211
2593
|
}
|
|
2212
2594
|
|
|
@@ -2244,10 +2626,14 @@ export class MmtpRouter {
|
|
|
2244
2626
|
data: Uint8Array,
|
|
2245
2627
|
packetId: number,
|
|
2246
2628
|
catalogTrackName?: string,
|
|
2247
|
-
):
|
|
2629
|
+
): boolean {
|
|
2248
2630
|
// A Worker-recovered block arrives here whole: defer the gate's drains
|
|
2249
2631
|
// until all its frames are in, as the in-band recovery path does.
|
|
2250
|
-
|
|
2632
|
+
let recovered = false;
|
|
2633
|
+
this.#withDeferredFecPokes(() => {
|
|
2634
|
+
recovered = this.#routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
|
|
2635
|
+
});
|
|
2636
|
+
return recovered;
|
|
2251
2637
|
}
|
|
2252
2638
|
|
|
2253
2639
|
/** Route a single recovered source symbol through the parser + dedup pipeline.
|
|
@@ -2258,10 +2644,10 @@ export class MmtpRouter {
|
|
|
2258
2644
|
packetId: number,
|
|
2259
2645
|
esi = 0,
|
|
2260
2646
|
catalogTrackName?: string,
|
|
2261
|
-
):
|
|
2647
|
+
): boolean {
|
|
2262
2648
|
const blockProfile = this.#blockProfileFor(packetId);
|
|
2263
2649
|
const result = processFecSymbol(data, { ownerPacketId: packetId, index: esi, blockProfile });
|
|
2264
|
-
this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
|
|
2650
|
+
return this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
|
|
2265
2651
|
}
|
|
2266
2652
|
|
|
2267
2653
|
// ── Keyframe timer cancellation ─────────────────────────────────
|
|
@@ -2298,6 +2684,9 @@ export class MmtpRouter {
|
|
|
2298
2684
|
}
|
|
2299
2685
|
}
|
|
2300
2686
|
this.#parser.resetTrack(packetId, catalogTrackName);
|
|
2687
|
+
const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
|
|
2688
|
+
for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
|
|
2689
|
+
for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
|
|
2301
2690
|
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
2302
2691
|
this.#trackSynced.delete(streamKey);
|
|
2303
2692
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
@@ -2310,7 +2699,6 @@ export class MmtpRouter {
|
|
|
2310
2699
|
this.#lastDirectSourceAudioCursorByStream.delete(streamKey);
|
|
2311
2700
|
this.#lastDirectEmittedAudioCursorByStream.delete(streamKey);
|
|
2312
2701
|
this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
|
|
2313
|
-
this.#manifestRecoveryQueues.delete(streamKey);
|
|
2314
2702
|
this.#deliveredVideoHighWaterByStream.delete(streamKey);
|
|
2315
2703
|
this.#stream(packetId, catalogTrackName).duplicates.clear();
|
|
2316
2704
|
const assemblersToReset = new Set<CmafBlockAssembler>();
|
|
@@ -2362,7 +2750,6 @@ export class MmtpRouter {
|
|
|
2362
2750
|
this.#lastDirectSourceAudioCursorByStream.delete(key);
|
|
2363
2751
|
this.#lastDirectEmittedAudioCursorByStream.delete(key);
|
|
2364
2752
|
this.#pendingDirectRecoveredAudioByStream.delete(key);
|
|
2365
|
-
this.#manifestRecoveryQueues.delete(key);
|
|
2366
2753
|
this.#trackInitDelivered.delete(key);
|
|
2367
2754
|
this.#parserDiagnostics.delete(key);
|
|
2368
2755
|
this.#pendingFecPokes.delete(key);
|
|
@@ -2658,8 +3045,9 @@ export class MmtpRouter {
|
|
|
2658
3045
|
this.#parserDiagnostics.clear();
|
|
2659
3046
|
this.#catalogTrackNamesByPacketId.clear();
|
|
2660
3047
|
this.#repairOwnersByPacketId.clear();
|
|
2661
|
-
|
|
2662
|
-
this.#
|
|
3048
|
+
for (const { receiver } of this.#provenance.values()) receiver.release();
|
|
3049
|
+
this.#provenance.clear();
|
|
3050
|
+
this.#provenanceStreamFlow.clear();
|
|
2663
3051
|
for (const assembler of this.#mseAssemblers.values()) assembler.dispose();
|
|
2664
3052
|
this.#mseAssemblers.clear();
|
|
2665
3053
|
}
|
|
@@ -2778,6 +3166,28 @@ export class MmtpRouter {
|
|
|
2778
3166
|
return this.#parser.fecReleaseEarliestHeldAtMs(packetId, catalogTrackName);
|
|
2779
3167
|
}
|
|
2780
3168
|
|
|
3169
|
+
/**
|
|
3170
|
+
* Whether a complete RAP above SS_ID `ssId` entered this stream's FEC
|
|
3171
|
+
* release gate (see FecReleaseSequencer.rapAbove).
|
|
3172
|
+
*/
|
|
3173
|
+
fecReleaseRapAbove(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean {
|
|
3174
|
+
return this.#parser.fecReleaseRapAbove(packetId, catalogTrackName, ssId);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Observe each complete RAP entering an FEC release gate, with the SS_ID
|
|
3179
|
+
* its frame completed in. Called synchronously from the frame push: an
|
|
3180
|
+
* observer that drives FEC state must defer that work.
|
|
3181
|
+
*/
|
|
3182
|
+
addFecRapObserver(
|
|
3183
|
+
observer: (packetId: number, catalogTrackName: string | undefined, ssId: number) => void,
|
|
3184
|
+
): () => void {
|
|
3185
|
+
this.#fecRapObservers.add(observer);
|
|
3186
|
+
return () => {
|
|
3187
|
+
this.#fecRapObservers.delete(observer);
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
|
|
2781
3191
|
#requestFecPoke(packetId: number, catalogTrackName: string | undefined): void {
|
|
2782
3192
|
if (this.#disposed) return;
|
|
2783
3193
|
if (this.#fecPokeDepth > 0) {
|
|
@@ -2828,6 +3238,10 @@ export class MmtpRouter {
|
|
|
2828
3238
|
this.#onFrameGap?.(decision);
|
|
2829
3239
|
return decision;
|
|
2830
3240
|
},
|
|
3241
|
+
isRap: (data, meta) => this.#isRapForGap(data, meta),
|
|
3242
|
+
onRap: (packetId, catalogTrackName, ssId) => {
|
|
3243
|
+
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
3244
|
+
},
|
|
2831
3245
|
onReleased: (data, meta) => {
|
|
2832
3246
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === undefined) return;
|
|
2833
3247
|
const timing = meta.authoredTiming;
|
|
@@ -3013,7 +3427,6 @@ export class MmtpRouter {
|
|
|
3013
3427
|
#processMMTP(
|
|
3014
3428
|
packet: Uint8Array,
|
|
3015
3429
|
source: "multicast" | "moq",
|
|
3016
|
-
manifestIdentity?: ManifestObjectIdentity,
|
|
3017
3430
|
manifestAdmitted = false,
|
|
3018
3431
|
ingress?: MmtpIngressContext,
|
|
3019
3432
|
overlapCopy = false,
|
|
@@ -3062,57 +3475,43 @@ export class MmtpRouter {
|
|
|
3062
3475
|
this.#classifyPacketId(info.packetId) !== null &&
|
|
3063
3476
|
this.#configuredBlockProfile(info.packetId) === undefined;
|
|
3064
3477
|
if (fecUnreadable && !manifestAdmitted) this.#noteUnreadableFecPayloadId(info.packetId, "source");
|
|
3478
|
+
if (source === "multicast" && manifestAdmitted && !info.isRepair && ingress?.flow !== undefined) {
|
|
3479
|
+
let byName = this.#provenanceStreamFlow.get(info.packetId);
|
|
3480
|
+
if (!byName) this.#provenanceStreamFlow.set(info.packetId, (byName = new Map()));
|
|
3481
|
+
byName.set(catalogTrackNameForPacket(ingress, info.packetId), ingress.flow);
|
|
3482
|
+
}
|
|
3065
3483
|
|
|
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";
|
|
3484
|
+
// A protected flow admits every datagram, repair included, before it is
|
|
3485
|
+
// read (7.2.7): an unadmitted repair symbol never reaches the decoder.
|
|
3486
|
+
if (source === "multicast" && !manifestAdmitted && ingress?.flow !== undefined) {
|
|
3487
|
+
const protectedFlow = this.#provenance.get(ingress.flow);
|
|
3488
|
+
// Named but not registered (before registration, after release): the
|
|
3489
|
+
// flow is protected, so nothing admits this datagram. Fail closed.
|
|
3490
|
+
if (!protectedFlow) {
|
|
3491
|
+
this.#unregisteredFlowDrops++;
|
|
3492
|
+
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
3109
3493
|
return;
|
|
3110
3494
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3495
|
+
// Held: released by a record (feedProvenanceRecord) or expired. Duplicate / overflow: dropped.
|
|
3496
|
+
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
3497
|
+
if (verdict === "admitted") {
|
|
3498
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
3499
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
3500
|
+
}
|
|
3501
|
+
else if (verdict === "held") {
|
|
3502
|
+
this.#heldIngress.set(packet, ingress!);
|
|
3503
|
+
this.#heldAt.set(packet, performance.now());
|
|
3504
|
+
}
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
// A MoQ packet of a track a protected flow lists is a datagram of that
|
|
3509
|
+
// flow on its unicast path (7.2.7): admitted before it is read, or held.
|
|
3510
|
+
if (source === "moq" && !manifestAdmitted) {
|
|
3511
|
+
const admit = this.#admitUnicast(info.packetId, packet, () =>
|
|
3512
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy, true), overlapCopy);
|
|
3513
|
+
if (admit !== "unlisted") {
|
|
3514
|
+
if (admit === "admitted") this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
3116
3515
|
return;
|
|
3117
3516
|
}
|
|
3118
3517
|
}
|
|
@@ -3127,6 +3526,11 @@ export class MmtpRouter {
|
|
|
3127
3526
|
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
3128
3527
|
if (ownerPacketId != null && this.#fecManager) {
|
|
3129
3528
|
if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
3529
|
+
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast).
|
|
3530
|
+
if (
|
|
3531
|
+
source === "moq" && !manifestAdmitted &&
|
|
3532
|
+
this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)
|
|
3533
|
+
) return;
|
|
3130
3534
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
3131
3535
|
const count = this.#repairPidCounts[info.packetId]!;
|
|
3132
3536
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -3203,8 +3607,8 @@ export class MmtpRouter {
|
|
|
3203
3607
|
// Start a fresh epoch for the stream before this packet is looked up.
|
|
3204
3608
|
// The reset needs the catalog identity, so a stream without one is not
|
|
3205
3609
|
// checked; an overlap copy is MoQ bytes and says nothing about the
|
|
3206
|
-
// multicast publisher, and neither does a packet
|
|
3207
|
-
//
|
|
3610
|
+
// multicast publisher, and neither does a packet a provenance hold queue
|
|
3611
|
+
// released: it predates the horizon because it waited, not because the
|
|
3208
3612
|
// publisher restarted.
|
|
3209
3613
|
if (
|
|
3210
3614
|
source === "multicast" &&
|
|
@@ -3433,37 +3837,30 @@ export class MmtpRouter {
|
|
|
3433
3837
|
sourceFec.symbolData,
|
|
3434
3838
|
catalogTrackName,
|
|
3435
3839
|
);
|
|
3436
|
-
if (recovered) {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
this.#fecManager.countRecovery(info.packetId, sourceFec.sbn, catalogTrackName);
|
|
3840
|
+
if (!recovered) {
|
|
3841
|
+
this.#noteAssistAdmitted(
|
|
3842
|
+
info.packetId, catalogTrackName, sourceFec.sbn,
|
|
3843
|
+
{ esi: serial32Distance(sourceFec.sbn, sourceFec.ssId) }, sourceFec.symbolData,
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
// Only route recovered blocks that needed actual repair; complete
|
|
3847
|
+
// one only once admission has released its recovered packets.
|
|
3848
|
+
if (recovered && !alreadyCompleted) {
|
|
3849
|
+
const fec = this.#fecManager;
|
|
3850
|
+
const sbn = sourceFec.sbn;
|
|
3851
|
+
const packetId = info.packetId;
|
|
3852
|
+
const tracked = this.#classifyPacketId(packetId) !== null;
|
|
3853
|
+
this.#routeRecoveredBlock(sbn, recovered, packetId, catalogTrackName, () => {
|
|
3451
3854
|
if (catalogTrackName === undefined) {
|
|
3452
|
-
|
|
3855
|
+
if (tracked) fec.cancelBlockTimeout(sbn, packetId);
|
|
3856
|
+
fec.countRecovery(packetId, sbn, catalogTrackName);
|
|
3857
|
+
fec.markBlockCompleted(sbn, packetId);
|
|
3453
3858
|
} else {
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
catalogTrackName,
|
|
3458
|
-
);
|
|
3859
|
+
if (tracked) fec.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
3860
|
+
fec.countRecovery(packetId, sbn, catalogTrackName);
|
|
3861
|
+
fec.markBlockCompleted(sbn, packetId, catalogTrackName);
|
|
3459
3862
|
}
|
|
3460
|
-
|
|
3461
|
-
sourceFec.sbn,
|
|
3462
|
-
recovered,
|
|
3463
|
-
info.packetId,
|
|
3464
|
-
catalogTrackName,
|
|
3465
|
-
);
|
|
3466
|
-
}
|
|
3863
|
+
});
|
|
3467
3864
|
}
|
|
3468
3865
|
}
|
|
3469
3866
|
}
|
|
@@ -4068,7 +4465,8 @@ export class MmtpRouter {
|
|
|
4068
4465
|
recovered: Uint8Array,
|
|
4069
4466
|
ownerPacketId?: number,
|
|
4070
4467
|
catalogTrackName?: string,
|
|
4071
|
-
|
|
4468
|
+
complete?: () => void,
|
|
4469
|
+
): boolean {
|
|
4072
4470
|
// MMTP path: use processFecBlock for MMTP-wrapped recovery.
|
|
4073
4471
|
// Recovered symbols are individual MMTP packets that may be fragments
|
|
4074
4472
|
// (FI=1/2/3) of a larger MFU. Route through the reassembler so
|
|
@@ -4092,118 +4490,83 @@ export class MmtpRouter {
|
|
|
4092
4490
|
ownerPacketId,
|
|
4093
4491
|
blockProfile,
|
|
4094
4492
|
});
|
|
4095
|
-
this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
4493
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
4096
4494
|
}
|
|
4097
4495
|
|
|
4496
|
+
/**
|
|
4497
|
+
* A recovered source packet of a protected flow is admitted on its own
|
|
4498
|
+
* digest: its bytes with the SS_ID it was sent with (SS_Start + ESI)
|
|
4499
|
+
* appended are the datagram the record listed (7.2.7). One that no record
|
|
4500
|
+
* lists is dropped; holding it would let it out later as a multicast arrival.
|
|
4501
|
+
* Returns whether the decode recovered the block: every packet it yields
|
|
4502
|
+
* that this router has not parsed already is admitted. A block one of whose
|
|
4503
|
+
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
4504
|
+
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
4505
|
+
* outside a protected flow.
|
|
4506
|
+
*/
|
|
4098
4507
|
#routeVerifiedFecSymbols(
|
|
4099
4508
|
blockId: number,
|
|
4100
4509
|
result: ProcessFecBlockResult,
|
|
4101
4510
|
ownerPacketId?: number,
|
|
4102
4511
|
catalogTrackName?: string,
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4512
|
+
complete?: () => void,
|
|
4513
|
+
): boolean {
|
|
4514
|
+
const receiver = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
4515
|
+
let recovered = true;
|
|
4516
|
+
if (receiver) {
|
|
4517
|
+
const parsed = this.#stream(ownerPacketId!, catalogTrackName).duplicates.parsedSymbols;
|
|
4518
|
+
result = {
|
|
4519
|
+
...result,
|
|
4520
|
+
symbols: result.symbols.filter((sym) => {
|
|
4521
|
+
const ssId = this.#fecAnchorForRecovered(blockId, sym.index);
|
|
4522
|
+
// A received copy was admitted on arrival; its recovered twin is a duplicate.
|
|
4523
|
+
if (parsed.has(ssId)) return false;
|
|
4524
|
+
let admitted = false;
|
|
4525
|
+
if (receiver === "ambiguous") {
|
|
4526
|
+
this.#ambiguousRecoveredDrops++;
|
|
4527
|
+
} else {
|
|
4528
|
+
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
4529
|
+
wire.set(sym.rawSymbol);
|
|
4530
|
+
new DataView(wire.buffer).setUint32(sym.rawSymbol.byteLength, ssId);
|
|
4531
|
+
admitted = receiver.admitRecovered(wire);
|
|
4532
|
+
}
|
|
4533
|
+
if (!admitted) recovered = false;
|
|
4534
|
+
return admitted;
|
|
4535
|
+
}),
|
|
4536
|
+
};
|
|
4112
4537
|
}
|
|
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();
|
|
4538
|
+
if (recovered) complete?.();
|
|
4539
|
+
this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
4540
|
+
return recovered;
|
|
4136
4541
|
}
|
|
4137
4542
|
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4543
|
+
/**
|
|
4544
|
+
* The receiver a recovered packet of `packetId` answers to: the flow that
|
|
4545
|
+
* admitted the stream's source datagrams. Before any was admitted, the one
|
|
4546
|
+
* flow whose registration lists the stream; more than one (a repair flow
|
|
4547
|
+
* also lists its owners' source tracks) cannot be told apart, so none.
|
|
4548
|
+
*/
|
|
4549
|
+
#provenanceFor(
|
|
4550
|
+
packetId: number,
|
|
4142
4551
|
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;
|
|
4552
|
+
): BcProvenanceV2Receiver | "ambiguous" | undefined {
|
|
4553
|
+
// A caller that names no track matches every name the packet id was admitted under.
|
|
4554
|
+
const byName = this.#provenanceStreamFlow.get(packetId);
|
|
4555
|
+
const flows = new Set(
|
|
4556
|
+
catalogTrackName === undefined || !byName
|
|
4557
|
+
? byName?.values()
|
|
4558
|
+
: byName.has(catalogTrackName) ? [byName.get(catalogTrackName)!] : [],
|
|
4559
|
+
);
|
|
4560
|
+
if (flows.size > 1) return "ambiguous";
|
|
4561
|
+
for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
|
|
4562
|
+
let match: BcProvenanceV2Receiver | undefined;
|
|
4563
|
+
for (const { receiver, ingress } of this.#provenance.values()) {
|
|
4564
|
+
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
4565
|
+
if (match) return "ambiguous";
|
|
4566
|
+
match = receiver;
|
|
4181
4567
|
}
|
|
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);
|
|
4568
|
+
}
|
|
4569
|
+
return match;
|
|
4207
4570
|
}
|
|
4208
4571
|
|
|
4209
4572
|
#routeProcessedFecSymbols(
|