@blockcast/mmt-container 2.2.0-main.efb4fa4521af → 2.2.0-main.fb5c7dfbe2de
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -17
- package/dist/browser-runtime.js +1336 -302
- package/dist/browser-runtime.js.map +3 -3
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/cmcd-producer.d.ts +60 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +207 -26
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +209 -29
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +206 -26
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +34 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +555 -79
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +13 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +980 -146
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +28 -18
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +206 -26
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/repair-layer-subscriptions.d.ts +6 -2
- package/dist/repair-layer-subscriptions.d.ts.map +1 -1
- package/dist/repair-layer-subscriptions.js +6 -3
- package/dist/repair-layer-subscriptions.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +206 -26
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +189 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +9 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-router.ts +548 -38
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +13 -5
- package/src/track-session.ts +4 -1
package/dist/mmtp-router.js
CHANGED
|
@@ -1610,14 +1610,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
1610
1610
|
});
|
|
1611
1611
|
|
|
1612
1612
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
1613
|
+
var MAX_PENDING_PACKETS = 256;
|
|
1613
1614
|
var AMTGatewayManager = class {
|
|
1614
1615
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1615
1616
|
messageHandler;
|
|
1617
|
+
messageListenerRegistered = false;
|
|
1616
1618
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
1617
1619
|
constructor() {
|
|
1618
1620
|
this.messageHandler = (event) => {
|
|
1619
1621
|
const data = event.data;
|
|
1620
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
1621
1622
|
if (data.type === "UDP_PACKET") {
|
|
1622
1623
|
this.handlePacket(data);
|
|
1623
1624
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -1626,12 +1627,22 @@ var AMTGatewayManager = class {
|
|
|
1626
1627
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
1627
1628
|
}
|
|
1628
1629
|
};
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
console.warn("[AMT] Service worker not available");
|
|
1630
|
+
}
|
|
1631
|
+
ensureMessageListener() {
|
|
1632
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
1633
|
+
return;
|
|
1634
1634
|
}
|
|
1635
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
1636
|
+
this.messageListenerRegistered = true;
|
|
1637
|
+
console.log("[AMT] Registered service worker message handler");
|
|
1638
|
+
}
|
|
1639
|
+
removeMessageListener() {
|
|
1640
|
+
if (!this.messageListenerRegistered)
|
|
1641
|
+
return;
|
|
1642
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
1643
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
1644
|
+
}
|
|
1645
|
+
this.messageListenerRegistered = false;
|
|
1635
1646
|
}
|
|
1636
1647
|
/**
|
|
1637
1648
|
* Subscribe via AMT tunnel
|
|
@@ -1641,6 +1652,7 @@ var AMTGatewayManager = class {
|
|
|
1641
1652
|
if (config.protected) {
|
|
1642
1653
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
1643
1654
|
}
|
|
1655
|
+
this.ensureMessageListener();
|
|
1644
1656
|
const keyStr = this.getKeyString(key);
|
|
1645
1657
|
if (this.subscriptions.has(keyStr)) {
|
|
1646
1658
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -1676,6 +1688,8 @@ var AMTGatewayManager = class {
|
|
|
1676
1688
|
this.subscriptions.delete(keyStr);
|
|
1677
1689
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
1678
1690
|
}
|
|
1691
|
+
if (this.subscriptions.size === 0)
|
|
1692
|
+
this.removeMessageListener();
|
|
1679
1693
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
1680
1694
|
}
|
|
1681
1695
|
}
|
|
@@ -1697,6 +1711,8 @@ var AMTGatewayManager = class {
|
|
|
1697
1711
|
} finally {
|
|
1698
1712
|
this.subscriptions.delete(handle);
|
|
1699
1713
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
1714
|
+
if (this.subscriptions.size === 0)
|
|
1715
|
+
this.removeMessageListener();
|
|
1700
1716
|
}
|
|
1701
1717
|
}
|
|
1702
1718
|
/**
|
|
@@ -1713,14 +1729,12 @@ var AMTGatewayManager = class {
|
|
|
1713
1729
|
if (!subscriptionId)
|
|
1714
1730
|
return;
|
|
1715
1731
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
1716
|
-
if (!subscription)
|
|
1717
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
1732
|
+
if (!subscription)
|
|
1718
1733
|
return;
|
|
1719
|
-
}
|
|
1720
1734
|
if (!subscription.handshakeComplete) {
|
|
1721
1735
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
1722
1736
|
const packetData2 = data;
|
|
1723
|
-
if (queue) {
|
|
1737
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
1724
1738
|
queue.push({
|
|
1725
1739
|
data: packetData2.data,
|
|
1726
1740
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -1746,19 +1760,15 @@ var AMTGatewayManager = class {
|
|
|
1746
1760
|
if (!subscriptionId)
|
|
1747
1761
|
return;
|
|
1748
1762
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
1749
|
-
if (!subscription)
|
|
1750
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
1763
|
+
if (!subscription)
|
|
1751
1764
|
return;
|
|
1752
|
-
}
|
|
1753
1765
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
1754
1766
|
}
|
|
1755
1767
|
/**
|
|
1756
1768
|
* Cleanup on destroy
|
|
1757
1769
|
*/
|
|
1758
1770
|
destroy() {
|
|
1759
|
-
|
|
1760
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
1761
|
-
}
|
|
1771
|
+
this.removeMessageListener();
|
|
1762
1772
|
const handles = Array.from(this.subscriptions.keys());
|
|
1763
1773
|
for (const handle of handles) {
|
|
1764
1774
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -7667,9 +7677,21 @@ var IsoBmffTimingResolver = class {
|
|
|
7667
7677
|
}
|
|
7668
7678
|
#track = null;
|
|
7669
7679
|
#fragments = /* @__PURE__ */ new Map();
|
|
7680
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
7681
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
7682
|
+
/**
|
|
7683
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
7684
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
7685
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
7686
|
+
*/
|
|
7687
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
7670
7688
|
#pendingSamples = 0;
|
|
7671
7689
|
#evictedSamples = 0;
|
|
7672
7690
|
#lastMovieFragmentRejection = null;
|
|
7691
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
7692
|
+
get initialized() {
|
|
7693
|
+
return this.#track !== null;
|
|
7694
|
+
}
|
|
7673
7695
|
get pendingSampleCount() {
|
|
7674
7696
|
return this.#pendingSamples;
|
|
7675
7697
|
}
|
|
@@ -7693,7 +7715,14 @@ var IsoBmffTimingResolver = class {
|
|
|
7693
7715
|
this.#track = timing;
|
|
7694
7716
|
return true;
|
|
7695
7717
|
}
|
|
7696
|
-
|
|
7718
|
+
/**
|
|
7719
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
7720
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
7721
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
7722
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
7723
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
7724
|
+
*/
|
|
7725
|
+
addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
7697
7726
|
this.#lastMovieFragmentRejection = null;
|
|
7698
7727
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
7699
7728
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -7719,6 +7748,8 @@ var IsoBmffTimingResolver = class {
|
|
|
7719
7748
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
7720
7749
|
);
|
|
7721
7750
|
}
|
|
7751
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
7752
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
7722
7753
|
let freed = 0;
|
|
7723
7754
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
7724
7755
|
freed += this.#evictOldestFragment();
|
|
@@ -7732,6 +7763,7 @@ var IsoBmffTimingResolver = class {
|
|
|
7732
7763
|
return false;
|
|
7733
7764
|
}
|
|
7734
7765
|
this.#fragments.set(key, [...parsed.samples]);
|
|
7766
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
7735
7767
|
this.#pendingSamples += parsed.samples.length;
|
|
7736
7768
|
return true;
|
|
7737
7769
|
}
|
|
@@ -7789,6 +7821,7 @@ var IsoBmffTimingResolver = class {
|
|
|
7789
7821
|
let live = 0;
|
|
7790
7822
|
for (const sample of samples) if (sample !== null) live++;
|
|
7791
7823
|
this.#fragments.delete(oldest);
|
|
7824
|
+
this.#firstSamples.delete(oldest);
|
|
7792
7825
|
this.#pendingSamples -= live;
|
|
7793
7826
|
this.#evictedSamples += live;
|
|
7794
7827
|
return live;
|
|
@@ -7804,12 +7837,23 @@ var IsoBmffTimingResolver = class {
|
|
|
7804
7837
|
if (!samples || !samples[index]) return false;
|
|
7805
7838
|
samples[index] = null;
|
|
7806
7839
|
this.#pendingSamples--;
|
|
7807
|
-
if (samples.every((value) => value === null))
|
|
7840
|
+
if (samples.every((value) => value === null)) {
|
|
7841
|
+
this.#fragments.delete(key);
|
|
7842
|
+
const first = this.#firstSamples.get(key);
|
|
7843
|
+
this.#firstSamples.delete(key);
|
|
7844
|
+
this.#delivered.delete(key);
|
|
7845
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
7846
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
7847
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7808
7850
|
return true;
|
|
7809
7851
|
}
|
|
7810
7852
|
reset() {
|
|
7811
7853
|
this.#track = null;
|
|
7812
7854
|
this.#fragments.clear();
|
|
7855
|
+
this.#firstSamples.clear();
|
|
7856
|
+
this.#delivered.clear();
|
|
7813
7857
|
this.#pendingSamples = 0;
|
|
7814
7858
|
}
|
|
7815
7859
|
};
|
|
@@ -7979,6 +8023,9 @@ function authoredGap(before, after) {
|
|
|
7979
8023
|
function ssIdAfter(a, b) {
|
|
7980
8024
|
return (a - b | 0) > 0;
|
|
7981
8025
|
}
|
|
8026
|
+
function holdsRebuilt(stream) {
|
|
8027
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
8028
|
+
}
|
|
7982
8029
|
var FecReleaseSequencer = class {
|
|
7983
8030
|
#streams = /* @__PURE__ */ new Map();
|
|
7984
8031
|
#gate;
|
|
@@ -8041,6 +8088,7 @@ var FecReleaseSequencer = class {
|
|
|
8041
8088
|
stream.lastSbn = void 0;
|
|
8042
8089
|
stream.discontinuity = false;
|
|
8043
8090
|
stream.rapSsId = void 0;
|
|
8091
|
+
stream.sourced = false;
|
|
8044
8092
|
}
|
|
8045
8093
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
8046
8094
|
this.#recordStat("droppedStale");
|
|
@@ -8067,6 +8115,7 @@ var FecReleaseSequencer = class {
|
|
|
8067
8115
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
8068
8116
|
else hi = mid;
|
|
8069
8117
|
}
|
|
8118
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
8070
8119
|
stream.buf.splice(lo, 0, frame);
|
|
8071
8120
|
this.#drainUnlessDeferred(stream);
|
|
8072
8121
|
}
|
|
@@ -8102,6 +8151,41 @@ var FecReleaseSequencer = class {
|
|
|
8102
8151
|
}
|
|
8103
8152
|
return false;
|
|
8104
8153
|
}
|
|
8154
|
+
/**
|
|
8155
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
8156
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
8157
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
8158
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
8159
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
8160
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
8161
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
8162
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
8163
|
+
* released.
|
|
8164
|
+
*/
|
|
8165
|
+
unsource(packetId, catalogTrackName) {
|
|
8166
|
+
for (const stream of this.#streams.values()) {
|
|
8167
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
8168
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
8169
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
8170
|
+
else this.#drain(stream, true);
|
|
8171
|
+
}
|
|
8172
|
+
stream.sourced = false;
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8176
|
+
/**
|
|
8177
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
8178
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
8179
|
+
* packetId.
|
|
8180
|
+
*/
|
|
8181
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
8182
|
+
for (const stream of this.#streams.values()) {
|
|
8183
|
+
if (stream.packetId !== packetId) continue;
|
|
8184
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
8185
|
+
if (holdsRebuilt(stream)) return true;
|
|
8186
|
+
}
|
|
8187
|
+
return false;
|
|
8188
|
+
}
|
|
8105
8189
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
8106
8190
|
poke(streamKey) {
|
|
8107
8191
|
if (streamKey !== void 0) {
|
|
@@ -8193,16 +8277,32 @@ var FecReleaseSequencer = class {
|
|
|
8193
8277
|
last: null,
|
|
8194
8278
|
lastSbn: void 0,
|
|
8195
8279
|
discontinuity: false,
|
|
8196
|
-
rapSsId: void 0
|
|
8280
|
+
rapSsId: void 0,
|
|
8281
|
+
sourced: false,
|
|
8282
|
+
draining: 0
|
|
8197
8283
|
};
|
|
8198
8284
|
this.#streams.set(streamKey, stream);
|
|
8199
8285
|
}
|
|
8200
8286
|
return stream;
|
|
8201
8287
|
}
|
|
8202
|
-
#
|
|
8288
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
8289
|
+
#drain(stream, asUnanchored = false) {
|
|
8290
|
+
stream.draining++;
|
|
8291
|
+
try {
|
|
8292
|
+
this.#drainLoop(stream, asUnanchored);
|
|
8293
|
+
} finally {
|
|
8294
|
+
stream.draining--;
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
#drainLoop(stream, asUnanchored) {
|
|
8203
8298
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
8204
8299
|
const head = stream.buf[0];
|
|
8205
8300
|
const last = stream.last;
|
|
8301
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
8302
|
+
stream.buf.shift();
|
|
8303
|
+
this.#release(stream, head, void 0);
|
|
8304
|
+
continue;
|
|
8305
|
+
}
|
|
8206
8306
|
if (stream.discontinuity) {
|
|
8207
8307
|
stream.discontinuity = false;
|
|
8208
8308
|
stream.buf.shift();
|
|
@@ -8216,9 +8316,10 @@ var FecReleaseSequencer = class {
|
|
|
8216
8316
|
}
|
|
8217
8317
|
const headSbn = head.meta.fecSbn;
|
|
8218
8318
|
const from = stream.lastSbn ?? headSbn;
|
|
8219
|
-
const to = headSbn ?? stream.lastSbn;
|
|
8319
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
8220
8320
|
let lost = [];
|
|
8221
8321
|
const unanchored = from === void 0 && to === void 0;
|
|
8322
|
+
let unresolved = false;
|
|
8222
8323
|
if (from !== void 0 && to !== void 0) {
|
|
8223
8324
|
const resolved = this.#gate.resolvedLoss(
|
|
8224
8325
|
stream.packetId,
|
|
@@ -8227,7 +8328,8 @@ var FecReleaseSequencer = class {
|
|
|
8227
8328
|
Math.max(from, to),
|
|
8228
8329
|
from
|
|
8229
8330
|
);
|
|
8230
|
-
|
|
8331
|
+
unresolved = resolved === null;
|
|
8332
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
8231
8333
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
8232
8334
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
8233
8335
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -8235,22 +8337,56 @@ var FecReleaseSequencer = class {
|
|
|
8235
8337
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
8236
8338
|
continue;
|
|
8237
8339
|
}
|
|
8238
|
-
lost = resolved;
|
|
8340
|
+
lost = resolved ?? [];
|
|
8239
8341
|
}
|
|
8240
8342
|
stream.buf.shift();
|
|
8241
8343
|
this.#release(
|
|
8242
8344
|
stream,
|
|
8243
8345
|
head,
|
|
8244
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
8346
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
8245
8347
|
);
|
|
8246
8348
|
}
|
|
8247
8349
|
}
|
|
8350
|
+
/**
|
|
8351
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
8352
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
8353
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
8354
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
8355
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
8356
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
8357
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
8358
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
8359
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
8360
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
8361
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
8362
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
8363
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
8364
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
8365
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
8366
|
+
*/
|
|
8367
|
+
#cursorPassed(stream, asUnanchored) {
|
|
8368
|
+
let at = -1;
|
|
8369
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
8370
|
+
const frame = stream.buf[i];
|
|
8371
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
8372
|
+
frame.passed = true;
|
|
8373
|
+
if (at < 0) at = i;
|
|
8374
|
+
} else if (frame.passed !== true) break;
|
|
8375
|
+
}
|
|
8376
|
+
if (at < 0) return false;
|
|
8377
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
8378
|
+
}
|
|
8248
8379
|
#release(stream, frame, decision) {
|
|
8380
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
8381
|
+
if (stale) this.#recordStat("droppedStale");
|
|
8249
8382
|
if (decision) frame.meta.gapBefore = decision;
|
|
8250
8383
|
stream.last = frame;
|
|
8251
|
-
if (frame.meta.fecSbn !== void 0
|
|
8384
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
8385
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
8386
|
+
}
|
|
8387
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
8252
8388
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
8253
|
-
this.#emit(frame.data, frame.meta);
|
|
8389
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
8254
8390
|
}
|
|
8255
8391
|
};
|
|
8256
8392
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -8291,6 +8427,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8291
8427
|
this.#reclaimedTimingSamples = 0;
|
|
8292
8428
|
this.#evictedTimingSamples = 0;
|
|
8293
8429
|
this.#rejectedMovieFragments = 0;
|
|
8430
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
8431
|
+
held: 0,
|
|
8432
|
+
expired: 0,
|
|
8433
|
+
discarded: 0,
|
|
8434
|
+
superseded: 0
|
|
8435
|
+
};
|
|
8294
8436
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
8295
8437
|
this.#fecGate = null;
|
|
8296
8438
|
this.#fecSequencer = null;
|
|
@@ -8399,6 +8541,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8399
8541
|
#reclaimedTimingSamples;
|
|
8400
8542
|
#evictedTimingSamples;
|
|
8401
8543
|
#rejectedMovieFragments;
|
|
8544
|
+
#recoveredHeadsAwaitingTiming;
|
|
8402
8545
|
#sequencers;
|
|
8403
8546
|
#fecGate;
|
|
8404
8547
|
#fecSequencer;
|
|
@@ -8504,11 +8647,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8504
8647
|
/**
|
|
8505
8648
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
8506
8649
|
*
|
|
8507
|
-
*
|
|
8508
|
-
*
|
|
8509
|
-
*
|
|
8510
|
-
*
|
|
8511
|
-
*
|
|
8650
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
8651
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
8652
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
8653
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
8654
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
8655
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
8512
8656
|
*/
|
|
8513
8657
|
get timingBudgetStats() {
|
|
8514
8658
|
return {
|
|
@@ -8519,6 +8663,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8519
8663
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
8520
8664
|
};
|
|
8521
8665
|
}
|
|
8666
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
8667
|
+
get recoveredHeadsAwaitingTiming() {
|
|
8668
|
+
let pending = 0;
|
|
8669
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
8670
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
8671
|
+
}
|
|
8672
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
8673
|
+
}
|
|
8522
8674
|
get reorderStats() {
|
|
8523
8675
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
8524
8676
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -8560,6 +8712,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8560
8712
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
8561
8713
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
8562
8714
|
}
|
|
8715
|
+
/** See FecReleaseSequencer.unsource. */
|
|
8716
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
8717
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
8718
|
+
}
|
|
8719
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
8720
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
8721
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
8722
|
+
}
|
|
8563
8723
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
8564
8724
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
8565
8725
|
if (!this.#fecSequencer) return;
|
|
@@ -8692,7 +8852,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8692
8852
|
const now = Date.now();
|
|
8693
8853
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
8694
8854
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
8695
|
-
awaitingTiming.shift();
|
|
8855
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
8696
8856
|
}
|
|
8697
8857
|
}
|
|
8698
8858
|
}
|
|
@@ -8810,11 +8970,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8810
8970
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
8811
8971
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
8812
8972
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
8813
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
8973
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
8814
8974
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
8815
8975
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
8816
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
8817
|
-
const admitted = resolver2.addMovieFragment(
|
|
8976
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
8977
|
+
const admitted = resolver2.addMovieFragment(
|
|
8978
|
+
mpu.mpuSequenceNumber,
|
|
8979
|
+
data,
|
|
8980
|
+
remaining,
|
|
8981
|
+
options?.recovered === true
|
|
8982
|
+
);
|
|
8818
8983
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
8819
8984
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
8820
8985
|
if (!admitted) {
|
|
@@ -8834,7 +8999,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8834
8999
|
};
|
|
8835
9000
|
}
|
|
8836
9001
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
8837
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
9002
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
8838
9003
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
8839
9004
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
8840
9005
|
return this.#drop("mfu-empty-payload");
|
|
@@ -8855,6 +9020,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8855
9020
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
8856
9021
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
8857
9022
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
9023
|
+
sequenceNumber: info.sequenceNumber,
|
|
8858
9024
|
arrivedAt: Date.now()
|
|
8859
9025
|
});
|
|
8860
9026
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -8880,7 +9046,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8880
9046
|
info,
|
|
8881
9047
|
resolver,
|
|
8882
9048
|
options?.recovered === true,
|
|
8883
|
-
options?.fecSbn
|
|
9049
|
+
options?.fecSbn,
|
|
9050
|
+
options?.fecSbnFirst
|
|
8884
9051
|
);
|
|
8885
9052
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
8886
9053
|
}
|
|
@@ -8973,7 +9140,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8973
9140
|
info,
|
|
8974
9141
|
resolver,
|
|
8975
9142
|
options?.recovered === true,
|
|
8976
|
-
options?.fecSbn
|
|
9143
|
+
options?.fecSbn,
|
|
9144
|
+
options?.fecSbnFirst
|
|
8977
9145
|
);
|
|
8978
9146
|
if (outcome.record !== void 0) return outcome.record;
|
|
8979
9147
|
return this.#drop(outcome.reason);
|
|
@@ -8993,7 +9161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
8993
9161
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
8994
9162
|
* is what keeps both true.
|
|
8995
9163
|
*/
|
|
8996
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
9164
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
8997
9165
|
const timing = resolver.sampleTiming(
|
|
8998
9166
|
combined.mpuSequenceNumber,
|
|
8999
9167
|
combined.movieFragmentSequenceNumber,
|
|
@@ -9010,6 +9178,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
9010
9178
|
);
|
|
9011
9179
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
9012
9180
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
9181
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
9013
9182
|
if (!resolver.consumeSample(
|
|
9014
9183
|
combined.mpuSequenceNumber,
|
|
9015
9184
|
combined.movieFragmentSequenceNumber,
|
|
@@ -9178,6 +9347,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
9178
9347
|
}
|
|
9179
9348
|
this.#emitFrame(data, meta);
|
|
9180
9349
|
}
|
|
9350
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
9351
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
9352
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
9353
|
+
}
|
|
9181
9354
|
/**
|
|
9182
9355
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
9183
9356
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -9239,6 +9412,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
9239
9412
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
9240
9413
|
this.#sequencers.clear();
|
|
9241
9414
|
this.#fecSequencer?.discard();
|
|
9415
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
9416
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
9417
|
+
}
|
|
9242
9418
|
this.#timingStreams.clear();
|
|
9243
9419
|
this.#pendingTimingSamples = 0;
|
|
9244
9420
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -9257,10 +9433,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
9257
9433
|
return resolver;
|
|
9258
9434
|
}
|
|
9259
9435
|
#awaitTiming(streamKey, head) {
|
|
9260
|
-
const
|
|
9261
|
-
if (!
|
|
9436
|
+
const state = this.#timingStreams.get(streamKey);
|
|
9437
|
+
if (!state) return;
|
|
9438
|
+
if (!head.options?.recovered) {
|
|
9439
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
9440
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
9441
|
+
state.awaitingTiming = kept;
|
|
9442
|
+
}
|
|
9443
|
+
const awaitingTiming = state.awaitingTiming;
|
|
9262
9444
|
awaitingTiming.push(head);
|
|
9263
|
-
if (
|
|
9445
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
9446
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
9447
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
9451
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
9452
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
9264
9453
|
}
|
|
9265
9454
|
/**
|
|
9266
9455
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -9288,6 +9477,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
9288
9477
|
const state = this.#timingStreams.get(streamKey);
|
|
9289
9478
|
if (!state) return;
|
|
9290
9479
|
this.#timingStreams.delete(streamKey);
|
|
9480
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
9291
9481
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
9292
9482
|
if (this.#pendingTimingSamples < 0) {
|
|
9293
9483
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -9828,7 +10018,18 @@ function keyframeOverlaySsId(header) {
|
|
|
9828
10018
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
9829
10019
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
9830
10020
|
}
|
|
10021
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
10022
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
10023
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
10024
|
+
}
|
|
10025
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
10026
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
10027
|
+
return headerPsn + offset >>> 0;
|
|
10028
|
+
}
|
|
10029
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
10030
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
9831
10031
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
10032
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
9832
10033
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
9833
10034
|
function assertStreamIdentity(role, stream) {
|
|
9834
10035
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -9939,6 +10140,7 @@ var MmtpRouter = class {
|
|
|
9939
10140
|
#provenance = /* @__PURE__ */ new Map();
|
|
9940
10141
|
#onFlowProtected = null;
|
|
9941
10142
|
#unadmittedListedDrops = 0;
|
|
10143
|
+
#admittedSourceDatagrams = 0;
|
|
9942
10144
|
#unlistedRepairSetAside = 0;
|
|
9943
10145
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
9944
10146
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -9995,6 +10197,26 @@ var MmtpRouter = class {
|
|
|
9995
10197
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
9996
10198
|
*/
|
|
9997
10199
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
10200
|
+
/**
|
|
10201
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
10202
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
10203
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
10204
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
10205
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
10206
|
+
* an MPU no member of which arrived).
|
|
10207
|
+
*/
|
|
10208
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
10209
|
+
/**
|
|
10210
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
10211
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
10212
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
10213
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
10214
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
10215
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
10216
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
10217
|
+
* this offset did.
|
|
10218
|
+
*/
|
|
10219
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
9998
10220
|
/**
|
|
9999
10221
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
10000
10222
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -10064,7 +10286,9 @@ var MmtpRouter = class {
|
|
|
10064
10286
|
recovered: 0,
|
|
10065
10287
|
restarts: 0,
|
|
10066
10288
|
unsequencedDropped: 0,
|
|
10067
|
-
protectedDropped: 0
|
|
10289
|
+
protectedDropped: 0,
|
|
10290
|
+
baseFed: 0,
|
|
10291
|
+
baseUndetermined: 0
|
|
10068
10292
|
};
|
|
10069
10293
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
10070
10294
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -10237,7 +10461,7 @@ var MmtpRouter = class {
|
|
|
10237
10461
|
const cb = this.#getFrameCallback(packetId);
|
|
10238
10462
|
if (isVideo && !assembler) {
|
|
10239
10463
|
if (frameMeta.recovered) {
|
|
10240
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
10464
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
10241
10465
|
} else {
|
|
10242
10466
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
10243
10467
|
}
|
|
@@ -10270,13 +10494,18 @@ var MmtpRouter = class {
|
|
|
10270
10494
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
10271
10495
|
}
|
|
10272
10496
|
};
|
|
10273
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
10497
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
10274
10498
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
10275
10499
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
10276
10500
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
10277
10501
|
}
|
|
10278
|
-
|
|
10279
|
-
|
|
10502
|
+
if (span === void 0) {
|
|
10503
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
10504
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
10505
|
+
} else {
|
|
10506
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
10507
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
10508
|
+
}
|
|
10280
10509
|
};
|
|
10281
10510
|
}
|
|
10282
10511
|
}
|
|
@@ -10321,8 +10550,9 @@ var MmtpRouter = class {
|
|
|
10321
10550
|
}
|
|
10322
10551
|
/**
|
|
10323
10552
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
10324
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
10325
|
-
*
|
|
10553
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
10554
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
10555
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
10326
10556
|
*/
|
|
10327
10557
|
get restampedRecoveredSequenceCount() {
|
|
10328
10558
|
return this.#restampedRecoveredSeqs;
|
|
@@ -10501,6 +10731,7 @@ var MmtpRouter = class {
|
|
|
10501
10731
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
10502
10732
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
10503
10733
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
10734
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
10504
10735
|
if (previous) {
|
|
10505
10736
|
previous.flush();
|
|
10506
10737
|
previous.dispose();
|
|
@@ -10627,6 +10858,10 @@ var MmtpRouter = class {
|
|
|
10627
10858
|
get keyframeOverlayStats() {
|
|
10628
10859
|
return { ...this.#overlayStats };
|
|
10629
10860
|
}
|
|
10861
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
10862
|
+
get recoveredHeadsAwaitingTiming() {
|
|
10863
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
10864
|
+
}
|
|
10630
10865
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
10631
10866
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
10632
10867
|
}
|
|
@@ -10647,7 +10882,8 @@ var MmtpRouter = class {
|
|
|
10647
10882
|
fec,
|
|
10648
10883
|
k,
|
|
10649
10884
|
offsets: /* @__PURE__ */ new Map(),
|
|
10650
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
10885
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
10886
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
10651
10887
|
};
|
|
10652
10888
|
this.#overlays.set(repairPacketId, inst);
|
|
10653
10889
|
return () => {
|
|
@@ -10677,12 +10913,18 @@ var MmtpRouter = class {
|
|
|
10677
10913
|
*/
|
|
10678
10914
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
10679
10915
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
10916
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
10917
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
10918
|
+
stream.overlayToBase.routed.clear();
|
|
10919
|
+
stream.overlayToBase.pending.clear();
|
|
10920
|
+
}
|
|
10680
10921
|
}
|
|
10681
10922
|
#resetOverlay(inst) {
|
|
10682
10923
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
10683
10924
|
inst.last = void 0;
|
|
10684
10925
|
inst.offsets.clear();
|
|
10685
10926
|
inst.moqBlocks.clear();
|
|
10927
|
+
inst.rebuilt.clear();
|
|
10686
10928
|
}
|
|
10687
10929
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
10688
10930
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -10733,18 +10975,33 @@ var MmtpRouter = class {
|
|
|
10733
10975
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
10734
10976
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
10735
10977
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
10736
|
-
* included (7.1, 7.4.3).
|
|
10737
|
-
|
|
10738
|
-
|
|
10978
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
10979
|
+
* FEC reads it.
|
|
10980
|
+
*
|
|
10981
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
10982
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
10983
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
10984
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
10985
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
10986
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
10987
|
+
*/
|
|
10988
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
10739
10989
|
if (this.#overlays.size === 0) return;
|
|
10740
10990
|
const ssId = keyframeOverlaySsId(header);
|
|
10741
10991
|
if (ssId === void 0) return;
|
|
10992
|
+
let rebuiltPsn;
|
|
10742
10993
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
10743
10994
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
10995
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
10996
|
+
inst.rebuilt.delete(ssId);
|
|
10744
10997
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
10745
10998
|
if (identity !== null) {
|
|
10746
10999
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
10747
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
11000
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
11001
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
11002
|
+
overlaySsId: ssId,
|
|
11003
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
11004
|
+
});
|
|
10748
11005
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
10749
11006
|
}
|
|
10750
11007
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -10766,6 +11023,14 @@ var MmtpRouter = class {
|
|
|
10766
11023
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
10767
11024
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
10768
11025
|
}
|
|
11026
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
11027
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
11028
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
11029
|
+
this.#restampedRecoveredSeqs++;
|
|
11030
|
+
} else if (baseSsId !== void 0) {
|
|
11031
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11032
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
11033
|
+
}
|
|
10769
11034
|
}
|
|
10770
11035
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
10771
11036
|
inst.moqBlocks.add(blockStart);
|
|
@@ -10799,11 +11064,16 @@ var MmtpRouter = class {
|
|
|
10799
11064
|
}
|
|
10800
11065
|
/**
|
|
10801
11066
|
* Route an overlay-recovered block's packets as source media of the
|
|
10802
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
10803
|
-
*
|
|
10804
|
-
*
|
|
10805
|
-
*
|
|
10806
|
-
*
|
|
11067
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
11068
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
11069
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
11070
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
11071
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
11072
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
11073
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
11074
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
11075
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
11076
|
+
* header's sequence and any other drops the packet.
|
|
10807
11077
|
*/
|
|
10808
11078
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
10809
11079
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -10823,22 +11093,139 @@ var MmtpRouter = class {
|
|
|
10823
11093
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
10824
11094
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
10825
11095
|
const restored = /* @__PURE__ */ new Set();
|
|
11096
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
11097
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
10826
11098
|
const symbols = result.symbols.flatMap((sym) => {
|
|
10827
|
-
const
|
|
10828
|
-
|
|
10829
|
-
|
|
11099
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
11100
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
11101
|
+
if (anchor !== void 0) {
|
|
11102
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
10830
11103
|
restored.add(packetSequenceNumber);
|
|
10831
|
-
|
|
11104
|
+
const out = { ...sym, packetSequenceNumber };
|
|
11105
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
11106
|
+
if (baseSsId !== void 0) {
|
|
11107
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
11108
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11109
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
11110
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
return [out];
|
|
11114
|
+
}
|
|
11115
|
+
if (!restamped) {
|
|
11116
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11117
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
11118
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
11119
|
+
if (baseSsId !== void 0) {
|
|
11120
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
11121
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
11122
|
+
} else {
|
|
11123
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
11124
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
11125
|
+
}
|
|
11126
|
+
return [sym];
|
|
10832
11127
|
}
|
|
10833
|
-
if (!restamped) return [sym];
|
|
10834
11128
|
this.#overlayStats.unsequencedDropped++;
|
|
10835
11129
|
return [];
|
|
10836
11130
|
});
|
|
10837
11131
|
this.#overlayStats.recovered++;
|
|
11132
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
11133
|
+
for (const sym of symbols) {
|
|
11134
|
+
const ssId = baseSsIds.get(sym.index);
|
|
11135
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
11136
|
+
const first = mpuFirst.get(mpu);
|
|
11137
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
11138
|
+
}
|
|
11139
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
11140
|
+
for (const sym of symbols) {
|
|
11141
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
11142
|
+
}
|
|
10838
11143
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
10839
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
11144
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
11145
|
+
baseSsIds,
|
|
11146
|
+
firstSsIds
|
|
10840
11147
|
});
|
|
10841
11148
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
11149
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
11150
|
+
}
|
|
11151
|
+
/**
|
|
11152
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
11153
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
11154
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
11155
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
11156
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
11157
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
11158
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
11159
|
+
* A base block no repair has stated yet takes it when one does
|
|
11160
|
+
* (#feedPendingRebuilt).
|
|
11161
|
+
*
|
|
11162
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
11163
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
11164
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
11165
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
11166
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
11167
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
11168
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
11169
|
+
* block's own arrivals cannot show.
|
|
11170
|
+
*/
|
|
11171
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
11172
|
+
const fec = this.#fecManager;
|
|
11173
|
+
const owner = inst.leg.sourcePacketId;
|
|
11174
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
11175
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
11176
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
11177
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
11178
|
+
const feeds = [];
|
|
11179
|
+
for (const sym of symbols) {
|
|
11180
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
11181
|
+
const ssId = baseSsIds.get(sym.index);
|
|
11182
|
+
if (ssId === void 0) {
|
|
11183
|
+
this.#overlayStats.baseUndetermined++;
|
|
11184
|
+
continue;
|
|
11185
|
+
}
|
|
11186
|
+
routed.delete(ssId);
|
|
11187
|
+
routed.add(ssId);
|
|
11188
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
11189
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
11190
|
+
}
|
|
11191
|
+
for (const [ssId, symbolData] of feeds) {
|
|
11192
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
11193
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
11194
|
+
}
|
|
11195
|
+
}
|
|
11196
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
11197
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
11198
|
+
const fec = this.#fecManager;
|
|
11199
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
11200
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
11201
|
+
if (coords == null) {
|
|
11202
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
11203
|
+
pending.delete(ssId);
|
|
11204
|
+
pending.set(ssId, symbolData.slice());
|
|
11205
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
11206
|
+
return;
|
|
11207
|
+
}
|
|
11208
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
11209
|
+
if (!fed.taken) return;
|
|
11210
|
+
this.#overlayStats.baseFed++;
|
|
11211
|
+
if (fed.recovered === null) return;
|
|
11212
|
+
const sbn = coords.sbn;
|
|
11213
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
11214
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
11215
|
+
fec.countRecovery(owner, sbn, name);
|
|
11216
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
11217
|
+
});
|
|
11218
|
+
}
|
|
11219
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
11220
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
11221
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
11222
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
11223
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
11224
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
11225
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
11226
|
+
pending.delete(ssId);
|
|
11227
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
11228
|
+
}
|
|
10842
11229
|
}
|
|
10843
11230
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
10844
11231
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -10978,6 +11365,10 @@ var MmtpRouter = class {
|
|
|
10978
11365
|
get unadmittedListedDropCount() {
|
|
10979
11366
|
return this.#unadmittedListedDrops;
|
|
10980
11367
|
}
|
|
11368
|
+
/** 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. */
|
|
11369
|
+
get admittedSourceDatagramCount() {
|
|
11370
|
+
return this.#admittedSourceDatagrams;
|
|
11371
|
+
}
|
|
10981
11372
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
10982
11373
|
get unlistedRepairSetAsideCount() {
|
|
10983
11374
|
return this.#unlistedRepairSetAside;
|
|
@@ -11464,6 +11855,8 @@ var MmtpRouter = class {
|
|
|
11464
11855
|
)) return;
|
|
11465
11856
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
11466
11857
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
11858
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
11859
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
11467
11860
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
11468
11861
|
const recovered = fec.feedRepairSymbol(
|
|
11469
11862
|
ownerPacketId,
|
|
@@ -11514,7 +11907,7 @@ var MmtpRouter = class {
|
|
|
11514
11907
|
catalogTrackName
|
|
11515
11908
|
)) continue;
|
|
11516
11909
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
11517
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
11910
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
11518
11911
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
11519
11912
|
}
|
|
11520
11913
|
if (payload.oversized) continue;
|
|
@@ -11601,6 +11994,8 @@ var MmtpRouter = class {
|
|
|
11601
11994
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
11602
11995
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
11603
11996
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
11997
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
11998
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
11604
11999
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
11605
12000
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
11606
12001
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -11727,6 +12122,12 @@ var MmtpRouter = class {
|
|
|
11727
12122
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
11728
12123
|
* own RAP cadence, so no timer is involved.
|
|
11729
12124
|
*
|
|
12125
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
12126
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
12127
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
12128
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
12129
|
+
* frames of the outgoing stream.
|
|
12130
|
+
*
|
|
11730
12131
|
* The commit is one synchronous transition inside the frame path: the
|
|
11731
12132
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
11732
12133
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -11777,6 +12178,9 @@ var MmtpRouter = class {
|
|
|
11777
12178
|
droppedIncomingFrames: 0
|
|
11778
12179
|
};
|
|
11779
12180
|
this.#staged = state;
|
|
12181
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
12182
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
12183
|
+
}
|
|
11780
12184
|
return {
|
|
11781
12185
|
cancel: () => {
|
|
11782
12186
|
if (this.#staged !== state) return false;
|
|
@@ -11859,7 +12263,17 @@ var MmtpRouter = class {
|
|
|
11859
12263
|
*/
|
|
11860
12264
|
#routeParsedFrame(data, meta) {
|
|
11861
12265
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
11862
|
-
if (this.#staged !== null
|
|
12266
|
+
if (this.#staged !== null) {
|
|
12267
|
+
let consumed = false;
|
|
12268
|
+
this.#fecPokeDepth++;
|
|
12269
|
+
try {
|
|
12270
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
12271
|
+
} finally {
|
|
12272
|
+
this.#fecPokeDepth--;
|
|
12273
|
+
this.#flushPendingFecPokes();
|
|
12274
|
+
}
|
|
12275
|
+
if (consumed) return;
|
|
12276
|
+
}
|
|
11863
12277
|
this.#deliverParsedFrame(data, meta);
|
|
11864
12278
|
}
|
|
11865
12279
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -12011,12 +12425,16 @@ var MmtpRouter = class {
|
|
|
12011
12425
|
this.#parser.deferFecRelease(work);
|
|
12012
12426
|
} finally {
|
|
12013
12427
|
this.#fecPokeDepth--;
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12428
|
+
this.#flushPendingFecPokes();
|
|
12429
|
+
}
|
|
12430
|
+
}
|
|
12431
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
12432
|
+
#flushPendingFecPokes() {
|
|
12433
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
12434
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
12435
|
+
this.#pendingFecPokes.clear();
|
|
12436
|
+
for (const poke of pokes) {
|
|
12437
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
12020
12438
|
}
|
|
12021
12439
|
}
|
|
12022
12440
|
}
|
|
@@ -12094,6 +12512,14 @@ var MmtpRouter = class {
|
|
|
12094
12512
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
12095
12513
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
12096
12514
|
},
|
|
12515
|
+
// A staged switch commits on when each side's frames reach it
|
|
12516
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
12517
|
+
// they did unanchored.
|
|
12518
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
12519
|
+
const staged = this.#staged;
|
|
12520
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
12521
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
12522
|
+
},
|
|
12097
12523
|
onReleased: (data, meta) => {
|
|
12098
12524
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
12099
12525
|
const timing = meta.authoredTiming;
|
|
@@ -12235,6 +12661,7 @@ var MmtpRouter = class {
|
|
|
12235
12661
|
const parsed = this.#parser.parsePacket(packet);
|
|
12236
12662
|
if (!parsed) return;
|
|
12237
12663
|
const info = parsed.info;
|
|
12664
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
12238
12665
|
if (source === "multicast" && !manifestAdmitted) {
|
|
12239
12666
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
12240
12667
|
}
|
|
@@ -12516,7 +12943,8 @@ var MmtpRouter = class {
|
|
|
12516
12943
|
parsed.header,
|
|
12517
12944
|
info.sequenceNumber,
|
|
12518
12945
|
info.rapFlag,
|
|
12519
|
-
source
|
|
12946
|
+
source,
|
|
12947
|
+
fecPayloadId?.ssId
|
|
12520
12948
|
);
|
|
12521
12949
|
}
|
|
12522
12950
|
}
|
|
@@ -12542,7 +12970,8 @@ var MmtpRouter = class {
|
|
|
12542
12970
|
source: parserOptions?.source,
|
|
12543
12971
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
12544
12972
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
12545
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
12973
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
12974
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
12546
12975
|
};
|
|
12547
12976
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
12548
12977
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -12666,7 +13095,8 @@ var MmtpRouter = class {
|
|
|
12666
13095
|
if (stream === void 0) {
|
|
12667
13096
|
stream = {
|
|
12668
13097
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
12669
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
13098
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
13099
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
12670
13100
|
};
|
|
12671
13101
|
byName.set(catalogTrackName, stream);
|
|
12672
13102
|
}
|
|
@@ -12781,8 +13211,16 @@ var MmtpRouter = class {
|
|
|
12781
13211
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
12782
13212
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
12783
13213
|
}
|
|
12784
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
13214
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
12785
13215
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
13216
|
+
if (this.#isFecTrack(packetId)) {
|
|
13217
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
13218
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
13219
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
13220
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
13221
|
+
return;
|
|
13222
|
+
}
|
|
13223
|
+
}
|
|
12786
13224
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
12787
13225
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
12788
13226
|
this.#drainDirectRecoveredVideo(
|
|
@@ -12936,6 +13374,13 @@ var MmtpRouter = class {
|
|
|
12936
13374
|
* frames by epoch only if pre-protection input ever matters.
|
|
12937
13375
|
*/
|
|
12938
13376
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
13377
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
13378
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
13379
|
+
result = {
|
|
13380
|
+
...result,
|
|
13381
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
13382
|
+
};
|
|
13383
|
+
}
|
|
12939
13384
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
12940
13385
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
12941
13386
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -12992,7 +13437,12 @@ var MmtpRouter = class {
|
|
|
12992
13437
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
12993
13438
|
if (ownerPacketId == null) return;
|
|
12994
13439
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
12995
|
-
const fecSbnOf = (index) =>
|
|
13440
|
+
const fecSbnOf = (index) => {
|
|
13441
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
13442
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
13443
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
13444
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
13445
|
+
};
|
|
12996
13446
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
12997
13447
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
12998
13448
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -13000,7 +13450,10 @@ var MmtpRouter = class {
|
|
|
13000
13450
|
...result,
|
|
13001
13451
|
symbols: result.symbols.map((sym) => {
|
|
13002
13452
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
13003
|
-
if (wire === sym.packetSequenceNumber)
|
|
13453
|
+
if (wire === sym.packetSequenceNumber) {
|
|
13454
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
13455
|
+
return sym;
|
|
13456
|
+
}
|
|
13004
13457
|
this.#restampedRecoveredSeqs++;
|
|
13005
13458
|
this.#restampedStreams.add(streamKey);
|
|
13006
13459
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -13036,6 +13489,29 @@ var MmtpRouter = class {
|
|
|
13036
13489
|
}
|
|
13037
13490
|
);
|
|
13038
13491
|
}
|
|
13492
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
13493
|
+
ownerPacketId,
|
|
13494
|
+
sym.fragment.mpuSequenceNumber,
|
|
13495
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
13496
|
+
sym.fragment.sampleNumber,
|
|
13497
|
+
catalogTrackName
|
|
13498
|
+
);
|
|
13499
|
+
for (const sym of result.symbols) {
|
|
13500
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
13501
|
+
this.#feedViaParser(
|
|
13502
|
+
sym.rawSymbol,
|
|
13503
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
13504
|
+
void 0,
|
|
13505
|
+
{
|
|
13506
|
+
skipFecPayloadId: true,
|
|
13507
|
+
recovered: true,
|
|
13508
|
+
deliveryProvenance,
|
|
13509
|
+
...fecSbnOf(sym.index),
|
|
13510
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
13511
|
+
}
|
|
13512
|
+
);
|
|
13513
|
+
}
|
|
13514
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
13039
13515
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
13040
13516
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
13041
13517
|
if (assembler) {
|