@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +1949 -374
- 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 +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- 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 +6 -5
- 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 +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- 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 +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- 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 +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- 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 +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- 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 +336 -59
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- 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.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- 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 +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -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, IsoSpanFailure, 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,92 @@ 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
|
+
* or only the overlay's own evidence did, which anchors the release gate
|
|
221
|
+
* alone (BLO-52085). */
|
|
222
|
+
baseUndetermined: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The newest received overlay member of one MPU. `psnOffset` (overlay SS_ID
|
|
227
|
+
* minus wire packet_sequence_number) restores a rebuilt packet's wire
|
|
228
|
+
* sequence; the member's own SS_IDs anchor a rebuilt packet's base SS_ID
|
|
229
|
+
* (overlayBaseSsId).
|
|
230
|
+
*/
|
|
231
|
+
interface OverlayMemberAnchor {
|
|
232
|
+
readonly psnOffset: number;
|
|
233
|
+
readonly overlaySsId: number;
|
|
234
|
+
/** The member's base SS_ID, from its Source FEC Payload ID. */
|
|
235
|
+
readonly baseSsId?: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The base SS_ID of a packet the overlay rebuilt at `overlaySsId`, which an
|
|
240
|
+
* overlay coordinate alone never yields (draft-ramadan-moq-mmt-02 7.1). Both
|
|
241
|
+
* SS_IDs advance by one per packet along a run of consecutive overlay
|
|
242
|
+
* members, so base minus overlay SS_ID is the anchor's along a run the wire
|
|
243
|
+
* proves to be all members: one over which the rebuilt packet's protected
|
|
244
|
+
* packet_sequence_number (`headerPsn`) advanced from the anchor's exactly as
|
|
245
|
+
* the overlay SS_ID did (`restoredPsn`), so no other packet lies inside it.
|
|
246
|
+
* A hop that re-stamped packet_sequence_number after protection fails the
|
|
247
|
+
* test. Undefined when it fails.
|
|
248
|
+
*/
|
|
249
|
+
function overlayBaseSsId(
|
|
250
|
+
anchor: OverlayMemberAnchor,
|
|
251
|
+
overlaySsId: number,
|
|
252
|
+
restoredPsn: number,
|
|
253
|
+
headerPsn: number,
|
|
254
|
+
): number | undefined {
|
|
255
|
+
if (anchor.baseSsId === undefined || headerPsn !== restoredPsn) return undefined;
|
|
256
|
+
return serial32Add(anchor.baseSsId, (overlaySsId - anchor.overlaySsId) | 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The base SS_ID of a rebuilt packet no received member of its MPU anchors,
|
|
261
|
+
* from its protected packet_sequence_number (`headerPsn`). SS_ID advances by
|
|
262
|
+
* one per FEC source packet (mmt-02 7.1) and psn by one per packet, so on a
|
|
263
|
+
* track whose every packet is an FEC source packet the sender keeps
|
|
264
|
+
* SS_ID - psn fixed. `offset` (SS_ID - psn of the stream's latest received
|
|
265
|
+
* source) is that sender value while evidence showed it (`unstampedOffset`):
|
|
266
|
+
* a base recovery's (#unstampedSsIdSeqOffsetByStream), or the overlay's
|
|
267
|
+
* alone (#overlayUnstampedSsIdSeqOffsetByStream). A hop that re-stamps psn
|
|
268
|
+
* moves the offset away from the one shown, which voids it, as does a packet
|
|
269
|
+
* with no SS_ID between the packet that showed it and the latest received
|
|
270
|
+
* source (the shown span). Undefined without both.
|
|
271
|
+
* ponytail: exact only if every packet from the rebuilt one to the latest
|
|
272
|
+
* received source is an FEC source packet, as FFmpeg's muxer sends a track
|
|
273
|
+
* it protects. fec-core 8.3 lets a publisher leave one unprotected
|
|
274
|
+
* (FEC Type 0): outside the shown span it goes unseen, and each one puts the
|
|
275
|
+
* rebuilt packet a base slot off. If such a publisher appears, have mmt-02
|
|
276
|
+
* require FEC Type 1 across a protected track, or refuse a headerPsn outside
|
|
277
|
+
* the shown span.
|
|
278
|
+
*/
|
|
279
|
+
function unstampedBaseSsId(
|
|
280
|
+
offset: number | undefined,
|
|
281
|
+
unstampedOffset: number | undefined,
|
|
282
|
+
headerPsn: number,
|
|
283
|
+
): number | undefined {
|
|
284
|
+
if (offset === undefined || offset !== unstampedOffset) return undefined;
|
|
285
|
+
return (headerPsn + offset) >>> 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Base SS_IDs of overlay-routed packets kept per stream (see OverlayToBase). */
|
|
289
|
+
const OVERLAY_ROUTED_LIMIT = 1024;
|
|
290
|
+
/** Rebuilt sources kept per stream for a base block no repair stated yet. */
|
|
291
|
+
const OVERLAY_PENDING_LIMIT = 256;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* One stream's keyframe-overlay recoveries on their way into its base
|
|
295
|
+
* decoder (joint decoding, BLO-52085).
|
|
296
|
+
*/
|
|
297
|
+
interface OverlayToBase {
|
|
298
|
+
/** Base SS_IDs of the packets an overlay recovery routed (as media, or held
|
|
299
|
+
* for their FT=1): a base recovery does not route them a second time. */
|
|
300
|
+
readonly routed: Set<number>;
|
|
301
|
+
/** Rebuilt sources, by base SS_ID, whose base block no repair stated yet. */
|
|
302
|
+
readonly pending: Map<number, Uint8Array>;
|
|
214
303
|
}
|
|
215
304
|
|
|
216
305
|
interface OverlayInstance {
|
|
@@ -222,14 +311,22 @@ interface OverlayInstance {
|
|
|
222
311
|
readonly k: number;
|
|
223
312
|
/** The newest overlay symbol seen: its overlay SS_ID and wire sequence. */
|
|
224
313
|
last?: { ssId: number; psn: number };
|
|
225
|
-
/** MPU sequence number →
|
|
226
|
-
readonly offsets: Map<number,
|
|
314
|
+
/** MPU sequence number → its newest received member. */
|
|
315
|
+
readonly offsets: Map<number, OverlayMemberAnchor>;
|
|
227
316
|
/** Overlay blocks a MoQ symbol fed (multicast-02 7.2.7). */
|
|
228
317
|
readonly moqBlocks: Set<number>;
|
|
318
|
+
/** Overlay SS_ID → protected packet_sequence_number of each packet rebuilt
|
|
319
|
+
* with no base SS_ID, until its base copy lands (#feedOverlaySource). */
|
|
320
|
+
readonly rebuilt: Map<number, number>;
|
|
229
321
|
}
|
|
230
322
|
|
|
231
323
|
/** Offsets kept per overlay, one per MPU: a keyframe's fragments share one. */
|
|
232
324
|
const OVERLAY_OFFSET_MPUS = 16;
|
|
325
|
+
/** Rebuilt packets kept per overlay whose base copy has not landed (OverlayInstance.rebuilt).
|
|
326
|
+
* ponytail: this cap is the only eviction for a base copy that never lands
|
|
327
|
+
* (oldest first; an overlay epoch reset clears all). Bound records by SS_ID
|
|
328
|
+
* age instead if stale ones ever crowd out live ones. */
|
|
329
|
+
const OVERLAY_REBUILT_LIMIT = 256;
|
|
233
330
|
/** A MoQ-fed overlay block is forgotten once it starts this many K_overlay
|
|
234
331
|
* behind the newest overlay SS_ID: the overlay locator, which keeps a fixed
|
|
235
332
|
* few blocks (no catalog timing), has retired it long before, so it can no
|
|
@@ -245,6 +342,14 @@ export interface MmtpIngressContext {
|
|
|
245
342
|
* the same ingress and `feedPacket` splits them off.
|
|
246
343
|
*/
|
|
247
344
|
flow?: string;
|
|
345
|
+
/**
|
|
346
|
+
* `USER_AGENT_VERIFIED` when the datagram came from the browser's own
|
|
347
|
+
* MulticastReceiver, which verified the flow's records itself and delivers
|
|
348
|
+
* only admitted data (`userAgentVerifiedIngress` in
|
|
349
|
+
* `@blockcast/mmt-transport`). A protected multicast datagram carrying it
|
|
350
|
+
* is admitted without a record. Any other value means nothing.
|
|
351
|
+
*/
|
|
352
|
+
admittedBy?: symbol;
|
|
248
353
|
}
|
|
249
354
|
|
|
250
355
|
/** One physical stream: an MMTP packet ID and the catalog track it carries. */
|
|
@@ -502,6 +607,9 @@ export type RoutedPacketMeta = {
|
|
|
502
607
|
mmtpTimestamp: bigint;
|
|
503
608
|
/**
|
|
504
609
|
* The MMTP header RAP flag: the packet starts a random access point. On a
|
|
610
|
+
* track under a keyframe overlay it is also set on the FT=0 and FT=1 data
|
|
611
|
+
* units that govern a covered keyframe (draft-ramadan-moq-mmt-02 7.4.2), so
|
|
612
|
+
* it can precede the keyframe's first MFU fragment. On a
|
|
505
613
|
* `recovered` packet it is read from the FEC-reconstructed header, which is
|
|
506
614
|
* byte-exact outside packet_sequence_number, so it is the flag the
|
|
507
615
|
* publisher sent. The router's own gap decisions deliberately ignore it for
|
|
@@ -565,6 +673,7 @@ interface StreamLedgers {
|
|
|
565
673
|
* the pair.
|
|
566
674
|
*/
|
|
567
675
|
readonly duplicates: StreamDuplicateLedgers;
|
|
676
|
+
readonly overlayToBase: OverlayToBase;
|
|
568
677
|
}
|
|
569
678
|
|
|
570
679
|
const NO_FEC_MANAGER_HORIZON: DuplicateHorizon = Object.freeze({
|
|
@@ -652,6 +761,17 @@ export interface FECManagerLike {
|
|
|
652
761
|
catalogTrackName?: string,
|
|
653
762
|
): Uint8Array | null;
|
|
654
763
|
|
|
764
|
+
/** Feed a source another FEC instance over the same packets rebuilt (a
|
|
765
|
+
* keyframe overlay) to its base block, which a repair must already have
|
|
766
|
+
* stated; not a network arrival (FECManager.feedRebuiltSourceSymbol).
|
|
767
|
+
* Optional: without it the overlay and the base decode apart. */
|
|
768
|
+
feedRebuiltSourceSymbol?(
|
|
769
|
+
packetId: number,
|
|
770
|
+
ssId: number,
|
|
771
|
+
symbolData: Uint8Array,
|
|
772
|
+
catalogTrackName?: string,
|
|
773
|
+
): RebuiltSourceFeed;
|
|
774
|
+
|
|
655
775
|
/** How long ago the stream's oldest open block with an FEC deadline took its first symbol (optional). */
|
|
656
776
|
oldestOpenBlockAgeMs?(packetId: number, catalogTrackName: string): number | undefined;
|
|
657
777
|
|
|
@@ -723,13 +843,14 @@ export interface FECManagerLike {
|
|
|
723
843
|
|
|
724
844
|
/** Track a first-arrival network source symbol for block completion and
|
|
725
845
|
* classification. Refreshes the inactivity deadline and marks clean when
|
|
726
|
-
* K arrive. */
|
|
846
|
+
* K arrive. `held`: a source held unplaced since it arrived, placed now. */
|
|
727
847
|
trackBlock(
|
|
728
848
|
sbn: number,
|
|
729
849
|
packetId: number,
|
|
730
850
|
catalogTrackName: string | undefined,
|
|
731
851
|
esi: number,
|
|
732
852
|
evidence: SourceSymbolEvidence,
|
|
853
|
+
held?: boolean,
|
|
733
854
|
): void;
|
|
734
855
|
|
|
735
856
|
/** True once the packetId has a per-track FEC configuration. */
|
|
@@ -899,6 +1020,7 @@ export interface FECManagerLike {
|
|
|
899
1020
|
packetId: number,
|
|
900
1021
|
catalogTrackName: string | undefined,
|
|
901
1022
|
blockClass: BlockClass,
|
|
1023
|
+
span?: IsoSpanFailure,
|
|
902
1024
|
) => void)
|
|
903
1025
|
| null;
|
|
904
1026
|
|
|
@@ -946,6 +1068,15 @@ function sourceSymbolEvidence(
|
|
|
946
1068
|
};
|
|
947
1069
|
}
|
|
948
1070
|
|
|
1071
|
+
/** One protected flow: its 7.2.7 receiver, and the ingress its datagrams arrive with. */
|
|
1072
|
+
interface ProvenanceEntry {
|
|
1073
|
+
receiver: BcProvenanceV2Receiver;
|
|
1074
|
+
ingress: MmtpIngressContext;
|
|
1075
|
+
protected?: boolean;
|
|
1076
|
+
/** A datagram the user agent verified admitted the flow (`admittedBy`). */
|
|
1077
|
+
userAgent?: boolean;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
949
1080
|
export class MmtpRouter {
|
|
950
1081
|
readonly #parser: MmtpContainerParser;
|
|
951
1082
|
// Source AND FEC-recovered MFU fragments share ONE reassembler — the parser's
|
|
@@ -955,12 +1086,10 @@ export class MmtpRouter {
|
|
|
955
1086
|
#defaultFecMapper: FecBlockMapper | null;
|
|
956
1087
|
#fecMappersByPacketId = new Map<number, FecBlockMapper>();
|
|
957
1088
|
/** 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
|
-
>();
|
|
1089
|
+
readonly #provenance = new Map<string, ProvenanceEntry>();
|
|
962
1090
|
#onFlowProtected: ((flow: string) => void) | null = null;
|
|
963
1091
|
#unadmittedListedDrops = 0;
|
|
1092
|
+
#admittedSourceDatagrams = 0;
|
|
964
1093
|
#unlistedRepairSetAside = 0;
|
|
965
1094
|
readonly #assistBlocks = new Map<string, AssistBlock>();
|
|
966
1095
|
readonly #assistFailures = new Map<string, number>();
|
|
@@ -1029,6 +1158,26 @@ export class MmtpRouter {
|
|
|
1029
1158
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
1030
1159
|
*/
|
|
1031
1160
|
#ssIdSeqOffsetByStream = new Map<string, number>();
|
|
1161
|
+
/**
|
|
1162
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
1163
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
1164
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
1165
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
1166
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
1167
|
+
* an MPU no member of which arrived).
|
|
1168
|
+
*/
|
|
1169
|
+
#unstampedSsIdSeqOffsetByStream = new Map<string, number>();
|
|
1170
|
+
/**
|
|
1171
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
1172
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
1173
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
1174
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
1175
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
1176
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
1177
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
1178
|
+
* this offset did.
|
|
1179
|
+
*/
|
|
1180
|
+
#overlayUnstampedSsIdSeqOffsetByStream = new Map<string, number>();
|
|
1032
1181
|
/**
|
|
1033
1182
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
1034
1183
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -1077,6 +1226,7 @@ export class MmtpRouter {
|
|
|
1077
1226
|
packetId: number,
|
|
1078
1227
|
catalogTrackName: string | undefined,
|
|
1079
1228
|
blockClass: BlockClass,
|
|
1229
|
+
span?: IsoSpanFailure,
|
|
1080
1230
|
) => void)
|
|
1081
1231
|
| null = null;
|
|
1082
1232
|
/** Typed observer of every gap decision the release gate makes. */
|
|
@@ -1106,7 +1256,7 @@ export class MmtpRouter {
|
|
|
1106
1256
|
#restampedStreams = new Set<string>();
|
|
1107
1257
|
#overlayStats: KeyframeOverlayStats = {
|
|
1108
1258
|
repairDropped: 0, ownerRefused: 0, recovered: 0, restarts: 0,
|
|
1109
|
-
unsequencedDropped: 0, protectedDropped: 0,
|
|
1259
|
+
unsequencedDropped: 0, protectedDropped: 0, baseFed: 0, baseUndetermined: 0,
|
|
1110
1260
|
};
|
|
1111
1261
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
1112
1262
|
#onDeltaBlockCompleteCb:
|
|
@@ -1174,6 +1324,7 @@ export class MmtpRouter {
|
|
|
1174
1324
|
packetId: number,
|
|
1175
1325
|
catalogTrackName: string | undefined,
|
|
1176
1326
|
blockClass: BlockClass,
|
|
1327
|
+
span?: IsoSpanFailure,
|
|
1177
1328
|
) => void;
|
|
1178
1329
|
/** Internal gap-decision sink owned by the composing transport. */
|
|
1179
1330
|
onFrameGap?: (decision: FrameGapDecision) => void;
|
|
@@ -1385,7 +1536,7 @@ export class MmtpRouter {
|
|
|
1385
1536
|
const cb = this.#getFrameCallback(packetId);
|
|
1386
1537
|
if (isVideo && !assembler) {
|
|
1387
1538
|
if (frameMeta.recovered) {
|
|
1388
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
1539
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
1389
1540
|
} else {
|
|
1390
1541
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
1391
1542
|
}
|
|
@@ -1407,6 +1558,7 @@ export class MmtpRouter {
|
|
|
1407
1558
|
packetId: number,
|
|
1408
1559
|
catalogTrackName: string | undefined,
|
|
1409
1560
|
blockClass: BlockClass,
|
|
1561
|
+
span?: IsoSpanFailure,
|
|
1410
1562
|
) => void)
|
|
1411
1563
|
| undefined,
|
|
1412
1564
|
): void {
|
|
@@ -1459,6 +1611,7 @@ export class MmtpRouter {
|
|
|
1459
1611
|
packetId: number,
|
|
1460
1612
|
catalogTrackName: string | undefined,
|
|
1461
1613
|
blockClass: BlockClass,
|
|
1614
|
+
span?: IsoSpanFailure,
|
|
1462
1615
|
) => {
|
|
1463
1616
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
1464
1617
|
if (
|
|
@@ -1467,8 +1620,13 @@ export class MmtpRouter {
|
|
|
1467
1620
|
) {
|
|
1468
1621
|
this.#onDeltaBlockCompleteCb?.(packetId, true); // failed=true
|
|
1469
1622
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1623
|
+
if (span === undefined) {
|
|
1624
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
1625
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
1626
|
+
} else {
|
|
1627
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
1628
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
1629
|
+
}
|
|
1472
1630
|
};
|
|
1473
1631
|
}
|
|
1474
1632
|
}
|
|
@@ -1521,8 +1679,9 @@ export class MmtpRouter {
|
|
|
1521
1679
|
|
|
1522
1680
|
/**
|
|
1523
1681
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
1524
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
1525
|
-
*
|
|
1682
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
1683
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
1684
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
1526
1685
|
*/
|
|
1527
1686
|
get restampedRecoveredSequenceCount(): number {
|
|
1528
1687
|
return this.#restampedRecoveredSeqs;
|
|
@@ -1746,6 +1905,11 @@ export class MmtpRouter {
|
|
|
1746
1905
|
): CmafBlockAssembler {
|
|
1747
1906
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
1748
1907
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
1908
|
+
// The new assembler has no source cursor: the release gate must not hold
|
|
1909
|
+
// an overlay keyframe for one either (FecReleaseSequencer.#cursorPassed).
|
|
1910
|
+
// What the gate holds that unanchored had already released to the
|
|
1911
|
+
// assembler being replaced goes to it first, and plays with its flush.
|
|
1912
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
1749
1913
|
if (previous) {
|
|
1750
1914
|
previous.flush();
|
|
1751
1915
|
previous.dispose();
|
|
@@ -1817,6 +1981,7 @@ export class MmtpRouter {
|
|
|
1817
1981
|
packetId: number,
|
|
1818
1982
|
catalogTrackName: string | undefined,
|
|
1819
1983
|
blockClass: BlockClass,
|
|
1984
|
+
span?: IsoSpanFailure,
|
|
1820
1985
|
) => void,
|
|
1821
1986
|
): void {
|
|
1822
1987
|
this.#onBlockFailed = cb;
|
|
@@ -1900,6 +2065,11 @@ export class MmtpRouter {
|
|
|
1900
2065
|
return { ...this.#overlayStats };
|
|
1901
2066
|
}
|
|
1902
2067
|
|
|
2068
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
2069
|
+
get recoveredHeadsAwaitingTiming(): Readonly<MmtpRecoveredHeadTimingStats> {
|
|
2070
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1903
2073
|
#isOverlayRepair(repairPacketId: number, repairTrackName?: string): boolean {
|
|
1904
2074
|
return this.#overlayLegs.has(repairPacketId) ||
|
|
1905
2075
|
(repairTrackName !== undefined && this.#overlayRepairNames.has(repairTrackName));
|
|
@@ -1916,7 +2086,7 @@ export class MmtpRouter {
|
|
|
1916
2086
|
const leg = this.#overlayLegs.get(repairPacketId);
|
|
1917
2087
|
if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
|
|
1918
2088
|
const inst: OverlayInstance = {
|
|
1919
|
-
repairPacketId, streamName, leg: { ...leg }, fec, k, offsets: new Map(), moqBlocks: new Set(),
|
|
2089
|
+
repairPacketId, streamName, leg: { ...leg }, fec, k, offsets: new Map(), moqBlocks: new Set(), rebuilt: new Map(),
|
|
1920
2090
|
};
|
|
1921
2091
|
this.#overlays.set(repairPacketId, inst);
|
|
1922
2092
|
return () => {
|
|
@@ -1951,6 +2121,13 @@ export class MmtpRouter {
|
|
|
1951
2121
|
*/
|
|
1952
2122
|
resetKeyframeOverlays(sourcePacketId: number, catalogTrackName?: string): void {
|
|
1953
2123
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
2124
|
+
// The base restarted with it: base SS_IDs kept for joint decoding name
|
|
2125
|
+
// the old epoch's packets.
|
|
2126
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
2127
|
+
if (catalogTrackName !== undefined && name !== catalogTrackName) continue;
|
|
2128
|
+
stream.overlayToBase.routed.clear();
|
|
2129
|
+
stream.overlayToBase.pending.clear();
|
|
2130
|
+
}
|
|
1954
2131
|
}
|
|
1955
2132
|
|
|
1956
2133
|
#resetOverlay(inst: OverlayInstance): void {
|
|
@@ -1958,6 +2135,7 @@ export class MmtpRouter {
|
|
|
1958
2135
|
inst.last = undefined;
|
|
1959
2136
|
inst.offsets.clear();
|
|
1960
2137
|
inst.moqBlocks.clear();
|
|
2138
|
+
inst.rebuilt.clear();
|
|
1961
2139
|
}
|
|
1962
2140
|
|
|
1963
2141
|
#overlaysForSource(sourcePacketId: number, catalogTrackName: string | undefined): OverlayInstance[] {
|
|
@@ -2028,7 +2206,15 @@ export class MmtpRouter {
|
|
|
2028
2206
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
2029
2207
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
2030
2208
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
2031
|
-
* included (7.1, 7.4.3).
|
|
2209
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
2210
|
+
* FEC reads it.
|
|
2211
|
+
*
|
|
2212
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
2213
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
2214
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
2215
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
2216
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
2217
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
2032
2218
|
*/
|
|
2033
2219
|
#feedOverlaySource(
|
|
2034
2220
|
packetId: number,
|
|
@@ -2038,16 +2224,35 @@ export class MmtpRouter {
|
|
|
2038
2224
|
sequenceNumber: number,
|
|
2039
2225
|
rapFlag: boolean,
|
|
2040
2226
|
source: MmtpSourcePath,
|
|
2227
|
+
baseSsId: number | undefined,
|
|
2041
2228
|
): void {
|
|
2042
2229
|
if (this.#overlays.size === 0) return;
|
|
2043
2230
|
const ssId = keyframeOverlaySsId(header);
|
|
2044
2231
|
if (ssId === undefined) return;
|
|
2232
|
+
let rebuiltPsn: number | undefined;
|
|
2045
2233
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
2046
2234
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
2235
|
+
// After the restart check: a new epoch forgets the old one's packets.
|
|
2236
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
2237
|
+
inst.rebuilt.delete(ssId);
|
|
2047
2238
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
2048
2239
|
if (identity !== null) {
|
|
2240
|
+
// The newest member anchors its MPU; one with no base SS_ID leaves the
|
|
2241
|
+
// MPU no base anchor rather than keep an older member's. A member that
|
|
2242
|
+
// is no FEC source packet of the base takes an overlay SS_ID and a psn
|
|
2243
|
+
// but no base SS_ID, so past it an older anchor would put each rebuilt
|
|
2244
|
+
// packet one base slot high, and overlayBaseSsId's psn test would pass
|
|
2245
|
+
// it. That costs a conforming stream nothing: every member carries the
|
|
2246
|
+
// base trailer (7.4.3), unread only while the base track is
|
|
2247
|
+
// unconfigured (fecUnreadable), which joint decoding skips, and a base
|
|
2248
|
+
// release (MulticastManager) resets or removes the overlay, so no
|
|
2249
|
+
// readable member precedes an unread one in its MPU.
|
|
2049
2250
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
2050
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
2251
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
2252
|
+
psnOffset: (ssId - sequenceNumber) >>> 0,
|
|
2253
|
+
overlaySsId: ssId,
|
|
2254
|
+
...(baseSsId === undefined ? {} : { baseSsId }),
|
|
2255
|
+
});
|
|
2051
2256
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value!);
|
|
2052
2257
|
}
|
|
2053
2258
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -2067,6 +2272,18 @@ export class MmtpRouter {
|
|
|
2067
2272
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
2068
2273
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
2069
2274
|
}
|
|
2275
|
+
if (rebuiltPsn === undefined || this.#classifyPacketId(packetId) === null) return;
|
|
2276
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
2277
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
2278
|
+
// Counted only. Marked re-stamped (#restampedStreams), the stream would
|
|
2279
|
+
// drop every later rebuilt packet no member anchors, which the overlay
|
|
2280
|
+
// route delivers unanchored until a base recovery shows the re-stamp:
|
|
2281
|
+
// a keyframe whose base copy is lost too would never play.
|
|
2282
|
+
this.#restampedRecoveredSeqs++;
|
|
2283
|
+
} else if (baseSsId !== undefined) {
|
|
2284
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
2285
|
+
if (offset === ((baseSsId - sequenceNumber) >>> 0)) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
2286
|
+
}
|
|
2070
2287
|
}
|
|
2071
2288
|
|
|
2072
2289
|
#noteOverlayMoqBlock(inst: OverlayInstance, blockStart: number): void {
|
|
@@ -2107,11 +2324,16 @@ export class MmtpRouter {
|
|
|
2107
2324
|
|
|
2108
2325
|
/**
|
|
2109
2326
|
* Route an overlay-recovered block's packets as source media of the
|
|
2110
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2327
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
2328
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
2329
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
2330
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
2331
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
2332
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
2333
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
2334
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
2335
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
2336
|
+
* header's sequence and any other drops the packet.
|
|
2115
2337
|
*/
|
|
2116
2338
|
#routeOverlayRecovered(inst: OverlayInstance, blockStart: number, recovered: Uint8Array): void {
|
|
2117
2339
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -2134,27 +2356,181 @@ export class MmtpRouter {
|
|
|
2134
2356
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
2135
2357
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
2136
2358
|
const restored = new Set<number>();
|
|
2359
|
+
// The base SS_IDs determined independently, by overlay ESI; those the
|
|
2360
|
+
// overlay alone showed anchor the release gate, not the base decoder.
|
|
2361
|
+
const baseSsIds = new Map<number, number>();
|
|
2362
|
+
const gateOnly = new Set<number>();
|
|
2137
2363
|
const symbols = result.symbols.flatMap((sym) => {
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
|
|
2364
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
2365
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
2366
|
+
if (anchor !== undefined) {
|
|
2367
|
+
const packetSequenceNumber = (overlaySsId - anchor.psnOffset) >>> 0;
|
|
2141
2368
|
restored.add(packetSequenceNumber);
|
|
2142
|
-
|
|
2369
|
+
const out = { ...sym, packetSequenceNumber };
|
|
2370
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
2371
|
+
if (baseSsId !== undefined) {
|
|
2372
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
2373
|
+
// Its protected psn is the wire sequence its member gives it, at
|
|
2374
|
+
// the base SS_ID that member gives it: at the stream's current
|
|
2375
|
+
// SS_ID offset, the stream unstamped, as a base recovery shows it
|
|
2376
|
+
// (#routeProcessedFecSymbols). A re-stamping hop fails the psn test.
|
|
2377
|
+
// Only for a packet the base did not receive: a received member's
|
|
2378
|
+
// slot holds the bytes it arrived with (the decoder keeps a
|
|
2379
|
+
// received source as given), whose psn is the wire one whatever a
|
|
2380
|
+
// hop did, so it passes the test against itself.
|
|
2381
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
2382
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) &&
|
|
2383
|
+
offset === ((baseSsId - packetSequenceNumber) >>> 0)) {
|
|
2384
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return [out];
|
|
2143
2388
|
}
|
|
2144
2389
|
// A stream re-stamped by a bridge that no base recovery has shown yet
|
|
2145
2390
|
// keeps the header's sequence too: from received packets alone the
|
|
2146
|
-
// router cannot tell (their psn is the wire one)
|
|
2147
|
-
//
|
|
2148
|
-
|
|
2391
|
+
// router cannot tell (their psn is the wire one), and the base copy of
|
|
2392
|
+
// a packet the overlay rebuilt only counts a re-stamp. Base recovery or
|
|
2393
|
+
// a FETCH still covers such a keyframe.
|
|
2394
|
+
if (!restamped) {
|
|
2395
|
+
// No member of this MPU arrived to anchor a base SS_ID. Evidence that
|
|
2396
|
+
// the stream is unstamped at its current offset determines one from
|
|
2397
|
+
// the packet's protected psn instead (unstampedBaseSsId): a base
|
|
2398
|
+
// recovery's, or else the overlay's own, which anchors the release
|
|
2399
|
+
// gate only (#overlayUnstampedSsIdSeqOffsetByStream). Without it, the
|
|
2400
|
+
// packet's base copy, if it lands, shows whether it is
|
|
2401
|
+
// (#feedOverlaySource).
|
|
2402
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
2403
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
2404
|
+
const baseSsId = fedSsId ??
|
|
2405
|
+
unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
2406
|
+
if (baseSsId !== undefined) {
|
|
2407
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
2408
|
+
if (fedSsId === undefined) gateOnly.add(sym.index);
|
|
2409
|
+
} else {
|
|
2410
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
2411
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value!);
|
|
2412
|
+
}
|
|
2413
|
+
return [sym];
|
|
2414
|
+
}
|
|
2149
2415
|
this.#overlayStats.unsequencedDropped++;
|
|
2150
2416
|
return [];
|
|
2151
2417
|
});
|
|
2152
2418
|
this.#overlayStats.recovered++;
|
|
2419
|
+
// Each packet's MPU starts, in this decode, at the lowest base SS_ID
|
|
2420
|
+
// determined for it: the release gate's span ends there.
|
|
2421
|
+
const mpuFirst = new Map<number, number>();
|
|
2422
|
+
for (const sym of symbols) {
|
|
2423
|
+
const ssId = baseSsIds.get(sym.index);
|
|
2424
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
2425
|
+
const first = mpuFirst.get(mpu);
|
|
2426
|
+
if (ssId !== undefined && (first === undefined || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
2427
|
+
}
|
|
2428
|
+
const firstSsIds = new Map<number, number>();
|
|
2429
|
+
for (const sym of symbols) {
|
|
2430
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber)!);
|
|
2431
|
+
}
|
|
2153
2432
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
2154
2433
|
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
2434
|
+
baseSsIds,
|
|
2435
|
+
firstSsIds,
|
|
2155
2436
|
});
|
|
2156
2437
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) =>
|
|
2157
2438
|
restored.has(sym.packetSequenceNumber)));
|
|
2439
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0
|
|
2440
|
+
? baseSsIds
|
|
2441
|
+
: new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/**
|
|
2445
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
2446
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
2447
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
2448
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
2449
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
2450
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
2451
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
2452
|
+
* A base block no repair has stated yet takes it when one does
|
|
2453
|
+
* (#feedPendingRebuilt).
|
|
2454
|
+
*
|
|
2455
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
2456
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
2457
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
2458
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
2459
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
2460
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
2461
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
2462
|
+
* block's own arrivals cannot show.
|
|
2463
|
+
*/
|
|
2464
|
+
#feedOverlayRecoveredToBase(
|
|
2465
|
+
inst: OverlayInstance,
|
|
2466
|
+
blockStart: number,
|
|
2467
|
+
symbols: ProcessFecBlockResult["symbols"],
|
|
2468
|
+
baseSsIds: ReadonlyMap<number, number>,
|
|
2469
|
+
): void {
|
|
2470
|
+
const fec = this.#fecManager;
|
|
2471
|
+
const owner = inst.leg.sourcePacketId;
|
|
2472
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
2473
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
2474
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
2475
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
2476
|
+
const feeds: Array<readonly [number, Uint8Array]> = [];
|
|
2477
|
+
for (const sym of symbols) {
|
|
2478
|
+
// A packet the base received already sits in its block.
|
|
2479
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
2480
|
+
const ssId = baseSsIds.get(sym.index);
|
|
2481
|
+
if (ssId === undefined) {
|
|
2482
|
+
this.#overlayStats.baseUndetermined++;
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
routed.delete(ssId);
|
|
2486
|
+
routed.add(ssId);
|
|
2487
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value!);
|
|
2488
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
2489
|
+
}
|
|
2490
|
+
// Any feed can recover the base block, whose routing must already skip
|
|
2491
|
+
// every packet this recovery routed: all are registered before the first.
|
|
2492
|
+
for (const [ssId, symbolData] of feeds) {
|
|
2493
|
+
// A recovery routed above can retire the owner (an in-place switch).
|
|
2494
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
2495
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
2500
|
+
#feedRebuiltToBase(owner: number, name: string | undefined, ssId: number, symbolData: Uint8Array): void {
|
|
2501
|
+
const fec = this.#fecManager;
|
|
2502
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
2503
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
2504
|
+
if (coords == null) {
|
|
2505
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
2506
|
+
pending.delete(ssId);
|
|
2507
|
+
// Its own copy: the symbol is a view of the whole recovered block.
|
|
2508
|
+
pending.set(ssId, symbolData.slice());
|
|
2509
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value!);
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
2513
|
+
if (!fed.taken) return;
|
|
2514
|
+
this.#overlayStats.baseFed++;
|
|
2515
|
+
if (fed.recovered === null) return;
|
|
2516
|
+
const sbn = coords.sbn;
|
|
2517
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
2518
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
2519
|
+
fec.countRecovery(owner, sbn, name);
|
|
2520
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
2525
|
+
#feedPendingRebuilt(owner: number, name: string | undefined, block: { start: number; length: number }): void {
|
|
2526
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
2527
|
+
if (pending === undefined || pending.size === 0) return;
|
|
2528
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
2529
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
2530
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
2531
|
+
pending.delete(ssId);
|
|
2532
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2533
|
+
}
|
|
2158
2534
|
}
|
|
2159
2535
|
|
|
2160
2536
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
@@ -2190,6 +2566,9 @@ export class MmtpRouter {
|
|
|
2190
2566
|
const mpu = sym.fragment.mpuSequenceNumber;
|
|
2191
2567
|
if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
|
|
2192
2568
|
if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
|
|
2569
|
+
// Not overlayBaseSsId: a wrong sbn only keeps or wrongly stops a keyframe
|
|
2570
|
+
// FETCH (§6.6, a SHOULD; no symbol moves), and wire psn plus offset holds
|
|
2571
|
+
// under a constant re-stamp shift, which overlayBaseSsId's psn test refuses.
|
|
2193
2572
|
const ssId = (sym.packetSequenceNumber + ssIdSeqOffset) >>> 0;
|
|
2194
2573
|
// ponytail: a base block no repair has stated yet (an undelimited hole) is not reported.
|
|
2195
2574
|
const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
|
|
@@ -2329,16 +2708,22 @@ export class MmtpRouter {
|
|
|
2329
2708
|
if (!entry) return;
|
|
2330
2709
|
entry.receiver.release();
|
|
2331
2710
|
entry.protected = false;
|
|
2711
|
+
entry.userAgent = false;
|
|
2332
2712
|
// ponytail: drops every governed block's record, not only this flow's; per-flow if joins multiply.
|
|
2333
2713
|
this.#assistBlocks.clear();
|
|
2334
2714
|
this.#assistFailures.clear();
|
|
2335
2715
|
}
|
|
2336
2716
|
|
|
2337
|
-
/**
|
|
2717
|
+
/** 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
2718
|
get unadmittedListedDropCount(): number {
|
|
2339
2719
|
return this.#unadmittedListedDrops;
|
|
2340
2720
|
}
|
|
2341
2721
|
|
|
2722
|
+
/** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
|
|
2723
|
+
get admittedSourceDatagramCount(): number {
|
|
2724
|
+
return this.#admittedSourceDatagrams;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2342
2727
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
2343
2728
|
get unlistedRepairSetAsideCount(): number {
|
|
2344
2729
|
return this.#unlistedRepairSetAside;
|
|
@@ -2384,6 +2769,14 @@ export class MmtpRouter {
|
|
|
2384
2769
|
return false;
|
|
2385
2770
|
}
|
|
2386
2771
|
|
|
2772
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
2773
|
+
#userAgentGoverns(packetId: number): boolean {
|
|
2774
|
+
for (const entry of this.#provenance.values()) {
|
|
2775
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
2776
|
+
}
|
|
2777
|
+
return false;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2387
2780
|
/**
|
|
2388
2781
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
2389
2782
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -2503,7 +2896,7 @@ export class MmtpRouter {
|
|
|
2503
2896
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2504
2897
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2505
2898
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2506
|
-
})) {
|
|
2899
|
+
}, true)) {
|
|
2507
2900
|
this.#unlistedAssistedRecoveries++;
|
|
2508
2901
|
this.#assistBlocks.delete(key);
|
|
2509
2902
|
return;
|
|
@@ -2965,6 +3358,11 @@ export class MmtpRouter {
|
|
|
2965
3358
|
)) return;
|
|
2966
3359
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
2967
3360
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
3361
|
+
// Rebuilt sources that waited for this block join it once the block took
|
|
3362
|
+
// a repair, so it has a deadline; still ahead of the repair, as the held
|
|
3363
|
+
// sources above are.
|
|
3364
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
3365
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
2968
3366
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2969
3367
|
const recovered = fec.feedRepairSymbol(
|
|
2970
3368
|
ownerPacketId,
|
|
@@ -3027,7 +3425,7 @@ export class MmtpRouter {
|
|
|
3027
3425
|
catalogTrackName,
|
|
3028
3426
|
)) continue;
|
|
3029
3427
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
3030
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
3428
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
3031
3429
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
3032
3430
|
}
|
|
3033
3431
|
if (payload.oversized) continue;
|
|
@@ -3145,6 +3543,8 @@ export class MmtpRouter {
|
|
|
3145
3543
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
3146
3544
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
3147
3545
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
3546
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
3547
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
3148
3548
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
3149
3549
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
3150
3550
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -3276,6 +3676,12 @@ export class MmtpRouter {
|
|
|
3276
3676
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
3277
3677
|
* own RAP cadence, so no timer is involved.
|
|
3278
3678
|
*
|
|
3679
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
3680
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
3681
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
3682
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
3683
|
+
* frames of the outgoing stream.
|
|
3684
|
+
*
|
|
3279
3685
|
* The commit is one synchronous transition inside the frame path: the
|
|
3280
3686
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
3281
3687
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -3328,6 +3734,16 @@ export class MmtpRouter {
|
|
|
3328
3734
|
droppedIncomingFrames: 0,
|
|
3329
3735
|
};
|
|
3330
3736
|
this.#staged = state;
|
|
3737
|
+
// Release what the outgoing stream's gate held only because it was
|
|
3738
|
+
// anchored (FecReleaseGate.releaseUnanchored): unanchored, it had already
|
|
3739
|
+
// reached the switch. Only a gate that holds a frame the overlay rebuilt
|
|
3740
|
+
// is poked: any other holds nothing for that reason, and a poke would
|
|
3741
|
+
// still force out a frame held past the hold limit, which before the
|
|
3742
|
+
// anchor waited for the next push. The incoming stream's gate is not
|
|
3743
|
+
// drained here: a RAP it released would commit before this call returns.
|
|
3744
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
3745
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
3746
|
+
}
|
|
3331
3747
|
return {
|
|
3332
3748
|
cancel: () => {
|
|
3333
3749
|
if (this.#staged !== state) return false;
|
|
@@ -3422,7 +3838,24 @@ export class MmtpRouter {
|
|
|
3422
3838
|
// same call. Nothing registers a packet ID mid-frame, so this drops
|
|
3423
3839
|
// only frames that lost their registration.
|
|
3424
3840
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
3425
|
-
if (this.#staged !== null
|
|
3841
|
+
if (this.#staged !== null) {
|
|
3842
|
+
// A switch staged from this frame's commit (onCommit: the ladder walk)
|
|
3843
|
+
// pokes its outgoing stream's gate, the stream just committed to, whose
|
|
3844
|
+
// drain may be what released this frame. The poke waits until the
|
|
3845
|
+
// commit has delivered that stream's init, held GOP and RAP. Run now,
|
|
3846
|
+
// it would release the frames behind them ahead of them: a delta is
|
|
3847
|
+
// dropped there (its stream is not synced yet), and a keyframe plays
|
|
3848
|
+
// ahead of the init and leaves the commit's frames behind it.
|
|
3849
|
+
let consumed = false;
|
|
3850
|
+
this.#fecPokeDepth++;
|
|
3851
|
+
try {
|
|
3852
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
3853
|
+
} finally {
|
|
3854
|
+
this.#fecPokeDepth--;
|
|
3855
|
+
this.#flushPendingFecPokes();
|
|
3856
|
+
}
|
|
3857
|
+
if (consumed) return;
|
|
3858
|
+
}
|
|
3426
3859
|
this.#deliverParsedFrame(data, meta);
|
|
3427
3860
|
}
|
|
3428
3861
|
|
|
@@ -3615,12 +4048,17 @@ export class MmtpRouter {
|
|
|
3615
4048
|
this.#parser.deferFecRelease(work);
|
|
3616
4049
|
} finally {
|
|
3617
4050
|
this.#fecPokeDepth--;
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
4051
|
+
this.#flushPendingFecPokes();
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
4056
|
+
#flushPendingFecPokes(): void {
|
|
4057
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
4058
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
4059
|
+
this.#pendingFecPokes.clear();
|
|
4060
|
+
for (const poke of pokes) {
|
|
4061
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
3624
4062
|
}
|
|
3625
4063
|
}
|
|
3626
4064
|
}
|
|
@@ -3709,6 +4147,14 @@ export class MmtpRouter {
|
|
|
3709
4147
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
3710
4148
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
3711
4149
|
},
|
|
4150
|
+
// A staged switch commits on when each side's frames reach it
|
|
4151
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
4152
|
+
// they did unanchored.
|
|
4153
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
4154
|
+
const staged = this.#staged;
|
|
4155
|
+
const stream = { packetId, ...(catalogTrackName === undefined ? {} : { catalogTrackName }) };
|
|
4156
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
4157
|
+
},
|
|
3712
4158
|
onReleased: (data, meta) => {
|
|
3713
4159
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === undefined) return;
|
|
3714
4160
|
const timing = meta.authoredTiming;
|
|
@@ -3904,6 +4350,8 @@ export class MmtpRouter {
|
|
|
3904
4350
|
const parsed = this.#parser.parsePacket(packet);
|
|
3905
4351
|
if (!parsed) return;
|
|
3906
4352
|
const info = parsed.info;
|
|
4353
|
+
// manifestAdmitted is set only by a receiver release or the user agent's own verification.
|
|
4354
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
3907
4355
|
// Every multicast packet is a delivery-gap sample, including one dropped
|
|
3908
4356
|
// below: the gap is a property of the link the FEC deadlines run on, not
|
|
3909
4357
|
// of which packets this router routes. The re-entry after a manifest
|
|
@@ -3959,6 +4407,15 @@ export class MmtpRouter {
|
|
|
3959
4407
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
3960
4408
|
return;
|
|
3961
4409
|
}
|
|
4410
|
+
// The user agent verified this datagram against the flow's records
|
|
4411
|
+
// itself, and the records never reach the page: admitted as is. The
|
|
4412
|
+
// flow stays registered, so its MoQ copies are still checked.
|
|
4413
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
4414
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
4415
|
+
protectedFlow.userAgent = true;
|
|
4416
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
3962
4419
|
// Held: released by a record (feedProvenanceRecord) or expired. Duplicate / overflow: dropped.
|
|
3963
4420
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
3964
4421
|
if (verdict === "admitted") {
|
|
@@ -3983,6 +4440,18 @@ export class MmtpRouter {
|
|
|
3983
4440
|
}
|
|
3984
4441
|
}
|
|
3985
4442
|
|
|
4443
|
+
// A multicast datagram that names no flow is unverified. Of a track a
|
|
4444
|
+
// user-agent-admitted flow lists it would feed that flow's live decoder,
|
|
4445
|
+
// whose recoveries are kept without a record (#routeVerifiedFecSymbols):
|
|
4446
|
+
// its source is dropped here and its repair set aside below, as for
|
|
4447
|
+
// unlisted MoQ. Under a record-only flow it is read as before: those
|
|
4448
|
+
// recoveries still need a record.
|
|
4449
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === undefined;
|
|
4450
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
4451
|
+
this.#unadmittedListedDrops++;
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
|
|
3986
4455
|
if (info.isRepair) {
|
|
3987
4456
|
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
3988
4457
|
// Read from the header parsed above rather than parsing it again
|
|
@@ -4003,9 +4472,10 @@ export class MmtpRouter {
|
|
|
4003
4472
|
);
|
|
4004
4473
|
if (!repair) return;
|
|
4005
4474
|
if (ownerPacketId != null && this.#fecManager) {
|
|
4006
|
-
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast)
|
|
4475
|
+
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast);
|
|
4476
|
+
// a flow-less multicast one is unverified.
|
|
4007
4477
|
if (
|
|
4008
|
-
source === "moq" && !manifestAdmitted &&
|
|
4478
|
+
((flowless && this.#userAgentGoverns(ownerPacketId)) || (source === "moq" && !manifestAdmitted)) &&
|
|
4009
4479
|
this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)
|
|
4010
4480
|
) return;
|
|
4011
4481
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
@@ -4345,6 +4815,7 @@ export class MmtpRouter {
|
|
|
4345
4815
|
if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
|
|
4346
4816
|
this.#feedOverlaySource(
|
|
4347
4817
|
info.packetId, catalogTrackName, packet, parsed.header, info.sequenceNumber, info.rapFlag, source,
|
|
4818
|
+
fecPayloadId?.ssId,
|
|
4348
4819
|
);
|
|
4349
4820
|
}
|
|
4350
4821
|
}
|
|
@@ -4369,6 +4840,7 @@ export class MmtpRouter {
|
|
|
4369
4840
|
deliveryProvenance?: MmtpDeliveryProvenance;
|
|
4370
4841
|
catalogTrackName?: string;
|
|
4371
4842
|
fecSbn?: number;
|
|
4843
|
+
fecSbnFirst?: number;
|
|
4372
4844
|
},
|
|
4373
4845
|
parsed?: ParsedMmtpPacket,
|
|
4374
4846
|
): void {
|
|
@@ -4392,6 +4864,7 @@ export class MmtpRouter {
|
|
|
4392
4864
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
4393
4865
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
4394
4866
|
...(parserOptions?.fecSbn === undefined ? {} : { fecSbn: parserOptions.fecSbn }),
|
|
4867
|
+
...(parserOptions?.fecSbnFirst === undefined ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }),
|
|
4395
4868
|
};
|
|
4396
4869
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
4397
4870
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -4573,6 +5046,7 @@ export class MmtpRouter {
|
|
|
4573
5046
|
stream = {
|
|
4574
5047
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
4575
5048
|
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
5049
|
+
overlayToBase: { routed: new Set(), pending: new Map() },
|
|
4576
5050
|
};
|
|
4577
5051
|
byName.set(catalogTrackName, stream);
|
|
4578
5052
|
}
|
|
@@ -4771,8 +5245,29 @@ export class MmtpRouter {
|
|
|
4771
5245
|
data: Uint8Array,
|
|
4772
5246
|
meta: RoutedFrameMeta,
|
|
4773
5247
|
cb: FrameCallback | null,
|
|
5248
|
+
/** The base SS_ID the FEC release gate ordered the frame by, if any. */
|
|
5249
|
+
fecSbn: number | undefined,
|
|
4774
5250
|
): void {
|
|
4775
5251
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
5252
|
+
// The FEC release gate let this frame out in order: only once every
|
|
5253
|
+
// block between it and the frame before it resolved, because it starts
|
|
5254
|
+
// where that frame ended, or (one the overlay rebuilt) once the base
|
|
5255
|
+
// path passed it. Nothing still in flight can precede it then, so it
|
|
5256
|
+
// plays now, as a source frame does; holding it for the next
|
|
5257
|
+
// intact frame cost every repaired burst its length in decoder input
|
|
5258
|
+
// (BLO-52085). An overlay recovery whose base SS_ID is undetermined names
|
|
5259
|
+
// no base block for the gate to wait on, so past a gap it may be ahead of
|
|
5260
|
+
// frames still in flight (an overlay that outran the wire, BLO-53342): it
|
|
5261
|
+
// keeps waiting for the source cursor below, unless it starts where the
|
|
5262
|
+
// last emitted frame ended (on this direct path, the delivered high water).
|
|
5263
|
+
if (this.#isFecTrack(packetId)) {
|
|
5264
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
5265
|
+
if (fecSbn !== undefined || (high !== undefined && authoredAdjacent(high, meta))) {
|
|
5266
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, frame => frame.timestamp <= meta.timestamp, cb);
|
|
5267
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
5268
|
+
return;
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
4776
5271
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
4777
5272
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
4778
5273
|
this.#drainDirectRecoveredVideo(
|
|
@@ -4950,6 +5445,7 @@ export class MmtpRouter {
|
|
|
4950
5445
|
ownerPacketId?: number,
|
|
4951
5446
|
catalogTrackName?: string,
|
|
4952
5447
|
complete?: () => void,
|
|
5448
|
+
assisted = false,
|
|
4953
5449
|
): boolean {
|
|
4954
5450
|
// MMTP path: use processFecBlock for MMTP-wrapped recovery.
|
|
4955
5451
|
// Recovered symbols are individual MMTP packets that may be fragments
|
|
@@ -4974,7 +5470,7 @@ export class MmtpRouter {
|
|
|
4974
5470
|
ownerPacketId,
|
|
4975
5471
|
blockProfile,
|
|
4976
5472
|
});
|
|
4977
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
5473
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
4978
5474
|
}
|
|
4979
5475
|
|
|
4980
5476
|
/**
|
|
@@ -4987,6 +5483,19 @@ export class MmtpRouter {
|
|
|
4987
5483
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
4988
5484
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
4989
5485
|
* outside a protected flow.
|
|
5486
|
+
*
|
|
5487
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
5488
|
+
* without a record: while the flow is protected that decoder takes only
|
|
5489
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
5490
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
5491
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
5492
|
+
* unlisted MoQ repair and still needs a record.
|
|
5493
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
5494
|
+
* ever mixes verified and unverified live inputs.
|
|
5495
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
5496
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
5497
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
5498
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
4990
5499
|
*/
|
|
4991
5500
|
#routeVerifiedFecSymbols(
|
|
4992
5501
|
blockId: number,
|
|
@@ -4994,8 +5503,22 @@ export class MmtpRouter {
|
|
|
4994
5503
|
ownerPacketId?: number,
|
|
4995
5504
|
catalogTrackName?: string,
|
|
4996
5505
|
complete?: () => void,
|
|
5506
|
+
assisted = false,
|
|
4997
5507
|
): boolean {
|
|
4998
|
-
|
|
5508
|
+
// A packet a keyframe overlay recovery routed before it fed the packet to
|
|
5509
|
+
// this block (#feedOverlayRecoveredToBase) is not routed again.
|
|
5510
|
+
const overlayRouted = ownerPacketId == null
|
|
5511
|
+
? undefined
|
|
5512
|
+
: this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
5513
|
+
if (overlayRouted !== undefined && overlayRouted.size > 0) {
|
|
5514
|
+
result = {
|
|
5515
|
+
...result,
|
|
5516
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index))),
|
|
5517
|
+
};
|
|
5518
|
+
}
|
|
5519
|
+
const entry = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
5520
|
+
const receiver = entry === undefined || entry === "ambiguous" ? entry : entry.receiver;
|
|
5521
|
+
const userAgent = !assisted && entry !== undefined && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
4999
5522
|
let recovered = true;
|
|
5000
5523
|
if (receiver) {
|
|
5001
5524
|
const parsed = this.#stream(ownerPacketId!, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -5008,6 +5531,8 @@ export class MmtpRouter {
|
|
|
5008
5531
|
let admitted = false;
|
|
5009
5532
|
if (receiver === "ambiguous") {
|
|
5010
5533
|
this.#ambiguousRecoveredDrops++;
|
|
5534
|
+
} else if (userAgent) {
|
|
5535
|
+
admitted = true;
|
|
5011
5536
|
} else {
|
|
5012
5537
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
5013
5538
|
wire.set(sym.rawSymbol);
|
|
@@ -5033,7 +5558,7 @@ export class MmtpRouter {
|
|
|
5033
5558
|
#provenanceFor(
|
|
5034
5559
|
packetId: number,
|
|
5035
5560
|
catalogTrackName: string | undefined,
|
|
5036
|
-
):
|
|
5561
|
+
): ProvenanceEntry | "ambiguous" | undefined {
|
|
5037
5562
|
// A caller that names no track matches every name the packet id was admitted under.
|
|
5038
5563
|
const byName = this.#provenanceStreamFlow.get(packetId);
|
|
5039
5564
|
const flows = new Set(
|
|
@@ -5042,12 +5567,12 @@ export class MmtpRouter {
|
|
|
5042
5567
|
: byName.has(catalogTrackName) ? [byName.get(catalogTrackName)!] : [],
|
|
5043
5568
|
);
|
|
5044
5569
|
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))) {
|
|
5570
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
5571
|
+
let match: ProvenanceEntry | undefined;
|
|
5572
|
+
for (const entry of this.#provenance.values()) {
|
|
5573
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
5049
5574
|
if (match) return "ambiguous";
|
|
5050
|
-
match =
|
|
5575
|
+
match = entry;
|
|
5051
5576
|
}
|
|
5052
5577
|
}
|
|
5053
5578
|
return match;
|
|
@@ -5060,15 +5585,25 @@ export class MmtpRouter {
|
|
|
5060
5585
|
catalogTrackName?: string,
|
|
5061
5586
|
/** Set for a keyframe-overlay recovery: `blockId` is an overlay
|
|
5062
5587
|
* coordinate, which yields no base SS_ID (mmt-02 7.2), and the packets'
|
|
5063
|
-
* wire sequences are already restored.
|
|
5064
|
-
|
|
5588
|
+
* wire sequences are already restored. `baseSsIds` holds, by ESI, the
|
|
5589
|
+
* base SS_IDs determined independently: a packet's release-gate anchor;
|
|
5590
|
+
* `firstSsIds` the lowest of them in its MPU (FrameMeta.fecSbnFirst). */
|
|
5591
|
+
overlay?: {
|
|
5592
|
+
deliveryProvenance: MmtpDeliveryProvenance;
|
|
5593
|
+
baseSsIds: ReadonlyMap<number, number>;
|
|
5594
|
+
firstSsIds: ReadonlyMap<number, number>;
|
|
5595
|
+
},
|
|
5065
5596
|
): void {
|
|
5066
5597
|
// Route recovered symbols to the track's callback. The ownerPacketId tells us
|
|
5067
5598
|
// which track this block belongs to — resolves per-track, then legacy fallback.
|
|
5068
5599
|
if (ownerPacketId == null) return; // no track association
|
|
5069
5600
|
if (this.#classifyPacketId(ownerPacketId) === null) return; // unknown track
|
|
5070
|
-
const fecSbnOf = (index: number): { fecSbn?: number } =>
|
|
5071
|
-
overlay
|
|
5601
|
+
const fecSbnOf = (index: number): { fecSbn?: number; fecSbnFirst?: number } => {
|
|
5602
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
5603
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
5604
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
5605
|
+
return fecSbn === undefined || fecSbnFirst === undefined ? {} : { fecSbn, fecSbnFirst };
|
|
5606
|
+
};
|
|
5072
5607
|
// A recovered symbol's header names the sequence the sender protected. A
|
|
5073
5608
|
// hop that re-stamps packet_sequence_number after protection (cast's
|
|
5074
5609
|
// publisher-rotation bridge) leaves the decode mixing every received
|
|
@@ -5084,7 +5619,10 @@ export class MmtpRouter {
|
|
|
5084
5619
|
...result,
|
|
5085
5620
|
symbols: result.symbols.map((sym) => {
|
|
5086
5621
|
const wire = (this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset) >>> 0;
|
|
5087
|
-
if (wire === sym.packetSequenceNumber)
|
|
5622
|
+
if (wire === sym.packetSequenceNumber) {
|
|
5623
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
5624
|
+
return sym;
|
|
5625
|
+
}
|
|
5088
5626
|
this.#restampedRecoveredSeqs++;
|
|
5089
5627
|
this.#restampedStreams.add(streamKey);
|
|
5090
5628
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -5141,6 +5679,43 @@ export class MmtpRouter {
|
|
|
5141
5679
|
);
|
|
5142
5680
|
}
|
|
5143
5681
|
|
|
5682
|
+
// A recovered head whose sample FT=1 has not named yet (an overlay that
|
|
5683
|
+
// outran the source path) would complete its data unit untimed: the unit
|
|
5684
|
+
// is dropped, marked completed, and the timed source copy is then refused
|
|
5685
|
+
// (BLO-53342). Hand it to the parser instead, which holds it in its
|
|
5686
|
+
// awaiting-timing queue and feeds it again when FT=1 lands; its recovered
|
|
5687
|
+
// continuations park in the reassembler meanwhile. If the source path
|
|
5688
|
+
// lost the head, this held copy is the keyframe (BLO-53369).
|
|
5689
|
+
const untimedHead = (sym: (typeof result.symbols)[number]): boolean =>
|
|
5690
|
+
sym.fragmentType === 2 &&
|
|
5691
|
+
sym.fragment.fragmentationIndicator === 1 &&
|
|
5692
|
+
sym.fragment.movieFragmentSequenceNumber !== undefined &&
|
|
5693
|
+
sym.fragment.sampleNumber !== undefined &&
|
|
5694
|
+
!this.#parser.hasAuthoredTimingFor(
|
|
5695
|
+
ownerPacketId,
|
|
5696
|
+
sym.fragment.mpuSequenceNumber,
|
|
5697
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
5698
|
+
sym.fragment.sampleNumber,
|
|
5699
|
+
catalogTrackName,
|
|
5700
|
+
);
|
|
5701
|
+
for (const sym of result.symbols) {
|
|
5702
|
+
if (!untimedHead(sym) ||
|
|
5703
|
+
this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
5704
|
+
this.#feedViaParser(
|
|
5705
|
+
sym.rawSymbol,
|
|
5706
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
5707
|
+
undefined,
|
|
5708
|
+
{
|
|
5709
|
+
skipFecPayloadId: true,
|
|
5710
|
+
recovered: true,
|
|
5711
|
+
deliveryProvenance,
|
|
5712
|
+
...fecSbnOf(sym.index),
|
|
5713
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
5714
|
+
},
|
|
5715
|
+
);
|
|
5716
|
+
}
|
|
5717
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
5718
|
+
|
|
5144
5719
|
// MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
|
|
5145
5720
|
// BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
|
|
5146
5721
|
// frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
|