@blockcast/mmt-container 2.2.0-main.4fb5899c44e6 → 2.2.0-main.66f097ed5157
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 +595 -61
- package/dist/browser-runtime.js.map +3 -3
- package/dist/fec-manager.d.ts +30 -0
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +196 -2
- package/dist/fec-manager.js.map +2 -2
- 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 +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/mmtp-container-parser.d.ts +75 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +157 -25
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +159 -28
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +156 -25
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +24 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +397 -57
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +595 -61
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +2 -2
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +156 -25
- package/dist/no-modules.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +156 -25
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +251 -2
- package/src/index.ts +1 -0
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +222 -28
- package/src/mmtp-router.ts +413 -39
package/dist/browser-runtime.js
CHANGED
|
@@ -8625,9 +8625,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
|
8625
8625
|
} catch (error) {
|
|
8626
8626
|
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8627
8627
|
}
|
|
8628
|
-
if (
|
|
8628
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8629
8629
|
await rx.close();
|
|
8630
|
-
throw unsupported(`integrity scheme ${String(
|
|
8630
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8631
8631
|
}
|
|
8632
8632
|
const events = new EventTarget();
|
|
8633
8633
|
let state = "connected";
|
|
@@ -12949,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12949
12949
|
#retentionBlocks;
|
|
12950
12950
|
#heldEvicted;
|
|
12951
12951
|
#newest;
|
|
12952
|
+
#newestSource;
|
|
12952
12953
|
#holes;
|
|
12953
12954
|
static #checkedRetention(retentionBlocks) {
|
|
12954
12955
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
@@ -12986,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12986
12987
|
observe(position) {
|
|
12987
12988
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12988
12989
|
}
|
|
12990
|
+
/**
|
|
12991
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
12992
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
12993
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
12994
|
+
* reordering.
|
|
12995
|
+
*/
|
|
12996
|
+
get newestSource() {
|
|
12997
|
+
return this.#newestSource;
|
|
12998
|
+
}
|
|
12999
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
13000
|
+
noteSource(ssId) {
|
|
13001
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
13002
|
+
this.#newestSource = ssId;
|
|
13003
|
+
return true;
|
|
13004
|
+
}
|
|
12989
13005
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12990
13006
|
get heldEvicted() {
|
|
12991
13007
|
return this.#heldEvicted;
|
|
@@ -13218,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13218
13234
|
this.#held.clear();
|
|
13219
13235
|
this.#coverageEnd = void 0;
|
|
13220
13236
|
this.#newest = void 0;
|
|
13237
|
+
this.#newestSource = void 0;
|
|
13221
13238
|
}
|
|
13222
13239
|
#claim(block) {
|
|
13223
13240
|
const placed = [];
|
|
@@ -13837,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13837
13854
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13838
13855
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13839
13856
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13857
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13840
13858
|
function isMulticastSymbolDelivery(value) {
|
|
13841
13859
|
return value === "datagram" || value === "ordered-relay";
|
|
13842
13860
|
}
|
|
@@ -13934,6 +13952,10 @@ var FECManager = class _FECManager {
|
|
|
13934
13952
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13935
13953
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13936
13954
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
13955
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
13956
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
13957
|
+
// not network arrivals and never make a block clean.
|
|
13958
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13937
13959
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13938
13960
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13939
13961
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -14223,6 +14245,7 @@ var FECManager = class _FECManager {
|
|
|
14223
14245
|
#pendingWorkerCleanupBounds;
|
|
14224
14246
|
#blockSourceCounts;
|
|
14225
14247
|
#blockSourceEsis;
|
|
14248
|
+
#blockRebuiltEsis;
|
|
14226
14249
|
#blockAuthoredLengths;
|
|
14227
14250
|
#ssbLengthConflictCount;
|
|
14228
14251
|
#blockRepairRsIds;
|
|
@@ -15545,6 +15568,7 @@ var FECManager = class _FECManager {
|
|
|
15545
15568
|
}
|
|
15546
15569
|
resetMap(this.#blockSourceCounts);
|
|
15547
15570
|
resetMap(this.#blockSourceEsis);
|
|
15571
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15548
15572
|
resetMap(this.#blockAuthoredLengths);
|
|
15549
15573
|
resetMap(this.#blockRepairRsIds);
|
|
15550
15574
|
resetMap(this.#blockRsbLengths);
|
|
@@ -15669,6 +15693,7 @@ var FECManager = class _FECManager {
|
|
|
15669
15693
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15670
15694
|
dropMap(this.#blockSourceCounts);
|
|
15671
15695
|
dropMap(this.#blockSourceEsis);
|
|
15696
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15672
15697
|
dropMap(this.#blockAuthoredLengths);
|
|
15673
15698
|
dropMap(this.#blockRepairRsIds);
|
|
15674
15699
|
dropMap(this.#blockRsbLengths);
|
|
@@ -15831,6 +15856,9 @@ var FECManager = class _FECManager {
|
|
|
15831
15856
|
this.#unindexBlockKey(key);
|
|
15832
15857
|
}
|
|
15833
15858
|
}
|
|
15859
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15860
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15861
|
+
}
|
|
15834
15862
|
return true;
|
|
15835
15863
|
}
|
|
15836
15864
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -16148,6 +16176,7 @@ var FECManager = class _FECManager {
|
|
|
16148
16176
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16149
16177
|
const evictedBefore = locator.heldEvicted;
|
|
16150
16178
|
const held = locator.hold(ssId, source);
|
|
16179
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16151
16180
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16152
16181
|
if (evicted > 0) {
|
|
16153
16182
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16743,7 +16772,15 @@ var FECManager = class _FECManager {
|
|
|
16743
16772
|
this.#isoSourceOversizeCount++;
|
|
16744
16773
|
return null;
|
|
16745
16774
|
}
|
|
16746
|
-
|
|
16775
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16776
|
+
}
|
|
16777
|
+
/**
|
|
16778
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16779
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16780
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16781
|
+
* stated for `ssId`.
|
|
16782
|
+
*/
|
|
16783
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16747
16784
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16748
16785
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16749
16786
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16772,6 +16809,74 @@ var FECManager = class _FECManager {
|
|
|
16772
16809
|
}
|
|
16773
16810
|
return recovered;
|
|
16774
16811
|
}
|
|
16812
|
+
/**
|
|
16813
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16814
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16815
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16816
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16817
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16818
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16819
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16820
|
+
*
|
|
16821
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16822
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16823
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16824
|
+
* feedSourceSymbol takes it.
|
|
16825
|
+
*
|
|
16826
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16827
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16828
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16829
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16830
|
+
* block as clean on its own side), the block is completed here as
|
|
16831
|
+
* recovered, with nothing left to route.
|
|
16832
|
+
*/
|
|
16833
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16834
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16835
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16836
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16837
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16838
|
+
const sbn = located.block.start;
|
|
16839
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16840
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16841
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16842
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16843
|
+
if (rebuilt === void 0) {
|
|
16844
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16845
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16846
|
+
}
|
|
16847
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16848
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16849
|
+
rebuilt.add(located.esi);
|
|
16850
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16851
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
16852
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
16853
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
16854
|
+
}
|
|
16855
|
+
}
|
|
16856
|
+
return { taken: true, recovered };
|
|
16857
|
+
}
|
|
16858
|
+
/**
|
|
16859
|
+
* A block that holds every one of its `length` sources, received or
|
|
16860
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16861
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16862
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16863
|
+
*/
|
|
16864
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16865
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16866
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16867
|
+
let present = rebuilt.size;
|
|
16868
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16869
|
+
if (present < length) return false;
|
|
16870
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16871
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16872
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16873
|
+
if (catalogTrackName === void 0) {
|
|
16874
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16875
|
+
} else {
|
|
16876
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16877
|
+
}
|
|
16878
|
+
return true;
|
|
16879
|
+
}
|
|
16775
16880
|
/**
|
|
16776
16881
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16777
16882
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16974,8 +17079,75 @@ var FECManager = class _FECManager {
|
|
|
16974
17079
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16975
17080
|
return null;
|
|
16976
17081
|
}
|
|
17082
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16977
17083
|
return recovered;
|
|
16978
17084
|
}
|
|
17085
|
+
/**
|
|
17086
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17087
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17088
|
+
* position past a block leaves the sources that block still misses
|
|
17089
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17090
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17091
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17092
|
+
* sent before it.
|
|
17093
|
+
*/
|
|
17094
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17095
|
+
const previous = locator.newestSource;
|
|
17096
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17097
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17098
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17099
|
+
this.#shortenUnrecoverableWait(
|
|
17100
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17101
|
+
block.start,
|
|
17102
|
+
packetId,
|
|
17103
|
+
catalogTrackName
|
|
17104
|
+
);
|
|
17105
|
+
}
|
|
17106
|
+
}
|
|
17107
|
+
/**
|
|
17108
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17109
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17110
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17111
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17112
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17113
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17114
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17115
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17116
|
+
* block.
|
|
17117
|
+
*/
|
|
17118
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17119
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17120
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17121
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17122
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
17123
|
+
const received = this.#blockSourceEsis.get(key);
|
|
17124
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
17125
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
17126
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17127
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17128
|
+
return void 0;
|
|
17129
|
+
}
|
|
17130
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17131
|
+
}
|
|
17132
|
+
/**
|
|
17133
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17134
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17135
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17136
|
+
*/
|
|
17137
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17138
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17139
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17140
|
+
if (config === void 0) return;
|
|
17141
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17142
|
+
if (waitMs === void 0) return;
|
|
17143
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17144
|
+
this.#armBlockTimeout(
|
|
17145
|
+
sbn,
|
|
17146
|
+
packetId,
|
|
17147
|
+
catalogTrackName,
|
|
17148
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17149
|
+
);
|
|
17150
|
+
}
|
|
16979
17151
|
/**
|
|
16980
17152
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16981
17153
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -17130,6 +17302,15 @@ var FECManager = class _FECManager {
|
|
|
17130
17302
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17131
17303
|
return;
|
|
17132
17304
|
}
|
|
17305
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17306
|
+
key,
|
|
17307
|
+
sbn,
|
|
17308
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17309
|
+
packetId,
|
|
17310
|
+
catalogTrackName
|
|
17311
|
+
)) {
|
|
17312
|
+
return;
|
|
17313
|
+
}
|
|
17133
17314
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17134
17315
|
}
|
|
17135
17316
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17138,6 +17319,7 @@ var FECManager = class _FECManager {
|
|
|
17138
17319
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17139
17320
|
this.#blockSourceCounts.delete(key);
|
|
17140
17321
|
this.#blockSourceEsis.delete(key);
|
|
17322
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17141
17323
|
this.#blockAuthoredLengths.delete(key);
|
|
17142
17324
|
this.#blockRepairRsIds.delete(key);
|
|
17143
17325
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17204,7 +17386,16 @@ var FECManager = class _FECManager {
|
|
|
17204
17386
|
this.#repairDeadlineMs(trackConfig)
|
|
17205
17387
|
);
|
|
17206
17388
|
}
|
|
17207
|
-
this.#
|
|
17389
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17390
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17391
|
+
return;
|
|
17392
|
+
}
|
|
17393
|
+
this.#armBlockTimeout(
|
|
17394
|
+
sbn,
|
|
17395
|
+
packetId,
|
|
17396
|
+
catalogTrackName,
|
|
17397
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17398
|
+
);
|
|
17208
17399
|
}
|
|
17209
17400
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17210
17401
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -17217,6 +17408,7 @@ var FECManager = class _FECManager {
|
|
|
17217
17408
|
}
|
|
17218
17409
|
this.#blockSourceCounts.delete(key);
|
|
17219
17410
|
this.#blockSourceEsis.delete(key);
|
|
17411
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17220
17412
|
this.#blockAuthoredLengths.delete(key);
|
|
17221
17413
|
this.#blockRepairRsIds.delete(key);
|
|
17222
17414
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17255,6 +17447,7 @@ var FECManager = class _FECManager {
|
|
|
17255
17447
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17256
17448
|
this.#blockSourceCounts.delete(key);
|
|
17257
17449
|
this.#blockSourceEsis.delete(key);
|
|
17450
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17258
17451
|
this.#blockAuthoredLengths.delete(key);
|
|
17259
17452
|
this.#blockRepairRsIds.delete(key);
|
|
17260
17453
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17346,6 +17539,7 @@ var FECManager = class _FECManager {
|
|
|
17346
17539
|
this.#blockKeysBySbn.clear();
|
|
17347
17540
|
this.#blockSourceCounts.clear();
|
|
17348
17541
|
this.#blockSourceEsis.clear();
|
|
17542
|
+
this.#blockRebuiltEsis.clear();
|
|
17349
17543
|
this.#blockAuthoredLengths.clear();
|
|
17350
17544
|
this.#blockRepairRsIds.clear();
|
|
17351
17545
|
this.#blockRsbLengths.clear();
|
|
@@ -17902,9 +18096,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17902
18096
|
}
|
|
17903
18097
|
#track = null;
|
|
17904
18098
|
#fragments = /* @__PURE__ */ new Map();
|
|
18099
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
18100
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
18101
|
+
/**
|
|
18102
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
18103
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
18104
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
18105
|
+
*/
|
|
18106
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17905
18107
|
#pendingSamples = 0;
|
|
17906
18108
|
#evictedSamples = 0;
|
|
17907
18109
|
#lastMovieFragmentRejection = null;
|
|
18110
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
18111
|
+
get initialized() {
|
|
18112
|
+
return this.#track !== null;
|
|
18113
|
+
}
|
|
17908
18114
|
get pendingSampleCount() {
|
|
17909
18115
|
return this.#pendingSamples;
|
|
17910
18116
|
}
|
|
@@ -17928,7 +18134,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17928
18134
|
this.#track = timing;
|
|
17929
18135
|
return true;
|
|
17930
18136
|
}
|
|
17931
|
-
|
|
18137
|
+
/**
|
|
18138
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
18139
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
18140
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
18141
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
18142
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
18143
|
+
*/
|
|
18144
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17932
18145
|
this.#lastMovieFragmentRejection = null;
|
|
17933
18146
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17934
18147
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17954,6 +18167,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17954
18167
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17955
18168
|
);
|
|
17956
18169
|
}
|
|
18170
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
18171
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17957
18172
|
let freed = 0;
|
|
17958
18173
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17959
18174
|
freed += this.#evictOldestFragment();
|
|
@@ -17967,6 +18182,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17967
18182
|
return false;
|
|
17968
18183
|
}
|
|
17969
18184
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18185
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17970
18186
|
this.#pendingSamples += parsed.samples.length;
|
|
17971
18187
|
return true;
|
|
17972
18188
|
}
|
|
@@ -18024,6 +18240,7 @@ var IsoBmffTimingResolver = class {
|
|
|
18024
18240
|
let live = 0;
|
|
18025
18241
|
for (const sample of samples) if (sample !== null) live++;
|
|
18026
18242
|
this.#fragments.delete(oldest);
|
|
18243
|
+
this.#firstSamples.delete(oldest);
|
|
18027
18244
|
this.#pendingSamples -= live;
|
|
18028
18245
|
this.#evictedSamples += live;
|
|
18029
18246
|
return live;
|
|
@@ -18039,12 +18256,23 @@ var IsoBmffTimingResolver = class {
|
|
|
18039
18256
|
if (!samples || !samples[index]) return false;
|
|
18040
18257
|
samples[index] = null;
|
|
18041
18258
|
this.#pendingSamples--;
|
|
18042
|
-
if (samples.every((value) => value === null))
|
|
18259
|
+
if (samples.every((value) => value === null)) {
|
|
18260
|
+
this.#fragments.delete(key);
|
|
18261
|
+
const first = this.#firstSamples.get(key);
|
|
18262
|
+
this.#firstSamples.delete(key);
|
|
18263
|
+
this.#delivered.delete(key);
|
|
18264
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18265
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18266
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18267
|
+
}
|
|
18268
|
+
}
|
|
18043
18269
|
return true;
|
|
18044
18270
|
}
|
|
18045
18271
|
reset() {
|
|
18046
18272
|
this.#track = null;
|
|
18047
18273
|
this.#fragments.clear();
|
|
18274
|
+
this.#firstSamples.clear();
|
|
18275
|
+
this.#delivered.clear();
|
|
18048
18276
|
this.#pendingSamples = 0;
|
|
18049
18277
|
}
|
|
18050
18278
|
};
|
|
@@ -18276,6 +18504,7 @@ var FecReleaseSequencer = class {
|
|
|
18276
18504
|
stream.lastSbn = void 0;
|
|
18277
18505
|
stream.discontinuity = false;
|
|
18278
18506
|
stream.rapSsId = void 0;
|
|
18507
|
+
stream.sourced = false;
|
|
18279
18508
|
}
|
|
18280
18509
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
18281
18510
|
this.#recordStat("droppedStale");
|
|
@@ -18302,6 +18531,7 @@ var FecReleaseSequencer = class {
|
|
|
18302
18531
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18303
18532
|
else hi = mid;
|
|
18304
18533
|
}
|
|
18534
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18305
18535
|
stream.buf.splice(lo, 0, frame);
|
|
18306
18536
|
this.#drainUnlessDeferred(stream);
|
|
18307
18537
|
}
|
|
@@ -18337,6 +18567,21 @@ var FecReleaseSequencer = class {
|
|
|
18337
18567
|
}
|
|
18338
18568
|
return false;
|
|
18339
18569
|
}
|
|
18570
|
+
/**
|
|
18571
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
18572
|
+
* frame yet. First release, to the sink being replaced, the frames the
|
|
18573
|
+
* unanchored path had already released to it (#cursorPassed): there they
|
|
18574
|
+
* waited in its queue, which its replacement flushes. Then treat the
|
|
18575
|
+
* streams as unsourced until a source frame is released.
|
|
18576
|
+
*/
|
|
18577
|
+
unsource(packetId, catalogTrackName) {
|
|
18578
|
+
for (const stream of this.#streams.values()) {
|
|
18579
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
18580
|
+
this.#drain(stream, true);
|
|
18581
|
+
stream.sourced = false;
|
|
18582
|
+
}
|
|
18583
|
+
}
|
|
18584
|
+
}
|
|
18340
18585
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18341
18586
|
poke(streamKey) {
|
|
18342
18587
|
if (streamKey !== void 0) {
|
|
@@ -18428,13 +18673,15 @@ var FecReleaseSequencer = class {
|
|
|
18428
18673
|
last: null,
|
|
18429
18674
|
lastSbn: void 0,
|
|
18430
18675
|
discontinuity: false,
|
|
18431
|
-
rapSsId: void 0
|
|
18676
|
+
rapSsId: void 0,
|
|
18677
|
+
sourced: false
|
|
18432
18678
|
};
|
|
18433
18679
|
this.#streams.set(streamKey, stream);
|
|
18434
18680
|
}
|
|
18435
18681
|
return stream;
|
|
18436
18682
|
}
|
|
18437
|
-
#
|
|
18683
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
18684
|
+
#drain(stream, asUnanchored = false) {
|
|
18438
18685
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18439
18686
|
const head = stream.buf[0];
|
|
18440
18687
|
const last = stream.last;
|
|
@@ -18451,9 +18698,10 @@ var FecReleaseSequencer = class {
|
|
|
18451
18698
|
}
|
|
18452
18699
|
const headSbn = head.meta.fecSbn;
|
|
18453
18700
|
const from = stream.lastSbn ?? headSbn;
|
|
18454
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18701
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18455
18702
|
let lost = [];
|
|
18456
18703
|
const unanchored = from === void 0 && to === void 0;
|
|
18704
|
+
let unresolved = false;
|
|
18457
18705
|
if (from !== void 0 && to !== void 0) {
|
|
18458
18706
|
const resolved = this.#gate.resolvedLoss(
|
|
18459
18707
|
stream.packetId,
|
|
@@ -18462,7 +18710,8 @@ var FecReleaseSequencer = class {
|
|
|
18462
18710
|
Math.max(from, to),
|
|
18463
18711
|
from
|
|
18464
18712
|
);
|
|
18465
|
-
|
|
18713
|
+
unresolved = resolved === null;
|
|
18714
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18466
18715
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18467
18716
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18468
18717
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18470,20 +18719,50 @@ var FecReleaseSequencer = class {
|
|
|
18470
18719
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18471
18720
|
continue;
|
|
18472
18721
|
}
|
|
18473
|
-
lost = resolved;
|
|
18722
|
+
lost = resolved ?? [];
|
|
18474
18723
|
}
|
|
18475
18724
|
stream.buf.shift();
|
|
18476
18725
|
this.#release(
|
|
18477
18726
|
stream,
|
|
18478
18727
|
head,
|
|
18479
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18728
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18480
18729
|
);
|
|
18481
18730
|
}
|
|
18482
18731
|
}
|
|
18732
|
+
/**
|
|
18733
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
18734
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
18735
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
18736
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
18737
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
18738
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
18739
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
18740
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
18741
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
18742
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
18743
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
18744
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
18745
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
18746
|
+
*/
|
|
18747
|
+
#cursorPassed(stream, asUnanchored) {
|
|
18748
|
+
let at2 = -1;
|
|
18749
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
18750
|
+
const frame = stream.buf[i];
|
|
18751
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
18752
|
+
frame.passed = true;
|
|
18753
|
+
if (at2 < 0) at2 = i;
|
|
18754
|
+
} else if (frame.passed !== true) break;
|
|
18755
|
+
}
|
|
18756
|
+
if (at2 < 0) return false;
|
|
18757
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
18758
|
+
}
|
|
18483
18759
|
#release(stream, frame, decision) {
|
|
18484
18760
|
if (decision) frame.meta.gapBefore = decision;
|
|
18485
18761
|
stream.last = frame;
|
|
18486
|
-
if (frame.meta.fecSbn !== void 0
|
|
18762
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
18763
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
18764
|
+
}
|
|
18765
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
18487
18766
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18488
18767
|
this.#emit(frame.data, frame.meta);
|
|
18489
18768
|
}
|
|
@@ -18526,6 +18805,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18526
18805
|
this.#reclaimedTimingSamples = 0;
|
|
18527
18806
|
this.#evictedTimingSamples = 0;
|
|
18528
18807
|
this.#rejectedMovieFragments = 0;
|
|
18808
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18809
|
+
held: 0,
|
|
18810
|
+
expired: 0,
|
|
18811
|
+
discarded: 0,
|
|
18812
|
+
superseded: 0
|
|
18813
|
+
};
|
|
18529
18814
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18530
18815
|
this.#fecGate = null;
|
|
18531
18816
|
this.#fecSequencer = null;
|
|
@@ -18634,6 +18919,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18634
18919
|
#reclaimedTimingSamples;
|
|
18635
18920
|
#evictedTimingSamples;
|
|
18636
18921
|
#rejectedMovieFragments;
|
|
18922
|
+
#recoveredHeadsAwaitingTiming;
|
|
18637
18923
|
#sequencers;
|
|
18638
18924
|
#fecGate;
|
|
18639
18925
|
#fecSequencer;
|
|
@@ -18739,11 +19025,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18739
19025
|
/**
|
|
18740
19026
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18741
19027
|
*
|
|
18742
|
-
*
|
|
18743
|
-
*
|
|
18744
|
-
*
|
|
18745
|
-
*
|
|
18746
|
-
*
|
|
19028
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
19029
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
19030
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
19031
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
19032
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
19033
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
18747
19034
|
*/
|
|
18748
19035
|
get timingBudgetStats() {
|
|
18749
19036
|
return {
|
|
@@ -18754,6 +19041,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18754
19041
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18755
19042
|
};
|
|
18756
19043
|
}
|
|
19044
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
19045
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19046
|
+
let pending = 0;
|
|
19047
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19048
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
19049
|
+
}
|
|
19050
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
19051
|
+
}
|
|
18757
19052
|
get reorderStats() {
|
|
18758
19053
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18759
19054
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18795,6 +19090,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18795
19090
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18796
19091
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18797
19092
|
}
|
|
19093
|
+
/** See FecReleaseSequencer.unsource. */
|
|
19094
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
19095
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
19096
|
+
}
|
|
18798
19097
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18799
19098
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18800
19099
|
if (!this.#fecSequencer) return;
|
|
@@ -18927,7 +19226,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18927
19226
|
const now = Date.now();
|
|
18928
19227
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18929
19228
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18930
|
-
awaitingTiming.shift();
|
|
19229
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18931
19230
|
}
|
|
18932
19231
|
}
|
|
18933
19232
|
}
|
|
@@ -19045,11 +19344,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19045
19344
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
19046
19345
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
19047
19346
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
19048
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19347
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19049
19348
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19050
19349
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19051
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19052
|
-
const admitted = resolver2.addMovieFragment(
|
|
19350
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19351
|
+
const admitted = resolver2.addMovieFragment(
|
|
19352
|
+
mpu.mpuSequenceNumber,
|
|
19353
|
+
data,
|
|
19354
|
+
remaining,
|
|
19355
|
+
options?.recovered === true
|
|
19356
|
+
);
|
|
19053
19357
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
19054
19358
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
19055
19359
|
if (!admitted) {
|
|
@@ -19069,7 +19373,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19069
19373
|
};
|
|
19070
19374
|
}
|
|
19071
19375
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
19072
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19376
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
19073
19377
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
19074
19378
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
19075
19379
|
return this.#drop("mfu-empty-payload");
|
|
@@ -19090,6 +19394,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19090
19394
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19091
19395
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19092
19396
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19397
|
+
sequenceNumber: info.sequenceNumber,
|
|
19093
19398
|
arrivedAt: Date.now()
|
|
19094
19399
|
});
|
|
19095
19400
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19115,7 +19420,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19115
19420
|
info,
|
|
19116
19421
|
resolver,
|
|
19117
19422
|
options?.recovered === true,
|
|
19118
|
-
options?.fecSbn
|
|
19423
|
+
options?.fecSbn,
|
|
19424
|
+
options?.fecSbnFirst
|
|
19119
19425
|
);
|
|
19120
19426
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19121
19427
|
}
|
|
@@ -19208,7 +19514,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19208
19514
|
info,
|
|
19209
19515
|
resolver,
|
|
19210
19516
|
options?.recovered === true,
|
|
19211
|
-
options?.fecSbn
|
|
19517
|
+
options?.fecSbn,
|
|
19518
|
+
options?.fecSbnFirst
|
|
19212
19519
|
);
|
|
19213
19520
|
if (outcome.record !== void 0) return outcome.record;
|
|
19214
19521
|
return this.#drop(outcome.reason);
|
|
@@ -19228,7 +19535,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19228
19535
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19229
19536
|
* is what keeps both true.
|
|
19230
19537
|
*/
|
|
19231
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19538
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19232
19539
|
const timing = resolver.sampleTiming(
|
|
19233
19540
|
combined.mpuSequenceNumber,
|
|
19234
19541
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19245,6 +19552,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19245
19552
|
);
|
|
19246
19553
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19247
19554
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19555
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19248
19556
|
if (!resolver.consumeSample(
|
|
19249
19557
|
combined.mpuSequenceNumber,
|
|
19250
19558
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19478,6 +19786,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19478
19786
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19479
19787
|
this.#sequencers.clear();
|
|
19480
19788
|
this.#fecSequencer?.discard();
|
|
19789
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19790
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19791
|
+
}
|
|
19481
19792
|
this.#timingStreams.clear();
|
|
19482
19793
|
this.#pendingTimingSamples = 0;
|
|
19483
19794
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19496,10 +19807,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19496
19807
|
return resolver;
|
|
19497
19808
|
}
|
|
19498
19809
|
#awaitTiming(streamKey, head) {
|
|
19499
|
-
const
|
|
19500
|
-
if (!
|
|
19810
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19811
|
+
if (!state) return;
|
|
19812
|
+
if (!head.options?.recovered) {
|
|
19813
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
19814
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
19815
|
+
state.awaitingTiming = kept;
|
|
19816
|
+
}
|
|
19817
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19501
19818
|
awaitingTiming.push(head);
|
|
19502
|
-
if (
|
|
19819
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
19820
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
19821
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
19822
|
+
}
|
|
19823
|
+
}
|
|
19824
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
19825
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
19826
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19503
19827
|
}
|
|
19504
19828
|
/**
|
|
19505
19829
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19527,6 +19851,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19527
19851
|
const state = this.#timingStreams.get(streamKey);
|
|
19528
19852
|
if (!state) return;
|
|
19529
19853
|
this.#timingStreams.delete(streamKey);
|
|
19854
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19530
19855
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19531
19856
|
if (this.#pendingTimingSamples < 0) {
|
|
19532
19857
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22733,6 +23058,16 @@ function keyframeOverlaySsId(header) {
|
|
|
22733
23058
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22734
23059
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22735
23060
|
}
|
|
23061
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23062
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23063
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23064
|
+
}
|
|
23065
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23066
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23067
|
+
return headerPsn + offset >>> 0;
|
|
23068
|
+
}
|
|
23069
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23070
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22736
23071
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
22737
23072
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22738
23073
|
function assertStreamIdentity(role, stream) {
|
|
@@ -22844,6 +23179,7 @@ var MmtpRouter = class {
|
|
|
22844
23179
|
#provenance = /* @__PURE__ */ new Map();
|
|
22845
23180
|
#onFlowProtected = null;
|
|
22846
23181
|
#unadmittedListedDrops = 0;
|
|
23182
|
+
#admittedSourceDatagrams = 0;
|
|
22847
23183
|
#unlistedRepairSetAside = 0;
|
|
22848
23184
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22849
23185
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22900,6 +23236,15 @@ var MmtpRouter = class {
|
|
|
22900
23236
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22901
23237
|
*/
|
|
22902
23238
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23239
|
+
/**
|
|
23240
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23241
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23242
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23243
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23244
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23245
|
+
* an MPU no member of which arrived).
|
|
23246
|
+
*/
|
|
23247
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22903
23248
|
/**
|
|
22904
23249
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22905
23250
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22969,7 +23314,9 @@ var MmtpRouter = class {
|
|
|
22969
23314
|
recovered: 0,
|
|
22970
23315
|
restarts: 0,
|
|
22971
23316
|
unsequencedDropped: 0,
|
|
22972
|
-
protectedDropped: 0
|
|
23317
|
+
protectedDropped: 0,
|
|
23318
|
+
baseFed: 0,
|
|
23319
|
+
baseUndetermined: 0
|
|
22973
23320
|
};
|
|
22974
23321
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22975
23322
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23142,7 +23489,7 @@ var MmtpRouter = class {
|
|
|
23142
23489
|
const cb = this.#getFrameCallback(packetId);
|
|
23143
23490
|
if (isVideo && !assembler) {
|
|
23144
23491
|
if (frameMeta.recovered) {
|
|
23145
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23492
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23146
23493
|
} else {
|
|
23147
23494
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23148
23495
|
}
|
|
@@ -23406,6 +23753,7 @@ var MmtpRouter = class {
|
|
|
23406
23753
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23407
23754
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23408
23755
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23756
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23409
23757
|
if (previous) {
|
|
23410
23758
|
previous.flush();
|
|
23411
23759
|
previous.dispose();
|
|
@@ -23532,6 +23880,10 @@ var MmtpRouter = class {
|
|
|
23532
23880
|
get keyframeOverlayStats() {
|
|
23533
23881
|
return { ...this.#overlayStats };
|
|
23534
23882
|
}
|
|
23883
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
23884
|
+
get recoveredHeadsAwaitingTiming() {
|
|
23885
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
23886
|
+
}
|
|
23535
23887
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23536
23888
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23537
23889
|
}
|
|
@@ -23582,6 +23934,11 @@ var MmtpRouter = class {
|
|
|
23582
23934
|
*/
|
|
23583
23935
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23584
23936
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
23937
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
23938
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
23939
|
+
stream.overlayToBase.routed.clear();
|
|
23940
|
+
stream.overlayToBase.pending.clear();
|
|
23941
|
+
}
|
|
23585
23942
|
}
|
|
23586
23943
|
#resetOverlay(inst) {
|
|
23587
23944
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
@@ -23638,9 +23995,10 @@ var MmtpRouter = class {
|
|
|
23638
23995
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23639
23996
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23640
23997
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23641
|
-
* included (7.1, 7.4.3).
|
|
23998
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
23999
|
+
* FEC reads it.
|
|
23642
24000
|
*/
|
|
23643
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
24001
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23644
24002
|
if (this.#overlays.size === 0) return;
|
|
23645
24003
|
const ssId = keyframeOverlaySsId(header);
|
|
23646
24004
|
if (ssId === void 0) return;
|
|
@@ -23649,7 +24007,11 @@ var MmtpRouter = class {
|
|
|
23649
24007
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23650
24008
|
if (identity !== null) {
|
|
23651
24009
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23652
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
24010
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
24011
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
24012
|
+
overlaySsId: ssId,
|
|
24013
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
24014
|
+
});
|
|
23653
24015
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23654
24016
|
}
|
|
23655
24017
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23704,11 +24066,16 @@ var MmtpRouter = class {
|
|
|
23704
24066
|
}
|
|
23705
24067
|
/**
|
|
23706
24068
|
* Route an overlay-recovered block's packets as source media of the
|
|
23707
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23708
|
-
*
|
|
23709
|
-
*
|
|
23710
|
-
*
|
|
23711
|
-
*
|
|
24069
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
24070
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
24071
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
24072
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
24073
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
24074
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
24075
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
24076
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
24077
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
24078
|
+
* header's sequence and any other drops the packet.
|
|
23712
24079
|
*/
|
|
23713
24080
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23714
24081
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23728,22 +24095,127 @@ var MmtpRouter = class {
|
|
|
23728
24095
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23729
24096
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23730
24097
|
const restored = /* @__PURE__ */ new Set();
|
|
24098
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
23731
24099
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23732
|
-
const
|
|
23733
|
-
if (
|
|
23734
|
-
const
|
|
24100
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24101
|
+
if (anchor !== void 0) {
|
|
24102
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24103
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23735
24104
|
restored.add(packetSequenceNumber);
|
|
23736
|
-
|
|
24105
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24106
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24107
|
+
if (baseSsId !== void 0) baseSsIds.set(sym.index, baseSsId);
|
|
24108
|
+
return [out];
|
|
24109
|
+
}
|
|
24110
|
+
if (!restamped) {
|
|
24111
|
+
const baseSsId = unstampedBaseSsId(
|
|
24112
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
24113
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
24114
|
+
sym.packetSequenceNumber
|
|
24115
|
+
);
|
|
24116
|
+
if (baseSsId !== void 0) baseSsIds.set(sym.index, baseSsId);
|
|
24117
|
+
return [sym];
|
|
23737
24118
|
}
|
|
23738
|
-
if (!restamped) return [sym];
|
|
23739
24119
|
this.#overlayStats.unsequencedDropped++;
|
|
23740
24120
|
return [];
|
|
23741
24121
|
});
|
|
23742
24122
|
this.#overlayStats.recovered++;
|
|
24123
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
24124
|
+
for (const sym of symbols) {
|
|
24125
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24126
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
24127
|
+
const first = mpuFirst.get(mpu);
|
|
24128
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
24129
|
+
}
|
|
24130
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
24131
|
+
for (const sym of symbols) {
|
|
24132
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
24133
|
+
}
|
|
23743
24134
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23744
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24135
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24136
|
+
baseSsIds,
|
|
24137
|
+
firstSsIds
|
|
23745
24138
|
});
|
|
23746
24139
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24140
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24141
|
+
}
|
|
24142
|
+
/**
|
|
24143
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24144
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24145
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
24146
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24147
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24148
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24149
|
+
* A base block no repair has stated yet takes it when one does
|
|
24150
|
+
* (#feedPendingRebuilt).
|
|
24151
|
+
*
|
|
24152
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24153
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24154
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24155
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24156
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24157
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24158
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24159
|
+
* block's own arrivals cannot show.
|
|
24160
|
+
*/
|
|
24161
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24162
|
+
const fec = this.#fecManager;
|
|
24163
|
+
const owner = inst.leg.sourcePacketId;
|
|
24164
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24165
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24166
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24167
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24168
|
+
const feeds = [];
|
|
24169
|
+
for (const sym of symbols) {
|
|
24170
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24171
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24172
|
+
if (ssId === void 0) {
|
|
24173
|
+
this.#overlayStats.baseUndetermined++;
|
|
24174
|
+
continue;
|
|
24175
|
+
}
|
|
24176
|
+
routed.delete(ssId);
|
|
24177
|
+
routed.add(ssId);
|
|
24178
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24179
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24180
|
+
}
|
|
24181
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24182
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24183
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24184
|
+
}
|
|
24185
|
+
}
|
|
24186
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24187
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24188
|
+
const fec = this.#fecManager;
|
|
24189
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24190
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24191
|
+
if (coords == null) {
|
|
24192
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24193
|
+
pending.delete(ssId);
|
|
24194
|
+
pending.set(ssId, symbolData.slice());
|
|
24195
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24196
|
+
return;
|
|
24197
|
+
}
|
|
24198
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24199
|
+
if (!fed.taken) return;
|
|
24200
|
+
this.#overlayStats.baseFed++;
|
|
24201
|
+
if (fed.recovered === null) return;
|
|
24202
|
+
const sbn = coords.sbn;
|
|
24203
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24204
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24205
|
+
fec.countRecovery(owner, sbn, name);
|
|
24206
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24207
|
+
});
|
|
24208
|
+
}
|
|
24209
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24210
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24211
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24212
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24213
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24214
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24215
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24216
|
+
pending.delete(ssId);
|
|
24217
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24218
|
+
}
|
|
23747
24219
|
}
|
|
23748
24220
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23749
24221
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23883,6 +24355,10 @@ var MmtpRouter = class {
|
|
|
23883
24355
|
get unadmittedListedDropCount() {
|
|
23884
24356
|
return this.#unadmittedListedDrops;
|
|
23885
24357
|
}
|
|
24358
|
+
/** 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. */
|
|
24359
|
+
get admittedSourceDatagramCount() {
|
|
24360
|
+
return this.#admittedSourceDatagrams;
|
|
24361
|
+
}
|
|
23886
24362
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23887
24363
|
get unlistedRepairSetAsideCount() {
|
|
23888
24364
|
return this.#unlistedRepairSetAside;
|
|
@@ -24369,6 +24845,8 @@ var MmtpRouter = class {
|
|
|
24369
24845
|
)) return;
|
|
24370
24846
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24371
24847
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
24848
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
24849
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24372
24850
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24373
24851
|
const recovered = fec.feedRepairSymbol(
|
|
24374
24852
|
ownerPacketId,
|
|
@@ -24506,6 +24984,7 @@ var MmtpRouter = class {
|
|
|
24506
24984
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24507
24985
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24508
24986
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
24987
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24509
24988
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24510
24989
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24511
24990
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24632,6 +25111,12 @@ var MmtpRouter = class {
|
|
|
24632
25111
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24633
25112
|
* own RAP cadence, so no timer is involved.
|
|
24634
25113
|
*
|
|
25114
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
25115
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
25116
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
25117
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
25118
|
+
* frames of the outgoing stream.
|
|
25119
|
+
*
|
|
24635
25120
|
* The commit is one synchronous transition inside the frame path: the
|
|
24636
25121
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24637
25122
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24682,6 +25167,7 @@ var MmtpRouter = class {
|
|
|
24682
25167
|
droppedIncomingFrames: 0
|
|
24683
25168
|
};
|
|
24684
25169
|
this.#staged = state;
|
|
25170
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
24685
25171
|
return {
|
|
24686
25172
|
cancel: () => {
|
|
24687
25173
|
if (this.#staged !== state) return false;
|
|
@@ -24999,6 +25485,14 @@ var MmtpRouter = class {
|
|
|
24999
25485
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
25000
25486
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
25001
25487
|
},
|
|
25488
|
+
// A staged switch commits on when each side's frames reach it
|
|
25489
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
25490
|
+
// they did unanchored.
|
|
25491
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
25492
|
+
const staged = this.#staged;
|
|
25493
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
25494
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
25495
|
+
},
|
|
25002
25496
|
onReleased: (data, meta) => {
|
|
25003
25497
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
25004
25498
|
const timing = meta.authoredTiming;
|
|
@@ -25140,6 +25634,7 @@ var MmtpRouter = class {
|
|
|
25140
25634
|
const parsed = this.#parser.parsePacket(packet);
|
|
25141
25635
|
if (!parsed) return;
|
|
25142
25636
|
const info = parsed.info;
|
|
25637
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25143
25638
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25144
25639
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25145
25640
|
}
|
|
@@ -25421,7 +25916,8 @@ var MmtpRouter = class {
|
|
|
25421
25916
|
parsed.header,
|
|
25422
25917
|
info.sequenceNumber,
|
|
25423
25918
|
info.rapFlag,
|
|
25424
|
-
source
|
|
25919
|
+
source,
|
|
25920
|
+
fecPayloadId?.ssId
|
|
25425
25921
|
);
|
|
25426
25922
|
}
|
|
25427
25923
|
}
|
|
@@ -25447,7 +25943,8 @@ var MmtpRouter = class {
|
|
|
25447
25943
|
source: parserOptions?.source,
|
|
25448
25944
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25449
25945
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25450
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
25946
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
25947
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25451
25948
|
};
|
|
25452
25949
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25453
25950
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25571,7 +26068,8 @@ var MmtpRouter = class {
|
|
|
25571
26068
|
if (stream === void 0) {
|
|
25572
26069
|
stream = {
|
|
25573
26070
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25574
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26071
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26072
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25575
26073
|
};
|
|
25576
26074
|
byName.set(catalogTrackName, stream);
|
|
25577
26075
|
}
|
|
@@ -25686,8 +26184,16 @@ var MmtpRouter = class {
|
|
|
25686
26184
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25687
26185
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25688
26186
|
}
|
|
25689
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26187
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25690
26188
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26189
|
+
if (this.#isFecTrack(packetId)) {
|
|
26190
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26191
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26192
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26193
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26194
|
+
return;
|
|
26195
|
+
}
|
|
26196
|
+
}
|
|
25691
26197
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25692
26198
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25693
26199
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25841,6 +26347,13 @@ var MmtpRouter = class {
|
|
|
25841
26347
|
* frames by epoch only if pre-protection input ever matters.
|
|
25842
26348
|
*/
|
|
25843
26349
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26350
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26351
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26352
|
+
result = {
|
|
26353
|
+
...result,
|
|
26354
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26355
|
+
};
|
|
26356
|
+
}
|
|
25844
26357
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25845
26358
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25846
26359
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25897,7 +26410,12 @@ var MmtpRouter = class {
|
|
|
25897
26410
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25898
26411
|
if (ownerPacketId == null) return;
|
|
25899
26412
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25900
|
-
const fecSbnOf = (index) =>
|
|
26413
|
+
const fecSbnOf = (index) => {
|
|
26414
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
26415
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
26416
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
26417
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
26418
|
+
};
|
|
25901
26419
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25902
26420
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25903
26421
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -25905,7 +26423,10 @@ var MmtpRouter = class {
|
|
|
25905
26423
|
...result,
|
|
25906
26424
|
symbols: result.symbols.map((sym) => {
|
|
25907
26425
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25908
|
-
if (wire === sym.packetSequenceNumber)
|
|
26426
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26427
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26428
|
+
return sym;
|
|
26429
|
+
}
|
|
25909
26430
|
this.#restampedRecoveredSeqs++;
|
|
25910
26431
|
this.#restampedStreams.add(streamKey);
|
|
25911
26432
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25941,16 +26462,29 @@ var MmtpRouter = class {
|
|
|
25941
26462
|
}
|
|
25942
26463
|
);
|
|
25943
26464
|
}
|
|
25944
|
-
|
|
25945
|
-
|
|
25946
|
-
|
|
25947
|
-
|
|
25948
|
-
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
25952
|
-
))
|
|
25953
|
-
|
|
26465
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26466
|
+
ownerPacketId,
|
|
26467
|
+
sym.fragment.mpuSequenceNumber,
|
|
26468
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26469
|
+
sym.fragment.sampleNumber,
|
|
26470
|
+
catalogTrackName
|
|
26471
|
+
);
|
|
26472
|
+
for (const sym of result.symbols) {
|
|
26473
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26474
|
+
this.#feedViaParser(
|
|
26475
|
+
sym.rawSymbol,
|
|
26476
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26477
|
+
void 0,
|
|
26478
|
+
{
|
|
26479
|
+
skipFecPayloadId: true,
|
|
26480
|
+
recovered: true,
|
|
26481
|
+
deliveryProvenance,
|
|
26482
|
+
...fecSbnOf(sym.index),
|
|
26483
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26484
|
+
}
|
|
26485
|
+
);
|
|
26486
|
+
}
|
|
26487
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25954
26488
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25955
26489
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25956
26490
|
if (assembler) {
|