@blockcast/mmt-container 2.1.0 → 2.2.0-main.079cd7664c0b
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 +1271 -247
- 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 +1718 -54
- 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 +15652 -14939
- 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 +344 -27
- 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,61 @@ 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
|
+
/** Base SS_IDs of overlay-routed packets kept per stream (see OverlayToBase). */
|
|
258
|
+
const OVERLAY_ROUTED_LIMIT = 1024;
|
|
259
|
+
/** Rebuilt sources kept per stream for a base block no repair stated yet. */
|
|
260
|
+
const OVERLAY_PENDING_LIMIT = 256;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One stream's keyframe-overlay recoveries on their way into its base
|
|
264
|
+
* decoder (joint decoding, BLO-52085).
|
|
265
|
+
*/
|
|
266
|
+
interface OverlayToBase {
|
|
267
|
+
/** Base SS_IDs of the packets an overlay recovery routed (as media, or held
|
|
268
|
+
* for their FT=1): a base recovery does not route them a second time. */
|
|
269
|
+
readonly routed: Set<number>;
|
|
270
|
+
/** Rebuilt sources, by base SS_ID, whose base block no repair stated yet. */
|
|
271
|
+
readonly pending: Map<number, Uint8Array>;
|
|
214
272
|
}
|
|
215
273
|
|
|
216
274
|
interface OverlayInstance {
|
|
@@ -222,8 +280,8 @@ interface OverlayInstance {
|
|
|
222
280
|
readonly k: number;
|
|
223
281
|
/** The newest overlay symbol seen: its overlay SS_ID and wire sequence. */
|
|
224
282
|
last?: { ssId: number; psn: number };
|
|
225
|
-
/** MPU sequence number →
|
|
226
|
-
readonly offsets: Map<number,
|
|
283
|
+
/** MPU sequence number → its newest received member. */
|
|
284
|
+
readonly offsets: Map<number, OverlayMemberAnchor>;
|
|
227
285
|
/** Overlay blocks a MoQ symbol fed (multicast-02 7.2.7). */
|
|
228
286
|
readonly moqBlocks: Set<number>;
|
|
229
287
|
}
|
|
@@ -245,6 +303,14 @@ export interface MmtpIngressContext {
|
|
|
245
303
|
* the same ingress and `feedPacket` splits them off.
|
|
246
304
|
*/
|
|
247
305
|
flow?: string;
|
|
306
|
+
/**
|
|
307
|
+
* `USER_AGENT_VERIFIED` when the datagram came from the browser's own
|
|
308
|
+
* MulticastReceiver, which verified the flow's records itself and delivers
|
|
309
|
+
* only admitted data (`userAgentVerifiedIngress` in
|
|
310
|
+
* `@blockcast/mmt-transport`). A protected multicast datagram carrying it
|
|
311
|
+
* is admitted without a record. Any other value means nothing.
|
|
312
|
+
*/
|
|
313
|
+
admittedBy?: symbol;
|
|
248
314
|
}
|
|
249
315
|
|
|
250
316
|
/** One physical stream: an MMTP packet ID and the catalog track it carries. */
|
|
@@ -502,6 +568,9 @@ export type RoutedPacketMeta = {
|
|
|
502
568
|
mmtpTimestamp: bigint;
|
|
503
569
|
/**
|
|
504
570
|
* The MMTP header RAP flag: the packet starts a random access point. On a
|
|
571
|
+
* track under a keyframe overlay it is also set on the FT=0 and FT=1 data
|
|
572
|
+
* units that govern a covered keyframe (draft-ramadan-moq-mmt-02 7.4.2), so
|
|
573
|
+
* it can precede the keyframe's first MFU fragment. On a
|
|
505
574
|
* `recovered` packet it is read from the FEC-reconstructed header, which is
|
|
506
575
|
* byte-exact outside packet_sequence_number, so it is the flag the
|
|
507
576
|
* publisher sent. The router's own gap decisions deliberately ignore it for
|
|
@@ -565,6 +634,7 @@ interface StreamLedgers {
|
|
|
565
634
|
* the pair.
|
|
566
635
|
*/
|
|
567
636
|
readonly duplicates: StreamDuplicateLedgers;
|
|
637
|
+
readonly overlayToBase: OverlayToBase;
|
|
568
638
|
}
|
|
569
639
|
|
|
570
640
|
const NO_FEC_MANAGER_HORIZON: DuplicateHorizon = Object.freeze({
|
|
@@ -652,6 +722,17 @@ export interface FECManagerLike {
|
|
|
652
722
|
catalogTrackName?: string,
|
|
653
723
|
): Uint8Array | null;
|
|
654
724
|
|
|
725
|
+
/** Feed a source another FEC instance over the same packets rebuilt (a
|
|
726
|
+
* keyframe overlay) to its base block, which a repair must already have
|
|
727
|
+
* stated; not a network arrival (FECManager.feedRebuiltSourceSymbol).
|
|
728
|
+
* Optional: without it the overlay and the base decode apart. */
|
|
729
|
+
feedRebuiltSourceSymbol?(
|
|
730
|
+
packetId: number,
|
|
731
|
+
ssId: number,
|
|
732
|
+
symbolData: Uint8Array,
|
|
733
|
+
catalogTrackName?: string,
|
|
734
|
+
): RebuiltSourceFeed;
|
|
735
|
+
|
|
655
736
|
/** How long ago the stream's oldest open block with an FEC deadline took its first symbol (optional). */
|
|
656
737
|
oldestOpenBlockAgeMs?(packetId: number, catalogTrackName: string): number | undefined;
|
|
657
738
|
|
|
@@ -946,6 +1027,15 @@ function sourceSymbolEvidence(
|
|
|
946
1027
|
};
|
|
947
1028
|
}
|
|
948
1029
|
|
|
1030
|
+
/** One protected flow: its 7.2.7 receiver, and the ingress its datagrams arrive with. */
|
|
1031
|
+
interface ProvenanceEntry {
|
|
1032
|
+
receiver: BcProvenanceV2Receiver;
|
|
1033
|
+
ingress: MmtpIngressContext;
|
|
1034
|
+
protected?: boolean;
|
|
1035
|
+
/** A datagram the user agent verified admitted the flow (`admittedBy`). */
|
|
1036
|
+
userAgent?: boolean;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
949
1039
|
export class MmtpRouter {
|
|
950
1040
|
readonly #parser: MmtpContainerParser;
|
|
951
1041
|
// Source AND FEC-recovered MFU fragments share ONE reassembler — the parser's
|
|
@@ -955,10 +1045,7 @@ export class MmtpRouter {
|
|
|
955
1045
|
#defaultFecMapper: FecBlockMapper | null;
|
|
956
1046
|
#fecMappersByPacketId = new Map<number, FecBlockMapper>();
|
|
957
1047
|
/** 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
|
-
>();
|
|
1048
|
+
readonly #provenance = new Map<string, ProvenanceEntry>();
|
|
962
1049
|
#onFlowProtected: ((flow: string) => void) | null = null;
|
|
963
1050
|
#unadmittedListedDrops = 0;
|
|
964
1051
|
#unlistedRepairSetAside = 0;
|
|
@@ -1106,7 +1193,7 @@ export class MmtpRouter {
|
|
|
1106
1193
|
#restampedStreams = new Set<string>();
|
|
1107
1194
|
#overlayStats: KeyframeOverlayStats = {
|
|
1108
1195
|
repairDropped: 0, ownerRefused: 0, recovered: 0, restarts: 0,
|
|
1109
|
-
unsequencedDropped: 0, protectedDropped: 0,
|
|
1196
|
+
unsequencedDropped: 0, protectedDropped: 0, baseFed: 0, baseUndetermined: 0,
|
|
1110
1197
|
};
|
|
1111
1198
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
1112
1199
|
#onDeltaBlockCompleteCb:
|
|
@@ -1385,7 +1472,7 @@ export class MmtpRouter {
|
|
|
1385
1472
|
const cb = this.#getFrameCallback(packetId);
|
|
1386
1473
|
if (isVideo && !assembler) {
|
|
1387
1474
|
if (frameMeta.recovered) {
|
|
1388
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
1475
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
1389
1476
|
} else {
|
|
1390
1477
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
1391
1478
|
}
|
|
@@ -1900,6 +1987,11 @@ export class MmtpRouter {
|
|
|
1900
1987
|
return { ...this.#overlayStats };
|
|
1901
1988
|
}
|
|
1902
1989
|
|
|
1990
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
1991
|
+
get recoveredHeadsAwaitingTiming(): Readonly<MmtpRecoveredHeadTimingStats> {
|
|
1992
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1903
1995
|
#isOverlayRepair(repairPacketId: number, repairTrackName?: string): boolean {
|
|
1904
1996
|
return this.#overlayLegs.has(repairPacketId) ||
|
|
1905
1997
|
(repairTrackName !== undefined && this.#overlayRepairNames.has(repairTrackName));
|
|
@@ -1951,6 +2043,13 @@ export class MmtpRouter {
|
|
|
1951
2043
|
*/
|
|
1952
2044
|
resetKeyframeOverlays(sourcePacketId: number, catalogTrackName?: string): void {
|
|
1953
2045
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
2046
|
+
// The base restarted with it: base SS_IDs kept for joint decoding name
|
|
2047
|
+
// the old epoch's packets.
|
|
2048
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
2049
|
+
if (catalogTrackName !== undefined && name !== catalogTrackName) continue;
|
|
2050
|
+
stream.overlayToBase.routed.clear();
|
|
2051
|
+
stream.overlayToBase.pending.clear();
|
|
2052
|
+
}
|
|
1954
2053
|
}
|
|
1955
2054
|
|
|
1956
2055
|
#resetOverlay(inst: OverlayInstance): void {
|
|
@@ -2028,7 +2127,8 @@ export class MmtpRouter {
|
|
|
2028
2127
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
2029
2128
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
2030
2129
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
2031
|
-
* included (7.1, 7.4.3).
|
|
2130
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
2131
|
+
* FEC reads it.
|
|
2032
2132
|
*/
|
|
2033
2133
|
#feedOverlaySource(
|
|
2034
2134
|
packetId: number,
|
|
@@ -2038,6 +2138,7 @@ export class MmtpRouter {
|
|
|
2038
2138
|
sequenceNumber: number,
|
|
2039
2139
|
rapFlag: boolean,
|
|
2040
2140
|
source: MmtpSourcePath,
|
|
2141
|
+
baseSsId: number | undefined,
|
|
2041
2142
|
): void {
|
|
2042
2143
|
if (this.#overlays.size === 0) return;
|
|
2043
2144
|
const ssId = keyframeOverlaySsId(header);
|
|
@@ -2047,7 +2148,11 @@ export class MmtpRouter {
|
|
|
2047
2148
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
2048
2149
|
if (identity !== null) {
|
|
2049
2150
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
2050
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
2151
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
2152
|
+
psnOffset: (ssId - sequenceNumber) >>> 0,
|
|
2153
|
+
overlaySsId: ssId,
|
|
2154
|
+
...(baseSsId === undefined ? {} : { baseSsId }),
|
|
2155
|
+
});
|
|
2051
2156
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value!);
|
|
2052
2157
|
}
|
|
2053
2158
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -2134,12 +2239,17 @@ export class MmtpRouter {
|
|
|
2134
2239
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
2135
2240
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
2136
2241
|
const restored = new Set<number>();
|
|
2242
|
+
const baseSsIds = new Map<ProcessFecBlockResult["symbols"][number], number>();
|
|
2137
2243
|
const symbols = result.symbols.flatMap((sym) => {
|
|
2138
|
-
const
|
|
2139
|
-
if (
|
|
2140
|
-
const
|
|
2244
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
2245
|
+
if (anchor !== undefined) {
|
|
2246
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
2247
|
+
const packetSequenceNumber = (overlaySsId - anchor.psnOffset) >>> 0;
|
|
2141
2248
|
restored.add(packetSequenceNumber);
|
|
2142
|
-
|
|
2249
|
+
const out = { ...sym, packetSequenceNumber };
|
|
2250
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
2251
|
+
if (baseSsId !== undefined) baseSsIds.set(out, baseSsId);
|
|
2252
|
+
return [out];
|
|
2143
2253
|
}
|
|
2144
2254
|
// A stream re-stamped by a bridge that no base recovery has shown yet
|
|
2145
2255
|
// keeps the header's sequence too: from received packets alone the
|
|
@@ -2155,6 +2265,91 @@ export class MmtpRouter {
|
|
|
2155
2265
|
});
|
|
2156
2266
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) =>
|
|
2157
2267
|
restored.has(sym.packetSequenceNumber)));
|
|
2268
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
2273
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId),
|
|
2274
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
2275
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
2276
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
2277
|
+
* A base block no repair has stated yet takes it when one does
|
|
2278
|
+
* (#feedPendingRebuilt).
|
|
2279
|
+
*
|
|
2280
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
2281
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
2282
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
2283
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
2284
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
2285
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
2286
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
2287
|
+
* block's own arrivals cannot show.
|
|
2288
|
+
*/
|
|
2289
|
+
#feedOverlayRecoveredToBase(
|
|
2290
|
+
inst: OverlayInstance,
|
|
2291
|
+
blockStart: number,
|
|
2292
|
+
symbols: ProcessFecBlockResult["symbols"],
|
|
2293
|
+
baseSsIds: ReadonlyMap<ProcessFecBlockResult["symbols"][number], number>,
|
|
2294
|
+
): void {
|
|
2295
|
+
const fec = this.#fecManager;
|
|
2296
|
+
const owner = inst.leg.sourcePacketId;
|
|
2297
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
2298
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
2299
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
2300
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
2301
|
+
const feeds: Array<readonly [number, Uint8Array]> = [];
|
|
2302
|
+
for (const sym of symbols) {
|
|
2303
|
+
// A packet the base received already sits in its block.
|
|
2304
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
2305
|
+
const ssId = baseSsIds.get(sym);
|
|
2306
|
+
if (ssId === undefined) {
|
|
2307
|
+
this.#overlayStats.baseUndetermined++;
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
routed.delete(ssId);
|
|
2311
|
+
routed.add(ssId);
|
|
2312
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value!);
|
|
2313
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
2314
|
+
}
|
|
2315
|
+
// Any feed can recover the base block, whose routing must already skip
|
|
2316
|
+
// every packet this recovery routed: all are registered before the first.
|
|
2317
|
+
for (const [ssId, symbolData] of feeds) this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
2321
|
+
#feedRebuiltToBase(owner: number, name: string | undefined, ssId: number, symbolData: Uint8Array): void {
|
|
2322
|
+
const fec = this.#fecManager;
|
|
2323
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
2324
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
2325
|
+
if (coords == null) {
|
|
2326
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
2327
|
+
pending.delete(ssId);
|
|
2328
|
+
pending.set(ssId, symbolData);
|
|
2329
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value!);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
2333
|
+
if (!fed.taken) return;
|
|
2334
|
+
this.#overlayStats.baseFed++;
|
|
2335
|
+
if (fed.recovered === null) return;
|
|
2336
|
+
const sbn = coords.sbn;
|
|
2337
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
2338
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
2339
|
+
fec.countRecovery(owner, sbn, name);
|
|
2340
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
2345
|
+
#feedPendingRebuilt(owner: number, name: string | undefined, block: { start: number; length: number }): void {
|
|
2346
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
2347
|
+
if (pending === undefined || pending.size === 0) return;
|
|
2348
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
2349
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
2350
|
+
pending.delete(ssId);
|
|
2351
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2352
|
+
}
|
|
2158
2353
|
}
|
|
2159
2354
|
|
|
2160
2355
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
@@ -2329,12 +2524,13 @@ export class MmtpRouter {
|
|
|
2329
2524
|
if (!entry) return;
|
|
2330
2525
|
entry.receiver.release();
|
|
2331
2526
|
entry.protected = false;
|
|
2527
|
+
entry.userAgent = false;
|
|
2332
2528
|
// ponytail: drops every governed block's record, not only this flow's; per-flow if joins multiply.
|
|
2333
2529
|
this.#assistBlocks.clear();
|
|
2334
2530
|
this.#assistFailures.clear();
|
|
2335
2531
|
}
|
|
2336
2532
|
|
|
2337
|
-
/**
|
|
2533
|
+
/** 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
2534
|
get unadmittedListedDropCount(): number {
|
|
2339
2535
|
return this.#unadmittedListedDrops;
|
|
2340
2536
|
}
|
|
@@ -2384,6 +2580,14 @@ export class MmtpRouter {
|
|
|
2384
2580
|
return false;
|
|
2385
2581
|
}
|
|
2386
2582
|
|
|
2583
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
2584
|
+
#userAgentGoverns(packetId: number): boolean {
|
|
2585
|
+
for (const entry of this.#provenance.values()) {
|
|
2586
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
2587
|
+
}
|
|
2588
|
+
return false;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2387
2591
|
/**
|
|
2388
2592
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
2389
2593
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -2503,7 +2707,7 @@ export class MmtpRouter {
|
|
|
2503
2707
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2504
2708
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2505
2709
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2506
|
-
})) {
|
|
2710
|
+
}, true)) {
|
|
2507
2711
|
this.#unlistedAssistedRecoveries++;
|
|
2508
2712
|
this.#assistBlocks.delete(key);
|
|
2509
2713
|
return;
|
|
@@ -2955,6 +3159,8 @@ export class MmtpRouter {
|
|
|
2955
3159
|
}
|
|
2956
3160
|
this.#replayIsoSources(ownerPacketId, catalogTrackName, blockStart, placement.placed);
|
|
2957
3161
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
3162
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
3163
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
2958
3164
|
if (!this.#feedFecTransportSymbol(
|
|
2959
3165
|
ownerPacketId,
|
|
2960
3166
|
blockStart,
|
|
@@ -3959,6 +4165,15 @@ export class MmtpRouter {
|
|
|
3959
4165
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
3960
4166
|
return;
|
|
3961
4167
|
}
|
|
4168
|
+
// The user agent verified this datagram against the flow's records
|
|
4169
|
+
// itself, and the records never reach the page: admitted as is. The
|
|
4170
|
+
// flow stays registered, so its MoQ copies are still checked.
|
|
4171
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
4172
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
4173
|
+
protectedFlow.userAgent = true;
|
|
4174
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
4175
|
+
return;
|
|
4176
|
+
}
|
|
3962
4177
|
// Held: released by a record (feedProvenanceRecord) or expired. Duplicate / overflow: dropped.
|
|
3963
4178
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
3964
4179
|
if (verdict === "admitted") {
|
|
@@ -3983,6 +4198,18 @@ export class MmtpRouter {
|
|
|
3983
4198
|
}
|
|
3984
4199
|
}
|
|
3985
4200
|
|
|
4201
|
+
// A multicast datagram that names no flow is unverified. Of a track a
|
|
4202
|
+
// user-agent-admitted flow lists it would feed that flow's live decoder,
|
|
4203
|
+
// whose recoveries are kept without a record (#routeVerifiedFecSymbols):
|
|
4204
|
+
// its source is dropped here and its repair set aside below, as for
|
|
4205
|
+
// unlisted MoQ. Under a record-only flow it is read as before: those
|
|
4206
|
+
// recoveries still need a record.
|
|
4207
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === undefined;
|
|
4208
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
4209
|
+
this.#unadmittedListedDrops++;
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
|
|
3986
4213
|
if (info.isRepair) {
|
|
3987
4214
|
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
3988
4215
|
// Read from the header parsed above rather than parsing it again
|
|
@@ -4003,9 +4230,10 @@ export class MmtpRouter {
|
|
|
4003
4230
|
);
|
|
4004
4231
|
if (!repair) return;
|
|
4005
4232
|
if (ownerPacketId != null && this.#fecManager) {
|
|
4006
|
-
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast)
|
|
4233
|
+
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast);
|
|
4234
|
+
// a flow-less multicast one is unverified.
|
|
4007
4235
|
if (
|
|
4008
|
-
source === "moq" && !manifestAdmitted &&
|
|
4236
|
+
((flowless && this.#userAgentGoverns(ownerPacketId)) || (source === "moq" && !manifestAdmitted)) &&
|
|
4009
4237
|
this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)
|
|
4010
4238
|
) return;
|
|
4011
4239
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
@@ -4345,6 +4573,7 @@ export class MmtpRouter {
|
|
|
4345
4573
|
if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
|
|
4346
4574
|
this.#feedOverlaySource(
|
|
4347
4575
|
info.packetId, catalogTrackName, packet, parsed.header, info.sequenceNumber, info.rapFlag, source,
|
|
4576
|
+
fecPayloadId?.ssId,
|
|
4348
4577
|
);
|
|
4349
4578
|
}
|
|
4350
4579
|
}
|
|
@@ -4573,6 +4802,7 @@ export class MmtpRouter {
|
|
|
4573
4802
|
stream = {
|
|
4574
4803
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
4575
4804
|
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
4805
|
+
overlayToBase: { routed: new Set(), pending: new Map() },
|
|
4576
4806
|
};
|
|
4577
4807
|
byName.set(catalogTrackName, stream);
|
|
4578
4808
|
}
|
|
@@ -4771,8 +5001,28 @@ export class MmtpRouter {
|
|
|
4771
5001
|
data: Uint8Array,
|
|
4772
5002
|
meta: RoutedFrameMeta,
|
|
4773
5003
|
cb: FrameCallback | null,
|
|
5004
|
+
/** The base SS_ID the FEC release gate ordered the frame by, if any. */
|
|
5005
|
+
fecSbn: number | undefined,
|
|
4774
5006
|
): void {
|
|
4775
5007
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
5008
|
+
// The FEC release gate let this frame out in order: only once every
|
|
5009
|
+
// block between it and the frame before it resolved, or because it
|
|
5010
|
+
// starts where that frame ended. Nothing still in flight can precede it
|
|
5011
|
+
// then, so it plays now, as a source frame does; holding it for the next
|
|
5012
|
+
// intact frame cost every repaired burst its length in decoder input
|
|
5013
|
+
// (BLO-52085). An overlay recovery names no base block for the gate to
|
|
5014
|
+
// wait on, so past a gap it may be ahead of frames still in flight (an
|
|
5015
|
+
// overlay that outran the wire, BLO-53342): it keeps waiting for the
|
|
5016
|
+
// source cursor below, unless it starts where the last emitted frame
|
|
5017
|
+
// ended (on this direct path, the delivered high water).
|
|
5018
|
+
if (this.#isFecTrack(packetId)) {
|
|
5019
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
5020
|
+
if (fecSbn !== undefined || (high !== undefined && authoredAdjacent(high, meta))) {
|
|
5021
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, frame => frame.timestamp <= meta.timestamp, cb);
|
|
5022
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
4776
5026
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
4777
5027
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
4778
5028
|
this.#drainDirectRecoveredVideo(
|
|
@@ -4950,6 +5200,7 @@ export class MmtpRouter {
|
|
|
4950
5200
|
ownerPacketId?: number,
|
|
4951
5201
|
catalogTrackName?: string,
|
|
4952
5202
|
complete?: () => void,
|
|
5203
|
+
assisted = false,
|
|
4953
5204
|
): boolean {
|
|
4954
5205
|
// MMTP path: use processFecBlock for MMTP-wrapped recovery.
|
|
4955
5206
|
// Recovered symbols are individual MMTP packets that may be fragments
|
|
@@ -4974,7 +5225,7 @@ export class MmtpRouter {
|
|
|
4974
5225
|
ownerPacketId,
|
|
4975
5226
|
blockProfile,
|
|
4976
5227
|
});
|
|
4977
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
5228
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
4978
5229
|
}
|
|
4979
5230
|
|
|
4980
5231
|
/**
|
|
@@ -4987,6 +5238,19 @@ export class MmtpRouter {
|
|
|
4987
5238
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
4988
5239
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
4989
5240
|
* outside a protected flow.
|
|
5241
|
+
*
|
|
5242
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
5243
|
+
* without a record: while the flow is protected that decoder takes only
|
|
5244
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
5245
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
5246
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
5247
|
+
* unlisted MoQ repair and still needs a record.
|
|
5248
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
5249
|
+
* ever mixes verified and unverified live inputs.
|
|
5250
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
5251
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
5252
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
5253
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
4990
5254
|
*/
|
|
4991
5255
|
#routeVerifiedFecSymbols(
|
|
4992
5256
|
blockId: number,
|
|
@@ -4994,8 +5258,22 @@ export class MmtpRouter {
|
|
|
4994
5258
|
ownerPacketId?: number,
|
|
4995
5259
|
catalogTrackName?: string,
|
|
4996
5260
|
complete?: () => void,
|
|
5261
|
+
assisted = false,
|
|
4997
5262
|
): boolean {
|
|
4998
|
-
|
|
5263
|
+
// A packet a keyframe overlay recovery routed before it fed the packet to
|
|
5264
|
+
// this block (#feedOverlayRecoveredToBase) is not routed again.
|
|
5265
|
+
const overlayRouted = ownerPacketId == null
|
|
5266
|
+
? undefined
|
|
5267
|
+
: this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
5268
|
+
if (overlayRouted !== undefined && overlayRouted.size > 0) {
|
|
5269
|
+
result = {
|
|
5270
|
+
...result,
|
|
5271
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index))),
|
|
5272
|
+
};
|
|
5273
|
+
}
|
|
5274
|
+
const entry = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
5275
|
+
const receiver = entry === undefined || entry === "ambiguous" ? entry : entry.receiver;
|
|
5276
|
+
const userAgent = !assisted && entry !== undefined && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
4999
5277
|
let recovered = true;
|
|
5000
5278
|
if (receiver) {
|
|
5001
5279
|
const parsed = this.#stream(ownerPacketId!, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -5008,6 +5286,8 @@ export class MmtpRouter {
|
|
|
5008
5286
|
let admitted = false;
|
|
5009
5287
|
if (receiver === "ambiguous") {
|
|
5010
5288
|
this.#ambiguousRecoveredDrops++;
|
|
5289
|
+
} else if (userAgent) {
|
|
5290
|
+
admitted = true;
|
|
5011
5291
|
} else {
|
|
5012
5292
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
5013
5293
|
wire.set(sym.rawSymbol);
|
|
@@ -5033,7 +5313,7 @@ export class MmtpRouter {
|
|
|
5033
5313
|
#provenanceFor(
|
|
5034
5314
|
packetId: number,
|
|
5035
5315
|
catalogTrackName: string | undefined,
|
|
5036
|
-
):
|
|
5316
|
+
): ProvenanceEntry | "ambiguous" | undefined {
|
|
5037
5317
|
// A caller that names no track matches every name the packet id was admitted under.
|
|
5038
5318
|
const byName = this.#provenanceStreamFlow.get(packetId);
|
|
5039
5319
|
const flows = new Set(
|
|
@@ -5042,12 +5322,12 @@ export class MmtpRouter {
|
|
|
5042
5322
|
: byName.has(catalogTrackName) ? [byName.get(catalogTrackName)!] : [],
|
|
5043
5323
|
);
|
|
5044
5324
|
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))) {
|
|
5325
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
5326
|
+
let match: ProvenanceEntry | undefined;
|
|
5327
|
+
for (const entry of this.#provenance.values()) {
|
|
5328
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
5049
5329
|
if (match) return "ambiguous";
|
|
5050
|
-
match =
|
|
5330
|
+
match = entry;
|
|
5051
5331
|
}
|
|
5052
5332
|
}
|
|
5053
5333
|
return match;
|
|
@@ -5141,6 +5421,43 @@ export class MmtpRouter {
|
|
|
5141
5421
|
);
|
|
5142
5422
|
}
|
|
5143
5423
|
|
|
5424
|
+
// A recovered head whose sample FT=1 has not named yet (an overlay that
|
|
5425
|
+
// outran the source path) would complete its data unit untimed: the unit
|
|
5426
|
+
// is dropped, marked completed, and the timed source copy is then refused
|
|
5427
|
+
// (BLO-53342). Hand it to the parser instead, which holds it in its
|
|
5428
|
+
// awaiting-timing queue and feeds it again when FT=1 lands; its recovered
|
|
5429
|
+
// continuations park in the reassembler meanwhile. If the source path
|
|
5430
|
+
// lost the head, this held copy is the keyframe (BLO-53369).
|
|
5431
|
+
const untimedHead = (sym: (typeof result.symbols)[number]): boolean =>
|
|
5432
|
+
sym.fragmentType === 2 &&
|
|
5433
|
+
sym.fragment.fragmentationIndicator === 1 &&
|
|
5434
|
+
sym.fragment.movieFragmentSequenceNumber !== undefined &&
|
|
5435
|
+
sym.fragment.sampleNumber !== undefined &&
|
|
5436
|
+
!this.#parser.hasAuthoredTimingFor(
|
|
5437
|
+
ownerPacketId,
|
|
5438
|
+
sym.fragment.mpuSequenceNumber,
|
|
5439
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
5440
|
+
sym.fragment.sampleNumber,
|
|
5441
|
+
catalogTrackName,
|
|
5442
|
+
);
|
|
5443
|
+
for (const sym of result.symbols) {
|
|
5444
|
+
if (!untimedHead(sym) ||
|
|
5445
|
+
this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
5446
|
+
this.#feedViaParser(
|
|
5447
|
+
sym.rawSymbol,
|
|
5448
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
5449
|
+
undefined,
|
|
5450
|
+
{
|
|
5451
|
+
skipFecPayloadId: true,
|
|
5452
|
+
recovered: true,
|
|
5453
|
+
deliveryProvenance,
|
|
5454
|
+
...fecSbnOf(sym.index),
|
|
5455
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
5456
|
+
},
|
|
5457
|
+
);
|
|
5458
|
+
}
|
|
5459
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
5460
|
+
|
|
5144
5461
|
// MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
|
|
5145
5462
|
// BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
|
|
5146
5463
|
// frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
|