@blockcast/mmt-container 2.2.0-main.75b39a2cdddf → 2.2.0-main.7e8a9d13c01c
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/dist/browser-runtime.js +634 -129
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +37 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +181 -52
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +39 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +80 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +21 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +114 -11
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +113 -12
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +114 -11
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +18 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +254 -44
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +13 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +455 -103
- package/dist/multicast-manager.js.map +2 -2
- package/dist/no-modules.js +114 -11
- package/dist/no-modules.js.map +2 -2
- 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/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-mmtp-container-parser.js +114 -11
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +156 -26
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +114 -31
- package/src/mmtp-container-parser.ts +181 -10
- package/src/mmtp-router.ts +231 -58
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/track-session.ts +3 -0
package/src/mmtp-router.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type MoqRepairFrame,
|
|
25
25
|
} from "@blockcast/mmt-base";
|
|
26
26
|
import { USER_AGENT_VERIFIED } from "@blockcast/mmt-transport";
|
|
27
|
-
import type { FECManager, IsoHeldSource, RebuiltSourceFeed, WasmFecDecoder } from "./fec-manager.js";
|
|
27
|
+
import type { FECManager, IsoHeldSource, IsoSpanFailure, RebuiltSourceFeed, WasmFecDecoder } from "./fec-manager.js";
|
|
28
28
|
import type { IsoPlacedSource, IsoRepairPlacement } from "./iso-block-locator.js";
|
|
29
29
|
import { isMfuReassemblyError } from "./mfu-reassembler.js";
|
|
30
30
|
import type {
|
|
@@ -216,7 +216,9 @@ export interface KeyframeOverlayStats {
|
|
|
216
216
|
protectedDropped: number;
|
|
217
217
|
/** Rebuilt packets fed to their base block's decoder (joint decoding). */
|
|
218
218
|
baseFed: number;
|
|
219
|
-
/** Rebuilt packets the base never took: no base SS_ID could be determined
|
|
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). */
|
|
220
222
|
baseUndetermined: number;
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -260,12 +262,12 @@ function overlayBaseSsId(
|
|
|
260
262
|
* one per FEC source packet (mmt-02 7.1) and psn by one per packet, so on a
|
|
261
263
|
* track whose every packet is an FEC source packet the sender keeps
|
|
262
264
|
* SS_ID - psn fixed. `offset` (SS_ID - psn of the stream's latest received
|
|
263
|
-
* source) is that sender value while
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* the one shown, which voids it, as does a packet
|
|
267
|
-
*
|
|
268
|
-
* span). Undefined without both.
|
|
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.
|
|
269
271
|
* ponytail: exact only if every packet from the rebuilt one to the latest
|
|
270
272
|
* received source is an FEC source packet, as FFmpeg's muxer sends a track
|
|
271
273
|
* it protects. fec-core 8.3 lets a publisher leave one unprotected
|
|
@@ -313,10 +315,18 @@ interface OverlayInstance {
|
|
|
313
315
|
readonly offsets: Map<number, OverlayMemberAnchor>;
|
|
314
316
|
/** Overlay blocks a MoQ symbol fed (multicast-02 7.2.7). */
|
|
315
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>;
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
/** Offsets kept per overlay, one per MPU: a keyframe's fragments share one. */
|
|
319
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;
|
|
320
330
|
/** A MoQ-fed overlay block is forgotten once it starts this many K_overlay
|
|
321
331
|
* behind the newest overlay SS_ID: the overlay locator, which keeps a fixed
|
|
322
332
|
* few blocks (no catalog timing), has retired it long before, so it can no
|
|
@@ -833,13 +843,14 @@ export interface FECManagerLike {
|
|
|
833
843
|
|
|
834
844
|
/** Track a first-arrival network source symbol for block completion and
|
|
835
845
|
* classification. Refreshes the inactivity deadline and marks clean when
|
|
836
|
-
* K arrive. */
|
|
846
|
+
* K arrive. `held`: a source held unplaced since it arrived, placed now. */
|
|
837
847
|
trackBlock(
|
|
838
848
|
sbn: number,
|
|
839
849
|
packetId: number,
|
|
840
850
|
catalogTrackName: string | undefined,
|
|
841
851
|
esi: number,
|
|
842
852
|
evidence: SourceSymbolEvidence,
|
|
853
|
+
held?: boolean,
|
|
843
854
|
): void;
|
|
844
855
|
|
|
845
856
|
/** True once the packetId has a per-track FEC configuration. */
|
|
@@ -1009,6 +1020,7 @@ export interface FECManagerLike {
|
|
|
1009
1020
|
packetId: number,
|
|
1010
1021
|
catalogTrackName: string | undefined,
|
|
1011
1022
|
blockClass: BlockClass,
|
|
1023
|
+
span?: IsoSpanFailure,
|
|
1012
1024
|
) => void)
|
|
1013
1025
|
| null;
|
|
1014
1026
|
|
|
@@ -1155,6 +1167,17 @@ export class MmtpRouter {
|
|
|
1155
1167
|
* an MPU no member of which arrived).
|
|
1156
1168
|
*/
|
|
1157
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>();
|
|
1158
1181
|
/**
|
|
1159
1182
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
1160
1183
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -1203,6 +1226,7 @@ export class MmtpRouter {
|
|
|
1203
1226
|
packetId: number,
|
|
1204
1227
|
catalogTrackName: string | undefined,
|
|
1205
1228
|
blockClass: BlockClass,
|
|
1229
|
+
span?: IsoSpanFailure,
|
|
1206
1230
|
) => void)
|
|
1207
1231
|
| null = null;
|
|
1208
1232
|
/** Typed observer of every gap decision the release gate makes. */
|
|
@@ -1300,6 +1324,7 @@ export class MmtpRouter {
|
|
|
1300
1324
|
packetId: number,
|
|
1301
1325
|
catalogTrackName: string | undefined,
|
|
1302
1326
|
blockClass: BlockClass,
|
|
1327
|
+
span?: IsoSpanFailure,
|
|
1303
1328
|
) => void;
|
|
1304
1329
|
/** Internal gap-decision sink owned by the composing transport. */
|
|
1305
1330
|
onFrameGap?: (decision: FrameGapDecision) => void;
|
|
@@ -1533,6 +1558,7 @@ export class MmtpRouter {
|
|
|
1533
1558
|
packetId: number,
|
|
1534
1559
|
catalogTrackName: string | undefined,
|
|
1535
1560
|
blockClass: BlockClass,
|
|
1561
|
+
span?: IsoSpanFailure,
|
|
1536
1562
|
) => void)
|
|
1537
1563
|
| undefined,
|
|
1538
1564
|
): void {
|
|
@@ -1585,6 +1611,7 @@ export class MmtpRouter {
|
|
|
1585
1611
|
packetId: number,
|
|
1586
1612
|
catalogTrackName: string | undefined,
|
|
1587
1613
|
blockClass: BlockClass,
|
|
1614
|
+
span?: IsoSpanFailure,
|
|
1588
1615
|
) => {
|
|
1589
1616
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
1590
1617
|
if (
|
|
@@ -1593,8 +1620,13 @@ export class MmtpRouter {
|
|
|
1593
1620
|
) {
|
|
1594
1621
|
this.#onDeltaBlockCompleteCb?.(packetId, true); // failed=true
|
|
1595
1622
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
+
}
|
|
1598
1630
|
};
|
|
1599
1631
|
}
|
|
1600
1632
|
}
|
|
@@ -1647,8 +1679,9 @@ export class MmtpRouter {
|
|
|
1647
1679
|
|
|
1648
1680
|
/**
|
|
1649
1681
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
1650
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
1651
|
-
*
|
|
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.
|
|
1652
1685
|
*/
|
|
1653
1686
|
get restampedRecoveredSequenceCount(): number {
|
|
1654
1687
|
return this.#restampedRecoveredSeqs;
|
|
@@ -1872,6 +1905,11 @@ export class MmtpRouter {
|
|
|
1872
1905
|
): CmafBlockAssembler {
|
|
1873
1906
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
1874
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);
|
|
1875
1913
|
if (previous) {
|
|
1876
1914
|
previous.flush();
|
|
1877
1915
|
previous.dispose();
|
|
@@ -1943,6 +1981,7 @@ export class MmtpRouter {
|
|
|
1943
1981
|
packetId: number,
|
|
1944
1982
|
catalogTrackName: string | undefined,
|
|
1945
1983
|
blockClass: BlockClass,
|
|
1984
|
+
span?: IsoSpanFailure,
|
|
1946
1985
|
) => void,
|
|
1947
1986
|
): void {
|
|
1948
1987
|
this.#onBlockFailed = cb;
|
|
@@ -2047,7 +2086,7 @@ export class MmtpRouter {
|
|
|
2047
2086
|
const leg = this.#overlayLegs.get(repairPacketId);
|
|
2048
2087
|
if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
|
|
2049
2088
|
const inst: OverlayInstance = {
|
|
2050
|
-
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(),
|
|
2051
2090
|
};
|
|
2052
2091
|
this.#overlays.set(repairPacketId, inst);
|
|
2053
2092
|
return () => {
|
|
@@ -2096,6 +2135,7 @@ export class MmtpRouter {
|
|
|
2096
2135
|
inst.last = undefined;
|
|
2097
2136
|
inst.offsets.clear();
|
|
2098
2137
|
inst.moqBlocks.clear();
|
|
2138
|
+
inst.rebuilt.clear();
|
|
2099
2139
|
}
|
|
2100
2140
|
|
|
2101
2141
|
#overlaysForSource(sourcePacketId: number, catalogTrackName: string | undefined): OverlayInstance[] {
|
|
@@ -2168,6 +2208,13 @@ export class MmtpRouter {
|
|
|
2168
2208
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
2169
2209
|
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
2170
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).
|
|
2171
2218
|
*/
|
|
2172
2219
|
#feedOverlaySource(
|
|
2173
2220
|
packetId: number,
|
|
@@ -2182,8 +2229,12 @@ export class MmtpRouter {
|
|
|
2182
2229
|
if (this.#overlays.size === 0) return;
|
|
2183
2230
|
const ssId = keyframeOverlaySsId(header);
|
|
2184
2231
|
if (ssId === undefined) return;
|
|
2232
|
+
let rebuiltPsn: number | undefined;
|
|
2185
2233
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
2186
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);
|
|
2187
2238
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
2188
2239
|
if (identity !== null) {
|
|
2189
2240
|
// The newest member anchors its MPU; one with no base SS_ID leaves the
|
|
@@ -2221,6 +2272,18 @@ export class MmtpRouter {
|
|
|
2221
2272
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
2222
2273
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
2223
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
|
+
}
|
|
2224
2287
|
}
|
|
2225
2288
|
|
|
2226
2289
|
#noteOverlayMoqBlock(inst: OverlayInstance, blockStart: number): void {
|
|
@@ -2261,11 +2324,16 @@ export class MmtpRouter {
|
|
|
2261
2324
|
|
|
2262
2325
|
/**
|
|
2263
2326
|
* Route an overlay-recovered block's packets as source media of the
|
|
2264
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
*
|
|
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.
|
|
2269
2337
|
*/
|
|
2270
2338
|
#routeOverlayRecovered(inst: OverlayInstance, blockStart: number, recovered: Uint8Array): void {
|
|
2271
2339
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -2288,51 +2356,96 @@ export class MmtpRouter {
|
|
|
2288
2356
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
2289
2357
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
2290
2358
|
const restored = new Set<number>();
|
|
2291
|
-
|
|
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>();
|
|
2292
2363
|
const symbols = result.symbols.flatMap((sym) => {
|
|
2364
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
2293
2365
|
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
2294
2366
|
if (anchor !== undefined) {
|
|
2295
|
-
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
2296
2367
|
const packetSequenceNumber = (overlaySsId - anchor.psnOffset) >>> 0;
|
|
2297
2368
|
restored.add(packetSequenceNumber);
|
|
2298
2369
|
const out = { ...sym, packetSequenceNumber };
|
|
2299
2370
|
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
2300
|
-
if (baseSsId !== undefined)
|
|
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
|
+
}
|
|
2301
2387
|
return [out];
|
|
2302
2388
|
}
|
|
2303
2389
|
// A stream re-stamped by a bridge that no base recovery has shown yet
|
|
2304
2390
|
// keeps the header's sequence too: from received packets alone the
|
|
2305
|
-
// router cannot tell (their psn is the wire one)
|
|
2306
|
-
//
|
|
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.
|
|
2307
2394
|
if (!restamped) {
|
|
2308
|
-
// No member of this MPU arrived to anchor a base SS_ID.
|
|
2309
|
-
//
|
|
2310
|
-
//
|
|
2311
|
-
//
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
);
|
|
2317
|
-
|
|
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
|
+
}
|
|
2318
2413
|
return [sym];
|
|
2319
2414
|
}
|
|
2320
2415
|
this.#overlayStats.unsequencedDropped++;
|
|
2321
2416
|
return [];
|
|
2322
2417
|
});
|
|
2323
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
|
+
}
|
|
2324
2432
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
2325
2433
|
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
2434
|
+
baseSsIds,
|
|
2435
|
+
firstSsIds,
|
|
2326
2436
|
});
|
|
2327
2437
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) =>
|
|
2328
2438
|
restored.has(sym.packetSequenceNumber)));
|
|
2329
|
-
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols,
|
|
2439
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0
|
|
2440
|
+
? baseSsIds
|
|
2441
|
+
: new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
2330
2442
|
}
|
|
2331
2443
|
|
|
2332
2444
|
/**
|
|
2333
2445
|
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
2334
2446
|
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
2335
|
-
* from a received member of its MPU, else unstampedBaseSsId
|
|
2447
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
2448
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
2336
2449
|
* so it is fed to the base decoder as a rebuilt source
|
|
2337
2450
|
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
2338
2451
|
* fewer to solve, and its repair can recover the block's other packets.
|
|
@@ -2352,7 +2465,7 @@ export class MmtpRouter {
|
|
|
2352
2465
|
inst: OverlayInstance,
|
|
2353
2466
|
blockStart: number,
|
|
2354
2467
|
symbols: ProcessFecBlockResult["symbols"],
|
|
2355
|
-
baseSsIds: ReadonlyMap<
|
|
2468
|
+
baseSsIds: ReadonlyMap<number, number>,
|
|
2356
2469
|
): void {
|
|
2357
2470
|
const fec = this.#fecManager;
|
|
2358
2471
|
const owner = inst.leg.sourcePacketId;
|
|
@@ -2364,7 +2477,7 @@ export class MmtpRouter {
|
|
|
2364
2477
|
for (const sym of symbols) {
|
|
2365
2478
|
// A packet the base received already sits in its block.
|
|
2366
2479
|
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
2367
|
-
const ssId = baseSsIds.get(sym);
|
|
2480
|
+
const ssId = baseSsIds.get(sym.index);
|
|
2368
2481
|
if (ssId === undefined) {
|
|
2369
2482
|
this.#overlayStats.baseUndetermined++;
|
|
2370
2483
|
continue;
|
|
@@ -3312,7 +3425,7 @@ export class MmtpRouter {
|
|
|
3312
3425
|
catalogTrackName,
|
|
3313
3426
|
)) continue;
|
|
3314
3427
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
3315
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
3428
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
3316
3429
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
3317
3430
|
}
|
|
3318
3431
|
if (payload.oversized) continue;
|
|
@@ -3431,6 +3544,7 @@ export class MmtpRouter {
|
|
|
3431
3544
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
3432
3545
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
3433
3546
|
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
3547
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
3434
3548
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
3435
3549
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
3436
3550
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -3562,6 +3676,12 @@ export class MmtpRouter {
|
|
|
3562
3676
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
3563
3677
|
* own RAP cadence, so no timer is involved.
|
|
3564
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
|
+
*
|
|
3565
3685
|
* The commit is one synchronous transition inside the frame path: the
|
|
3566
3686
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
3567
3687
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -3614,6 +3734,16 @@ export class MmtpRouter {
|
|
|
3614
3734
|
droppedIncomingFrames: 0,
|
|
3615
3735
|
};
|
|
3616
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
|
+
}
|
|
3617
3747
|
return {
|
|
3618
3748
|
cancel: () => {
|
|
3619
3749
|
if (this.#staged !== state) return false;
|
|
@@ -3708,7 +3838,24 @@ export class MmtpRouter {
|
|
|
3708
3838
|
// same call. Nothing registers a packet ID mid-frame, so this drops
|
|
3709
3839
|
// only frames that lost their registration.
|
|
3710
3840
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
3711
|
-
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
|
+
}
|
|
3712
3859
|
this.#deliverParsedFrame(data, meta);
|
|
3713
3860
|
}
|
|
3714
3861
|
|
|
@@ -3901,12 +4048,17 @@ export class MmtpRouter {
|
|
|
3901
4048
|
this.#parser.deferFecRelease(work);
|
|
3902
4049
|
} finally {
|
|
3903
4050
|
this.#fecPokeDepth--;
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
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);
|
|
3910
4062
|
}
|
|
3911
4063
|
}
|
|
3912
4064
|
}
|
|
@@ -3995,6 +4147,14 @@ export class MmtpRouter {
|
|
|
3995
4147
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
3996
4148
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
3997
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
|
+
},
|
|
3998
4158
|
onReleased: (data, meta) => {
|
|
3999
4159
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === undefined) return;
|
|
4000
4160
|
const timing = meta.authoredTiming;
|
|
@@ -4680,6 +4840,7 @@ export class MmtpRouter {
|
|
|
4680
4840
|
deliveryProvenance?: MmtpDeliveryProvenance;
|
|
4681
4841
|
catalogTrackName?: string;
|
|
4682
4842
|
fecSbn?: number;
|
|
4843
|
+
fecSbnFirst?: number;
|
|
4683
4844
|
},
|
|
4684
4845
|
parsed?: ParsedMmtpPacket,
|
|
4685
4846
|
): void {
|
|
@@ -4703,6 +4864,7 @@ export class MmtpRouter {
|
|
|
4703
4864
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
4704
4865
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
4705
4866
|
...(parserOptions?.fecSbn === undefined ? {} : { fecSbn: parserOptions.fecSbn }),
|
|
4867
|
+
...(parserOptions?.fecSbnFirst === undefined ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }),
|
|
4706
4868
|
};
|
|
4707
4869
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
4708
4870
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -5088,15 +5250,16 @@ export class MmtpRouter {
|
|
|
5088
5250
|
): void {
|
|
5089
5251
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
5090
5252
|
// The FEC release gate let this frame out in order: only once every
|
|
5091
|
-
// block between it and the frame before it resolved,
|
|
5092
|
-
//
|
|
5093
|
-
//
|
|
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
|
|
5094
5257
|
// intact frame cost every repaired burst its length in decoder input
|
|
5095
|
-
// (BLO-52085). An overlay recovery
|
|
5096
|
-
// wait on, so past a gap it may be ahead of
|
|
5097
|
-
// overlay that outran the wire, BLO-53342): it
|
|
5098
|
-
// source cursor below, unless it starts where the
|
|
5099
|
-
// ended (on this direct path, the delivered high water).
|
|
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).
|
|
5100
5263
|
if (this.#isFecTrack(packetId)) {
|
|
5101
5264
|
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
5102
5265
|
if (fecSbn !== undefined || (high !== undefined && authoredAdjacent(high, meta))) {
|
|
@@ -5422,15 +5585,25 @@ export class MmtpRouter {
|
|
|
5422
5585
|
catalogTrackName?: string,
|
|
5423
5586
|
/** Set for a keyframe-overlay recovery: `blockId` is an overlay
|
|
5424
5587
|
* coordinate, which yields no base SS_ID (mmt-02 7.2), and the packets'
|
|
5425
|
-
* wire sequences are already restored.
|
|
5426
|
-
|
|
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
|
+
},
|
|
5427
5596
|
): void {
|
|
5428
5597
|
// Route recovered symbols to the track's callback. The ownerPacketId tells us
|
|
5429
5598
|
// which track this block belongs to — resolves per-track, then legacy fallback.
|
|
5430
5599
|
if (ownerPacketId == null) return; // no track association
|
|
5431
5600
|
if (this.#classifyPacketId(ownerPacketId) === null) return; // unknown track
|
|
5432
|
-
const fecSbnOf = (index: number): { fecSbn?: number } =>
|
|
5433
|
-
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
|
+
};
|
|
5434
5607
|
// A recovered symbol's header names the sequence the sender protected. A
|
|
5435
5608
|
// hop that re-stamps packet_sequence_number after protection (cast's
|
|
5436
5609
|
// publisher-rotation bridge) leaves the decode mixing every received
|