@blockcast/mmt-container 2.1.0 → 2.2.0-main.02321582f1ca
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 +29 -17
- package/dist/browser-runtime.js +1302 -249
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +213 -6
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +37 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +98 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +25 -2
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1749 -56
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +15 -2
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15683 -14941
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +256 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +96 -8
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +414 -29
- package/src/multicast-manager.ts +26 -5
- package/src/multicast-transport.ts +44 -20
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
package/src/mmtp-router.ts
CHANGED
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
type MMTPHeader,
|
|
24
24
|
type MoqRepairFrame,
|
|
25
25
|
} from "@blockcast/mmt-base";
|
|
26
|
-
import
|
|
26
|
+
import { USER_AGENT_VERIFIED } from "@blockcast/mmt-transport";
|
|
27
|
+
import type { FECManager, IsoHeldSource, RebuiltSourceFeed, WasmFecDecoder } from "./fec-manager.js";
|
|
27
28
|
import type { IsoPlacedSource, IsoRepairPlacement } from "./iso-block-locator.js";
|
|
28
29
|
import { isMfuReassemblyError } from "./mfu-reassembler.js";
|
|
29
30
|
import type {
|
|
@@ -47,10 +48,12 @@ import {
|
|
|
47
48
|
} from "./cmaf-block-assembler.js";
|
|
48
49
|
import {
|
|
49
50
|
MmtpContainerParser,
|
|
51
|
+
authoredAdjacent,
|
|
50
52
|
isMfuKeyframeMetadata,
|
|
51
53
|
type FecReleaseGate,
|
|
52
54
|
type FrameMeta as ParserFrameMeta,
|
|
53
55
|
type ParsedMmtpPacket,
|
|
56
|
+
type MmtpRecoveredHeadTimingStats,
|
|
54
57
|
} from "./mmtp-container-parser.js";
|
|
55
58
|
import { type TrackRole } from "./carp-catalog.js";
|
|
56
59
|
import {
|
|
@@ -211,6 +214,82 @@ export interface KeyframeOverlayStats {
|
|
|
211
214
|
restarts: number;
|
|
212
215
|
unsequencedDropped: number;
|
|
213
216
|
protectedDropped: number;
|
|
217
|
+
/** Rebuilt packets fed to their base block's decoder (joint decoding). */
|
|
218
|
+
baseFed: number;
|
|
219
|
+
/** Rebuilt packets the base never took: no base SS_ID could be determined. */
|
|
220
|
+
baseUndetermined: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The newest received overlay member of one MPU. `psnOffset` (overlay SS_ID
|
|
225
|
+
* minus wire packet_sequence_number) restores a rebuilt packet's wire
|
|
226
|
+
* sequence; the member's own SS_IDs anchor a rebuilt packet's base SS_ID
|
|
227
|
+
* (overlayBaseSsId).
|
|
228
|
+
*/
|
|
229
|
+
interface OverlayMemberAnchor {
|
|
230
|
+
readonly psnOffset: number;
|
|
231
|
+
readonly overlaySsId: number;
|
|
232
|
+
/** The member's base SS_ID, from its Source FEC Payload ID. */
|
|
233
|
+
readonly baseSsId?: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The base SS_ID of a packet the overlay rebuilt at `overlaySsId`, which an
|
|
238
|
+
* overlay coordinate alone never yields (draft-ramadan-moq-mmt-02 7.1). Both
|
|
239
|
+
* SS_IDs advance by one per packet along a run of consecutive overlay
|
|
240
|
+
* members, so base minus overlay SS_ID is the anchor's along a run the wire
|
|
241
|
+
* proves to be all members: one over which the rebuilt packet's protected
|
|
242
|
+
* packet_sequence_number (`headerPsn`) advanced from the anchor's exactly as
|
|
243
|
+
* the overlay SS_ID did (`restoredPsn`), so no other packet lies inside it.
|
|
244
|
+
* A hop that re-stamped packet_sequence_number after protection fails the
|
|
245
|
+
* test. Undefined when it fails.
|
|
246
|
+
*/
|
|
247
|
+
function overlayBaseSsId(
|
|
248
|
+
anchor: OverlayMemberAnchor,
|
|
249
|
+
overlaySsId: number,
|
|
250
|
+
restoredPsn: number,
|
|
251
|
+
headerPsn: number,
|
|
252
|
+
): number | undefined {
|
|
253
|
+
if (anchor.baseSsId === undefined || headerPsn !== restoredPsn) return undefined;
|
|
254
|
+
return serial32Add(anchor.baseSsId, (overlaySsId - anchor.overlaySsId) | 0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The base SS_ID of a rebuilt packet no received member of its MPU anchors,
|
|
259
|
+
* from its protected packet_sequence_number (`headerPsn`). SS_ID advances by
|
|
260
|
+
* one per FEC source packet (mmt-02 7.1) and psn by one per packet, so on a
|
|
261
|
+
* track whose every packet is an FEC source packet the sender keeps
|
|
262
|
+
* SS_ID - psn fixed. `offset` (SS_ID - psn of the stream's latest received
|
|
263
|
+
* source) is that sender value while a base recovery showed it
|
|
264
|
+
* (`unstampedOffset`): a recovered packet's header psn equal to its SS_ID
|
|
265
|
+
* minus the same offset. A hop that re-stamps psn, or a packet with no
|
|
266
|
+
* SS_ID, moves the offset away from the one shown, which voids it. Undefined
|
|
267
|
+
* without both.
|
|
268
|
+
*/
|
|
269
|
+
function unstampedBaseSsId(
|
|
270
|
+
offset: number | undefined,
|
|
271
|
+
unstampedOffset: number | undefined,
|
|
272
|
+
headerPsn: number,
|
|
273
|
+
): number | undefined {
|
|
274
|
+
if (offset === undefined || offset !== unstampedOffset) return undefined;
|
|
275
|
+
return (headerPsn + offset) >>> 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Base SS_IDs of overlay-routed packets kept per stream (see OverlayToBase). */
|
|
279
|
+
const OVERLAY_ROUTED_LIMIT = 1024;
|
|
280
|
+
/** Rebuilt sources kept per stream for a base block no repair stated yet. */
|
|
281
|
+
const OVERLAY_PENDING_LIMIT = 256;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* One stream's keyframe-overlay recoveries on their way into its base
|
|
285
|
+
* decoder (joint decoding, BLO-52085).
|
|
286
|
+
*/
|
|
287
|
+
interface OverlayToBase {
|
|
288
|
+
/** Base SS_IDs of the packets an overlay recovery routed (as media, or held
|
|
289
|
+
* for their FT=1): a base recovery does not route them a second time. */
|
|
290
|
+
readonly routed: Set<number>;
|
|
291
|
+
/** Rebuilt sources, by base SS_ID, whose base block no repair stated yet. */
|
|
292
|
+
readonly pending: Map<number, Uint8Array>;
|
|
214
293
|
}
|
|
215
294
|
|
|
216
295
|
interface OverlayInstance {
|
|
@@ -222,8 +301,8 @@ interface OverlayInstance {
|
|
|
222
301
|
readonly k: number;
|
|
223
302
|
/** The newest overlay symbol seen: its overlay SS_ID and wire sequence. */
|
|
224
303
|
last?: { ssId: number; psn: number };
|
|
225
|
-
/** MPU sequence number →
|
|
226
|
-
readonly offsets: Map<number,
|
|
304
|
+
/** MPU sequence number → its newest received member. */
|
|
305
|
+
readonly offsets: Map<number, OverlayMemberAnchor>;
|
|
227
306
|
/** Overlay blocks a MoQ symbol fed (multicast-02 7.2.7). */
|
|
228
307
|
readonly moqBlocks: Set<number>;
|
|
229
308
|
}
|
|
@@ -245,6 +324,14 @@ export interface MmtpIngressContext {
|
|
|
245
324
|
* the same ingress and `feedPacket` splits them off.
|
|
246
325
|
*/
|
|
247
326
|
flow?: string;
|
|
327
|
+
/**
|
|
328
|
+
* `USER_AGENT_VERIFIED` when the datagram came from the browser's own
|
|
329
|
+
* MulticastReceiver, which verified the flow's records itself and delivers
|
|
330
|
+
* only admitted data (`userAgentVerifiedIngress` in
|
|
331
|
+
* `@blockcast/mmt-transport`). A protected multicast datagram carrying it
|
|
332
|
+
* is admitted without a record. Any other value means nothing.
|
|
333
|
+
*/
|
|
334
|
+
admittedBy?: symbol;
|
|
248
335
|
}
|
|
249
336
|
|
|
250
337
|
/** One physical stream: an MMTP packet ID and the catalog track it carries. */
|
|
@@ -502,6 +589,9 @@ export type RoutedPacketMeta = {
|
|
|
502
589
|
mmtpTimestamp: bigint;
|
|
503
590
|
/**
|
|
504
591
|
* The MMTP header RAP flag: the packet starts a random access point. On a
|
|
592
|
+
* track under a keyframe overlay it is also set on the FT=0 and FT=1 data
|
|
593
|
+
* units that govern a covered keyframe (draft-ramadan-moq-mmt-02 7.4.2), so
|
|
594
|
+
* it can precede the keyframe's first MFU fragment. On a
|
|
505
595
|
* `recovered` packet it is read from the FEC-reconstructed header, which is
|
|
506
596
|
* byte-exact outside packet_sequence_number, so it is the flag the
|
|
507
597
|
* publisher sent. The router's own gap decisions deliberately ignore it for
|
|
@@ -565,6 +655,7 @@ interface StreamLedgers {
|
|
|
565
655
|
* the pair.
|
|
566
656
|
*/
|
|
567
657
|
readonly duplicates: StreamDuplicateLedgers;
|
|
658
|
+
readonly overlayToBase: OverlayToBase;
|
|
568
659
|
}
|
|
569
660
|
|
|
570
661
|
const NO_FEC_MANAGER_HORIZON: DuplicateHorizon = Object.freeze({
|
|
@@ -652,6 +743,17 @@ export interface FECManagerLike {
|
|
|
652
743
|
catalogTrackName?: string,
|
|
653
744
|
): Uint8Array | null;
|
|
654
745
|
|
|
746
|
+
/** Feed a source another FEC instance over the same packets rebuilt (a
|
|
747
|
+
* keyframe overlay) to its base block, which a repair must already have
|
|
748
|
+
* stated; not a network arrival (FECManager.feedRebuiltSourceSymbol).
|
|
749
|
+
* Optional: without it the overlay and the base decode apart. */
|
|
750
|
+
feedRebuiltSourceSymbol?(
|
|
751
|
+
packetId: number,
|
|
752
|
+
ssId: number,
|
|
753
|
+
symbolData: Uint8Array,
|
|
754
|
+
catalogTrackName?: string,
|
|
755
|
+
): RebuiltSourceFeed;
|
|
756
|
+
|
|
655
757
|
/** How long ago the stream's oldest open block with an FEC deadline took its first symbol (optional). */
|
|
656
758
|
oldestOpenBlockAgeMs?(packetId: number, catalogTrackName: string): number | undefined;
|
|
657
759
|
|
|
@@ -946,6 +1048,15 @@ function sourceSymbolEvidence(
|
|
|
946
1048
|
};
|
|
947
1049
|
}
|
|
948
1050
|
|
|
1051
|
+
/** One protected flow: its 7.2.7 receiver, and the ingress its datagrams arrive with. */
|
|
1052
|
+
interface ProvenanceEntry {
|
|
1053
|
+
receiver: BcProvenanceV2Receiver;
|
|
1054
|
+
ingress: MmtpIngressContext;
|
|
1055
|
+
protected?: boolean;
|
|
1056
|
+
/** A datagram the user agent verified admitted the flow (`admittedBy`). */
|
|
1057
|
+
userAgent?: boolean;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
949
1060
|
export class MmtpRouter {
|
|
950
1061
|
readonly #parser: MmtpContainerParser;
|
|
951
1062
|
// Source AND FEC-recovered MFU fragments share ONE reassembler — the parser's
|
|
@@ -955,10 +1066,7 @@ export class MmtpRouter {
|
|
|
955
1066
|
#defaultFecMapper: FecBlockMapper | null;
|
|
956
1067
|
#fecMappersByPacketId = new Map<number, FecBlockMapper>();
|
|
957
1068
|
/** Protected flows: one 7.2.7 receiver each, and the ingress its datagrams arrive with. */
|
|
958
|
-
readonly #provenance = new Map<
|
|
959
|
-
string,
|
|
960
|
-
{ receiver: BcProvenanceV2Receiver; ingress: MmtpIngressContext; protected?: boolean }
|
|
961
|
-
>();
|
|
1069
|
+
readonly #provenance = new Map<string, ProvenanceEntry>();
|
|
962
1070
|
#onFlowProtected: ((flow: string) => void) | null = null;
|
|
963
1071
|
#unadmittedListedDrops = 0;
|
|
964
1072
|
#unlistedRepairSetAside = 0;
|
|
@@ -1029,6 +1137,14 @@ export class MmtpRouter {
|
|
|
1029
1137
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
1030
1138
|
*/
|
|
1031
1139
|
#ssIdSeqOffsetByStream = new Map<string, number>();
|
|
1140
|
+
/**
|
|
1141
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
1142
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
1143
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
1144
|
+
* current offset, no hop re-stamps the stream and a packet's protected psn
|
|
1145
|
+
* names its base SS_ID (joint decoding of an MPU no member of which arrived).
|
|
1146
|
+
*/
|
|
1147
|
+
#unstampedSsIdSeqOffsetByStream = new Map<string, number>();
|
|
1032
1148
|
/**
|
|
1033
1149
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
1034
1150
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -1106,7 +1222,7 @@ export class MmtpRouter {
|
|
|
1106
1222
|
#restampedStreams = new Set<string>();
|
|
1107
1223
|
#overlayStats: KeyframeOverlayStats = {
|
|
1108
1224
|
repairDropped: 0, ownerRefused: 0, recovered: 0, restarts: 0,
|
|
1109
|
-
unsequencedDropped: 0, protectedDropped: 0,
|
|
1225
|
+
unsequencedDropped: 0, protectedDropped: 0, baseFed: 0, baseUndetermined: 0,
|
|
1110
1226
|
};
|
|
1111
1227
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
1112
1228
|
#onDeltaBlockCompleteCb:
|
|
@@ -1385,7 +1501,7 @@ export class MmtpRouter {
|
|
|
1385
1501
|
const cb = this.#getFrameCallback(packetId);
|
|
1386
1502
|
if (isVideo && !assembler) {
|
|
1387
1503
|
if (frameMeta.recovered) {
|
|
1388
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
1504
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
1389
1505
|
} else {
|
|
1390
1506
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
1391
1507
|
}
|
|
@@ -1900,6 +2016,11 @@ export class MmtpRouter {
|
|
|
1900
2016
|
return { ...this.#overlayStats };
|
|
1901
2017
|
}
|
|
1902
2018
|
|
|
2019
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
2020
|
+
get recoveredHeadsAwaitingTiming(): Readonly<MmtpRecoveredHeadTimingStats> {
|
|
2021
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1903
2024
|
#isOverlayRepair(repairPacketId: number, repairTrackName?: string): boolean {
|
|
1904
2025
|
return this.#overlayLegs.has(repairPacketId) ||
|
|
1905
2026
|
(repairTrackName !== undefined && this.#overlayRepairNames.has(repairTrackName));
|
|
@@ -1951,6 +2072,13 @@ export class MmtpRouter {
|
|
|
1951
2072
|
*/
|
|
1952
2073
|
resetKeyframeOverlays(sourcePacketId: number, catalogTrackName?: string): void {
|
|
1953
2074
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
2075
|
+
// The base restarted with it: base SS_IDs kept for joint decoding name
|
|
2076
|
+
// the old epoch's packets.
|
|
2077
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
2078
|
+
if (catalogTrackName !== undefined && name !== catalogTrackName) continue;
|
|
2079
|
+
stream.overlayToBase.routed.clear();
|
|
2080
|
+
stream.overlayToBase.pending.clear();
|
|
2081
|
+
}
|
|
1954
2082
|
}
|
|
1955
2083
|
|
|
1956
2084
|
#resetOverlay(inst: OverlayInstance): void {
|
|
@@ -2028,7 +2156,8 @@ export class MmtpRouter {
|
|
|
2028
2156
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
2029
2157
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
2030
2158
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
2031
|
-
* included (7.1, 7.4.3).
|
|
2159
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
2160
|
+
* FEC reads it.
|
|
2032
2161
|
*/
|
|
2033
2162
|
#feedOverlaySource(
|
|
2034
2163
|
packetId: number,
|
|
@@ -2038,6 +2167,7 @@ export class MmtpRouter {
|
|
|
2038
2167
|
sequenceNumber: number,
|
|
2039
2168
|
rapFlag: boolean,
|
|
2040
2169
|
source: MmtpSourcePath,
|
|
2170
|
+
baseSsId: number | undefined,
|
|
2041
2171
|
): void {
|
|
2042
2172
|
if (this.#overlays.size === 0) return;
|
|
2043
2173
|
const ssId = keyframeOverlaySsId(header);
|
|
@@ -2046,8 +2176,22 @@ export class MmtpRouter {
|
|
|
2046
2176
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
2047
2177
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
2048
2178
|
if (identity !== null) {
|
|
2179
|
+
// The newest member anchors its MPU; one with no base SS_ID leaves the
|
|
2180
|
+
// MPU no base anchor rather than keep an older member's. A member that
|
|
2181
|
+
// is no FEC source packet of the base takes an overlay SS_ID and a psn
|
|
2182
|
+
// but no base SS_ID, so past it an older anchor would put each rebuilt
|
|
2183
|
+
// packet one base slot high, and overlayBaseSsId's psn test would pass
|
|
2184
|
+
// it. That costs a conforming stream nothing: every member carries the
|
|
2185
|
+
// base trailer (7.4.3), unread only while the base track is
|
|
2186
|
+
// unconfigured (fecUnreadable), which joint decoding skips, and a base
|
|
2187
|
+
// release (MulticastManager) resets or removes the overlay, so no
|
|
2188
|
+
// readable member precedes an unread one in its MPU.
|
|
2049
2189
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
2050
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
2190
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
2191
|
+
psnOffset: (ssId - sequenceNumber) >>> 0,
|
|
2192
|
+
overlaySsId: ssId,
|
|
2193
|
+
...(baseSsId === undefined ? {} : { baseSsId }),
|
|
2194
|
+
});
|
|
2051
2195
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value!);
|
|
2052
2196
|
}
|
|
2053
2197
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -2134,18 +2278,35 @@ export class MmtpRouter {
|
|
|
2134
2278
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
2135
2279
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
2136
2280
|
const restored = new Set<number>();
|
|
2281
|
+
const baseSsIds = new Map<ProcessFecBlockResult["symbols"][number], number>();
|
|
2137
2282
|
const symbols = result.symbols.flatMap((sym) => {
|
|
2138
|
-
const
|
|
2139
|
-
if (
|
|
2140
|
-
const
|
|
2283
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
2284
|
+
if (anchor !== undefined) {
|
|
2285
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
2286
|
+
const packetSequenceNumber = (overlaySsId - anchor.psnOffset) >>> 0;
|
|
2141
2287
|
restored.add(packetSequenceNumber);
|
|
2142
|
-
|
|
2288
|
+
const out = { ...sym, packetSequenceNumber };
|
|
2289
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
2290
|
+
if (baseSsId !== undefined) baseSsIds.set(out, baseSsId);
|
|
2291
|
+
return [out];
|
|
2143
2292
|
}
|
|
2144
2293
|
// A stream re-stamped by a bridge that no base recovery has shown yet
|
|
2145
2294
|
// keeps the header's sequence too: from received packets alone the
|
|
2146
2295
|
// router cannot tell (their psn is the wire one). Base recovery or a
|
|
2147
2296
|
// FETCH still covers such a keyframe.
|
|
2148
|
-
if (!restamped)
|
|
2297
|
+
if (!restamped) {
|
|
2298
|
+
// No member of this MPU arrived to anchor a base SS_ID. A base
|
|
2299
|
+
// recovery that showed the stream unstamped at its current offset
|
|
2300
|
+
// determines one from the packet's protected psn instead
|
|
2301
|
+
// (unstampedBaseSsId).
|
|
2302
|
+
const baseSsId = unstampedBaseSsId(
|
|
2303
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
2304
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
2305
|
+
sym.packetSequenceNumber,
|
|
2306
|
+
);
|
|
2307
|
+
if (baseSsId !== undefined) baseSsIds.set(sym, baseSsId);
|
|
2308
|
+
return [sym];
|
|
2309
|
+
}
|
|
2149
2310
|
this.#overlayStats.unsequencedDropped++;
|
|
2150
2311
|
return [];
|
|
2151
2312
|
});
|
|
@@ -2155,6 +2316,98 @@ export class MmtpRouter {
|
|
|
2155
2316
|
});
|
|
2156
2317
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) =>
|
|
2157
2318
|
restored.has(sym.packetSequenceNumber)));
|
|
2319
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
2324
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
2325
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
2326
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
2327
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
2328
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
2329
|
+
* A base block no repair has stated yet takes it when one does
|
|
2330
|
+
* (#feedPendingRebuilt).
|
|
2331
|
+
*
|
|
2332
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
2333
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
2334
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
2335
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
2336
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
2337
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
2338
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
2339
|
+
* block's own arrivals cannot show.
|
|
2340
|
+
*/
|
|
2341
|
+
#feedOverlayRecoveredToBase(
|
|
2342
|
+
inst: OverlayInstance,
|
|
2343
|
+
blockStart: number,
|
|
2344
|
+
symbols: ProcessFecBlockResult["symbols"],
|
|
2345
|
+
baseSsIds: ReadonlyMap<ProcessFecBlockResult["symbols"][number], number>,
|
|
2346
|
+
): void {
|
|
2347
|
+
const fec = this.#fecManager;
|
|
2348
|
+
const owner = inst.leg.sourcePacketId;
|
|
2349
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
2350
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
2351
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
2352
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
2353
|
+
const feeds: Array<readonly [number, Uint8Array]> = [];
|
|
2354
|
+
for (const sym of symbols) {
|
|
2355
|
+
// A packet the base received already sits in its block.
|
|
2356
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
2357
|
+
const ssId = baseSsIds.get(sym);
|
|
2358
|
+
if (ssId === undefined) {
|
|
2359
|
+
this.#overlayStats.baseUndetermined++;
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
routed.delete(ssId);
|
|
2363
|
+
routed.add(ssId);
|
|
2364
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value!);
|
|
2365
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
2366
|
+
}
|
|
2367
|
+
// Any feed can recover the base block, whose routing must already skip
|
|
2368
|
+
// every packet this recovery routed: all are registered before the first.
|
|
2369
|
+
for (const [ssId, symbolData] of feeds) {
|
|
2370
|
+
// A recovery routed above can retire the owner (an in-place switch).
|
|
2371
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
2372
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
2377
|
+
#feedRebuiltToBase(owner: number, name: string | undefined, ssId: number, symbolData: Uint8Array): void {
|
|
2378
|
+
const fec = this.#fecManager;
|
|
2379
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
2380
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
2381
|
+
if (coords == null) {
|
|
2382
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
2383
|
+
pending.delete(ssId);
|
|
2384
|
+
// Its own copy: the symbol is a view of the whole recovered block.
|
|
2385
|
+
pending.set(ssId, symbolData.slice());
|
|
2386
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value!);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
2390
|
+
if (!fed.taken) return;
|
|
2391
|
+
this.#overlayStats.baseFed++;
|
|
2392
|
+
if (fed.recovered === null) return;
|
|
2393
|
+
const sbn = coords.sbn;
|
|
2394
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
2395
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
2396
|
+
fec.countRecovery(owner, sbn, name);
|
|
2397
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
2402
|
+
#feedPendingRebuilt(owner: number, name: string | undefined, block: { start: number; length: number }): void {
|
|
2403
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
2404
|
+
if (pending === undefined || pending.size === 0) return;
|
|
2405
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
2406
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
2407
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
2408
|
+
pending.delete(ssId);
|
|
2409
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2410
|
+
}
|
|
2158
2411
|
}
|
|
2159
2412
|
|
|
2160
2413
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
@@ -2190,6 +2443,9 @@ export class MmtpRouter {
|
|
|
2190
2443
|
const mpu = sym.fragment.mpuSequenceNumber;
|
|
2191
2444
|
if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
|
|
2192
2445
|
if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
|
|
2446
|
+
// Not overlayBaseSsId: a wrong sbn only keeps or wrongly stops a keyframe
|
|
2447
|
+
// FETCH (§6.6, a SHOULD; no symbol moves), and wire psn plus offset holds
|
|
2448
|
+
// under a constant re-stamp shift, which overlayBaseSsId's psn test refuses.
|
|
2193
2449
|
const ssId = (sym.packetSequenceNumber + ssIdSeqOffset) >>> 0;
|
|
2194
2450
|
// ponytail: a base block no repair has stated yet (an undelimited hole) is not reported.
|
|
2195
2451
|
const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
|
|
@@ -2329,12 +2585,13 @@ export class MmtpRouter {
|
|
|
2329
2585
|
if (!entry) return;
|
|
2330
2586
|
entry.receiver.release();
|
|
2331
2587
|
entry.protected = false;
|
|
2588
|
+
entry.userAgent = false;
|
|
2332
2589
|
// ponytail: drops every governed block's record, not only this flow's; per-flow if joins multiply.
|
|
2333
2590
|
this.#assistBlocks.clear();
|
|
2334
2591
|
this.#assistFailures.clear();
|
|
2335
2592
|
}
|
|
2336
2593
|
|
|
2337
|
-
/**
|
|
2594
|
+
/** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
|
|
2338
2595
|
get unadmittedListedDropCount(): number {
|
|
2339
2596
|
return this.#unadmittedListedDrops;
|
|
2340
2597
|
}
|
|
@@ -2384,6 +2641,14 @@ export class MmtpRouter {
|
|
|
2384
2641
|
return false;
|
|
2385
2642
|
}
|
|
2386
2643
|
|
|
2644
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
2645
|
+
#userAgentGoverns(packetId: number): boolean {
|
|
2646
|
+
for (const entry of this.#provenance.values()) {
|
|
2647
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
2648
|
+
}
|
|
2649
|
+
return false;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2387
2652
|
/**
|
|
2388
2653
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
2389
2654
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -2503,7 +2768,7 @@ export class MmtpRouter {
|
|
|
2503
2768
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2504
2769
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2505
2770
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2506
|
-
})) {
|
|
2771
|
+
}, true)) {
|
|
2507
2772
|
this.#unlistedAssistedRecoveries++;
|
|
2508
2773
|
this.#assistBlocks.delete(key);
|
|
2509
2774
|
return;
|
|
@@ -2965,6 +3230,11 @@ export class MmtpRouter {
|
|
|
2965
3230
|
)) return;
|
|
2966
3231
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
2967
3232
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
3233
|
+
// Rebuilt sources that waited for this block join it once the block took
|
|
3234
|
+
// a repair, so it has a deadline; still ahead of the repair, as the held
|
|
3235
|
+
// sources above are.
|
|
3236
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
3237
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
2968
3238
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2969
3239
|
const recovered = fec.feedRepairSymbol(
|
|
2970
3240
|
ownerPacketId,
|
|
@@ -3145,6 +3415,7 @@ export class MmtpRouter {
|
|
|
3145
3415
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
3146
3416
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
3147
3417
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
3418
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
3148
3419
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
3149
3420
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
3150
3421
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -3959,6 +4230,15 @@ export class MmtpRouter {
|
|
|
3959
4230
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
3960
4231
|
return;
|
|
3961
4232
|
}
|
|
4233
|
+
// The user agent verified this datagram against the flow's records
|
|
4234
|
+
// itself, and the records never reach the page: admitted as is. The
|
|
4235
|
+
// flow stays registered, so its MoQ copies are still checked.
|
|
4236
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
4237
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
4238
|
+
protectedFlow.userAgent = true;
|
|
4239
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
3962
4242
|
// Held: released by a record (feedProvenanceRecord) or expired. Duplicate / overflow: dropped.
|
|
3963
4243
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
3964
4244
|
if (verdict === "admitted") {
|
|
@@ -3983,6 +4263,18 @@ export class MmtpRouter {
|
|
|
3983
4263
|
}
|
|
3984
4264
|
}
|
|
3985
4265
|
|
|
4266
|
+
// A multicast datagram that names no flow is unverified. Of a track a
|
|
4267
|
+
// user-agent-admitted flow lists it would feed that flow's live decoder,
|
|
4268
|
+
// whose recoveries are kept without a record (#routeVerifiedFecSymbols):
|
|
4269
|
+
// its source is dropped here and its repair set aside below, as for
|
|
4270
|
+
// unlisted MoQ. Under a record-only flow it is read as before: those
|
|
4271
|
+
// recoveries still need a record.
|
|
4272
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === undefined;
|
|
4273
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
4274
|
+
this.#unadmittedListedDrops++;
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
|
|
3986
4278
|
if (info.isRepair) {
|
|
3987
4279
|
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
3988
4280
|
// Read from the header parsed above rather than parsing it again
|
|
@@ -4003,9 +4295,10 @@ export class MmtpRouter {
|
|
|
4003
4295
|
);
|
|
4004
4296
|
if (!repair) return;
|
|
4005
4297
|
if (ownerPacketId != null && this.#fecManager) {
|
|
4006
|
-
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast)
|
|
4298
|
+
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast);
|
|
4299
|
+
// a flow-less multicast one is unverified.
|
|
4007
4300
|
if (
|
|
4008
|
-
source === "moq" && !manifestAdmitted &&
|
|
4301
|
+
((flowless && this.#userAgentGoverns(ownerPacketId)) || (source === "moq" && !manifestAdmitted)) &&
|
|
4009
4302
|
this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)
|
|
4010
4303
|
) return;
|
|
4011
4304
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
@@ -4345,6 +4638,7 @@ export class MmtpRouter {
|
|
|
4345
4638
|
if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
|
|
4346
4639
|
this.#feedOverlaySource(
|
|
4347
4640
|
info.packetId, catalogTrackName, packet, parsed.header, info.sequenceNumber, info.rapFlag, source,
|
|
4641
|
+
fecPayloadId?.ssId,
|
|
4348
4642
|
);
|
|
4349
4643
|
}
|
|
4350
4644
|
}
|
|
@@ -4573,6 +4867,7 @@ export class MmtpRouter {
|
|
|
4573
4867
|
stream = {
|
|
4574
4868
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
4575
4869
|
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
4870
|
+
overlayToBase: { routed: new Set(), pending: new Map() },
|
|
4576
4871
|
};
|
|
4577
4872
|
byName.set(catalogTrackName, stream);
|
|
4578
4873
|
}
|
|
@@ -4771,8 +5066,28 @@ export class MmtpRouter {
|
|
|
4771
5066
|
data: Uint8Array,
|
|
4772
5067
|
meta: RoutedFrameMeta,
|
|
4773
5068
|
cb: FrameCallback | null,
|
|
5069
|
+
/** The base SS_ID the FEC release gate ordered the frame by, if any. */
|
|
5070
|
+
fecSbn: number | undefined,
|
|
4774
5071
|
): void {
|
|
4775
5072
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
5073
|
+
// The FEC release gate let this frame out in order: only once every
|
|
5074
|
+
// block between it and the frame before it resolved, or because it
|
|
5075
|
+
// starts where that frame ended. Nothing still in flight can precede it
|
|
5076
|
+
// then, so it plays now, as a source frame does; holding it for the next
|
|
5077
|
+
// intact frame cost every repaired burst its length in decoder input
|
|
5078
|
+
// (BLO-52085). An overlay recovery names no base block for the gate to
|
|
5079
|
+
// wait on, so past a gap it may be ahead of frames still in flight (an
|
|
5080
|
+
// overlay that outran the wire, BLO-53342): it keeps waiting for the
|
|
5081
|
+
// source cursor below, unless it starts where the last emitted frame
|
|
5082
|
+
// ended (on this direct path, the delivered high water).
|
|
5083
|
+
if (this.#isFecTrack(packetId)) {
|
|
5084
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
5085
|
+
if (fecSbn !== undefined || (high !== undefined && authoredAdjacent(high, meta))) {
|
|
5086
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, frame => frame.timestamp <= meta.timestamp, cb);
|
|
5087
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
4776
5091
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
4777
5092
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
4778
5093
|
this.#drainDirectRecoveredVideo(
|
|
@@ -4950,6 +5265,7 @@ export class MmtpRouter {
|
|
|
4950
5265
|
ownerPacketId?: number,
|
|
4951
5266
|
catalogTrackName?: string,
|
|
4952
5267
|
complete?: () => void,
|
|
5268
|
+
assisted = false,
|
|
4953
5269
|
): boolean {
|
|
4954
5270
|
// MMTP path: use processFecBlock for MMTP-wrapped recovery.
|
|
4955
5271
|
// Recovered symbols are individual MMTP packets that may be fragments
|
|
@@ -4974,7 +5290,7 @@ export class MmtpRouter {
|
|
|
4974
5290
|
ownerPacketId,
|
|
4975
5291
|
blockProfile,
|
|
4976
5292
|
});
|
|
4977
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
5293
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
4978
5294
|
}
|
|
4979
5295
|
|
|
4980
5296
|
/**
|
|
@@ -4987,6 +5303,19 @@ export class MmtpRouter {
|
|
|
4987
5303
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
4988
5304
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
4989
5305
|
* outside a protected flow.
|
|
5306
|
+
*
|
|
5307
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
5308
|
+
* without a record: while the flow is protected that decoder takes only
|
|
5309
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
5310
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
5311
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
5312
|
+
* unlisted MoQ repair and still needs a record.
|
|
5313
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
5314
|
+
* ever mixes verified and unverified live inputs.
|
|
5315
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
5316
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
5317
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
5318
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
4990
5319
|
*/
|
|
4991
5320
|
#routeVerifiedFecSymbols(
|
|
4992
5321
|
blockId: number,
|
|
@@ -4994,8 +5323,22 @@ export class MmtpRouter {
|
|
|
4994
5323
|
ownerPacketId?: number,
|
|
4995
5324
|
catalogTrackName?: string,
|
|
4996
5325
|
complete?: () => void,
|
|
5326
|
+
assisted = false,
|
|
4997
5327
|
): boolean {
|
|
4998
|
-
|
|
5328
|
+
// A packet a keyframe overlay recovery routed before it fed the packet to
|
|
5329
|
+
// this block (#feedOverlayRecoveredToBase) is not routed again.
|
|
5330
|
+
const overlayRouted = ownerPacketId == null
|
|
5331
|
+
? undefined
|
|
5332
|
+
: this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
5333
|
+
if (overlayRouted !== undefined && overlayRouted.size > 0) {
|
|
5334
|
+
result = {
|
|
5335
|
+
...result,
|
|
5336
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index))),
|
|
5337
|
+
};
|
|
5338
|
+
}
|
|
5339
|
+
const entry = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
5340
|
+
const receiver = entry === undefined || entry === "ambiguous" ? entry : entry.receiver;
|
|
5341
|
+
const userAgent = !assisted && entry !== undefined && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
4999
5342
|
let recovered = true;
|
|
5000
5343
|
if (receiver) {
|
|
5001
5344
|
const parsed = this.#stream(ownerPacketId!, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -5008,6 +5351,8 @@ export class MmtpRouter {
|
|
|
5008
5351
|
let admitted = false;
|
|
5009
5352
|
if (receiver === "ambiguous") {
|
|
5010
5353
|
this.#ambiguousRecoveredDrops++;
|
|
5354
|
+
} else if (userAgent) {
|
|
5355
|
+
admitted = true;
|
|
5011
5356
|
} else {
|
|
5012
5357
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
5013
5358
|
wire.set(sym.rawSymbol);
|
|
@@ -5033,7 +5378,7 @@ export class MmtpRouter {
|
|
|
5033
5378
|
#provenanceFor(
|
|
5034
5379
|
packetId: number,
|
|
5035
5380
|
catalogTrackName: string | undefined,
|
|
5036
|
-
):
|
|
5381
|
+
): ProvenanceEntry | "ambiguous" | undefined {
|
|
5037
5382
|
// A caller that names no track matches every name the packet id was admitted under.
|
|
5038
5383
|
const byName = this.#provenanceStreamFlow.get(packetId);
|
|
5039
5384
|
const flows = new Set(
|
|
@@ -5042,12 +5387,12 @@ export class MmtpRouter {
|
|
|
5042
5387
|
: byName.has(catalogTrackName) ? [byName.get(catalogTrackName)!] : [],
|
|
5043
5388
|
);
|
|
5044
5389
|
if (flows.size > 1) return "ambiguous";
|
|
5045
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
5046
|
-
let match:
|
|
5047
|
-
for (const
|
|
5048
|
-
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
5390
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
5391
|
+
let match: ProvenanceEntry | undefined;
|
|
5392
|
+
for (const entry of this.#provenance.values()) {
|
|
5393
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
5049
5394
|
if (match) return "ambiguous";
|
|
5050
|
-
match =
|
|
5395
|
+
match = entry;
|
|
5051
5396
|
}
|
|
5052
5397
|
}
|
|
5053
5398
|
return match;
|
|
@@ -5084,7 +5429,10 @@ export class MmtpRouter {
|
|
|
5084
5429
|
...result,
|
|
5085
5430
|
symbols: result.symbols.map((sym) => {
|
|
5086
5431
|
const wire = (this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset) >>> 0;
|
|
5087
|
-
if (wire === sym.packetSequenceNumber)
|
|
5432
|
+
if (wire === sym.packetSequenceNumber) {
|
|
5433
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
5434
|
+
return sym;
|
|
5435
|
+
}
|
|
5088
5436
|
this.#restampedRecoveredSeqs++;
|
|
5089
5437
|
this.#restampedStreams.add(streamKey);
|
|
5090
5438
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -5141,6 +5489,43 @@ export class MmtpRouter {
|
|
|
5141
5489
|
);
|
|
5142
5490
|
}
|
|
5143
5491
|
|
|
5492
|
+
// A recovered head whose sample FT=1 has not named yet (an overlay that
|
|
5493
|
+
// outran the source path) would complete its data unit untimed: the unit
|
|
5494
|
+
// is dropped, marked completed, and the timed source copy is then refused
|
|
5495
|
+
// (BLO-53342). Hand it to the parser instead, which holds it in its
|
|
5496
|
+
// awaiting-timing queue and feeds it again when FT=1 lands; its recovered
|
|
5497
|
+
// continuations park in the reassembler meanwhile. If the source path
|
|
5498
|
+
// lost the head, this held copy is the keyframe (BLO-53369).
|
|
5499
|
+
const untimedHead = (sym: (typeof result.symbols)[number]): boolean =>
|
|
5500
|
+
sym.fragmentType === 2 &&
|
|
5501
|
+
sym.fragment.fragmentationIndicator === 1 &&
|
|
5502
|
+
sym.fragment.movieFragmentSequenceNumber !== undefined &&
|
|
5503
|
+
sym.fragment.sampleNumber !== undefined &&
|
|
5504
|
+
!this.#parser.hasAuthoredTimingFor(
|
|
5505
|
+
ownerPacketId,
|
|
5506
|
+
sym.fragment.mpuSequenceNumber,
|
|
5507
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
5508
|
+
sym.fragment.sampleNumber,
|
|
5509
|
+
catalogTrackName,
|
|
5510
|
+
);
|
|
5511
|
+
for (const sym of result.symbols) {
|
|
5512
|
+
if (!untimedHead(sym) ||
|
|
5513
|
+
this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
5514
|
+
this.#feedViaParser(
|
|
5515
|
+
sym.rawSymbol,
|
|
5516
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
5517
|
+
undefined,
|
|
5518
|
+
{
|
|
5519
|
+
skipFecPayloadId: true,
|
|
5520
|
+
recovered: true,
|
|
5521
|
+
deliveryProvenance,
|
|
5522
|
+
...fecSbnOf(sym.index),
|
|
5523
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
5524
|
+
},
|
|
5525
|
+
);
|
|
5526
|
+
}
|
|
5527
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
5528
|
+
|
|
5144
5529
|
// MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
|
|
5145
5530
|
// BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
|
|
5146
5531
|
// frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
|