@blockcast/mmt-container 2.2.0-main.c3a95413582c → 2.2.0-main.d8ba524564ba
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 +748 -79
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delivery-catalog.js +17 -12
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +222 -6
- 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 +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +204 -26
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +206 -29
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +203 -26
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +27 -3
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +524 -71
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +748 -79
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +19 -14
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +203 -26
- package/dist/no-modules.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +203 -26
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +277 -6
- 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 +298 -29
- package/src/mmtp-router.ts +510 -37
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";
|
|
@@ -8746,6 +8746,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8746
8746
|
});
|
|
8747
8747
|
|
|
8748
8748
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
8749
|
+
var MAX_PENDING_PACKETS = 256;
|
|
8749
8750
|
var AMTGatewayManager = class {
|
|
8750
8751
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8751
8752
|
messageHandler;
|
|
@@ -8754,7 +8755,6 @@ var AMTGatewayManager = class {
|
|
|
8754
8755
|
constructor() {
|
|
8755
8756
|
this.messageHandler = (event) => {
|
|
8756
8757
|
const data = event.data;
|
|
8757
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8758
8758
|
if (data.type === "UDP_PACKET") {
|
|
8759
8759
|
this.handlePacket(data);
|
|
8760
8760
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8772,6 +8772,14 @@ var AMTGatewayManager = class {
|
|
|
8772
8772
|
this.messageListenerRegistered = true;
|
|
8773
8773
|
console.log("[AMT] Registered service worker message handler");
|
|
8774
8774
|
}
|
|
8775
|
+
removeMessageListener() {
|
|
8776
|
+
if (!this.messageListenerRegistered)
|
|
8777
|
+
return;
|
|
8778
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8779
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8780
|
+
}
|
|
8781
|
+
this.messageListenerRegistered = false;
|
|
8782
|
+
}
|
|
8775
8783
|
/**
|
|
8776
8784
|
* Subscribe via AMT tunnel
|
|
8777
8785
|
*/
|
|
@@ -8816,6 +8824,8 @@ var AMTGatewayManager = class {
|
|
|
8816
8824
|
this.subscriptions.delete(keyStr);
|
|
8817
8825
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8818
8826
|
}
|
|
8827
|
+
if (this.subscriptions.size === 0)
|
|
8828
|
+
this.removeMessageListener();
|
|
8819
8829
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8820
8830
|
}
|
|
8821
8831
|
}
|
|
@@ -8837,6 +8847,8 @@ var AMTGatewayManager = class {
|
|
|
8837
8847
|
} finally {
|
|
8838
8848
|
this.subscriptions.delete(handle);
|
|
8839
8849
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8850
|
+
if (this.subscriptions.size === 0)
|
|
8851
|
+
this.removeMessageListener();
|
|
8840
8852
|
}
|
|
8841
8853
|
}
|
|
8842
8854
|
/**
|
|
@@ -8853,14 +8865,12 @@ var AMTGatewayManager = class {
|
|
|
8853
8865
|
if (!subscriptionId)
|
|
8854
8866
|
return;
|
|
8855
8867
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8856
|
-
if (!subscription)
|
|
8857
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8868
|
+
if (!subscription)
|
|
8858
8869
|
return;
|
|
8859
|
-
}
|
|
8860
8870
|
if (!subscription.handshakeComplete) {
|
|
8861
8871
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8862
8872
|
const packetData2 = data;
|
|
8863
|
-
if (queue) {
|
|
8873
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8864
8874
|
queue.push({
|
|
8865
8875
|
data: packetData2.data,
|
|
8866
8876
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8886,20 +8896,15 @@ var AMTGatewayManager = class {
|
|
|
8886
8896
|
if (!subscriptionId)
|
|
8887
8897
|
return;
|
|
8888
8898
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8889
|
-
if (!subscription)
|
|
8890
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8899
|
+
if (!subscription)
|
|
8891
8900
|
return;
|
|
8892
|
-
}
|
|
8893
8901
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8894
8902
|
}
|
|
8895
8903
|
/**
|
|
8896
8904
|
* Cleanup on destroy
|
|
8897
8905
|
*/
|
|
8898
8906
|
destroy() {
|
|
8899
|
-
|
|
8900
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8901
|
-
this.messageListenerRegistered = false;
|
|
8902
|
-
}
|
|
8907
|
+
this.removeMessageListener();
|
|
8903
8908
|
const handles = Array.from(this.subscriptions.keys());
|
|
8904
8909
|
for (const handle of handles) {
|
|
8905
8910
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -12944,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12944
12949
|
#retentionBlocks;
|
|
12945
12950
|
#heldEvicted;
|
|
12946
12951
|
#newest;
|
|
12952
|
+
#newestSource;
|
|
12947
12953
|
#holes;
|
|
12948
12954
|
static #checkedRetention(retentionBlocks) {
|
|
12949
12955
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
@@ -12981,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12981
12987
|
observe(position) {
|
|
12982
12988
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12983
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
|
+
}
|
|
12984
13005
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12985
13006
|
get heldEvicted() {
|
|
12986
13007
|
return this.#heldEvicted;
|
|
@@ -13213,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13213
13234
|
this.#held.clear();
|
|
13214
13235
|
this.#coverageEnd = void 0;
|
|
13215
13236
|
this.#newest = void 0;
|
|
13237
|
+
this.#newestSource = void 0;
|
|
13216
13238
|
}
|
|
13217
13239
|
#claim(block) {
|
|
13218
13240
|
const placed = [];
|
|
@@ -13832,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13832
13854
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13833
13855
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13834
13856
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13857
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13835
13858
|
function isMulticastSymbolDelivery(value) {
|
|
13836
13859
|
return value === "datagram" || value === "ordered-relay";
|
|
13837
13860
|
}
|
|
@@ -13929,6 +13952,10 @@ var FECManager = class _FECManager {
|
|
|
13929
13952
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13930
13953
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13931
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();
|
|
13932
13959
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13933
13960
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13934
13961
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -14057,6 +14084,10 @@ var FECManager = class _FECManager {
|
|
|
14057
14084
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
14058
14085
|
// One classifier per physical stream, built from that stream's geometry.
|
|
14059
14086
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
14087
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
14088
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
14089
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
14090
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
14060
14091
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
14061
14092
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
14062
14093
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -14214,6 +14245,7 @@ var FECManager = class _FECManager {
|
|
|
14214
14245
|
#pendingWorkerCleanupBounds;
|
|
14215
14246
|
#blockSourceCounts;
|
|
14216
14247
|
#blockSourceEsis;
|
|
14248
|
+
#blockRebuiltEsis;
|
|
14217
14249
|
#blockAuthoredLengths;
|
|
14218
14250
|
#ssbLengthConflictCount;
|
|
14219
14251
|
#blockRepairRsIds;
|
|
@@ -14256,6 +14288,7 @@ var FECManager = class _FECManager {
|
|
|
14256
14288
|
#orderedFrontiers;
|
|
14257
14289
|
#orderedPendingBlocks;
|
|
14258
14290
|
#classifiers;
|
|
14291
|
+
#noSpanRaps;
|
|
14259
14292
|
#streamGeometries;
|
|
14260
14293
|
#failedBlocks;
|
|
14261
14294
|
#resolvedBlockClasses;
|
|
@@ -14917,6 +14950,7 @@ var FECManager = class _FECManager {
|
|
|
14917
14950
|
};
|
|
14918
14951
|
const discontinuity = () => {
|
|
14919
14952
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
14953
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14920
14954
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14921
14955
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14922
14956
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -15329,6 +15363,9 @@ var FECManager = class _FECManager {
|
|
|
15329
15363
|
}
|
|
15330
15364
|
if (policyChanged) {
|
|
15331
15365
|
this.#bumpEpoch(packetId);
|
|
15366
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15367
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15368
|
+
}
|
|
15332
15369
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15333
15370
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15334
15371
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15531,6 +15568,7 @@ var FECManager = class _FECManager {
|
|
|
15531
15568
|
}
|
|
15532
15569
|
resetMap(this.#blockSourceCounts);
|
|
15533
15570
|
resetMap(this.#blockSourceEsis);
|
|
15571
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15534
15572
|
resetMap(this.#blockAuthoredLengths);
|
|
15535
15573
|
resetMap(this.#blockRepairRsIds);
|
|
15536
15574
|
resetMap(this.#blockRsbLengths);
|
|
@@ -15564,6 +15602,7 @@ var FECManager = class _FECManager {
|
|
|
15564
15602
|
resetMap(this.#isoLocators);
|
|
15565
15603
|
resetMap(this.#sourceAnchors);
|
|
15566
15604
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15605
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15567
15606
|
this.#bumpEpoch(packetId);
|
|
15568
15607
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15569
15608
|
for (const key of keys.keys()) {
|
|
@@ -15654,6 +15693,7 @@ var FECManager = class _FECManager {
|
|
|
15654
15693
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15655
15694
|
dropMap(this.#blockSourceCounts);
|
|
15656
15695
|
dropMap(this.#blockSourceEsis);
|
|
15696
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15657
15697
|
dropMap(this.#blockAuthoredLengths);
|
|
15658
15698
|
dropMap(this.#blockRepairRsIds);
|
|
15659
15699
|
dropMap(this.#blockRsbLengths);
|
|
@@ -15670,6 +15710,7 @@ var FECManager = class _FECManager {
|
|
|
15670
15710
|
dropMap(this.#isoLocators);
|
|
15671
15711
|
dropMap(this.#sourceAnchors);
|
|
15672
15712
|
dropMap(this.#classifiers);
|
|
15713
|
+
dropMap(this.#noSpanRaps);
|
|
15673
15714
|
dropMap(this.#streamGeometries);
|
|
15674
15715
|
dropSet(this.#pendingWorkerCleanups);
|
|
15675
15716
|
dropSet(this.#completedBlocks);
|
|
@@ -15815,6 +15856,9 @@ var FECManager = class _FECManager {
|
|
|
15815
15856
|
this.#unindexBlockKey(key);
|
|
15816
15857
|
}
|
|
15817
15858
|
}
|
|
15859
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15860
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15861
|
+
}
|
|
15818
15862
|
return true;
|
|
15819
15863
|
}
|
|
15820
15864
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -16132,6 +16176,7 @@ var FECManager = class _FECManager {
|
|
|
16132
16176
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16133
16177
|
const evictedBefore = locator.heldEvicted;
|
|
16134
16178
|
const held = locator.hold(ssId, source);
|
|
16179
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16135
16180
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16136
16181
|
if (evicted > 0) {
|
|
16137
16182
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16300,11 +16345,14 @@ var FECManager = class _FECManager {
|
|
|
16300
16345
|
/**
|
|
16301
16346
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16302
16347
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16303
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
16348
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
16349
|
+
* and neither has a stream without catalog timing until it releases two
|
|
16350
|
+
* RAPs inside one MPU.
|
|
16304
16351
|
*/
|
|
16305
16352
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16306
|
-
|
|
16307
|
-
return this.#
|
|
16353
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16354
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
16355
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
16308
16356
|
}
|
|
16309
16357
|
/** A block's class now, or frozen at its resolution. */
|
|
16310
16358
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16330,7 +16378,14 @@ var FECManager = class _FECManager {
|
|
|
16330
16378
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16331
16379
|
*/
|
|
16332
16380
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16333
|
-
if (!this.#trackConfigs.has(packetId)
|
|
16381
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
16382
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
16383
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16384
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
16385
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
16386
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
16387
|
+
return;
|
|
16388
|
+
}
|
|
16334
16389
|
this.#applyClassifierChanges(
|
|
16335
16390
|
packetId,
|
|
16336
16391
|
catalogTrackName,
|
|
@@ -16717,7 +16772,15 @@ var FECManager = class _FECManager {
|
|
|
16717
16772
|
this.#isoSourceOversizeCount++;
|
|
16718
16773
|
return null;
|
|
16719
16774
|
}
|
|
16720
|
-
|
|
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) {
|
|
16721
16784
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16722
16785
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16723
16786
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16746,6 +16809,74 @@ var FECManager = class _FECManager {
|
|
|
16746
16809
|
}
|
|
16747
16810
|
return recovered;
|
|
16748
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
|
+
}
|
|
16749
16880
|
/**
|
|
16750
16881
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16751
16882
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16948,8 +17079,75 @@ var FECManager = class _FECManager {
|
|
|
16948
17079
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16949
17080
|
return null;
|
|
16950
17081
|
}
|
|
17082
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16951
17083
|
return recovered;
|
|
16952
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
|
+
}
|
|
16953
17151
|
/**
|
|
16954
17152
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16955
17153
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -17104,6 +17302,15 @@ var FECManager = class _FECManager {
|
|
|
17104
17302
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17105
17303
|
return;
|
|
17106
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
|
+
}
|
|
17107
17314
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17108
17315
|
}
|
|
17109
17316
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17112,6 +17319,7 @@ var FECManager = class _FECManager {
|
|
|
17112
17319
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17113
17320
|
this.#blockSourceCounts.delete(key);
|
|
17114
17321
|
this.#blockSourceEsis.delete(key);
|
|
17322
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17115
17323
|
this.#blockAuthoredLengths.delete(key);
|
|
17116
17324
|
this.#blockRepairRsIds.delete(key);
|
|
17117
17325
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17178,7 +17386,16 @@ var FECManager = class _FECManager {
|
|
|
17178
17386
|
this.#repairDeadlineMs(trackConfig)
|
|
17179
17387
|
);
|
|
17180
17388
|
}
|
|
17181
|
-
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
|
+
);
|
|
17182
17399
|
}
|
|
17183
17400
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17184
17401
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -17191,6 +17408,7 @@ var FECManager = class _FECManager {
|
|
|
17191
17408
|
}
|
|
17192
17409
|
this.#blockSourceCounts.delete(key);
|
|
17193
17410
|
this.#blockSourceEsis.delete(key);
|
|
17411
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17194
17412
|
this.#blockAuthoredLengths.delete(key);
|
|
17195
17413
|
this.#blockRepairRsIds.delete(key);
|
|
17196
17414
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17229,6 +17447,7 @@ var FECManager = class _FECManager {
|
|
|
17229
17447
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17230
17448
|
this.#blockSourceCounts.delete(key);
|
|
17231
17449
|
this.#blockSourceEsis.delete(key);
|
|
17450
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17232
17451
|
this.#blockAuthoredLengths.delete(key);
|
|
17233
17452
|
this.#blockRepairRsIds.delete(key);
|
|
17234
17453
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17320,6 +17539,7 @@ var FECManager = class _FECManager {
|
|
|
17320
17539
|
this.#blockKeysBySbn.clear();
|
|
17321
17540
|
this.#blockSourceCounts.clear();
|
|
17322
17541
|
this.#blockSourceEsis.clear();
|
|
17542
|
+
this.#blockRebuiltEsis.clear();
|
|
17323
17543
|
this.#blockAuthoredLengths.clear();
|
|
17324
17544
|
this.#blockRepairRsIds.clear();
|
|
17325
17545
|
this.#blockRsbLengths.clear();
|
|
@@ -17345,6 +17565,7 @@ var FECManager = class _FECManager {
|
|
|
17345
17565
|
this.#blockAnchorsMs.clear();
|
|
17346
17566
|
this.#blockCatalogTrackNames.clear();
|
|
17347
17567
|
this.#classifiers.clear();
|
|
17568
|
+
this.#noSpanRaps.clear();
|
|
17348
17569
|
this.#streamGeometries.clear();
|
|
17349
17570
|
this.#failedBlocks.clear();
|
|
17350
17571
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17875,9 +18096,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17875
18096
|
}
|
|
17876
18097
|
#track = null;
|
|
17877
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();
|
|
17878
18107
|
#pendingSamples = 0;
|
|
17879
18108
|
#evictedSamples = 0;
|
|
17880
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
|
+
}
|
|
17881
18114
|
get pendingSampleCount() {
|
|
17882
18115
|
return this.#pendingSamples;
|
|
17883
18116
|
}
|
|
@@ -17901,7 +18134,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17901
18134
|
this.#track = timing;
|
|
17902
18135
|
return true;
|
|
17903
18136
|
}
|
|
17904
|
-
|
|
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) {
|
|
17905
18145
|
this.#lastMovieFragmentRejection = null;
|
|
17906
18146
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17907
18147
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17927,6 +18167,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17927
18167
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17928
18168
|
);
|
|
17929
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;
|
|
17930
18172
|
let freed = 0;
|
|
17931
18173
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17932
18174
|
freed += this.#evictOldestFragment();
|
|
@@ -17940,6 +18182,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17940
18182
|
return false;
|
|
17941
18183
|
}
|
|
17942
18184
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18185
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17943
18186
|
this.#pendingSamples += parsed.samples.length;
|
|
17944
18187
|
return true;
|
|
17945
18188
|
}
|
|
@@ -17997,6 +18240,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17997
18240
|
let live = 0;
|
|
17998
18241
|
for (const sample of samples) if (sample !== null) live++;
|
|
17999
18242
|
this.#fragments.delete(oldest);
|
|
18243
|
+
this.#firstSamples.delete(oldest);
|
|
18000
18244
|
this.#pendingSamples -= live;
|
|
18001
18245
|
this.#evictedSamples += live;
|
|
18002
18246
|
return live;
|
|
@@ -18012,12 +18256,23 @@ var IsoBmffTimingResolver = class {
|
|
|
18012
18256
|
if (!samples || !samples[index]) return false;
|
|
18013
18257
|
samples[index] = null;
|
|
18014
18258
|
this.#pendingSamples--;
|
|
18015
|
-
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
|
+
}
|
|
18016
18269
|
return true;
|
|
18017
18270
|
}
|
|
18018
18271
|
reset() {
|
|
18019
18272
|
this.#track = null;
|
|
18020
18273
|
this.#fragments.clear();
|
|
18274
|
+
this.#firstSamples.clear();
|
|
18275
|
+
this.#delivered.clear();
|
|
18021
18276
|
this.#pendingSamples = 0;
|
|
18022
18277
|
}
|
|
18023
18278
|
};
|
|
@@ -18187,6 +18442,9 @@ function authoredGap(before, after) {
|
|
|
18187
18442
|
function ssIdAfter(a, b) {
|
|
18188
18443
|
return (a - b | 0) > 0;
|
|
18189
18444
|
}
|
|
18445
|
+
function holdsRebuilt(stream) {
|
|
18446
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18447
|
+
}
|
|
18190
18448
|
var FecReleaseSequencer = class {
|
|
18191
18449
|
#streams = /* @__PURE__ */ new Map();
|
|
18192
18450
|
#gate;
|
|
@@ -18249,6 +18507,7 @@ var FecReleaseSequencer = class {
|
|
|
18249
18507
|
stream.lastSbn = void 0;
|
|
18250
18508
|
stream.discontinuity = false;
|
|
18251
18509
|
stream.rapSsId = void 0;
|
|
18510
|
+
stream.sourced = false;
|
|
18252
18511
|
}
|
|
18253
18512
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
18254
18513
|
this.#recordStat("droppedStale");
|
|
@@ -18275,6 +18534,7 @@ var FecReleaseSequencer = class {
|
|
|
18275
18534
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18276
18535
|
else hi = mid;
|
|
18277
18536
|
}
|
|
18537
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18278
18538
|
stream.buf.splice(lo, 0, frame);
|
|
18279
18539
|
this.#drainUnlessDeferred(stream);
|
|
18280
18540
|
}
|
|
@@ -18310,6 +18570,38 @@ var FecReleaseSequencer = class {
|
|
|
18310
18570
|
}
|
|
18311
18571
|
return false;
|
|
18312
18572
|
}
|
|
18573
|
+
/**
|
|
18574
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
18575
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
18576
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
18577
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
18578
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
18579
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
18580
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
18581
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
18582
|
+
* released.
|
|
18583
|
+
*/
|
|
18584
|
+
unsource(packetId, catalogTrackName) {
|
|
18585
|
+
for (const stream of this.#streams.values()) {
|
|
18586
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
18587
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) this.#drain(stream, true);
|
|
18588
|
+
stream.sourced = false;
|
|
18589
|
+
}
|
|
18590
|
+
}
|
|
18591
|
+
}
|
|
18592
|
+
/**
|
|
18593
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
18594
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
18595
|
+
* packetId.
|
|
18596
|
+
*/
|
|
18597
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
18598
|
+
for (const stream of this.#streams.values()) {
|
|
18599
|
+
if (stream.packetId !== packetId) continue;
|
|
18600
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
18601
|
+
if (holdsRebuilt(stream)) return true;
|
|
18602
|
+
}
|
|
18603
|
+
return false;
|
|
18604
|
+
}
|
|
18313
18605
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18314
18606
|
poke(streamKey) {
|
|
18315
18607
|
if (streamKey !== void 0) {
|
|
@@ -18401,16 +18693,32 @@ var FecReleaseSequencer = class {
|
|
|
18401
18693
|
last: null,
|
|
18402
18694
|
lastSbn: void 0,
|
|
18403
18695
|
discontinuity: false,
|
|
18404
|
-
rapSsId: void 0
|
|
18696
|
+
rapSsId: void 0,
|
|
18697
|
+
sourced: false,
|
|
18698
|
+
draining: 0
|
|
18405
18699
|
};
|
|
18406
18700
|
this.#streams.set(streamKey, stream);
|
|
18407
18701
|
}
|
|
18408
18702
|
return stream;
|
|
18409
18703
|
}
|
|
18410
|
-
#
|
|
18704
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
18705
|
+
#drain(stream, asUnanchored = false) {
|
|
18706
|
+
stream.draining++;
|
|
18707
|
+
try {
|
|
18708
|
+
this.#drainLoop(stream, asUnanchored);
|
|
18709
|
+
} finally {
|
|
18710
|
+
stream.draining--;
|
|
18711
|
+
}
|
|
18712
|
+
}
|
|
18713
|
+
#drainLoop(stream, asUnanchored) {
|
|
18411
18714
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18412
18715
|
const head = stream.buf[0];
|
|
18413
18716
|
const last = stream.last;
|
|
18717
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
18718
|
+
stream.buf.shift();
|
|
18719
|
+
this.#release(stream, head, void 0);
|
|
18720
|
+
continue;
|
|
18721
|
+
}
|
|
18414
18722
|
if (stream.discontinuity) {
|
|
18415
18723
|
stream.discontinuity = false;
|
|
18416
18724
|
stream.buf.shift();
|
|
@@ -18424,9 +18732,10 @@ var FecReleaseSequencer = class {
|
|
|
18424
18732
|
}
|
|
18425
18733
|
const headSbn = head.meta.fecSbn;
|
|
18426
18734
|
const from = stream.lastSbn ?? headSbn;
|
|
18427
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18735
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18428
18736
|
let lost = [];
|
|
18429
18737
|
const unanchored = from === void 0 && to === void 0;
|
|
18738
|
+
let unresolved = false;
|
|
18430
18739
|
if (from !== void 0 && to !== void 0) {
|
|
18431
18740
|
const resolved = this.#gate.resolvedLoss(
|
|
18432
18741
|
stream.packetId,
|
|
@@ -18435,7 +18744,8 @@ var FecReleaseSequencer = class {
|
|
|
18435
18744
|
Math.max(from, to),
|
|
18436
18745
|
from
|
|
18437
18746
|
);
|
|
18438
|
-
|
|
18747
|
+
unresolved = resolved === null;
|
|
18748
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18439
18749
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18440
18750
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18441
18751
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18443,22 +18753,56 @@ var FecReleaseSequencer = class {
|
|
|
18443
18753
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18444
18754
|
continue;
|
|
18445
18755
|
}
|
|
18446
|
-
lost = resolved;
|
|
18756
|
+
lost = resolved ?? [];
|
|
18447
18757
|
}
|
|
18448
18758
|
stream.buf.shift();
|
|
18449
18759
|
this.#release(
|
|
18450
18760
|
stream,
|
|
18451
18761
|
head,
|
|
18452
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18762
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18453
18763
|
);
|
|
18454
18764
|
}
|
|
18455
18765
|
}
|
|
18766
|
+
/**
|
|
18767
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
18768
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
18769
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
18770
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
18771
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
18772
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
18773
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
18774
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
18775
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
18776
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
18777
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
18778
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
18779
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
18780
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
18781
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
18782
|
+
*/
|
|
18783
|
+
#cursorPassed(stream, asUnanchored) {
|
|
18784
|
+
let at2 = -1;
|
|
18785
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
18786
|
+
const frame = stream.buf[i];
|
|
18787
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
18788
|
+
frame.passed = true;
|
|
18789
|
+
if (at2 < 0) at2 = i;
|
|
18790
|
+
} else if (frame.passed !== true) break;
|
|
18791
|
+
}
|
|
18792
|
+
if (at2 < 0) return false;
|
|
18793
|
+
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));
|
|
18794
|
+
}
|
|
18456
18795
|
#release(stream, frame, decision) {
|
|
18796
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18797
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18457
18798
|
if (decision) frame.meta.gapBefore = decision;
|
|
18458
18799
|
stream.last = frame;
|
|
18459
|
-
if (frame.meta.fecSbn !== void 0
|
|
18800
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
18801
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
18802
|
+
}
|
|
18803
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
18460
18804
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18461
|
-
this.#emit(frame.data, frame.meta);
|
|
18805
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18462
18806
|
}
|
|
18463
18807
|
};
|
|
18464
18808
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -18499,6 +18843,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18499
18843
|
this.#reclaimedTimingSamples = 0;
|
|
18500
18844
|
this.#evictedTimingSamples = 0;
|
|
18501
18845
|
this.#rejectedMovieFragments = 0;
|
|
18846
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18847
|
+
held: 0,
|
|
18848
|
+
expired: 0,
|
|
18849
|
+
discarded: 0,
|
|
18850
|
+
superseded: 0
|
|
18851
|
+
};
|
|
18502
18852
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18503
18853
|
this.#fecGate = null;
|
|
18504
18854
|
this.#fecSequencer = null;
|
|
@@ -18607,6 +18957,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18607
18957
|
#reclaimedTimingSamples;
|
|
18608
18958
|
#evictedTimingSamples;
|
|
18609
18959
|
#rejectedMovieFragments;
|
|
18960
|
+
#recoveredHeadsAwaitingTiming;
|
|
18610
18961
|
#sequencers;
|
|
18611
18962
|
#fecGate;
|
|
18612
18963
|
#fecSequencer;
|
|
@@ -18712,11 +19063,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18712
19063
|
/**
|
|
18713
19064
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18714
19065
|
*
|
|
18715
|
-
*
|
|
18716
|
-
*
|
|
18717
|
-
*
|
|
18718
|
-
*
|
|
18719
|
-
*
|
|
19066
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
19067
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
19068
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
19069
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
19070
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
19071
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
18720
19072
|
*/
|
|
18721
19073
|
get timingBudgetStats() {
|
|
18722
19074
|
return {
|
|
@@ -18727,6 +19079,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18727
19079
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18728
19080
|
};
|
|
18729
19081
|
}
|
|
19082
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
19083
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19084
|
+
let pending = 0;
|
|
19085
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19086
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
19087
|
+
}
|
|
19088
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
19089
|
+
}
|
|
18730
19090
|
get reorderStats() {
|
|
18731
19091
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18732
19092
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18768,6 +19128,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18768
19128
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18769
19129
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18770
19130
|
}
|
|
19131
|
+
/** See FecReleaseSequencer.unsource. */
|
|
19132
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
19133
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
19134
|
+
}
|
|
19135
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
19136
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
19137
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
19138
|
+
}
|
|
18771
19139
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18772
19140
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18773
19141
|
if (!this.#fecSequencer) return;
|
|
@@ -18900,7 +19268,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18900
19268
|
const now = Date.now();
|
|
18901
19269
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18902
19270
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18903
|
-
awaitingTiming.shift();
|
|
19271
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18904
19272
|
}
|
|
18905
19273
|
}
|
|
18906
19274
|
}
|
|
@@ -19018,11 +19386,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19018
19386
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
19019
19387
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
19020
19388
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
19021
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19389
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19022
19390
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19023
19391
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19024
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19025
|
-
const admitted = resolver2.addMovieFragment(
|
|
19392
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19393
|
+
const admitted = resolver2.addMovieFragment(
|
|
19394
|
+
mpu.mpuSequenceNumber,
|
|
19395
|
+
data,
|
|
19396
|
+
remaining,
|
|
19397
|
+
options?.recovered === true
|
|
19398
|
+
);
|
|
19026
19399
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
19027
19400
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
19028
19401
|
if (!admitted) {
|
|
@@ -19042,7 +19415,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19042
19415
|
};
|
|
19043
19416
|
}
|
|
19044
19417
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
19045
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19418
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
19046
19419
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
19047
19420
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
19048
19421
|
return this.#drop("mfu-empty-payload");
|
|
@@ -19063,6 +19436,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19063
19436
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19064
19437
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19065
19438
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19439
|
+
sequenceNumber: info.sequenceNumber,
|
|
19066
19440
|
arrivedAt: Date.now()
|
|
19067
19441
|
});
|
|
19068
19442
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19088,7 +19462,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19088
19462
|
info,
|
|
19089
19463
|
resolver,
|
|
19090
19464
|
options?.recovered === true,
|
|
19091
|
-
options?.fecSbn
|
|
19465
|
+
options?.fecSbn,
|
|
19466
|
+
options?.fecSbnFirst
|
|
19092
19467
|
);
|
|
19093
19468
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19094
19469
|
}
|
|
@@ -19181,7 +19556,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19181
19556
|
info,
|
|
19182
19557
|
resolver,
|
|
19183
19558
|
options?.recovered === true,
|
|
19184
|
-
options?.fecSbn
|
|
19559
|
+
options?.fecSbn,
|
|
19560
|
+
options?.fecSbnFirst
|
|
19185
19561
|
);
|
|
19186
19562
|
if (outcome.record !== void 0) return outcome.record;
|
|
19187
19563
|
return this.#drop(outcome.reason);
|
|
@@ -19201,7 +19577,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19201
19577
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19202
19578
|
* is what keeps both true.
|
|
19203
19579
|
*/
|
|
19204
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19580
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19205
19581
|
const timing = resolver.sampleTiming(
|
|
19206
19582
|
combined.mpuSequenceNumber,
|
|
19207
19583
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19218,6 +19594,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19218
19594
|
);
|
|
19219
19595
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19220
19596
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19597
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19221
19598
|
if (!resolver.consumeSample(
|
|
19222
19599
|
combined.mpuSequenceNumber,
|
|
19223
19600
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19386,6 +19763,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19386
19763
|
}
|
|
19387
19764
|
this.#emitFrame(data, meta);
|
|
19388
19765
|
}
|
|
19766
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19767
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19768
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19769
|
+
}
|
|
19389
19770
|
/**
|
|
19390
19771
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19391
19772
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19447,6 +19828,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19447
19828
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19448
19829
|
this.#sequencers.clear();
|
|
19449
19830
|
this.#fecSequencer?.discard();
|
|
19831
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19832
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19833
|
+
}
|
|
19450
19834
|
this.#timingStreams.clear();
|
|
19451
19835
|
this.#pendingTimingSamples = 0;
|
|
19452
19836
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19465,10 +19849,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19465
19849
|
return resolver;
|
|
19466
19850
|
}
|
|
19467
19851
|
#awaitTiming(streamKey, head) {
|
|
19468
|
-
const
|
|
19469
|
-
if (!
|
|
19852
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19853
|
+
if (!state) return;
|
|
19854
|
+
if (!head.options?.recovered) {
|
|
19855
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
19856
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
19857
|
+
state.awaitingTiming = kept;
|
|
19858
|
+
}
|
|
19859
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19470
19860
|
awaitingTiming.push(head);
|
|
19471
|
-
if (
|
|
19861
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
19862
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
19863
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
19864
|
+
}
|
|
19865
|
+
}
|
|
19866
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
19867
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
19868
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19472
19869
|
}
|
|
19473
19870
|
/**
|
|
19474
19871
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19496,6 +19893,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19496
19893
|
const state = this.#timingStreams.get(streamKey);
|
|
19497
19894
|
if (!state) return;
|
|
19498
19895
|
this.#timingStreams.delete(streamKey);
|
|
19896
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19499
19897
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19500
19898
|
if (this.#pendingTimingSamples < 0) {
|
|
19501
19899
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22702,7 +23100,18 @@ function keyframeOverlaySsId(header) {
|
|
|
22702
23100
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22703
23101
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22704
23102
|
}
|
|
23103
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23104
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23105
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23106
|
+
}
|
|
23107
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23108
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23109
|
+
return headerPsn + offset >>> 0;
|
|
23110
|
+
}
|
|
23111
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23112
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22705
23113
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23114
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
22706
23115
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22707
23116
|
function assertStreamIdentity(role, stream) {
|
|
22708
23117
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -22813,6 +23222,7 @@ var MmtpRouter = class {
|
|
|
22813
23222
|
#provenance = /* @__PURE__ */ new Map();
|
|
22814
23223
|
#onFlowProtected = null;
|
|
22815
23224
|
#unadmittedListedDrops = 0;
|
|
23225
|
+
#admittedSourceDatagrams = 0;
|
|
22816
23226
|
#unlistedRepairSetAside = 0;
|
|
22817
23227
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22818
23228
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22869,6 +23279,18 @@ var MmtpRouter = class {
|
|
|
22869
23279
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22870
23280
|
*/
|
|
22871
23281
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23282
|
+
/**
|
|
23283
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23284
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23285
|
+
* equal to its SS_ID minus that offset; or a packet the overlay rebuilt
|
|
23286
|
+
* did, its base copy carrying its protected psn (#feedOverlaySource), or
|
|
23287
|
+
* its protected psn passing overlayBaseSsId's test at that offset
|
|
23288
|
+
* (#routeOverlayRecovered). While it equals the stream's current offset,
|
|
23289
|
+
* no hop re-stamps the stream, and a packet's protected psn names its
|
|
23290
|
+
* base SS_ID within unstampedBaseSsId's limits (joint decoding of an MPU
|
|
23291
|
+
* no member of which arrived).
|
|
23292
|
+
*/
|
|
23293
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22872
23294
|
/**
|
|
22873
23295
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22874
23296
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22930,7 +23352,9 @@ var MmtpRouter = class {
|
|
|
22930
23352
|
/** Installed overlay instances, by overlay repair PID (opt-in only). */
|
|
22931
23353
|
#overlays = /* @__PURE__ */ new Map();
|
|
22932
23354
|
#overlayRecoveredObservers = /* @__PURE__ */ new Set();
|
|
22933
|
-
/** Streams whose recovered sequence the base path ever re-stamped
|
|
23355
|
+
/** Streams whose recovered sequence the base path ever re-stamped, or
|
|
23356
|
+
* where an overlay-rebuilt packet's base copy carried another psn than
|
|
23357
|
+
* the sender protected. */
|
|
22934
23358
|
#restampedStreams = /* @__PURE__ */ new Set();
|
|
22935
23359
|
#overlayStats = {
|
|
22936
23360
|
repairDropped: 0,
|
|
@@ -22938,7 +23362,9 @@ var MmtpRouter = class {
|
|
|
22938
23362
|
recovered: 0,
|
|
22939
23363
|
restarts: 0,
|
|
22940
23364
|
unsequencedDropped: 0,
|
|
22941
|
-
protectedDropped: 0
|
|
23365
|
+
protectedDropped: 0,
|
|
23366
|
+
baseFed: 0,
|
|
23367
|
+
baseUndetermined: 0
|
|
22942
23368
|
};
|
|
22943
23369
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22944
23370
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23111,7 +23537,7 @@ var MmtpRouter = class {
|
|
|
23111
23537
|
const cb = this.#getFrameCallback(packetId);
|
|
23112
23538
|
if (isVideo && !assembler) {
|
|
23113
23539
|
if (frameMeta.recovered) {
|
|
23114
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23540
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23115
23541
|
} else {
|
|
23116
23542
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23117
23543
|
}
|
|
@@ -23195,8 +23621,9 @@ var MmtpRouter = class {
|
|
|
23195
23621
|
}
|
|
23196
23622
|
/**
|
|
23197
23623
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23198
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23199
|
-
*
|
|
23624
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
23625
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
23626
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
23200
23627
|
*/
|
|
23201
23628
|
get restampedRecoveredSequenceCount() {
|
|
23202
23629
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23375,6 +23802,7 @@ var MmtpRouter = class {
|
|
|
23375
23802
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23376
23803
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23377
23804
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23805
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23378
23806
|
if (previous) {
|
|
23379
23807
|
previous.flush();
|
|
23380
23808
|
previous.dispose();
|
|
@@ -23501,6 +23929,10 @@ var MmtpRouter = class {
|
|
|
23501
23929
|
get keyframeOverlayStats() {
|
|
23502
23930
|
return { ...this.#overlayStats };
|
|
23503
23931
|
}
|
|
23932
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
23933
|
+
get recoveredHeadsAwaitingTiming() {
|
|
23934
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
23935
|
+
}
|
|
23504
23936
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23505
23937
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23506
23938
|
}
|
|
@@ -23521,7 +23953,8 @@ var MmtpRouter = class {
|
|
|
23521
23953
|
fec,
|
|
23522
23954
|
k,
|
|
23523
23955
|
offsets: /* @__PURE__ */ new Map(),
|
|
23524
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
23956
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
23957
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23525
23958
|
};
|
|
23526
23959
|
this.#overlays.set(repairPacketId, inst);
|
|
23527
23960
|
return () => {
|
|
@@ -23551,12 +23984,18 @@ var MmtpRouter = class {
|
|
|
23551
23984
|
*/
|
|
23552
23985
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23553
23986
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
23987
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
23988
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
23989
|
+
stream.overlayToBase.routed.clear();
|
|
23990
|
+
stream.overlayToBase.pending.clear();
|
|
23991
|
+
}
|
|
23554
23992
|
}
|
|
23555
23993
|
#resetOverlay(inst) {
|
|
23556
23994
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23557
23995
|
inst.last = void 0;
|
|
23558
23996
|
inst.offsets.clear();
|
|
23559
23997
|
inst.moqBlocks.clear();
|
|
23998
|
+
inst.rebuilt.clear();
|
|
23560
23999
|
}
|
|
23561
24000
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23562
24001
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23607,18 +24046,32 @@ var MmtpRouter = class {
|
|
|
23607
24046
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23608
24047
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23609
24048
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23610
|
-
* included (7.1, 7.4.3).
|
|
24049
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
24050
|
+
* FEC reads it.
|
|
24051
|
+
*
|
|
24052
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
24053
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
24054
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
24055
|
+
* the stream unstamped at the SS_ID offset it states (unstampedBaseSsId);
|
|
24056
|
+
* any other shows a hop that re-stamps it.
|
|
23611
24057
|
*/
|
|
23612
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
24058
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23613
24059
|
if (this.#overlays.size === 0) return;
|
|
23614
24060
|
const ssId = keyframeOverlaySsId(header);
|
|
23615
24061
|
if (ssId === void 0) return;
|
|
24062
|
+
let rebuiltPsn;
|
|
23616
24063
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23617
24064
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24065
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24066
|
+
inst.rebuilt.delete(ssId);
|
|
23618
24067
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23619
24068
|
if (identity !== null) {
|
|
23620
24069
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23621
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
24070
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
24071
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
24072
|
+
overlaySsId: ssId,
|
|
24073
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
24074
|
+
});
|
|
23622
24075
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23623
24076
|
}
|
|
23624
24077
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23640,6 +24093,15 @@ var MmtpRouter = class {
|
|
|
23640
24093
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23641
24094
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23642
24095
|
}
|
|
24096
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
24097
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
24098
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
24099
|
+
this.#restampedRecoveredSeqs++;
|
|
24100
|
+
this.#restampedStreams.add(streamKey);
|
|
24101
|
+
} else if (baseSsId !== void 0) {
|
|
24102
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24103
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#unstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24104
|
+
}
|
|
23643
24105
|
}
|
|
23644
24106
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23645
24107
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23673,11 +24135,16 @@ var MmtpRouter = class {
|
|
|
23673
24135
|
}
|
|
23674
24136
|
/**
|
|
23675
24137
|
* Route an overlay-recovered block's packets as source media of the
|
|
23676
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23677
|
-
*
|
|
23678
|
-
*
|
|
23679
|
-
*
|
|
23680
|
-
*
|
|
24138
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
24139
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
24140
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
24141
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
24142
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
24143
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
24144
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
24145
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
24146
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
24147
|
+
* header's sequence and any other drops the packet.
|
|
23681
24148
|
*/
|
|
23682
24149
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23683
24150
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23697,22 +24164,136 @@ var MmtpRouter = class {
|
|
|
23697
24164
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23698
24165
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23699
24166
|
const restored = /* @__PURE__ */ new Set();
|
|
24167
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
23700
24168
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23701
|
-
const
|
|
23702
|
-
|
|
23703
|
-
|
|
24169
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24170
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24171
|
+
if (anchor !== void 0) {
|
|
24172
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23704
24173
|
restored.add(packetSequenceNumber);
|
|
23705
|
-
|
|
24174
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24175
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24176
|
+
if (baseSsId !== void 0) {
|
|
24177
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24178
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24179
|
+
if (offset === baseSsId - packetSequenceNumber >>> 0) this.#unstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24180
|
+
}
|
|
24181
|
+
return [out];
|
|
24182
|
+
}
|
|
24183
|
+
if (!restamped) {
|
|
24184
|
+
const baseSsId = unstampedBaseSsId(
|
|
24185
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
24186
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
24187
|
+
sym.packetSequenceNumber
|
|
24188
|
+
);
|
|
24189
|
+
if (baseSsId !== void 0) {
|
|
24190
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24191
|
+
} else {
|
|
24192
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
24193
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
24194
|
+
}
|
|
24195
|
+
return [sym];
|
|
23706
24196
|
}
|
|
23707
|
-
if (!restamped) return [sym];
|
|
23708
24197
|
this.#overlayStats.unsequencedDropped++;
|
|
23709
24198
|
return [];
|
|
23710
24199
|
});
|
|
23711
24200
|
this.#overlayStats.recovered++;
|
|
24201
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
24202
|
+
for (const sym of symbols) {
|
|
24203
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24204
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
24205
|
+
const first = mpuFirst.get(mpu);
|
|
24206
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
24207
|
+
}
|
|
24208
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
24209
|
+
for (const sym of symbols) {
|
|
24210
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
24211
|
+
}
|
|
23712
24212
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23713
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24213
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24214
|
+
baseSsIds,
|
|
24215
|
+
firstSsIds
|
|
23714
24216
|
});
|
|
23715
24217
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24218
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24219
|
+
}
|
|
24220
|
+
/**
|
|
24221
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24222
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24223
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
24224
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24225
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24226
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24227
|
+
* A base block no repair has stated yet takes it when one does
|
|
24228
|
+
* (#feedPendingRebuilt).
|
|
24229
|
+
*
|
|
24230
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24231
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24232
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24233
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24234
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24235
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24236
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24237
|
+
* block's own arrivals cannot show.
|
|
24238
|
+
*/
|
|
24239
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24240
|
+
const fec = this.#fecManager;
|
|
24241
|
+
const owner = inst.leg.sourcePacketId;
|
|
24242
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24243
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24244
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24245
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24246
|
+
const feeds = [];
|
|
24247
|
+
for (const sym of symbols) {
|
|
24248
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24249
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24250
|
+
if (ssId === void 0) {
|
|
24251
|
+
this.#overlayStats.baseUndetermined++;
|
|
24252
|
+
continue;
|
|
24253
|
+
}
|
|
24254
|
+
routed.delete(ssId);
|
|
24255
|
+
routed.add(ssId);
|
|
24256
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24257
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24258
|
+
}
|
|
24259
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24260
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24261
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24262
|
+
}
|
|
24263
|
+
}
|
|
24264
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24265
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24266
|
+
const fec = this.#fecManager;
|
|
24267
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24268
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24269
|
+
if (coords == null) {
|
|
24270
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24271
|
+
pending.delete(ssId);
|
|
24272
|
+
pending.set(ssId, symbolData.slice());
|
|
24273
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24274
|
+
return;
|
|
24275
|
+
}
|
|
24276
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24277
|
+
if (!fed.taken) return;
|
|
24278
|
+
this.#overlayStats.baseFed++;
|
|
24279
|
+
if (fed.recovered === null) return;
|
|
24280
|
+
const sbn = coords.sbn;
|
|
24281
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24282
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24283
|
+
fec.countRecovery(owner, sbn, name);
|
|
24284
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24285
|
+
});
|
|
24286
|
+
}
|
|
24287
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24288
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24289
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24290
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24291
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24292
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24293
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24294
|
+
pending.delete(ssId);
|
|
24295
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24296
|
+
}
|
|
23716
24297
|
}
|
|
23717
24298
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23718
24299
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23852,6 +24433,10 @@ var MmtpRouter = class {
|
|
|
23852
24433
|
get unadmittedListedDropCount() {
|
|
23853
24434
|
return this.#unadmittedListedDrops;
|
|
23854
24435
|
}
|
|
24436
|
+
/** 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. */
|
|
24437
|
+
get admittedSourceDatagramCount() {
|
|
24438
|
+
return this.#admittedSourceDatagrams;
|
|
24439
|
+
}
|
|
23855
24440
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23856
24441
|
get unlistedRepairSetAsideCount() {
|
|
23857
24442
|
return this.#unlistedRepairSetAside;
|
|
@@ -24338,6 +24923,8 @@ var MmtpRouter = class {
|
|
|
24338
24923
|
)) return;
|
|
24339
24924
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24340
24925
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
24926
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
24927
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24341
24928
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24342
24929
|
const recovered = fec.feedRepairSymbol(
|
|
24343
24930
|
ownerPacketId,
|
|
@@ -24475,6 +25062,7 @@ var MmtpRouter = class {
|
|
|
24475
25062
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24476
25063
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24477
25064
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
25065
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24478
25066
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24479
25067
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24480
25068
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24601,6 +25189,12 @@ var MmtpRouter = class {
|
|
|
24601
25189
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24602
25190
|
* own RAP cadence, so no timer is involved.
|
|
24603
25191
|
*
|
|
25192
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
25193
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
25194
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
25195
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
25196
|
+
* frames of the outgoing stream.
|
|
25197
|
+
*
|
|
24604
25198
|
* The commit is one synchronous transition inside the frame path: the
|
|
24605
25199
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24606
25200
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24651,6 +25245,9 @@ var MmtpRouter = class {
|
|
|
24651
25245
|
droppedIncomingFrames: 0
|
|
24652
25246
|
};
|
|
24653
25247
|
this.#staged = state;
|
|
25248
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25249
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25250
|
+
}
|
|
24654
25251
|
return {
|
|
24655
25252
|
cancel: () => {
|
|
24656
25253
|
if (this.#staged !== state) return false;
|
|
@@ -24733,7 +25330,17 @@ var MmtpRouter = class {
|
|
|
24733
25330
|
*/
|
|
24734
25331
|
#routeParsedFrame(data, meta) {
|
|
24735
25332
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
24736
|
-
if (this.#staged !== null
|
|
25333
|
+
if (this.#staged !== null) {
|
|
25334
|
+
let consumed = false;
|
|
25335
|
+
this.#fecPokeDepth++;
|
|
25336
|
+
try {
|
|
25337
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
25338
|
+
} finally {
|
|
25339
|
+
this.#fecPokeDepth--;
|
|
25340
|
+
this.#flushPendingFecPokes();
|
|
25341
|
+
}
|
|
25342
|
+
if (consumed) return;
|
|
25343
|
+
}
|
|
24737
25344
|
this.#deliverParsedFrame(data, meta);
|
|
24738
25345
|
}
|
|
24739
25346
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -24885,12 +25492,16 @@ var MmtpRouter = class {
|
|
|
24885
25492
|
this.#parser.deferFecRelease(work);
|
|
24886
25493
|
} finally {
|
|
24887
25494
|
this.#fecPokeDepth--;
|
|
24888
|
-
|
|
24889
|
-
|
|
24890
|
-
|
|
24891
|
-
|
|
24892
|
-
|
|
24893
|
-
|
|
25495
|
+
this.#flushPendingFecPokes();
|
|
25496
|
+
}
|
|
25497
|
+
}
|
|
25498
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
25499
|
+
#flushPendingFecPokes() {
|
|
25500
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
25501
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
25502
|
+
this.#pendingFecPokes.clear();
|
|
25503
|
+
for (const poke of pokes) {
|
|
25504
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
24894
25505
|
}
|
|
24895
25506
|
}
|
|
24896
25507
|
}
|
|
@@ -24968,6 +25579,14 @@ var MmtpRouter = class {
|
|
|
24968
25579
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
24969
25580
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
24970
25581
|
},
|
|
25582
|
+
// A staged switch commits on when each side's frames reach it
|
|
25583
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
25584
|
+
// they did unanchored.
|
|
25585
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
25586
|
+
const staged = this.#staged;
|
|
25587
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
25588
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
25589
|
+
},
|
|
24971
25590
|
onReleased: (data, meta) => {
|
|
24972
25591
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
24973
25592
|
const timing = meta.authoredTiming;
|
|
@@ -25109,6 +25728,7 @@ var MmtpRouter = class {
|
|
|
25109
25728
|
const parsed = this.#parser.parsePacket(packet);
|
|
25110
25729
|
if (!parsed) return;
|
|
25111
25730
|
const info = parsed.info;
|
|
25731
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25112
25732
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25113
25733
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25114
25734
|
}
|
|
@@ -25390,7 +26010,8 @@ var MmtpRouter = class {
|
|
|
25390
26010
|
parsed.header,
|
|
25391
26011
|
info.sequenceNumber,
|
|
25392
26012
|
info.rapFlag,
|
|
25393
|
-
source
|
|
26013
|
+
source,
|
|
26014
|
+
fecPayloadId?.ssId
|
|
25394
26015
|
);
|
|
25395
26016
|
}
|
|
25396
26017
|
}
|
|
@@ -25416,7 +26037,8 @@ var MmtpRouter = class {
|
|
|
25416
26037
|
source: parserOptions?.source,
|
|
25417
26038
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25418
26039
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25419
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26040
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26041
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25420
26042
|
};
|
|
25421
26043
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25422
26044
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25540,7 +26162,8 @@ var MmtpRouter = class {
|
|
|
25540
26162
|
if (stream === void 0) {
|
|
25541
26163
|
stream = {
|
|
25542
26164
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25543
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26165
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26166
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25544
26167
|
};
|
|
25545
26168
|
byName.set(catalogTrackName, stream);
|
|
25546
26169
|
}
|
|
@@ -25655,8 +26278,16 @@ var MmtpRouter = class {
|
|
|
25655
26278
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25656
26279
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25657
26280
|
}
|
|
25658
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26281
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25659
26282
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26283
|
+
if (this.#isFecTrack(packetId)) {
|
|
26284
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26285
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26286
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26287
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26288
|
+
return;
|
|
26289
|
+
}
|
|
26290
|
+
}
|
|
25660
26291
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25661
26292
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25662
26293
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25810,6 +26441,13 @@ var MmtpRouter = class {
|
|
|
25810
26441
|
* frames by epoch only if pre-protection input ever matters.
|
|
25811
26442
|
*/
|
|
25812
26443
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26444
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26445
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26446
|
+
result = {
|
|
26447
|
+
...result,
|
|
26448
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26449
|
+
};
|
|
26450
|
+
}
|
|
25813
26451
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25814
26452
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25815
26453
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25866,7 +26504,12 @@ var MmtpRouter = class {
|
|
|
25866
26504
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25867
26505
|
if (ownerPacketId == null) return;
|
|
25868
26506
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25869
|
-
const fecSbnOf = (index) =>
|
|
26507
|
+
const fecSbnOf = (index) => {
|
|
26508
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
26509
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
26510
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
26511
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
26512
|
+
};
|
|
25870
26513
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25871
26514
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25872
26515
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -25874,7 +26517,10 @@ var MmtpRouter = class {
|
|
|
25874
26517
|
...result,
|
|
25875
26518
|
symbols: result.symbols.map((sym) => {
|
|
25876
26519
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25877
|
-
if (wire === sym.packetSequenceNumber)
|
|
26520
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26521
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26522
|
+
return sym;
|
|
26523
|
+
}
|
|
25878
26524
|
this.#restampedRecoveredSeqs++;
|
|
25879
26525
|
this.#restampedStreams.add(streamKey);
|
|
25880
26526
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25910,6 +26556,29 @@ var MmtpRouter = class {
|
|
|
25910
26556
|
}
|
|
25911
26557
|
);
|
|
25912
26558
|
}
|
|
26559
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26560
|
+
ownerPacketId,
|
|
26561
|
+
sym.fragment.mpuSequenceNumber,
|
|
26562
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26563
|
+
sym.fragment.sampleNumber,
|
|
26564
|
+
catalogTrackName
|
|
26565
|
+
);
|
|
26566
|
+
for (const sym of result.symbols) {
|
|
26567
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26568
|
+
this.#feedViaParser(
|
|
26569
|
+
sym.rawSymbol,
|
|
26570
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26571
|
+
void 0,
|
|
26572
|
+
{
|
|
26573
|
+
skipFecPayloadId: true,
|
|
26574
|
+
recovered: true,
|
|
26575
|
+
deliveryProvenance,
|
|
26576
|
+
...fecSbnOf(sym.index),
|
|
26577
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26578
|
+
}
|
|
26579
|
+
);
|
|
26580
|
+
}
|
|
26581
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25913
26582
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25914
26583
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25915
26584
|
if (assembler) {
|