@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/browser-runtime.js
CHANGED
|
@@ -851,6 +851,17 @@ function readRepairFecPayloadIdFields(mmtpPacket, parsedHeader) {
|
|
|
851
851
|
dataOffset: off + REPAIR_FEC_PID_SIZE
|
|
852
852
|
};
|
|
853
853
|
}
|
|
854
|
+
function extractRepairFecPayloadId(mmtpPacket) {
|
|
855
|
+
const fields = readRepairFecPayloadIdFields(mmtpPacket);
|
|
856
|
+
if (!fields)
|
|
857
|
+
return null;
|
|
858
|
+
return {
|
|
859
|
+
ssStart: fields.ssStart,
|
|
860
|
+
rsbLength: fields.rsbLength,
|
|
861
|
+
rsId: fields.rsId,
|
|
862
|
+
ssbLength: fields.ssbLength
|
|
863
|
+
};
|
|
864
|
+
}
|
|
854
865
|
function parseMoqRepairFrame(frame) {
|
|
855
866
|
return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame));
|
|
856
867
|
}
|
|
@@ -7988,8 +7999,8 @@ var MulticastSubscriptionPool = class {
|
|
|
7988
7999
|
record.claims--;
|
|
7989
8000
|
return this.acquire(gateway, config, consumer);
|
|
7990
8001
|
}
|
|
7991
|
-
const
|
|
7992
|
-
entry.consumers.set(
|
|
8002
|
+
const token2 = Symbol(key);
|
|
8003
|
+
entry.consumers.set(token2, consumer);
|
|
7993
8004
|
let released = false;
|
|
7994
8005
|
return {
|
|
7995
8006
|
key,
|
|
@@ -7997,7 +8008,7 @@ var MulticastSubscriptionPool = class {
|
|
|
7997
8008
|
if (released)
|
|
7998
8009
|
return;
|
|
7999
8010
|
released = true;
|
|
8000
|
-
entry.consumers.delete(
|
|
8011
|
+
entry.consumers.delete(token2);
|
|
8001
8012
|
record.claims--;
|
|
8002
8013
|
if (record.claims > 0)
|
|
8003
8014
|
return;
|
|
@@ -8625,9 +8636,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
|
8625
8636
|
} catch (error) {
|
|
8626
8637
|
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8627
8638
|
}
|
|
8628
|
-
if (
|
|
8639
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8629
8640
|
await rx.close();
|
|
8630
|
-
throw unsupported(`integrity scheme ${String(
|
|
8641
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8631
8642
|
}
|
|
8632
8643
|
const events = new EventTarget();
|
|
8633
8644
|
let state = "connected";
|
|
@@ -8746,14 +8757,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8746
8757
|
});
|
|
8747
8758
|
|
|
8748
8759
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
8760
|
+
var MAX_PENDING_PACKETS = 256;
|
|
8749
8761
|
var AMTGatewayManager = class {
|
|
8750
8762
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8751
8763
|
messageHandler;
|
|
8764
|
+
messageListenerRegistered = false;
|
|
8752
8765
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
8753
8766
|
constructor() {
|
|
8754
8767
|
this.messageHandler = (event) => {
|
|
8755
8768
|
const data = event.data;
|
|
8756
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8757
8769
|
if (data.type === "UDP_PACKET") {
|
|
8758
8770
|
this.handlePacket(data);
|
|
8759
8771
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8762,12 +8774,22 @@ var AMTGatewayManager = class {
|
|
|
8762
8774
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
8763
8775
|
}
|
|
8764
8776
|
};
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
console.warn("[AMT] Service worker not available");
|
|
8777
|
+
}
|
|
8778
|
+
ensureMessageListener() {
|
|
8779
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
8780
|
+
return;
|
|
8770
8781
|
}
|
|
8782
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
8783
|
+
this.messageListenerRegistered = true;
|
|
8784
|
+
console.log("[AMT] Registered service worker message handler");
|
|
8785
|
+
}
|
|
8786
|
+
removeMessageListener() {
|
|
8787
|
+
if (!this.messageListenerRegistered)
|
|
8788
|
+
return;
|
|
8789
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8790
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8791
|
+
}
|
|
8792
|
+
this.messageListenerRegistered = false;
|
|
8771
8793
|
}
|
|
8772
8794
|
/**
|
|
8773
8795
|
* Subscribe via AMT tunnel
|
|
@@ -8777,6 +8799,7 @@ var AMTGatewayManager = class {
|
|
|
8777
8799
|
if (config.protected) {
|
|
8778
8800
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
8779
8801
|
}
|
|
8802
|
+
this.ensureMessageListener();
|
|
8780
8803
|
const keyStr = this.getKeyString(key);
|
|
8781
8804
|
if (this.subscriptions.has(keyStr)) {
|
|
8782
8805
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -8812,6 +8835,8 @@ var AMTGatewayManager = class {
|
|
|
8812
8835
|
this.subscriptions.delete(keyStr);
|
|
8813
8836
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8814
8837
|
}
|
|
8838
|
+
if (this.subscriptions.size === 0)
|
|
8839
|
+
this.removeMessageListener();
|
|
8815
8840
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8816
8841
|
}
|
|
8817
8842
|
}
|
|
@@ -8833,6 +8858,8 @@ var AMTGatewayManager = class {
|
|
|
8833
8858
|
} finally {
|
|
8834
8859
|
this.subscriptions.delete(handle);
|
|
8835
8860
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8861
|
+
if (this.subscriptions.size === 0)
|
|
8862
|
+
this.removeMessageListener();
|
|
8836
8863
|
}
|
|
8837
8864
|
}
|
|
8838
8865
|
/**
|
|
@@ -8849,14 +8876,12 @@ var AMTGatewayManager = class {
|
|
|
8849
8876
|
if (!subscriptionId)
|
|
8850
8877
|
return;
|
|
8851
8878
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8852
|
-
if (!subscription)
|
|
8853
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8879
|
+
if (!subscription)
|
|
8854
8880
|
return;
|
|
8855
|
-
}
|
|
8856
8881
|
if (!subscription.handshakeComplete) {
|
|
8857
8882
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8858
8883
|
const packetData2 = data;
|
|
8859
|
-
if (queue) {
|
|
8884
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8860
8885
|
queue.push({
|
|
8861
8886
|
data: packetData2.data,
|
|
8862
8887
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8882,19 +8907,15 @@ var AMTGatewayManager = class {
|
|
|
8882
8907
|
if (!subscriptionId)
|
|
8883
8908
|
return;
|
|
8884
8909
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8885
|
-
if (!subscription)
|
|
8886
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8910
|
+
if (!subscription)
|
|
8887
8911
|
return;
|
|
8888
|
-
}
|
|
8889
8912
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8890
8913
|
}
|
|
8891
8914
|
/**
|
|
8892
8915
|
* Cleanup on destroy
|
|
8893
8916
|
*/
|
|
8894
8917
|
destroy() {
|
|
8895
|
-
|
|
8896
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8897
|
-
}
|
|
8918
|
+
this.removeMessageListener();
|
|
8898
8919
|
const handles = Array.from(this.subscriptions.keys());
|
|
8899
8920
|
for (const handle of handles) {
|
|
8900
8921
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -12425,6 +12446,7 @@ var TrackSession = class {
|
|
|
12425
12446
|
this.#state = "idle";
|
|
12426
12447
|
this.#subscription = null;
|
|
12427
12448
|
this.#disarmFec();
|
|
12449
|
+
this.#unsuppress();
|
|
12428
12450
|
}
|
|
12429
12451
|
// -----------------------------------------------------------------------
|
|
12430
12452
|
// Internal
|
|
@@ -12918,6 +12940,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12918
12940
|
this.#heldEvicted = 0;
|
|
12919
12941
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
12920
12942
|
this.#holes = [];
|
|
12943
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
12944
|
+
this.#expired = [];
|
|
12945
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
12946
|
+
this.#newlyExpired = [];
|
|
12947
|
+
/**
|
|
12948
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
12949
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
12950
|
+
*/
|
|
12951
|
+
this.#closedBehind = [];
|
|
12921
12952
|
/**
|
|
12922
12953
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
12923
12954
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -12939,7 +12970,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12939
12970
|
#retentionBlocks;
|
|
12940
12971
|
#heldEvicted;
|
|
12941
12972
|
#newest;
|
|
12973
|
+
#newestSource;
|
|
12942
12974
|
#holes;
|
|
12975
|
+
#expired;
|
|
12976
|
+
#newlyExpired;
|
|
12977
|
+
#closedBehind;
|
|
12943
12978
|
static #checkedRetention(retentionBlocks) {
|
|
12944
12979
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
12945
12980
|
throw new Error(
|
|
@@ -12976,6 +13011,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12976
13011
|
observe(position) {
|
|
12977
13012
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12978
13013
|
}
|
|
13014
|
+
/**
|
|
13015
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
13016
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
13017
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
13018
|
+
* reordering.
|
|
13019
|
+
*/
|
|
13020
|
+
get newestSource() {
|
|
13021
|
+
return this.#newestSource;
|
|
13022
|
+
}
|
|
13023
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
13024
|
+
noteSource(ssId) {
|
|
13025
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
13026
|
+
this.#newestSource = ssId;
|
|
13027
|
+
return true;
|
|
13028
|
+
}
|
|
12979
13029
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12980
13030
|
get heldEvicted() {
|
|
12981
13031
|
return this.#heldEvicted;
|
|
@@ -12994,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12994
13044
|
}
|
|
12995
13045
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
12996
13046
|
locate(ssId) {
|
|
12997
|
-
for (
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13047
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
13048
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
13049
|
+
const block = blocks[i];
|
|
13050
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
13051
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
13052
|
+
}
|
|
13001
13053
|
}
|
|
13002
13054
|
}
|
|
13003
13055
|
return null;
|
|
13004
13056
|
}
|
|
13005
|
-
/** Number of pending
|
|
13057
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
13006
13058
|
get holeCount() {
|
|
13007
|
-
return this.#holes.length;
|
|
13059
|
+
return this.#holes.length + this.#expired.length;
|
|
13060
|
+
}
|
|
13061
|
+
/**
|
|
13062
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
13063
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
13064
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
13065
|
+
* release gate does not wait for it. Its owner must close it
|
|
13066
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
13067
|
+
*/
|
|
13068
|
+
takeExpiredHoles() {
|
|
13069
|
+
return this.#newlyExpired.splice(0);
|
|
13008
13070
|
}
|
|
13009
13071
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
13010
13072
|
blockEndingAt(end) {
|
|
@@ -13060,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13060
13122
|
/**
|
|
13061
13123
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
13062
13124
|
* block states its range and places every held source in it; later repair
|
|
13063
|
-
* symbols of the same block change nothing. A range
|
|
13064
|
-
*
|
|
13065
|
-
*
|
|
13125
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
13126
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
13127
|
+
* a known block (a different length, or an overlap with another block or
|
|
13128
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
13129
|
+
* block changes.
|
|
13066
13130
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
13067
13131
|
* A block so far past `newest` that stating it would retire the position
|
|
13068
13132
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -13081,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13081
13145
|
const known = this.blockAt(start);
|
|
13082
13146
|
if (known !== void 0) {
|
|
13083
13147
|
if (known.span) {
|
|
13084
|
-
|
|
13148
|
+
if (known.length === length) {
|
|
13149
|
+
const block2 = { start, length, span: false };
|
|
13150
|
+
this.#blocks[this.#blocks.indexOf(known)] = block2;
|
|
13151
|
+
return { status: "placed", block: block2, placed: [] };
|
|
13152
|
+
}
|
|
13153
|
+
return length < known.length ? {
|
|
13154
|
+
status: "overlap",
|
|
13155
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
13156
|
+
ofSpan: known
|
|
13157
|
+
} : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
|
|
13085
13158
|
}
|
|
13086
13159
|
if (known.length !== length) {
|
|
13087
13160
|
return {
|
|
@@ -13149,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13149
13222
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
13150
13223
|
}
|
|
13151
13224
|
/**
|
|
13152
|
-
* Turn every
|
|
13153
|
-
*
|
|
13154
|
-
*
|
|
13225
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
13226
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
13227
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
13228
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
13229
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
13230
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
13155
13231
|
*/
|
|
13156
13232
|
closeHolesWithin(start, length) {
|
|
13157
13233
|
const closed = [];
|
|
13158
|
-
for (const
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13234
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
13235
|
+
for (const hole of [...holes]) {
|
|
13236
|
+
if (!isoBlockContains(start, length, hole.start)) continue;
|
|
13237
|
+
holes.splice(holes.indexOf(hole), 1);
|
|
13238
|
+
const spanBlock = { start: hole.start, length: hole.length, span: true };
|
|
13239
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
|
|
13240
|
+
if (holes === this.#holes) {
|
|
13241
|
+
this.#blocks.push(spanBlock);
|
|
13242
|
+
} else {
|
|
13243
|
+
this.#closedBehind.push(spanBlock);
|
|
13244
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
|
|
13245
|
+
}
|
|
13246
|
+
}
|
|
13164
13247
|
}
|
|
13165
13248
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
13166
13249
|
}
|
|
@@ -13174,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13174
13257
|
return void 0;
|
|
13175
13258
|
}
|
|
13176
13259
|
/**
|
|
13177
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
13178
|
-
* lies more than `retentionBlocks` blocks behind
|
|
13179
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13180
|
-
* horizon uses
|
|
13260
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
13261
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
13262
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13263
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
13264
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
13181
13265
|
*/
|
|
13182
13266
|
retireBehind(position) {
|
|
13183
13267
|
this.observe(position);
|
|
13184
13268
|
const horizon = this.#retentionBlocks;
|
|
13185
13269
|
const retired = [];
|
|
13270
|
+
this.#closedBehind.length = 0;
|
|
13186
13271
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
13187
13272
|
const block = this.#blocks[i];
|
|
13188
13273
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -13194,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13194
13279
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
13195
13280
|
const hole = this.#holes[i];
|
|
13196
13281
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
13197
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
13282
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
|
|
13283
|
+
this.#holes.splice(i, 1);
|
|
13284
|
+
this.#expired.push(hole);
|
|
13285
|
+
this.#newlyExpired.push(hole);
|
|
13198
13286
|
}
|
|
13199
13287
|
for (const ssId of [...this.#held.keys()]) {
|
|
13200
|
-
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#
|
|
13288
|
+
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon && !this.#holes.some((hole) => isoBlockContains(hole.start, hole.length, ssId)) && !this.#expired.some((hole) => isoBlockContains(hole.start, hole.length, ssId))) {
|
|
13289
|
+
this.#held.delete(ssId);
|
|
13290
|
+
}
|
|
13201
13291
|
}
|
|
13202
13292
|
return retired;
|
|
13203
13293
|
}
|
|
@@ -13205,9 +13295,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13205
13295
|
reset() {
|
|
13206
13296
|
this.#blocks.length = 0;
|
|
13207
13297
|
this.#holes.length = 0;
|
|
13298
|
+
this.#expired.length = 0;
|
|
13299
|
+
this.#newlyExpired.length = 0;
|
|
13300
|
+
this.#closedBehind.length = 0;
|
|
13208
13301
|
this.#held.clear();
|
|
13209
13302
|
this.#coverageEnd = void 0;
|
|
13210
13303
|
this.#newest = void 0;
|
|
13304
|
+
this.#newestSource = void 0;
|
|
13211
13305
|
}
|
|
13212
13306
|
#claim(block) {
|
|
13213
13307
|
const placed = [];
|
|
@@ -13827,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13827
13921
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13828
13922
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13829
13923
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13924
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13830
13925
|
function isMulticastSymbolDelivery(value) {
|
|
13831
13926
|
return value === "datagram" || value === "ordered-relay";
|
|
13832
13927
|
}
|
|
@@ -13920,10 +14015,18 @@ var FECManager = class _FECManager {
|
|
|
13920
14015
|
// 'pending' forever, which would park every later frame of its stream in
|
|
13921
14016
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
13922
14017
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
14018
|
+
/**
|
|
14019
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
14020
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
14021
|
+
*/
|
|
13923
14022
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
13924
14023
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13925
14024
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13926
14025
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
14026
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
14027
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
14028
|
+
// not network arrivals and never make a block clean.
|
|
14029
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13927
14030
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13928
14031
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13929
14032
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -13932,6 +14035,9 @@ var FECManager = class _FECManager {
|
|
|
13932
14035
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
13933
14036
|
// symbols; waiting for K fails every one of them.
|
|
13934
14037
|
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
14038
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
14039
|
+
// a repair states the block there or the span resolves.
|
|
14040
|
+
this.#isoSpanKeys = /* @__PURE__ */ new Set();
|
|
13935
14041
|
this.#ssbLengthConflictCount = 0;
|
|
13936
14042
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
13937
14043
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -14052,6 +14158,10 @@ var FECManager = class _FECManager {
|
|
|
14052
14158
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
14053
14159
|
// One classifier per physical stream, built from that stream's geometry.
|
|
14054
14160
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
14161
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
14162
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
14163
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
14164
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
14055
14165
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
14056
14166
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
14057
14167
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -14125,7 +14235,10 @@ var FECManager = class _FECManager {
|
|
|
14125
14235
|
// arrives via onBlockRecovered callback.
|
|
14126
14236
|
this.onBlockStarted = null;
|
|
14127
14237
|
this.onBlockComplete = null;
|
|
14128
|
-
/**
|
|
14238
|
+
/**
|
|
14239
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
14240
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
14241
|
+
*/
|
|
14129
14242
|
this.onBlockFailed = null;
|
|
14130
14243
|
/**
|
|
14131
14244
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -14167,6 +14280,8 @@ var FECManager = class _FECManager {
|
|
|
14167
14280
|
* for it, which it must be.
|
|
14168
14281
|
*/
|
|
14169
14282
|
this.#onUndelimitedLoss = null;
|
|
14283
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
14284
|
+
this.#onIsoHoleExpired = null;
|
|
14170
14285
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
14171
14286
|
this.onBlockVanished = null;
|
|
14172
14287
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -14209,7 +14324,9 @@ var FECManager = class _FECManager {
|
|
|
14209
14324
|
#pendingWorkerCleanupBounds;
|
|
14210
14325
|
#blockSourceCounts;
|
|
14211
14326
|
#blockSourceEsis;
|
|
14327
|
+
#blockRebuiltEsis;
|
|
14212
14328
|
#blockAuthoredLengths;
|
|
14329
|
+
#isoSpanKeys;
|
|
14213
14330
|
#ssbLengthConflictCount;
|
|
14214
14331
|
#blockRepairRsIds;
|
|
14215
14332
|
#blockRsbLengths;
|
|
@@ -14251,6 +14368,7 @@ var FECManager = class _FECManager {
|
|
|
14251
14368
|
#orderedFrontiers;
|
|
14252
14369
|
#orderedPendingBlocks;
|
|
14253
14370
|
#classifiers;
|
|
14371
|
+
#noSpanRaps;
|
|
14254
14372
|
#streamGeometries;
|
|
14255
14373
|
#failedBlocks;
|
|
14256
14374
|
#resolvedBlockClasses;
|
|
@@ -14275,6 +14393,7 @@ var FECManager = class _FECManager {
|
|
|
14275
14393
|
#decoderUnavailableEvents;
|
|
14276
14394
|
#repairSymbolSizeMismatchCount;
|
|
14277
14395
|
#onUndelimitedLoss;
|
|
14396
|
+
#onIsoHoleExpired;
|
|
14278
14397
|
#encodeTrackName(catalogTrackName) {
|
|
14279
14398
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
14280
14399
|
if (encoded === void 0) {
|
|
@@ -14746,7 +14865,10 @@ var FECManager = class _FECManager {
|
|
|
14746
14865
|
timer.handle = null;
|
|
14747
14866
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
14748
14867
|
};
|
|
14749
|
-
timer.handle = setTimeout(
|
|
14868
|
+
timer.handle = setTimeout(
|
|
14869
|
+
onFire,
|
|
14870
|
+
this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
|
|
14871
|
+
);
|
|
14750
14872
|
this.#repairDeadlines.set(key, timer);
|
|
14751
14873
|
}
|
|
14752
14874
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -14890,8 +15012,14 @@ var FECManager = class _FECManager {
|
|
|
14890
15012
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
14891
15013
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
14892
15014
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
15015
|
+
*
|
|
15016
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
15017
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
15018
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
15019
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
15020
|
+
* it (BLO-52085).
|
|
14893
15021
|
*/
|
|
14894
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
15022
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
14895
15023
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
14896
15024
|
const now = performance.now();
|
|
14897
15025
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -14912,6 +15040,7 @@ var FECManager = class _FECManager {
|
|
|
14912
15040
|
};
|
|
14913
15041
|
const discontinuity = () => {
|
|
14914
15042
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
15043
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14915
15044
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14916
15045
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14917
15046
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -14929,7 +15058,7 @@ var FECManager = class _FECManager {
|
|
|
14929
15058
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
14930
15059
|
};
|
|
14931
15060
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
14932
|
-
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
15061
|
+
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
14933
15062
|
discontinuity();
|
|
14934
15063
|
return;
|
|
14935
15064
|
}
|
|
@@ -15191,17 +15320,21 @@ var FECManager = class _FECManager {
|
|
|
15191
15320
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
15192
15321
|
this.#repairHoldUntilMs.delete(key);
|
|
15193
15322
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
15194
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
15323
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
15195
15324
|
};
|
|
15196
|
-
timer.handle = setTimeout(
|
|
15325
|
+
timer.handle = setTimeout(
|
|
15326
|
+
onFire,
|
|
15327
|
+
this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
|
|
15328
|
+
);
|
|
15197
15329
|
this.#activeBlocks.set(key, timer);
|
|
15198
15330
|
this.#indexBlockKey(key, sbn, packetId);
|
|
15199
15331
|
}
|
|
15200
15332
|
/**
|
|
15201
15333
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
15202
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
15334
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
15335
|
+
* is when it was due: its deadline, or now.
|
|
15203
15336
|
*/
|
|
15204
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
15337
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
15205
15338
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
15206
15339
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
15207
15340
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -15217,7 +15350,7 @@ var FECManager = class _FECManager {
|
|
|
15217
15350
|
if (workerClient) {
|
|
15218
15351
|
this.#pendingWorkerCleanups.add(key);
|
|
15219
15352
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
15220
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
15353
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
15221
15354
|
workerClient.cleanup(sbn);
|
|
15222
15355
|
} else {
|
|
15223
15356
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -15324,6 +15457,9 @@ var FECManager = class _FECManager {
|
|
|
15324
15457
|
}
|
|
15325
15458
|
if (policyChanged) {
|
|
15326
15459
|
this.#bumpEpoch(packetId);
|
|
15460
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15461
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15462
|
+
}
|
|
15327
15463
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15328
15464
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15329
15465
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15526,7 +15662,9 @@ var FECManager = class _FECManager {
|
|
|
15526
15662
|
}
|
|
15527
15663
|
resetMap(this.#blockSourceCounts);
|
|
15528
15664
|
resetMap(this.#blockSourceEsis);
|
|
15665
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15529
15666
|
resetMap(this.#blockAuthoredLengths);
|
|
15667
|
+
resetSet(this.#isoSpanKeys);
|
|
15530
15668
|
resetMap(this.#blockRepairRsIds);
|
|
15531
15669
|
resetMap(this.#blockRsbLengths);
|
|
15532
15670
|
resetMap(this.#heldLayerStates);
|
|
@@ -15559,6 +15697,7 @@ var FECManager = class _FECManager {
|
|
|
15559
15697
|
resetMap(this.#isoLocators);
|
|
15560
15698
|
resetMap(this.#sourceAnchors);
|
|
15561
15699
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15700
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15562
15701
|
this.#bumpEpoch(packetId);
|
|
15563
15702
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15564
15703
|
for (const key of keys.keys()) {
|
|
@@ -15649,7 +15788,9 @@ var FECManager = class _FECManager {
|
|
|
15649
15788
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15650
15789
|
dropMap(this.#blockSourceCounts);
|
|
15651
15790
|
dropMap(this.#blockSourceEsis);
|
|
15791
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15652
15792
|
dropMap(this.#blockAuthoredLengths);
|
|
15793
|
+
dropSet(this.#isoSpanKeys);
|
|
15653
15794
|
dropMap(this.#blockRepairRsIds);
|
|
15654
15795
|
dropMap(this.#blockRsbLengths);
|
|
15655
15796
|
dropMap(this.#blockPathCounts);
|
|
@@ -15665,6 +15806,7 @@ var FECManager = class _FECManager {
|
|
|
15665
15806
|
dropMap(this.#isoLocators);
|
|
15666
15807
|
dropMap(this.#sourceAnchors);
|
|
15667
15808
|
dropMap(this.#classifiers);
|
|
15809
|
+
dropMap(this.#noSpanRaps);
|
|
15668
15810
|
dropMap(this.#streamGeometries);
|
|
15669
15811
|
dropSet(this.#pendingWorkerCleanups);
|
|
15670
15812
|
dropSet(this.#completedBlocks);
|
|
@@ -15810,6 +15952,9 @@ var FECManager = class _FECManager {
|
|
|
15810
15952
|
this.#unindexBlockKey(key);
|
|
15811
15953
|
}
|
|
15812
15954
|
}
|
|
15955
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15956
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15957
|
+
}
|
|
15813
15958
|
return true;
|
|
15814
15959
|
}
|
|
15815
15960
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -15988,8 +16133,9 @@ var FECManager = class _FECManager {
|
|
|
15988
16133
|
return true;
|
|
15989
16134
|
}
|
|
15990
16135
|
/**
|
|
15991
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
15992
|
-
*
|
|
16136
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
16137
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
16138
|
+
* multicast wait, which may already have passed.
|
|
15993
16139
|
*/
|
|
15994
16140
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
15995
16141
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -16127,6 +16273,8 @@ var FECManager = class _FECManager {
|
|
|
16127
16273
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16128
16274
|
const evictedBefore = locator.heldEvicted;
|
|
16129
16275
|
const held = locator.hold(ssId, source);
|
|
16276
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16277
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16130
16278
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16131
16279
|
if (evicted > 0) {
|
|
16132
16280
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16147,17 +16295,20 @@ var FECManager = class _FECManager {
|
|
|
16147
16295
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
16148
16296
|
*/
|
|
16149
16297
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
16150
|
-
const
|
|
16298
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16299
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
16300
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16151
16301
|
if (placement.status !== "placed") {
|
|
16152
16302
|
this.#isoRepairRefusedCount++;
|
|
16153
16303
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
16154
16304
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
16155
16305
|
}
|
|
16306
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
16156
16307
|
return placement;
|
|
16157
16308
|
}
|
|
16309
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
16158
16310
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
16159
16311
|
if (placement.hole !== void 0) {
|
|
16160
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
16161
16312
|
const hole = placement.hole.block;
|
|
16162
16313
|
const previous = locator.blockEndingAt(hole.start);
|
|
16163
16314
|
const loss = {
|
|
@@ -16178,17 +16329,29 @@ var FECManager = class _FECManager {
|
|
|
16178
16329
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
16179
16330
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
16180
16331
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
16181
|
-
* nobody to close it.
|
|
16332
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
16333
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
16334
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
16335
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
16182
16336
|
*/
|
|
16183
|
-
setUndelimitedLossHandler(handler) {
|
|
16337
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
16184
16338
|
this.#onUndelimitedLoss = handler;
|
|
16339
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
16185
16340
|
this.#deferIsoGaps = handler !== null;
|
|
16186
16341
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
16187
16342
|
}
|
|
16343
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
16344
|
+
#settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
|
|
16345
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
16346
|
+
const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
|
|
16347
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
|
|
16348
|
+
}
|
|
16349
|
+
}
|
|
16188
16350
|
/**
|
|
16189
|
-
* The range repair for a hole ended: every hole
|
|
16190
|
-
*
|
|
16191
|
-
* with the held sources they placed, for the caller to feed
|
|
16351
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
16352
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
16353
|
+
* spans with the held sources they placed, for the caller to feed
|
|
16354
|
+
* (MmtpRouter).
|
|
16192
16355
|
*/
|
|
16193
16356
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
16194
16357
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -16207,7 +16370,10 @@ var FECManager = class _FECManager {
|
|
|
16207
16370
|
*/
|
|
16208
16371
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
16209
16372
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
16210
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
16373
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
16374
|
+
this.#blockAuthoredLengths.set(key, span.block.length);
|
|
16375
|
+
this.#isoSpanKeys.add(key);
|
|
16376
|
+
}
|
|
16211
16377
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
16212
16378
|
}
|
|
16213
16379
|
/**
|
|
@@ -16295,11 +16461,14 @@ var FECManager = class _FECManager {
|
|
|
16295
16461
|
/**
|
|
16296
16462
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16297
16463
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16298
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
16464
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
16465
|
+
* and neither has a stream without catalog timing until it releases two
|
|
16466
|
+
* RAPs inside one MPU.
|
|
16299
16467
|
*/
|
|
16300
16468
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16301
|
-
|
|
16302
|
-
return this.#
|
|
16469
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16470
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
16471
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
16303
16472
|
}
|
|
16304
16473
|
/** A block's class now, or frozen at its resolution. */
|
|
16305
16474
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16325,7 +16494,14 @@ var FECManager = class _FECManager {
|
|
|
16325
16494
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16326
16495
|
*/
|
|
16327
16496
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16328
|
-
if (!this.#trackConfigs.has(packetId)
|
|
16497
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
16498
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
16499
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16500
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
16501
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
16502
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
16503
|
+
return;
|
|
16504
|
+
}
|
|
16329
16505
|
this.#applyClassifierChanges(
|
|
16330
16506
|
packetId,
|
|
16331
16507
|
catalogTrackName,
|
|
@@ -16538,11 +16714,14 @@ var FECManager = class _FECManager {
|
|
|
16538
16714
|
});
|
|
16539
16715
|
client.onCleanupComplete((sbn) => {
|
|
16540
16716
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
16541
|
-
const
|
|
16717
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
16718
|
+
const cleanupKeys = owed.filter((key) => {
|
|
16542
16719
|
const block = this.#parseBlockKey(key);
|
|
16543
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
16720
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
16544
16721
|
});
|
|
16545
|
-
|
|
16722
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
|
|
16723
|
+
const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
|
|
16724
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
16546
16725
|
});
|
|
16547
16726
|
client.onError((msg, code) => {
|
|
16548
16727
|
const requestedKind = code ?? msg;
|
|
@@ -16612,9 +16791,11 @@ var FECManager = class _FECManager {
|
|
|
16612
16791
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
16613
16792
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
16614
16793
|
*/
|
|
16615
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
16794
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
16616
16795
|
this.#clearOwedCleanupBound(key);
|
|
16617
16796
|
const bound = {
|
|
16797
|
+
dueAtMs,
|
|
16798
|
+
askedAtMs: performance.now(),
|
|
16618
16799
|
handle: setTimeout(() => {
|
|
16619
16800
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
16620
16801
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -16712,7 +16893,15 @@ var FECManager = class _FECManager {
|
|
|
16712
16893
|
this.#isoSourceOversizeCount++;
|
|
16713
16894
|
return null;
|
|
16714
16895
|
}
|
|
16715
|
-
|
|
16896
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16897
|
+
}
|
|
16898
|
+
/**
|
|
16899
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16900
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16901
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16902
|
+
* stated for `ssId`.
|
|
16903
|
+
*/
|
|
16904
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16716
16905
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16717
16906
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16718
16907
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16741,6 +16930,74 @@ var FECManager = class _FECManager {
|
|
|
16741
16930
|
}
|
|
16742
16931
|
return recovered;
|
|
16743
16932
|
}
|
|
16933
|
+
/**
|
|
16934
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16935
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16936
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16937
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16938
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16939
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16940
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16941
|
+
*
|
|
16942
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16943
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16944
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16945
|
+
* feedSourceSymbol takes it.
|
|
16946
|
+
*
|
|
16947
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16948
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16949
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16950
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16951
|
+
* block as clean on its own side), the block is completed here as
|
|
16952
|
+
* recovered, with nothing left to route.
|
|
16953
|
+
*/
|
|
16954
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16955
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16956
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16957
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16958
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16959
|
+
const sbn = located.block.start;
|
|
16960
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16961
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16962
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16963
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16964
|
+
if (rebuilt === void 0) {
|
|
16965
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16966
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16967
|
+
}
|
|
16968
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16969
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16970
|
+
rebuilt.add(located.esi);
|
|
16971
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16972
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
16973
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
16974
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16977
|
+
return { taken: true, recovered };
|
|
16978
|
+
}
|
|
16979
|
+
/**
|
|
16980
|
+
* A block that holds every one of its `length` sources, received or
|
|
16981
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16982
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16983
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16984
|
+
*/
|
|
16985
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16986
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16987
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16988
|
+
let present = rebuilt.size;
|
|
16989
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16990
|
+
if (present < length) return false;
|
|
16991
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16992
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16993
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16994
|
+
if (catalogTrackName === void 0) {
|
|
16995
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16996
|
+
} else {
|
|
16997
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16998
|
+
}
|
|
16999
|
+
return true;
|
|
17000
|
+
}
|
|
16744
17001
|
/**
|
|
16745
17002
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16746
17003
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16943,8 +17200,75 @@ var FECManager = class _FECManager {
|
|
|
16943
17200
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16944
17201
|
return null;
|
|
16945
17202
|
}
|
|
17203
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16946
17204
|
return recovered;
|
|
16947
17205
|
}
|
|
17206
|
+
/**
|
|
17207
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17208
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17209
|
+
* position past a block leaves the sources that block still misses
|
|
17210
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17211
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17212
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17213
|
+
* sent before it.
|
|
17214
|
+
*/
|
|
17215
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17216
|
+
const previous = locator.newestSource;
|
|
17217
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17218
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17219
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17220
|
+
this.#shortenUnrecoverableWait(
|
|
17221
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17222
|
+
block.start,
|
|
17223
|
+
packetId,
|
|
17224
|
+
catalogTrackName
|
|
17225
|
+
);
|
|
17226
|
+
}
|
|
17227
|
+
}
|
|
17228
|
+
/**
|
|
17229
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17230
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17231
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17232
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17233
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17234
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17235
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17236
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17237
|
+
* block.
|
|
17238
|
+
*/
|
|
17239
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17240
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17241
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17242
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17243
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
17244
|
+
const received = this.#blockSourceEsis.get(key);
|
|
17245
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
17246
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
17247
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17248
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17249
|
+
return void 0;
|
|
17250
|
+
}
|
|
17251
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17252
|
+
}
|
|
17253
|
+
/**
|
|
17254
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17255
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17256
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17257
|
+
*/
|
|
17258
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17259
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17260
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17261
|
+
if (config === void 0) return;
|
|
17262
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17263
|
+
if (waitMs === void 0) return;
|
|
17264
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17265
|
+
this.#armBlockTimeout(
|
|
17266
|
+
sbn,
|
|
17267
|
+
packetId,
|
|
17268
|
+
catalogTrackName,
|
|
17269
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17270
|
+
);
|
|
17271
|
+
}
|
|
16948
17272
|
/**
|
|
16949
17273
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16950
17274
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -17042,12 +17366,13 @@ var FECManager = class _FECManager {
|
|
|
17042
17366
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
17043
17367
|
* classification evidence, and marks the block clean when all of its
|
|
17044
17368
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
17045
|
-
* otherwise K.
|
|
17369
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
17370
|
+
* placed only now (see #noteSbnSeen).
|
|
17046
17371
|
*
|
|
17047
17372
|
* Throws for a packetId that was never configured, so a classified block
|
|
17048
17373
|
* never reads the manager-global K or timeout.
|
|
17049
17374
|
*/
|
|
17050
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
17375
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
17051
17376
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
17052
17377
|
if (trackConfig === void 0) {
|
|
17053
17378
|
throw new Error(
|
|
@@ -17063,7 +17388,8 @@ var FECManager = class _FECManager {
|
|
|
17063
17388
|
catalogTrackName,
|
|
17064
17389
|
"source",
|
|
17065
17390
|
evidence.packetSequenceNumber,
|
|
17066
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
17391
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
17392
|
+
held
|
|
17067
17393
|
);
|
|
17068
17394
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
17069
17395
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -17099,6 +17425,15 @@ var FECManager = class _FECManager {
|
|
|
17099
17425
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17100
17426
|
return;
|
|
17101
17427
|
}
|
|
17428
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17429
|
+
key,
|
|
17430
|
+
sbn,
|
|
17431
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17432
|
+
packetId,
|
|
17433
|
+
catalogTrackName
|
|
17434
|
+
)) {
|
|
17435
|
+
return;
|
|
17436
|
+
}
|
|
17102
17437
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17103
17438
|
}
|
|
17104
17439
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17107,7 +17442,9 @@ var FECManager = class _FECManager {
|
|
|
17107
17442
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17108
17443
|
this.#blockSourceCounts.delete(key);
|
|
17109
17444
|
this.#blockSourceEsis.delete(key);
|
|
17445
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17110
17446
|
this.#blockAuthoredLengths.delete(key);
|
|
17447
|
+
this.#isoSpanKeys.delete(key);
|
|
17111
17448
|
this.#blockRepairRsIds.delete(key);
|
|
17112
17449
|
this.#blockRsbLengths.delete(key);
|
|
17113
17450
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -17138,6 +17475,19 @@ var FECManager = class _FECManager {
|
|
|
17138
17475
|
#receiptNow() {
|
|
17139
17476
|
return this.#receiptAtMs ?? performance.now();
|
|
17140
17477
|
}
|
|
17478
|
+
/**
|
|
17479
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
17480
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
17481
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
17482
|
+
* that timer would have, never later than a delay counted from now, and
|
|
17483
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
17484
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
17485
|
+
* from another fraction of a ms (BLO-52085).
|
|
17486
|
+
*/
|
|
17487
|
+
#receiptTimerDelayMs(deadlineMs, minMs) {
|
|
17488
|
+
const receivedAtMs = this.#receiptAtMs;
|
|
17489
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
|
|
17490
|
+
}
|
|
17141
17491
|
/**
|
|
17142
17492
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
17143
17493
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -17173,10 +17523,20 @@ var FECManager = class _FECManager {
|
|
|
17173
17523
|
this.#repairDeadlineMs(trackConfig)
|
|
17174
17524
|
);
|
|
17175
17525
|
}
|
|
17176
|
-
this.#
|
|
17526
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17527
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17528
|
+
return;
|
|
17529
|
+
}
|
|
17530
|
+
this.#armBlockTimeout(
|
|
17531
|
+
sbn,
|
|
17532
|
+
packetId,
|
|
17533
|
+
catalogTrackName,
|
|
17534
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17535
|
+
);
|
|
17177
17536
|
}
|
|
17178
17537
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17179
17538
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
17539
|
+
const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
|
|
17180
17540
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
17181
17541
|
this.#clearRepairDeadline(key);
|
|
17182
17542
|
this.#clearBlockTimer(key);
|
|
@@ -17186,6 +17546,7 @@ var FECManager = class _FECManager {
|
|
|
17186
17546
|
}
|
|
17187
17547
|
this.#blockSourceCounts.delete(key);
|
|
17188
17548
|
this.#blockSourceEsis.delete(key);
|
|
17549
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17189
17550
|
this.#blockAuthoredLengths.delete(key);
|
|
17190
17551
|
this.#blockRepairRsIds.delete(key);
|
|
17191
17552
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17195,7 +17556,8 @@ var FECManager = class _FECManager {
|
|
|
17195
17556
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
17196
17557
|
this.#fecStats.blocksFailed++;
|
|
17197
17558
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
17198
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17559
|
+
if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17560
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
17199
17561
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
17200
17562
|
}
|
|
17201
17563
|
/**
|
|
@@ -17224,7 +17586,9 @@ var FECManager = class _FECManager {
|
|
|
17224
17586
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17225
17587
|
this.#blockSourceCounts.delete(key);
|
|
17226
17588
|
this.#blockSourceEsis.delete(key);
|
|
17589
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17227
17590
|
this.#blockAuthoredLengths.delete(key);
|
|
17591
|
+
this.#isoSpanKeys.delete(key);
|
|
17228
17592
|
this.#blockRepairRsIds.delete(key);
|
|
17229
17593
|
this.#blockRsbLengths.delete(key);
|
|
17230
17594
|
this.#clearBlockTimer(key);
|
|
@@ -17315,7 +17679,9 @@ var FECManager = class _FECManager {
|
|
|
17315
17679
|
this.#blockKeysBySbn.clear();
|
|
17316
17680
|
this.#blockSourceCounts.clear();
|
|
17317
17681
|
this.#blockSourceEsis.clear();
|
|
17682
|
+
this.#blockRebuiltEsis.clear();
|
|
17318
17683
|
this.#blockAuthoredLengths.clear();
|
|
17684
|
+
this.#isoSpanKeys.clear();
|
|
17319
17685
|
this.#blockRepairRsIds.clear();
|
|
17320
17686
|
this.#blockRsbLengths.clear();
|
|
17321
17687
|
this.#heldLayerStates.clear();
|
|
@@ -17340,6 +17706,7 @@ var FECManager = class _FECManager {
|
|
|
17340
17706
|
this.#blockAnchorsMs.clear();
|
|
17341
17707
|
this.#blockCatalogTrackNames.clear();
|
|
17342
17708
|
this.#classifiers.clear();
|
|
17709
|
+
this.#noSpanRaps.clear();
|
|
17343
17710
|
this.#streamGeometries.clear();
|
|
17344
17711
|
this.#failedBlocks.clear();
|
|
17345
17712
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17870,9 +18237,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17870
18237
|
}
|
|
17871
18238
|
#track = null;
|
|
17872
18239
|
#fragments = /* @__PURE__ */ new Map();
|
|
18240
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
18241
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
18242
|
+
/**
|
|
18243
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
18244
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
18245
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
18246
|
+
*/
|
|
18247
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17873
18248
|
#pendingSamples = 0;
|
|
17874
18249
|
#evictedSamples = 0;
|
|
17875
18250
|
#lastMovieFragmentRejection = null;
|
|
18251
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
18252
|
+
get initialized() {
|
|
18253
|
+
return this.#track !== null;
|
|
18254
|
+
}
|
|
17876
18255
|
get pendingSampleCount() {
|
|
17877
18256
|
return this.#pendingSamples;
|
|
17878
18257
|
}
|
|
@@ -17896,7 +18275,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17896
18275
|
this.#track = timing;
|
|
17897
18276
|
return true;
|
|
17898
18277
|
}
|
|
17899
|
-
|
|
18278
|
+
/**
|
|
18279
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
18280
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
18281
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
18282
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
18283
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
18284
|
+
*/
|
|
18285
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17900
18286
|
this.#lastMovieFragmentRejection = null;
|
|
17901
18287
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17902
18288
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17922,6 +18308,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17922
18308
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17923
18309
|
);
|
|
17924
18310
|
}
|
|
18311
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
18312
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17925
18313
|
let freed = 0;
|
|
17926
18314
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17927
18315
|
freed += this.#evictOldestFragment();
|
|
@@ -17935,6 +18323,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17935
18323
|
return false;
|
|
17936
18324
|
}
|
|
17937
18325
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18326
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17938
18327
|
this.#pendingSamples += parsed.samples.length;
|
|
17939
18328
|
return true;
|
|
17940
18329
|
}
|
|
@@ -17992,6 +18381,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17992
18381
|
let live = 0;
|
|
17993
18382
|
for (const sample of samples) if (sample !== null) live++;
|
|
17994
18383
|
this.#fragments.delete(oldest);
|
|
18384
|
+
this.#firstSamples.delete(oldest);
|
|
17995
18385
|
this.#pendingSamples -= live;
|
|
17996
18386
|
this.#evictedSamples += live;
|
|
17997
18387
|
return live;
|
|
@@ -18007,12 +18397,23 @@ var IsoBmffTimingResolver = class {
|
|
|
18007
18397
|
if (!samples || !samples[index]) return false;
|
|
18008
18398
|
samples[index] = null;
|
|
18009
18399
|
this.#pendingSamples--;
|
|
18010
|
-
if (samples.every((value) => value === null))
|
|
18400
|
+
if (samples.every((value) => value === null)) {
|
|
18401
|
+
this.#fragments.delete(key);
|
|
18402
|
+
const first = this.#firstSamples.get(key);
|
|
18403
|
+
this.#firstSamples.delete(key);
|
|
18404
|
+
this.#delivered.delete(key);
|
|
18405
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18406
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18407
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18408
|
+
}
|
|
18409
|
+
}
|
|
18011
18410
|
return true;
|
|
18012
18411
|
}
|
|
18013
18412
|
reset() {
|
|
18014
18413
|
this.#track = null;
|
|
18015
18414
|
this.#fragments.clear();
|
|
18415
|
+
this.#firstSamples.clear();
|
|
18416
|
+
this.#delivered.clear();
|
|
18016
18417
|
this.#pendingSamples = 0;
|
|
18017
18418
|
}
|
|
18018
18419
|
};
|
|
@@ -18182,6 +18583,9 @@ function authoredGap(before, after) {
|
|
|
18182
18583
|
function ssIdAfter(a, b) {
|
|
18183
18584
|
return (a - b | 0) > 0;
|
|
18184
18585
|
}
|
|
18586
|
+
function holdsRebuilt(stream) {
|
|
18587
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18588
|
+
}
|
|
18185
18589
|
var FecReleaseSequencer = class {
|
|
18186
18590
|
#streams = /* @__PURE__ */ new Map();
|
|
18187
18591
|
#gate;
|
|
@@ -18244,6 +18648,7 @@ var FecReleaseSequencer = class {
|
|
|
18244
18648
|
stream.lastSbn = void 0;
|
|
18245
18649
|
stream.discontinuity = false;
|
|
18246
18650
|
stream.rapSsId = void 0;
|
|
18651
|
+
stream.sourced = false;
|
|
18247
18652
|
}
|
|
18248
18653
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
18249
18654
|
this.#recordStat("droppedStale");
|
|
@@ -18270,6 +18675,7 @@ var FecReleaseSequencer = class {
|
|
|
18270
18675
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18271
18676
|
else hi = mid;
|
|
18272
18677
|
}
|
|
18678
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18273
18679
|
stream.buf.splice(lo, 0, frame);
|
|
18274
18680
|
this.#drainUnlessDeferred(stream);
|
|
18275
18681
|
}
|
|
@@ -18305,6 +18711,41 @@ var FecReleaseSequencer = class {
|
|
|
18305
18711
|
}
|
|
18306
18712
|
return false;
|
|
18307
18713
|
}
|
|
18714
|
+
/**
|
|
18715
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
18716
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
18717
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
18718
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
18719
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
18720
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
18721
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
18722
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
18723
|
+
* released.
|
|
18724
|
+
*/
|
|
18725
|
+
unsource(packetId, catalogTrackName) {
|
|
18726
|
+
for (const stream of this.#streams.values()) {
|
|
18727
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
18728
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
18729
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
18730
|
+
else this.#drain(stream, true);
|
|
18731
|
+
}
|
|
18732
|
+
stream.sourced = false;
|
|
18733
|
+
}
|
|
18734
|
+
}
|
|
18735
|
+
}
|
|
18736
|
+
/**
|
|
18737
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
18738
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
18739
|
+
* packetId.
|
|
18740
|
+
*/
|
|
18741
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
18742
|
+
for (const stream of this.#streams.values()) {
|
|
18743
|
+
if (stream.packetId !== packetId) continue;
|
|
18744
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
18745
|
+
if (holdsRebuilt(stream)) return true;
|
|
18746
|
+
}
|
|
18747
|
+
return false;
|
|
18748
|
+
}
|
|
18308
18749
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18309
18750
|
poke(streamKey) {
|
|
18310
18751
|
if (streamKey !== void 0) {
|
|
@@ -18396,16 +18837,32 @@ var FecReleaseSequencer = class {
|
|
|
18396
18837
|
last: null,
|
|
18397
18838
|
lastSbn: void 0,
|
|
18398
18839
|
discontinuity: false,
|
|
18399
|
-
rapSsId: void 0
|
|
18840
|
+
rapSsId: void 0,
|
|
18841
|
+
sourced: false,
|
|
18842
|
+
draining: 0
|
|
18400
18843
|
};
|
|
18401
18844
|
this.#streams.set(streamKey, stream);
|
|
18402
18845
|
}
|
|
18403
18846
|
return stream;
|
|
18404
18847
|
}
|
|
18405
|
-
#
|
|
18848
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
18849
|
+
#drain(stream, asUnanchored = false) {
|
|
18850
|
+
stream.draining++;
|
|
18851
|
+
try {
|
|
18852
|
+
this.#drainLoop(stream, asUnanchored);
|
|
18853
|
+
} finally {
|
|
18854
|
+
stream.draining--;
|
|
18855
|
+
}
|
|
18856
|
+
}
|
|
18857
|
+
#drainLoop(stream, asUnanchored) {
|
|
18406
18858
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18407
18859
|
const head = stream.buf[0];
|
|
18408
18860
|
const last = stream.last;
|
|
18861
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
18862
|
+
stream.buf.shift();
|
|
18863
|
+
this.#release(stream, head, void 0);
|
|
18864
|
+
continue;
|
|
18865
|
+
}
|
|
18409
18866
|
if (stream.discontinuity) {
|
|
18410
18867
|
stream.discontinuity = false;
|
|
18411
18868
|
stream.buf.shift();
|
|
@@ -18419,9 +18876,10 @@ var FecReleaseSequencer = class {
|
|
|
18419
18876
|
}
|
|
18420
18877
|
const headSbn = head.meta.fecSbn;
|
|
18421
18878
|
const from = stream.lastSbn ?? headSbn;
|
|
18422
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18879
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18423
18880
|
let lost = [];
|
|
18424
18881
|
const unanchored = from === void 0 && to === void 0;
|
|
18882
|
+
let unresolved = false;
|
|
18425
18883
|
if (from !== void 0 && to !== void 0) {
|
|
18426
18884
|
const resolved = this.#gate.resolvedLoss(
|
|
18427
18885
|
stream.packetId,
|
|
@@ -18430,7 +18888,8 @@ var FecReleaseSequencer = class {
|
|
|
18430
18888
|
Math.max(from, to),
|
|
18431
18889
|
from
|
|
18432
18890
|
);
|
|
18433
|
-
|
|
18891
|
+
unresolved = resolved === null;
|
|
18892
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18434
18893
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18435
18894
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18436
18895
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18438,22 +18897,56 @@ var FecReleaseSequencer = class {
|
|
|
18438
18897
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18439
18898
|
continue;
|
|
18440
18899
|
}
|
|
18441
|
-
lost = resolved;
|
|
18900
|
+
lost = resolved ?? [];
|
|
18442
18901
|
}
|
|
18443
18902
|
stream.buf.shift();
|
|
18444
18903
|
this.#release(
|
|
18445
18904
|
stream,
|
|
18446
18905
|
head,
|
|
18447
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18906
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18448
18907
|
);
|
|
18449
18908
|
}
|
|
18450
18909
|
}
|
|
18910
|
+
/**
|
|
18911
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
18912
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
18913
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
18914
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
18915
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
18916
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
18917
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
18918
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
18919
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
18920
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
18921
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
18922
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
18923
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
18924
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
18925
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
18926
|
+
*/
|
|
18927
|
+
#cursorPassed(stream, asUnanchored) {
|
|
18928
|
+
let at2 = -1;
|
|
18929
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
18930
|
+
const frame = stream.buf[i];
|
|
18931
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
18932
|
+
frame.passed = true;
|
|
18933
|
+
if (at2 < 0) at2 = i;
|
|
18934
|
+
} else if (frame.passed !== true) break;
|
|
18935
|
+
}
|
|
18936
|
+
if (at2 < 0) return false;
|
|
18937
|
+
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));
|
|
18938
|
+
}
|
|
18451
18939
|
#release(stream, frame, decision) {
|
|
18940
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18941
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18452
18942
|
if (decision) frame.meta.gapBefore = decision;
|
|
18453
18943
|
stream.last = frame;
|
|
18454
|
-
if (frame.meta.fecSbn !== void 0
|
|
18944
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
18945
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
18946
|
+
}
|
|
18947
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
18455
18948
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18456
|
-
this.#emit(frame.data, frame.meta);
|
|
18949
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18457
18950
|
}
|
|
18458
18951
|
};
|
|
18459
18952
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -18494,6 +18987,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18494
18987
|
this.#reclaimedTimingSamples = 0;
|
|
18495
18988
|
this.#evictedTimingSamples = 0;
|
|
18496
18989
|
this.#rejectedMovieFragments = 0;
|
|
18990
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18991
|
+
held: 0,
|
|
18992
|
+
expired: 0,
|
|
18993
|
+
discarded: 0,
|
|
18994
|
+
superseded: 0
|
|
18995
|
+
};
|
|
18497
18996
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18498
18997
|
this.#fecGate = null;
|
|
18499
18998
|
this.#fecSequencer = null;
|
|
@@ -18602,6 +19101,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18602
19101
|
#reclaimedTimingSamples;
|
|
18603
19102
|
#evictedTimingSamples;
|
|
18604
19103
|
#rejectedMovieFragments;
|
|
19104
|
+
#recoveredHeadsAwaitingTiming;
|
|
18605
19105
|
#sequencers;
|
|
18606
19106
|
#fecGate;
|
|
18607
19107
|
#fecSequencer;
|
|
@@ -18707,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18707
19207
|
/**
|
|
18708
19208
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18709
19209
|
*
|
|
18710
|
-
*
|
|
18711
|
-
*
|
|
18712
|
-
*
|
|
18713
|
-
*
|
|
18714
|
-
*
|
|
19210
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
19211
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
19212
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
19213
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
19214
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
19215
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
18715
19216
|
*/
|
|
18716
19217
|
get timingBudgetStats() {
|
|
18717
19218
|
return {
|
|
@@ -18722,6 +19223,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18722
19223
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18723
19224
|
};
|
|
18724
19225
|
}
|
|
19226
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
19227
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19228
|
+
let pending = 0;
|
|
19229
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19230
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
19231
|
+
}
|
|
19232
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
19233
|
+
}
|
|
18725
19234
|
get reorderStats() {
|
|
18726
19235
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18727
19236
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18763,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18763
19272
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18764
19273
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18765
19274
|
}
|
|
19275
|
+
/** See FecReleaseSequencer.unsource. */
|
|
19276
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
19277
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
19278
|
+
}
|
|
19279
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
19280
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
19281
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
19282
|
+
}
|
|
18766
19283
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18767
19284
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18768
19285
|
if (!this.#fecSequencer) return;
|
|
@@ -18850,10 +19367,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18850
19367
|
get reassemblerDataUnitsReturned() {
|
|
18851
19368
|
return this.#reassemblerDataUnitsReturned;
|
|
18852
19369
|
}
|
|
18853
|
-
#countOutcome(
|
|
18854
|
-
const count = (this.#feedOutcomes.get(
|
|
18855
|
-
this.#feedOutcomes.set(
|
|
18856
|
-
this.#lastOutcome =
|
|
19370
|
+
#countOutcome(token2) {
|
|
19371
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
19372
|
+
this.#feedOutcomes.set(token2, count);
|
|
19373
|
+
this.#lastOutcome = token2;
|
|
18857
19374
|
this.#lastOutcomeCount = count;
|
|
18858
19375
|
}
|
|
18859
19376
|
#countDeferredOutcome(reason) {
|
|
@@ -18895,7 +19412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18895
19412
|
const now = Date.now();
|
|
18896
19413
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18897
19414
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18898
|
-
awaitingTiming.shift();
|
|
19415
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18899
19416
|
}
|
|
18900
19417
|
}
|
|
18901
19418
|
}
|
|
@@ -19013,11 +19530,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19013
19530
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
19014
19531
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
19015
19532
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
19016
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19533
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19017
19534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19018
19535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19019
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19020
|
-
const admitted = resolver2.addMovieFragment(
|
|
19536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19537
|
+
const admitted = resolver2.addMovieFragment(
|
|
19538
|
+
mpu.mpuSequenceNumber,
|
|
19539
|
+
data,
|
|
19540
|
+
remaining,
|
|
19541
|
+
options?.recovered === true
|
|
19542
|
+
);
|
|
19021
19543
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
19022
19544
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
19023
19545
|
if (!admitted) {
|
|
@@ -19037,7 +19559,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19037
19559
|
};
|
|
19038
19560
|
}
|
|
19039
19561
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
19040
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19562
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
19041
19563
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
19042
19564
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
19043
19565
|
return this.#drop("mfu-empty-payload");
|
|
@@ -19058,6 +19580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19058
19580
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19059
19581
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19060
19582
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19583
|
+
sequenceNumber: info.sequenceNumber,
|
|
19061
19584
|
arrivedAt: Date.now()
|
|
19062
19585
|
});
|
|
19063
19586
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19083,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19083
19606
|
info,
|
|
19084
19607
|
resolver,
|
|
19085
19608
|
options?.recovered === true,
|
|
19086
|
-
options?.fecSbn
|
|
19609
|
+
options?.fecSbn,
|
|
19610
|
+
options?.fecSbnFirst
|
|
19087
19611
|
);
|
|
19088
19612
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19089
19613
|
}
|
|
@@ -19176,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19176
19700
|
info,
|
|
19177
19701
|
resolver,
|
|
19178
19702
|
options?.recovered === true,
|
|
19179
|
-
options?.fecSbn
|
|
19703
|
+
options?.fecSbn,
|
|
19704
|
+
options?.fecSbnFirst
|
|
19180
19705
|
);
|
|
19181
19706
|
if (outcome.record !== void 0) return outcome.record;
|
|
19182
19707
|
return this.#drop(outcome.reason);
|
|
@@ -19196,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19196
19721
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19197
19722
|
* is what keeps both true.
|
|
19198
19723
|
*/
|
|
19199
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19724
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19200
19725
|
const timing = resolver.sampleTiming(
|
|
19201
19726
|
combined.mpuSequenceNumber,
|
|
19202
19727
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19213,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19213
19738
|
);
|
|
19214
19739
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19215
19740
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19741
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19216
19742
|
if (!resolver.consumeSample(
|
|
19217
19743
|
combined.mpuSequenceNumber,
|
|
19218
19744
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19381,6 +19907,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19381
19907
|
}
|
|
19382
19908
|
this.#emitFrame(data, meta);
|
|
19383
19909
|
}
|
|
19910
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19911
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19912
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19913
|
+
}
|
|
19384
19914
|
/**
|
|
19385
19915
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19386
19916
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19442,6 +19972,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19442
19972
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19443
19973
|
this.#sequencers.clear();
|
|
19444
19974
|
this.#fecSequencer?.discard();
|
|
19975
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19976
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19977
|
+
}
|
|
19445
19978
|
this.#timingStreams.clear();
|
|
19446
19979
|
this.#pendingTimingSamples = 0;
|
|
19447
19980
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19460,10 +19993,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19460
19993
|
return resolver;
|
|
19461
19994
|
}
|
|
19462
19995
|
#awaitTiming(streamKey, head) {
|
|
19463
|
-
const
|
|
19464
|
-
if (!
|
|
19996
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19997
|
+
if (!state) return;
|
|
19998
|
+
if (!head.options?.recovered) {
|
|
19999
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
20000
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
20001
|
+
state.awaitingTiming = kept;
|
|
20002
|
+
}
|
|
20003
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19465
20004
|
awaitingTiming.push(head);
|
|
19466
|
-
if (
|
|
20005
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
20006
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
20007
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
20008
|
+
}
|
|
20009
|
+
}
|
|
20010
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
20011
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
20012
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19467
20013
|
}
|
|
19468
20014
|
/**
|
|
19469
20015
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19491,6 +20037,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19491
20037
|
const state = this.#timingStreams.get(streamKey);
|
|
19492
20038
|
if (!state) return;
|
|
19493
20039
|
this.#timingStreams.delete(streamKey);
|
|
20040
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19494
20041
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19495
20042
|
if (this.#pendingTimingSamples < 0) {
|
|
19496
20043
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22697,7 +23244,18 @@ function keyframeOverlaySsId(header) {
|
|
|
22697
23244
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22698
23245
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22699
23246
|
}
|
|
23247
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23248
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23249
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23250
|
+
}
|
|
23251
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23252
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23253
|
+
return headerPsn + offset >>> 0;
|
|
23254
|
+
}
|
|
23255
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23256
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22700
23257
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23258
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
22701
23259
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22702
23260
|
function assertStreamIdentity(role, stream) {
|
|
22703
23261
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -22808,6 +23366,7 @@ var MmtpRouter = class {
|
|
|
22808
23366
|
#provenance = /* @__PURE__ */ new Map();
|
|
22809
23367
|
#onFlowProtected = null;
|
|
22810
23368
|
#unadmittedListedDrops = 0;
|
|
23369
|
+
#admittedSourceDatagrams = 0;
|
|
22811
23370
|
#unlistedRepairSetAside = 0;
|
|
22812
23371
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22813
23372
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22864,6 +23423,26 @@ var MmtpRouter = class {
|
|
|
22864
23423
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22865
23424
|
*/
|
|
22866
23425
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23426
|
+
/**
|
|
23427
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23428
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23429
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23430
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23431
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23432
|
+
* an MPU no member of which arrived).
|
|
23433
|
+
*/
|
|
23434
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23435
|
+
/**
|
|
23436
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
23437
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
23438
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
23439
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
23440
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
23441
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
23442
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
23443
|
+
* this offset did.
|
|
23444
|
+
*/
|
|
23445
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22867
23446
|
/**
|
|
22868
23447
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22869
23448
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22933,7 +23512,9 @@ var MmtpRouter = class {
|
|
|
22933
23512
|
recovered: 0,
|
|
22934
23513
|
restarts: 0,
|
|
22935
23514
|
unsequencedDropped: 0,
|
|
22936
|
-
protectedDropped: 0
|
|
23515
|
+
protectedDropped: 0,
|
|
23516
|
+
baseFed: 0,
|
|
23517
|
+
baseUndetermined: 0
|
|
22937
23518
|
};
|
|
22938
23519
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22939
23520
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23106,7 +23687,7 @@ var MmtpRouter = class {
|
|
|
23106
23687
|
const cb = this.#getFrameCallback(packetId);
|
|
23107
23688
|
if (isVideo && !assembler) {
|
|
23108
23689
|
if (frameMeta.recovered) {
|
|
23109
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23690
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23110
23691
|
} else {
|
|
23111
23692
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23112
23693
|
}
|
|
@@ -23139,13 +23720,18 @@ var MmtpRouter = class {
|
|
|
23139
23720
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
23140
23721
|
}
|
|
23141
23722
|
};
|
|
23142
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23723
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23143
23724
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
23144
23725
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
23145
23726
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
23146
23727
|
}
|
|
23147
|
-
|
|
23148
|
-
|
|
23728
|
+
if (span === void 0) {
|
|
23729
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23730
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23731
|
+
} else {
|
|
23732
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23733
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23734
|
+
}
|
|
23149
23735
|
};
|
|
23150
23736
|
}
|
|
23151
23737
|
}
|
|
@@ -23190,8 +23776,9 @@ var MmtpRouter = class {
|
|
|
23190
23776
|
}
|
|
23191
23777
|
/**
|
|
23192
23778
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23193
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23194
|
-
*
|
|
23779
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
23780
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
23781
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
23195
23782
|
*/
|
|
23196
23783
|
get restampedRecoveredSequenceCount() {
|
|
23197
23784
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23370,6 +23957,7 @@ var MmtpRouter = class {
|
|
|
23370
23957
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23371
23958
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23372
23959
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23960
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23373
23961
|
if (previous) {
|
|
23374
23962
|
previous.flush();
|
|
23375
23963
|
previous.dispose();
|
|
@@ -23496,6 +24084,10 @@ var MmtpRouter = class {
|
|
|
23496
24084
|
get keyframeOverlayStats() {
|
|
23497
24085
|
return { ...this.#overlayStats };
|
|
23498
24086
|
}
|
|
24087
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
24088
|
+
get recoveredHeadsAwaitingTiming() {
|
|
24089
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
24090
|
+
}
|
|
23499
24091
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23500
24092
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23501
24093
|
}
|
|
@@ -23516,7 +24108,8 @@ var MmtpRouter = class {
|
|
|
23516
24108
|
fec,
|
|
23517
24109
|
k,
|
|
23518
24110
|
offsets: /* @__PURE__ */ new Map(),
|
|
23519
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24111
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24112
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23520
24113
|
};
|
|
23521
24114
|
this.#overlays.set(repairPacketId, inst);
|
|
23522
24115
|
return () => {
|
|
@@ -23546,12 +24139,18 @@ var MmtpRouter = class {
|
|
|
23546
24139
|
*/
|
|
23547
24140
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23548
24141
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
24142
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
24143
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
24144
|
+
stream.overlayToBase.routed.clear();
|
|
24145
|
+
stream.overlayToBase.pending.clear();
|
|
24146
|
+
}
|
|
23549
24147
|
}
|
|
23550
24148
|
#resetOverlay(inst) {
|
|
23551
24149
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23552
24150
|
inst.last = void 0;
|
|
23553
24151
|
inst.offsets.clear();
|
|
23554
24152
|
inst.moqBlocks.clear();
|
|
24153
|
+
inst.rebuilt.clear();
|
|
23555
24154
|
}
|
|
23556
24155
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23557
24156
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23602,18 +24201,33 @@ var MmtpRouter = class {
|
|
|
23602
24201
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23603
24202
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23604
24203
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23605
|
-
* included (7.1, 7.4.3).
|
|
23606
|
-
|
|
23607
|
-
|
|
24204
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
24205
|
+
* FEC reads it.
|
|
24206
|
+
*
|
|
24207
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
24208
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
24209
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
24210
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
24211
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
24212
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
24213
|
+
*/
|
|
24214
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23608
24215
|
if (this.#overlays.size === 0) return;
|
|
23609
24216
|
const ssId = keyframeOverlaySsId(header);
|
|
23610
24217
|
if (ssId === void 0) return;
|
|
24218
|
+
let rebuiltPsn;
|
|
23611
24219
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23612
24220
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24221
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24222
|
+
inst.rebuilt.delete(ssId);
|
|
23613
24223
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23614
24224
|
if (identity !== null) {
|
|
23615
24225
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23616
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
24226
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
24227
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
24228
|
+
overlaySsId: ssId,
|
|
24229
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
24230
|
+
});
|
|
23617
24231
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23618
24232
|
}
|
|
23619
24233
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23635,6 +24249,14 @@ var MmtpRouter = class {
|
|
|
23635
24249
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23636
24250
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23637
24251
|
}
|
|
24252
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
24253
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
24254
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
24255
|
+
this.#restampedRecoveredSeqs++;
|
|
24256
|
+
} else if (baseSsId !== void 0) {
|
|
24257
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24258
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24259
|
+
}
|
|
23638
24260
|
}
|
|
23639
24261
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23640
24262
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23668,11 +24290,16 @@ var MmtpRouter = class {
|
|
|
23668
24290
|
}
|
|
23669
24291
|
/**
|
|
23670
24292
|
* Route an overlay-recovered block's packets as source media of the
|
|
23671
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23672
|
-
*
|
|
23673
|
-
*
|
|
23674
|
-
*
|
|
23675
|
-
*
|
|
24293
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
24294
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
24295
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
24296
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
24297
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
24298
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
24299
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
24300
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
24301
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
24302
|
+
* header's sequence and any other drops the packet.
|
|
23676
24303
|
*/
|
|
23677
24304
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23678
24305
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23692,22 +24319,139 @@ var MmtpRouter = class {
|
|
|
23692
24319
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23693
24320
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23694
24321
|
const restored = /* @__PURE__ */ new Set();
|
|
24322
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24323
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
23695
24324
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23696
|
-
const
|
|
23697
|
-
|
|
23698
|
-
|
|
24325
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24326
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24327
|
+
if (anchor !== void 0) {
|
|
24328
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23699
24329
|
restored.add(packetSequenceNumber);
|
|
23700
|
-
|
|
24330
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24331
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24332
|
+
if (baseSsId !== void 0) {
|
|
24333
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24334
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24335
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
24336
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24337
|
+
}
|
|
24338
|
+
}
|
|
24339
|
+
return [out];
|
|
24340
|
+
}
|
|
24341
|
+
if (!restamped) {
|
|
24342
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24343
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24344
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24345
|
+
if (baseSsId !== void 0) {
|
|
24346
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24347
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
24348
|
+
} else {
|
|
24349
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
24350
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
24351
|
+
}
|
|
24352
|
+
return [sym];
|
|
23701
24353
|
}
|
|
23702
|
-
if (!restamped) return [sym];
|
|
23703
24354
|
this.#overlayStats.unsequencedDropped++;
|
|
23704
24355
|
return [];
|
|
23705
24356
|
});
|
|
23706
24357
|
this.#overlayStats.recovered++;
|
|
24358
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
24359
|
+
for (const sym of symbols) {
|
|
24360
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24361
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
24362
|
+
const first = mpuFirst.get(mpu);
|
|
24363
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
24364
|
+
}
|
|
24365
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
24366
|
+
for (const sym of symbols) {
|
|
24367
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
24368
|
+
}
|
|
23707
24369
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23708
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24370
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24371
|
+
baseSsIds,
|
|
24372
|
+
firstSsIds
|
|
23709
24373
|
});
|
|
23710
24374
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24375
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
24376
|
+
}
|
|
24377
|
+
/**
|
|
24378
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24379
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24380
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
24381
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
24382
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24383
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24384
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24385
|
+
* A base block no repair has stated yet takes it when one does
|
|
24386
|
+
* (#feedPendingRebuilt).
|
|
24387
|
+
*
|
|
24388
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24389
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24390
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24391
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24392
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24393
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24394
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24395
|
+
* block's own arrivals cannot show.
|
|
24396
|
+
*/
|
|
24397
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24398
|
+
const fec = this.#fecManager;
|
|
24399
|
+
const owner = inst.leg.sourcePacketId;
|
|
24400
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24401
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24402
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24403
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24404
|
+
const feeds = [];
|
|
24405
|
+
for (const sym of symbols) {
|
|
24406
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24407
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24408
|
+
if (ssId === void 0) {
|
|
24409
|
+
this.#overlayStats.baseUndetermined++;
|
|
24410
|
+
continue;
|
|
24411
|
+
}
|
|
24412
|
+
routed.delete(ssId);
|
|
24413
|
+
routed.add(ssId);
|
|
24414
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24415
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24416
|
+
}
|
|
24417
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24418
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24419
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24420
|
+
}
|
|
24421
|
+
}
|
|
24422
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24423
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24424
|
+
const fec = this.#fecManager;
|
|
24425
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24426
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24427
|
+
if (coords == null) {
|
|
24428
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24429
|
+
pending.delete(ssId);
|
|
24430
|
+
pending.set(ssId, symbolData.slice());
|
|
24431
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24432
|
+
return;
|
|
24433
|
+
}
|
|
24434
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24435
|
+
if (!fed.taken) return;
|
|
24436
|
+
this.#overlayStats.baseFed++;
|
|
24437
|
+
if (fed.recovered === null) return;
|
|
24438
|
+
const sbn = coords.sbn;
|
|
24439
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24440
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24441
|
+
fec.countRecovery(owner, sbn, name);
|
|
24442
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24443
|
+
});
|
|
24444
|
+
}
|
|
24445
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24446
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24447
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24448
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24449
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24450
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24451
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24452
|
+
pending.delete(ssId);
|
|
24453
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24454
|
+
}
|
|
23711
24455
|
}
|
|
23712
24456
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23713
24457
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23847,6 +24591,10 @@ var MmtpRouter = class {
|
|
|
23847
24591
|
get unadmittedListedDropCount() {
|
|
23848
24592
|
return this.#unadmittedListedDrops;
|
|
23849
24593
|
}
|
|
24594
|
+
/** 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. */
|
|
24595
|
+
get admittedSourceDatagramCount() {
|
|
24596
|
+
return this.#admittedSourceDatagrams;
|
|
24597
|
+
}
|
|
23850
24598
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23851
24599
|
get unlistedRepairSetAsideCount() {
|
|
23852
24600
|
return this.#unlistedRepairSetAside;
|
|
@@ -24333,6 +25081,8 @@ var MmtpRouter = class {
|
|
|
24333
25081
|
)) return;
|
|
24334
25082
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24335
25083
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
25084
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
25085
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24336
25086
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24337
25087
|
const recovered = fec.feedRepairSymbol(
|
|
24338
25088
|
ownerPacketId,
|
|
@@ -24383,7 +25133,7 @@ var MmtpRouter = class {
|
|
|
24383
25133
|
catalogTrackName
|
|
24384
25134
|
)) continue;
|
|
24385
25135
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24386
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25136
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24387
25137
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24388
25138
|
}
|
|
24389
25139
|
if (payload.oversized) continue;
|
|
@@ -24470,6 +25220,8 @@ var MmtpRouter = class {
|
|
|
24470
25220
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24471
25221
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24472
25222
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
25223
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25224
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24473
25225
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24474
25226
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24475
25227
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24596,6 +25348,12 @@ var MmtpRouter = class {
|
|
|
24596
25348
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24597
25349
|
* own RAP cadence, so no timer is involved.
|
|
24598
25350
|
*
|
|
25351
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
25352
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
25353
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
25354
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
25355
|
+
* frames of the outgoing stream.
|
|
25356
|
+
*
|
|
24599
25357
|
* The commit is one synchronous transition inside the frame path: the
|
|
24600
25358
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24601
25359
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24646,6 +25404,9 @@ var MmtpRouter = class {
|
|
|
24646
25404
|
droppedIncomingFrames: 0
|
|
24647
25405
|
};
|
|
24648
25406
|
this.#staged = state;
|
|
25407
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25408
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25409
|
+
}
|
|
24649
25410
|
return {
|
|
24650
25411
|
cancel: () => {
|
|
24651
25412
|
if (this.#staged !== state) return false;
|
|
@@ -24728,7 +25489,17 @@ var MmtpRouter = class {
|
|
|
24728
25489
|
*/
|
|
24729
25490
|
#routeParsedFrame(data, meta) {
|
|
24730
25491
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
24731
|
-
if (this.#staged !== null
|
|
25492
|
+
if (this.#staged !== null) {
|
|
25493
|
+
let consumed = false;
|
|
25494
|
+
this.#fecPokeDepth++;
|
|
25495
|
+
try {
|
|
25496
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
25497
|
+
} finally {
|
|
25498
|
+
this.#fecPokeDepth--;
|
|
25499
|
+
this.#flushPendingFecPokes();
|
|
25500
|
+
}
|
|
25501
|
+
if (consumed) return;
|
|
25502
|
+
}
|
|
24732
25503
|
this.#deliverParsedFrame(data, meta);
|
|
24733
25504
|
}
|
|
24734
25505
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -24880,12 +25651,16 @@ var MmtpRouter = class {
|
|
|
24880
25651
|
this.#parser.deferFecRelease(work);
|
|
24881
25652
|
} finally {
|
|
24882
25653
|
this.#fecPokeDepth--;
|
|
24883
|
-
|
|
24884
|
-
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
|
|
25654
|
+
this.#flushPendingFecPokes();
|
|
25655
|
+
}
|
|
25656
|
+
}
|
|
25657
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
25658
|
+
#flushPendingFecPokes() {
|
|
25659
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
25660
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
25661
|
+
this.#pendingFecPokes.clear();
|
|
25662
|
+
for (const poke of pokes) {
|
|
25663
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
24889
25664
|
}
|
|
24890
25665
|
}
|
|
24891
25666
|
}
|
|
@@ -24963,6 +25738,14 @@ var MmtpRouter = class {
|
|
|
24963
25738
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
24964
25739
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
24965
25740
|
},
|
|
25741
|
+
// A staged switch commits on when each side's frames reach it
|
|
25742
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
25743
|
+
// they did unanchored.
|
|
25744
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
25745
|
+
const staged = this.#staged;
|
|
25746
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
25747
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
25748
|
+
},
|
|
24966
25749
|
onReleased: (data, meta) => {
|
|
24967
25750
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
24968
25751
|
const timing = meta.authoredTiming;
|
|
@@ -25104,6 +25887,7 @@ var MmtpRouter = class {
|
|
|
25104
25887
|
const parsed = this.#parser.parsePacket(packet);
|
|
25105
25888
|
if (!parsed) return;
|
|
25106
25889
|
const info = parsed.info;
|
|
25890
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25107
25891
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25108
25892
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25109
25893
|
}
|
|
@@ -25385,7 +26169,8 @@ var MmtpRouter = class {
|
|
|
25385
26169
|
parsed.header,
|
|
25386
26170
|
info.sequenceNumber,
|
|
25387
26171
|
info.rapFlag,
|
|
25388
|
-
source
|
|
26172
|
+
source,
|
|
26173
|
+
fecPayloadId?.ssId
|
|
25389
26174
|
);
|
|
25390
26175
|
}
|
|
25391
26176
|
}
|
|
@@ -25411,7 +26196,8 @@ var MmtpRouter = class {
|
|
|
25411
26196
|
source: parserOptions?.source,
|
|
25412
26197
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25413
26198
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25414
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26199
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26200
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25415
26201
|
};
|
|
25416
26202
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25417
26203
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25535,7 +26321,8 @@ var MmtpRouter = class {
|
|
|
25535
26321
|
if (stream === void 0) {
|
|
25536
26322
|
stream = {
|
|
25537
26323
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25538
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26324
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26325
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25539
26326
|
};
|
|
25540
26327
|
byName.set(catalogTrackName, stream);
|
|
25541
26328
|
}
|
|
@@ -25650,8 +26437,16 @@ var MmtpRouter = class {
|
|
|
25650
26437
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25651
26438
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25652
26439
|
}
|
|
25653
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26440
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25654
26441
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26442
|
+
if (this.#isFecTrack(packetId)) {
|
|
26443
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26444
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26445
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26446
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26447
|
+
return;
|
|
26448
|
+
}
|
|
26449
|
+
}
|
|
25655
26450
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25656
26451
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25657
26452
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25805,6 +26600,13 @@ var MmtpRouter = class {
|
|
|
25805
26600
|
* frames by epoch only if pre-protection input ever matters.
|
|
25806
26601
|
*/
|
|
25807
26602
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26603
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26604
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26605
|
+
result = {
|
|
26606
|
+
...result,
|
|
26607
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26608
|
+
};
|
|
26609
|
+
}
|
|
25808
26610
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25809
26611
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25810
26612
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25861,7 +26663,12 @@ var MmtpRouter = class {
|
|
|
25861
26663
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25862
26664
|
if (ownerPacketId == null) return;
|
|
25863
26665
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25864
|
-
const fecSbnOf = (index) =>
|
|
26666
|
+
const fecSbnOf = (index) => {
|
|
26667
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
26668
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
26669
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
26670
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
26671
|
+
};
|
|
25865
26672
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25866
26673
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25867
26674
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -25869,7 +26676,10 @@ var MmtpRouter = class {
|
|
|
25869
26676
|
...result,
|
|
25870
26677
|
symbols: result.symbols.map((sym) => {
|
|
25871
26678
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25872
|
-
if (wire === sym.packetSequenceNumber)
|
|
26679
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26680
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26681
|
+
return sym;
|
|
26682
|
+
}
|
|
25873
26683
|
this.#restampedRecoveredSeqs++;
|
|
25874
26684
|
this.#restampedStreams.add(streamKey);
|
|
25875
26685
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25905,6 +26715,29 @@ var MmtpRouter = class {
|
|
|
25905
26715
|
}
|
|
25906
26716
|
);
|
|
25907
26717
|
}
|
|
26718
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26719
|
+
ownerPacketId,
|
|
26720
|
+
sym.fragment.mpuSequenceNumber,
|
|
26721
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26722
|
+
sym.fragment.sampleNumber,
|
|
26723
|
+
catalogTrackName
|
|
26724
|
+
);
|
|
26725
|
+
for (const sym of result.symbols) {
|
|
26726
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26727
|
+
this.#feedViaParser(
|
|
26728
|
+
sym.rawSymbol,
|
|
26729
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26730
|
+
void 0,
|
|
26731
|
+
{
|
|
26732
|
+
skipFecPayloadId: true,
|
|
26733
|
+
recovered: true,
|
|
26734
|
+
deliveryProvenance,
|
|
26735
|
+
...fecSbnOf(sym.index),
|
|
26736
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26737
|
+
}
|
|
26738
|
+
);
|
|
26739
|
+
}
|
|
26740
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25908
26741
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25909
26742
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25910
26743
|
if (assembler) {
|
|
@@ -27631,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
27631
28464
|
function isSymbolCounts(value) {
|
|
27632
28465
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
27633
28466
|
}
|
|
28467
|
+
function isSpanFailure(value) {
|
|
28468
|
+
if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
|
|
28469
|
+
const length = value["length"];
|
|
28470
|
+
const sources = value["sources"];
|
|
28471
|
+
return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
|
|
28472
|
+
}
|
|
27634
28473
|
function isNullableNonNegative(value) {
|
|
27635
28474
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
27636
28475
|
}
|
|
@@ -27650,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
27650
28489
|
case "recovered":
|
|
27651
28490
|
return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
|
|
27652
28491
|
case "failed":
|
|
27653
|
-
return hasExactKeys(
|
|
28492
|
+
return hasExactKeys(
|
|
28493
|
+
value,
|
|
28494
|
+
"span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
|
|
28495
|
+
) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
|
|
27654
28496
|
case "repair-preempted":
|
|
27655
28497
|
case "repair-deferred":
|
|
27656
28498
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -27916,7 +28758,7 @@ var MulticastManager = class {
|
|
|
27916
28758
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
27917
28759
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
27918
28760
|
},
|
|
27919
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28761
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
27920
28762
|
const pathCount = this.#fec.blockPathCount(
|
|
27921
28763
|
sbn,
|
|
27922
28764
|
packetId,
|
|
@@ -27934,7 +28776,8 @@ var MulticastManager = class {
|
|
|
27934
28776
|
symbols: {
|
|
27935
28777
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
27936
28778
|
moq: pathCount?.moqSymbols ?? 0
|
|
27937
|
-
}
|
|
28779
|
+
},
|
|
28780
|
+
...span === void 0 ? {} : { span }
|
|
27938
28781
|
});
|
|
27939
28782
|
},
|
|
27940
28783
|
onFrameGap: (decision) => {
|
|
@@ -29314,10 +30157,12 @@ var MulticastManager = class {
|
|
|
29314
30157
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
29315
30158
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
29316
30159
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
29317
|
-
* once its range FETCH ends (or none is sent).
|
|
30160
|
+
* once its range FETCH ends (or none is sent). `onExpired` is told of a
|
|
30161
|
+
* pending hole the stream runs past the locator's horizon: returning true,
|
|
30162
|
+
* the caller closes it (`closeIsoHoles`); otherwise it is discarded.
|
|
29318
30163
|
*/
|
|
29319
|
-
onUndelimitedLoss(cb) {
|
|
29320
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30164
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30165
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
29321
30166
|
}
|
|
29322
30167
|
/**
|
|
29323
30168
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -29553,7 +30398,8 @@ var MulticastManager = class {
|
|
|
29553
30398
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
29554
30399
|
...event,
|
|
29555
30400
|
block: Object.freeze({ ...event.block }),
|
|
29556
|
-
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
|
|
30401
|
+
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
|
|
30402
|
+
..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
|
|
29557
30403
|
}
|
|
29558
30404
|
);
|
|
29559
30405
|
if (this.#logRepairLifecycle) {
|
|
@@ -29666,7 +30512,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29666
30512
|
blockProfile,
|
|
29667
30513
|
k: opts.fecK,
|
|
29668
30514
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29669
|
-
// and CMCD `
|
|
30515
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29670
30516
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29671
30517
|
symbolSize: opts.fecSymbolSize,
|
|
29672
30518
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -30208,24 +31054,24 @@ var MediaRouter = class {
|
|
|
30208
31054
|
if (!this.#multicastVideoKeyed) return;
|
|
30209
31055
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
30210
31056
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
30211
|
-
const
|
|
31057
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
30212
31058
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
30213
31059
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
30214
|
-
this.#commitMulticastActivation(
|
|
31060
|
+
this.#commitMulticastActivation(token2);
|
|
30215
31061
|
return;
|
|
30216
31062
|
}
|
|
30217
31063
|
const ready = Promise.resolve(maybeReady);
|
|
30218
|
-
this.#pendingMulticastToken =
|
|
31064
|
+
this.#pendingMulticastToken = token2;
|
|
30219
31065
|
this.#sourceSwitchReady = ready;
|
|
30220
31066
|
void ready.then(
|
|
30221
|
-
() => this.#commitMulticastActivation(
|
|
30222
|
-
(err) => this.#rejectMulticastActivation(
|
|
31067
|
+
() => this.#commitMulticastActivation(token2),
|
|
31068
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
30223
31069
|
).finally(() => {
|
|
30224
31070
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
30225
31071
|
});
|
|
30226
31072
|
}
|
|
30227
|
-
#commitMulticastActivation(
|
|
30228
|
-
if (
|
|
31073
|
+
#commitMulticastActivation(token2) {
|
|
31074
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30229
31075
|
this.#active = "multicast";
|
|
30230
31076
|
this.#suppressed = true;
|
|
30231
31077
|
this.#pendingMulticastToken = null;
|
|
@@ -30236,9 +31082,9 @@ var MediaRouter = class {
|
|
|
30236
31082
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
30237
31083
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
30238
31084
|
}
|
|
30239
|
-
#rejectMulticastActivation(
|
|
30240
|
-
if (
|
|
30241
|
-
if (this.#pendingMulticastToken ===
|
|
31085
|
+
#rejectMulticastActivation(token2, err) {
|
|
31086
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
31087
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
30242
31088
|
this.#pendingMulticastVideo = null;
|
|
30243
31089
|
this.#pendingMulticastAudio = null;
|
|
30244
31090
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -31207,63 +32053,65 @@ var FecStarvationDetector = class {
|
|
|
31207
32053
|
|
|
31208
32054
|
// src/cmcd-producer.ts
|
|
31209
32055
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
31210
|
-
|
|
31211
|
-
|
|
31212
|
-
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
}
|
|
31229
|
-
if (snap.multicastSource != null) {
|
|
31230
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
31231
|
-
}
|
|
31232
|
-
if (snap.amtRelay != null) {
|
|
31233
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
31234
|
-
}
|
|
31235
|
-
return params;
|
|
31236
|
-
}
|
|
31237
|
-
function formatQueryString(params) {
|
|
31238
|
-
return Object.keys(params).sort().map((key) => {
|
|
31239
|
-
const v = params[key];
|
|
31240
|
-
if (typeof v === "string") return `${key}=${sanitize(v)}`;
|
|
31241
|
-
return `${key}=${v}`;
|
|
32056
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
32057
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
32058
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
32059
|
+
var TRANSPORT_TOKEN = {
|
|
32060
|
+
native: "ssm",
|
|
32061
|
+
amt: "amt",
|
|
32062
|
+
moq: "moq"
|
|
32063
|
+
};
|
|
32064
|
+
function sfString(value) {
|
|
32065
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
32066
|
+
}
|
|
32067
|
+
function encodeCmcdRecord(record) {
|
|
32068
|
+
return Object.keys(record).sort().map((key) => {
|
|
32069
|
+
const value = record[key];
|
|
32070
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
32071
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
32072
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
32073
|
+
return `${key}=${value.token}`;
|
|
31242
32074
|
}).join(",");
|
|
31243
32075
|
}
|
|
32076
|
+
var token = (value) => ({ token: value });
|
|
32077
|
+
function putCustom(record, name, value) {
|
|
32078
|
+
if (value === void 0) return;
|
|
32079
|
+
if (typeof value === "number") value = String(value);
|
|
32080
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
32081
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
32082
|
+
}
|
|
31244
32083
|
var CmcdProducer = class {
|
|
31245
|
-
#onCmcdParams;
|
|
31246
32084
|
#onCmcdPost;
|
|
32085
|
+
#intervalMs;
|
|
32086
|
+
#sessionId;
|
|
32087
|
+
#now;
|
|
32088
|
+
#sn = 0;
|
|
32089
|
+
#lastHeartbeat = null;
|
|
31247
32090
|
#transport;
|
|
31248
32091
|
#lastStats;
|
|
31249
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
31250
32092
|
#playMode = null;
|
|
31251
32093
|
#deltaFailRate = null;
|
|
31252
32094
|
#switchMiscount = 0;
|
|
31253
32095
|
#manifestStats = null;
|
|
31254
32096
|
#fecFailClosed = null;
|
|
32097
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
32098
|
+
#lifecycle = null;
|
|
31255
32099
|
constructor(options) {
|
|
31256
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
31257
32100
|
this.#onCmcdPost = options.onCmcdPost;
|
|
32101
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
32102
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
32103
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
32104
|
+
}
|
|
32105
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
32106
|
+
this.#now = options.now ?? Date.now;
|
|
31258
32107
|
}
|
|
31259
32108
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
31260
32109
|
updateTransport(snapshot) {
|
|
31261
32110
|
this.#transport = snapshot;
|
|
31262
32111
|
}
|
|
31263
32112
|
/**
|
|
31264
|
-
* Set the current play-mode
|
|
31265
|
-
*
|
|
31266
|
-
* (CLAUDE.md: error on bad init).
|
|
32113
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
32114
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
31267
32115
|
*/
|
|
31268
32116
|
updatePlayMode(mode) {
|
|
31269
32117
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -31274,9 +32122,8 @@ var CmcdProducer = class {
|
|
|
31274
32122
|
this.#playMode = mode;
|
|
31275
32123
|
}
|
|
31276
32124
|
/**
|
|
31277
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
31278
|
-
* `
|
|
31279
|
-
* Omitted on single-track streams.
|
|
32125
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
32126
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
31280
32127
|
*/
|
|
31281
32128
|
updateDeltaFailRate(rate) {
|
|
31282
32129
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -31286,112 +32133,139 @@ var CmcdProducer = class {
|
|
|
31286
32133
|
}
|
|
31287
32134
|
this.#deltaFailRate = rate;
|
|
31288
32135
|
}
|
|
31289
|
-
/**
|
|
31290
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
31291
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
31292
|
-
*/
|
|
32136
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
31293
32137
|
incrementSwitchMiscount() {
|
|
31294
32138
|
this.#switchMiscount += 1;
|
|
31295
32139
|
}
|
|
31296
32140
|
/**
|
|
31297
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
31298
|
-
*
|
|
31299
|
-
* `
|
|
31300
|
-
* stream playing without FEC is told apart from one
|
|
32141
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
32142
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
32143
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
32144
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
32145
|
+
* with no loss.
|
|
31301
32146
|
*/
|
|
31302
32147
|
updateFecFailClosed(reason) {
|
|
32148
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
31303
32149
|
this.#fecFailClosed = reason;
|
|
32150
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
31304
32151
|
}
|
|
31305
32152
|
updateManifestVerification(stats) {
|
|
31306
32153
|
this.#manifestStats = stats;
|
|
31307
32154
|
}
|
|
31308
32155
|
/**
|
|
31309
|
-
*
|
|
31310
|
-
*
|
|
31311
|
-
*
|
|
31312
|
-
*
|
|
32156
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
32157
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
32158
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
32159
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
31313
32160
|
*/
|
|
32161
|
+
onRepairLifecycle(event) {
|
|
32162
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
32163
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
32164
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
32165
|
+
else this.#lifecycle.fetch++;
|
|
32166
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
32167
|
+
this.#lifecycle.failedKeyframe++;
|
|
32168
|
+
}
|
|
32169
|
+
}
|
|
32170
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
31314
32171
|
emitSwitchDrift(driftUs) {
|
|
31315
32172
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
31316
32173
|
throw new Error(
|
|
31317
32174
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
31318
32175
|
);
|
|
31319
32176
|
}
|
|
31320
|
-
const
|
|
31321
|
-
|
|
31322
|
-
|
|
31323
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31324
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
31325
|
-
this.#emit(params);
|
|
32177
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
32178
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
32179
|
+
this.#post([record]);
|
|
31326
32180
|
}
|
|
31327
|
-
/**
|
|
31328
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31329
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31330
|
-
*/
|
|
32181
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31331
32182
|
onSnapshot(data) {
|
|
31332
32183
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31333
32184
|
if (!videoTrack) return;
|
|
31334
32185
|
this.#lastStats = videoTrack;
|
|
31335
|
-
|
|
31336
|
-
...buildFecParams(videoTrack),
|
|
31337
|
-
...buildTransportParams(this.#transport)
|
|
31338
|
-
};
|
|
31339
|
-
if (this.#playMode !== null) {
|
|
31340
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
31341
|
-
}
|
|
31342
|
-
if (this.#fecFailClosed) {
|
|
31343
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
31344
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
31345
|
-
}
|
|
31346
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31347
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
31348
|
-
}
|
|
31349
|
-
if (this.#switchMiscount > 0) {
|
|
31350
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31351
|
-
}
|
|
31352
|
-
if (this.#manifestStats) {
|
|
31353
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
31354
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
31355
|
-
}
|
|
31356
|
-
this.#emit(params);
|
|
32186
|
+
this.#heartbeat();
|
|
31357
32187
|
}
|
|
31358
32188
|
/**
|
|
31359
|
-
*
|
|
31360
|
-
*
|
|
32189
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
32190
|
+
* heartbeat when due.
|
|
31361
32191
|
*/
|
|
31362
32192
|
onTransportSnapshot(data) {
|
|
31363
32193
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31364
32194
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31365
32195
|
}
|
|
31366
32196
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31367
|
-
if (data.switchMiscount !== void 0
|
|
31368
|
-
|
|
32197
|
+
if (data.switchMiscount !== void 0) {
|
|
32198
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
32199
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
32200
|
+
}
|
|
32201
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31369
32202
|
}
|
|
31370
32203
|
this.#transport = data.transport;
|
|
31371
|
-
|
|
31372
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
31373
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
31374
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
31375
|
-
this.#emit(params);
|
|
32204
|
+
this.#heartbeat();
|
|
31376
32205
|
}
|
|
31377
|
-
/**
|
|
31378
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
31379
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
31380
|
-
* x-bc-ct (one-shot).
|
|
31381
|
-
*/
|
|
32206
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31382
32207
|
onTransition(event) {
|
|
31383
|
-
const
|
|
31384
|
-
|
|
31385
|
-
|
|
32208
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
32209
|
+
putCustom(record, "ct", token(event));
|
|
32210
|
+
this.#putTransport(record);
|
|
32211
|
+
this.#post([record]);
|
|
32212
|
+
}
|
|
32213
|
+
#base(event) {
|
|
32214
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
32215
|
+
}
|
|
32216
|
+
#heartbeat() {
|
|
32217
|
+
const now = this.#now();
|
|
32218
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
32219
|
+
this.#lastHeartbeat = now;
|
|
32220
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
32221
|
+
}
|
|
32222
|
+
#putTransport(record) {
|
|
32223
|
+
const snap = this.#transport;
|
|
32224
|
+
if (!snap) return;
|
|
32225
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
32226
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
32227
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
32228
|
+
}
|
|
32229
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
32230
|
+
}
|
|
32231
|
+
/** The `e=ce` counter report. */
|
|
32232
|
+
#counters() {
|
|
32233
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
32234
|
+
const stats = this.#lastStats;
|
|
32235
|
+
if (stats) {
|
|
32236
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
32237
|
+
putCustom(record, "fk", stats.config.k);
|
|
32238
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
32239
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
32240
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
32241
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
32242
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
32243
|
+
}
|
|
32244
|
+
if (this.#lifecycle) {
|
|
32245
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
32246
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
32247
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
32248
|
+
}
|
|
32249
|
+
this.#putTransport(record);
|
|
32250
|
+
if (this.#playMode !== null) {
|
|
32251
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
32252
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
32253
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
32254
|
+
}
|
|
31386
32255
|
}
|
|
31387
|
-
|
|
31388
|
-
|
|
31389
|
-
|
|
32256
|
+
if (this.#fecFailClosed) {
|
|
32257
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
32258
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
32259
|
+
}
|
|
32260
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
32261
|
+
if (this.#manifestStats) {
|
|
32262
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
32263
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
32264
|
+
}
|
|
32265
|
+
return record;
|
|
31390
32266
|
}
|
|
31391
|
-
|
|
31392
|
-
|
|
31393
|
-
this.#onCmcdParams?.(params);
|
|
31394
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
32267
|
+
#post(records) {
|
|
32268
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31395
32269
|
}
|
|
31396
32270
|
};
|
|
31397
32271
|
|
|
@@ -31487,6 +32361,19 @@ function resolveRepairLayers(catalog, source) {
|
|
|
31487
32361
|
totalBaseRepairSymbols: binding.totalBaseRepairSymbols
|
|
31488
32362
|
};
|
|
31489
32363
|
}
|
|
32364
|
+
function valueAt(catalog, pointer) {
|
|
32365
|
+
let node = catalog;
|
|
32366
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
32367
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
32368
|
+
node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
32369
|
+
}
|
|
32370
|
+
return node;
|
|
32371
|
+
}
|
|
32372
|
+
function catalogTrackDeliveryMode(catalog, source) {
|
|
32373
|
+
const binding = bindingOf(catalog, source);
|
|
32374
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
|
|
32375
|
+
return entry?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32376
|
+
}
|
|
31490
32377
|
function moqtPriority2(value) {
|
|
31491
32378
|
return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
|
|
31492
32379
|
}
|
|
@@ -31530,6 +32417,7 @@ function fecParams(catalog, track, fps, sampleRate) {
|
|
|
31530
32417
|
repairTrackName: base.repairTrack,
|
|
31531
32418
|
repairLayers: repair.baseLayers,
|
|
31532
32419
|
repairOverlays: repair.overlays,
|
|
32420
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track),
|
|
31533
32421
|
...mediaRate
|
|
31534
32422
|
};
|
|
31535
32423
|
}
|
|
@@ -32054,7 +32942,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32054
32942
|
* or interleaved sequences read as loss.
|
|
32055
32943
|
*/
|
|
32056
32944
|
noteSourceSequence(seq) {
|
|
32057
|
-
if (this.#disposed || this.#
|
|
32945
|
+
if (this.#disposed || this.#delivery() === "stream") return;
|
|
32058
32946
|
seq >>>= 0;
|
|
32059
32947
|
if (this.#lastSeq === void 0) {
|
|
32060
32948
|
this.#lastSeq = seq;
|
|
@@ -32087,12 +32975,12 @@ var RepairLayerSubscriptions = class {
|
|
|
32087
32975
|
this.#expected = 0;
|
|
32088
32976
|
this.#lost = 0;
|
|
32089
32977
|
this.#wantFewer = 0;
|
|
32090
|
-
this.#layerCount = Math.min(this.#layerCount, this.#layers().length);
|
|
32978
|
+
this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
|
|
32091
32979
|
this.#reconcile();
|
|
32092
32980
|
}
|
|
32093
32981
|
snapshot() {
|
|
32094
32982
|
return {
|
|
32095
|
-
delivery: this.#
|
|
32983
|
+
delivery: this.#delivery(),
|
|
32096
32984
|
layerCount: this.#layerCount,
|
|
32097
32985
|
subscribedTracks: [...this.#subs.keys()],
|
|
32098
32986
|
repairPackets: this.#repairPackets,
|
|
@@ -32106,6 +32994,9 @@ var RepairLayerSubscriptions = class {
|
|
|
32106
32994
|
this.#closeAll();
|
|
32107
32995
|
this.#disposed = true;
|
|
32108
32996
|
}
|
|
32997
|
+
#delivery() {
|
|
32998
|
+
return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32999
|
+
}
|
|
32109
33000
|
#layers() {
|
|
32110
33001
|
return this.#source.repairLayers ?? [];
|
|
32111
33002
|
}
|
|
@@ -32180,6 +33071,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32180
33071
|
|
|
32181
33072
|
// src/repair-fetch.ts
|
|
32182
33073
|
var MOQT_PRIORITY_MAX = 255;
|
|
33074
|
+
var WALK_LEAD_MS = 3;
|
|
32183
33075
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
32184
33076
|
function isMoqtPriority(value) {
|
|
32185
33077
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -32230,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
32230
33122
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
32231
33123
|
};
|
|
32232
33124
|
const clearEntry = (entry) => {
|
|
32233
|
-
|
|
32234
|
-
|
|
33125
|
+
const { range } = entry;
|
|
33126
|
+
if (range) {
|
|
33127
|
+
atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
|
|
33128
|
+
} else {
|
|
33129
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
33130
|
+
}
|
|
33131
|
+
};
|
|
33132
|
+
const atReport = (range, work) => {
|
|
33133
|
+
const fec = mc.fecManager;
|
|
33134
|
+
if (range.groups !== null || fec.withReceiptTime === void 0) work();
|
|
33135
|
+
else fec.withReceiptTime(range.reportedAt, work);
|
|
32235
33136
|
};
|
|
32236
33137
|
const releaseHold = (entry) => {
|
|
32237
33138
|
if (entry.range) return;
|
|
@@ -32298,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
32298
33199
|
const admit = (entry, now) => {
|
|
32299
33200
|
const budgetMs = entry.deadlineAt - now;
|
|
32300
33201
|
const rttMs = readRtt();
|
|
32301
|
-
const reason =
|
|
33202
|
+
const reason = refusal(entry, now, rttMs);
|
|
32302
33203
|
if (reason === null) return rttMs;
|
|
32303
33204
|
skip(entry, reason, rttMs, budgetMs);
|
|
32304
33205
|
return null;
|
|
32305
33206
|
};
|
|
33207
|
+
const refusal = (entry, now, rttMs) => !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < entry.deadlineAt - now ? null : "rtt-exceeds-budget";
|
|
32306
33208
|
const retire = (entry) => {
|
|
32307
33209
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
32308
33210
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
32309
33211
|
};
|
|
32310
|
-
const
|
|
33212
|
+
const holdsBack = (entry, priority, videoWalks = true) => {
|
|
33213
|
+
if (entry.priority >= priority) return false;
|
|
33214
|
+
const { walk } = entry;
|
|
33215
|
+
if (walk === void 0) return true;
|
|
33216
|
+
if (entry.priority !== 0 || !videoWalks) return false;
|
|
33217
|
+
return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
|
|
33218
|
+
};
|
|
33219
|
+
const outstandingBelow = (priority, videoWalks = true) => {
|
|
32311
33220
|
for (const entry of entries.values()) {
|
|
32312
|
-
if (entry
|
|
33221
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
32313
33222
|
}
|
|
32314
33223
|
return false;
|
|
32315
33224
|
};
|
|
33225
|
+
const yieldVideoWalks = (queued) => {
|
|
33226
|
+
const now = performance.now();
|
|
33227
|
+
const rttMs = readRtt();
|
|
33228
|
+
const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
|
|
33229
|
+
if (!due) return;
|
|
33230
|
+
for (const entry of entries.values()) {
|
|
33231
|
+
const walk = entry.walk;
|
|
33232
|
+
if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
|
|
33233
|
+
if (walk.group.controller.signal.aborted) continue;
|
|
33234
|
+
walk.yielded = true;
|
|
33235
|
+
walk.group.controller.abort();
|
|
33236
|
+
}
|
|
33237
|
+
};
|
|
32316
33238
|
const preemptBestEffort = () => {
|
|
32317
33239
|
for (const entry of entries.values()) {
|
|
32318
33240
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -32341,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
32341
33263
|
}
|
|
32342
33264
|
const rttMs = admit(entry, now);
|
|
32343
33265
|
if (rttMs === null) return;
|
|
32344
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33266
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33267
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
32345
33268
|
if (counted) entry.attempts++;
|
|
32346
33269
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
32347
33270
|
rttMs,
|
|
@@ -32363,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
32363
33286
|
}
|
|
32364
33287
|
void execute(entry, run);
|
|
32365
33288
|
};
|
|
33289
|
+
const leaveWalkedGroup = (block) => {
|
|
33290
|
+
for (const entry of entries.values()) {
|
|
33291
|
+
const group = entry.walk?.group;
|
|
33292
|
+
if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
|
|
33293
|
+
group.controller.abort();
|
|
33294
|
+
}
|
|
33295
|
+
}
|
|
33296
|
+
};
|
|
32366
33297
|
const dispatch = () => {
|
|
32367
33298
|
if (disposed) return;
|
|
32368
33299
|
const now = performance.now();
|
|
@@ -32387,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32387
33318
|
for (const entry of queued) {
|
|
32388
33319
|
if (entry.priority === 0) start(entry);
|
|
32389
33320
|
}
|
|
33321
|
+
yieldVideoWalks(queued);
|
|
32390
33322
|
if (outstandingBelow(1)) return;
|
|
32391
33323
|
for (const entry of queued) {
|
|
32392
33324
|
if (entry.priority === 1) start(entry);
|
|
@@ -32413,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32413
33345
|
}
|
|
32414
33346
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
32415
33347
|
};
|
|
33348
|
+
const groupServed = (sbn, packetId, catalogTrackName) => {
|
|
33349
|
+
if (mc.fecManager.blockSymbolCounts === void 0) return false;
|
|
33350
|
+
const counts = symbolCounts(sbn, packetId, catalogTrackName);
|
|
33351
|
+
if (counts === void 0 || counts.authoredLength === void 0) return true;
|
|
33352
|
+
return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
|
|
33353
|
+
};
|
|
33354
|
+
const statedEnd = (sbn, packetId, catalogTrackName) => {
|
|
33355
|
+
const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
|
|
33356
|
+
return length === void 0 ? void 0 : serial32Add(sbn, length);
|
|
33357
|
+
};
|
|
32416
33358
|
const finish = (entry, run, fed, error) => {
|
|
32417
33359
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
32418
33360
|
return;
|
|
@@ -32457,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
32457
33399
|
signal: run.controller.signal,
|
|
32458
33400
|
subscriberPriority: entry.subscriberPriority
|
|
32459
33401
|
};
|
|
33402
|
+
const { range } = entry;
|
|
33403
|
+
const feed = (packet) => {
|
|
33404
|
+
if (entry.catalogTrackName === void 0) {
|
|
33405
|
+
mc.processRepairFromMoQ(packet, entry.packetId);
|
|
33406
|
+
} else {
|
|
33407
|
+
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
33408
|
+
}
|
|
33409
|
+
};
|
|
32460
33410
|
try {
|
|
32461
|
-
const packets = entry.
|
|
33411
|
+
const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
|
|
32462
33412
|
...common,
|
|
32463
|
-
startGroup:
|
|
32464
|
-
endGroup:
|
|
32465
|
-
lostSsId:
|
|
32466
|
-
})
|
|
33413
|
+
startGroup: range.groups.startGroup,
|
|
33414
|
+
endGroup: range.groups.endGroup,
|
|
33415
|
+
lostSsId: range.lostSsId
|
|
33416
|
+
});
|
|
32467
33417
|
for await (const packet of packets) {
|
|
32468
33418
|
if (run.stopped !== null || entry.running !== run) break;
|
|
32469
33419
|
fed++;
|
|
32470
|
-
if (
|
|
32471
|
-
|
|
32472
|
-
} else {
|
|
32473
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
32474
|
-
}
|
|
33420
|
+
if (range === void 0) feed(packet);
|
|
33421
|
+
else atReport(range, () => feed(packet));
|
|
32475
33422
|
}
|
|
32476
33423
|
finish(entry, run, fed, void 0);
|
|
32477
33424
|
} catch (error) {
|
|
32478
33425
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
32479
33426
|
}
|
|
32480
33427
|
};
|
|
33428
|
+
async function* walkHole(entry, walk, hole, common) {
|
|
33429
|
+
const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
|
|
33430
|
+
for (let first = true; !common.signal.aborted; first = false) {
|
|
33431
|
+
if (!first && entry.priority !== 0 && outstandingBelow(1)) {
|
|
33432
|
+
stop(entry, "preempted");
|
|
33433
|
+
note("repair-preempted", entry);
|
|
33434
|
+
return;
|
|
33435
|
+
}
|
|
33436
|
+
if (!first && entry.priority === 0) {
|
|
33437
|
+
dispatch();
|
|
33438
|
+
if (common.signal.aborted) return;
|
|
33439
|
+
}
|
|
33440
|
+
const sbn = walk.next;
|
|
33441
|
+
let next;
|
|
33442
|
+
if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
|
|
33443
|
+
const rsIds = /* @__PURE__ */ new Set();
|
|
33444
|
+
const group = new AbortController();
|
|
33445
|
+
const cancel = () => group.abort();
|
|
33446
|
+
common.signal.addEventListener("abort", cancel, { once: true });
|
|
33447
|
+
walk.group = { sbn, controller: group };
|
|
33448
|
+
try {
|
|
33449
|
+
for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
|
|
33450
|
+
yield packet;
|
|
33451
|
+
const id = extractRepairFecPayloadId(packet);
|
|
33452
|
+
if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
|
|
33453
|
+
next ??= serial32Add(sbn, id.ssbLength);
|
|
33454
|
+
rsIds.add(id.rsId);
|
|
33455
|
+
if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
|
|
33456
|
+
group.abort();
|
|
33457
|
+
break;
|
|
33458
|
+
}
|
|
33459
|
+
}
|
|
33460
|
+
} catch (error) {
|
|
33461
|
+
if (!group.signal.aborted || common.signal.aborted) throw error;
|
|
33462
|
+
} finally {
|
|
33463
|
+
common.signal.removeEventListener("abort", cancel);
|
|
33464
|
+
walk.group = null;
|
|
33465
|
+
}
|
|
33466
|
+
if (walk.yielded) {
|
|
33467
|
+
walk.yielded = false;
|
|
33468
|
+
continue;
|
|
33469
|
+
}
|
|
33470
|
+
}
|
|
33471
|
+
next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
|
|
33472
|
+
if (next === void 0) return;
|
|
33473
|
+
if (serial32Distance(entry.sbn, next) > lostAt) return;
|
|
33474
|
+
walk.next = next;
|
|
33475
|
+
}
|
|
33476
|
+
}
|
|
32481
33477
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
32482
33478
|
for (const entry of [...entries.values()]) {
|
|
32483
33479
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -32673,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
32673
33669
|
throw error;
|
|
32674
33670
|
}
|
|
32675
33671
|
};
|
|
32676
|
-
|
|
33672
|
+
const trackKind = (packetId) => {
|
|
33673
|
+
try {
|
|
33674
|
+
return mc.fecManager.trackKindFor?.(packetId);
|
|
33675
|
+
} catch {
|
|
33676
|
+
return void 0;
|
|
33677
|
+
}
|
|
33678
|
+
};
|
|
33679
|
+
const walkDeadlineAt = (packetId, now, deadlineAt) => {
|
|
33680
|
+
let repairDeadlineMs;
|
|
33681
|
+
try {
|
|
33682
|
+
repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
|
|
33683
|
+
} catch {
|
|
33684
|
+
repairDeadlineMs = void 0;
|
|
33685
|
+
}
|
|
33686
|
+
if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
|
|
33687
|
+
return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
|
|
33688
|
+
};
|
|
33689
|
+
const onHoleExpired = (hole) => {
|
|
33690
|
+
if (disposed) return false;
|
|
33691
|
+
const entry = [...entries.values()].find((candidate) => candidate.walk !== void 0 && candidate.packetId === hole.packetId && candidate.catalogTrackName === hole.catalogTrackName && serial32Distance(candidate.sbn, hole.holeStart) < candidate.range.holeLength);
|
|
33692
|
+
if (entry === void 0) return false;
|
|
33693
|
+
queueMicrotask(() => {
|
|
33694
|
+
if (entries.get(entry.key) !== entry) return;
|
|
33695
|
+
retire(entry);
|
|
33696
|
+
stop(entry, "expired");
|
|
33697
|
+
clearEntry(entry);
|
|
33698
|
+
dispatch();
|
|
33699
|
+
});
|
|
33700
|
+
return true;
|
|
33701
|
+
};
|
|
33702
|
+
if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
|
|
32677
33703
|
mc.onUndelimitedLoss((loss) => {
|
|
32678
33704
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
32679
33705
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -32681,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
32681
33707
|
const blockProfile = blockProfileOf(packetId);
|
|
32682
33708
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
32683
33709
|
if (!target) return close();
|
|
32684
|
-
const
|
|
32685
|
-
const
|
|
32686
|
-
|
|
33710
|
+
const walked = opts.rangeTransport === void 0;
|
|
33711
|
+
const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
|
|
33712
|
+
const priority = repairPriorityForClass(holeClass);
|
|
33713
|
+
const groups = walked ? null : rangeFor(loss, target.repairTrack);
|
|
33714
|
+
if (!walked && groups === null) {
|
|
32687
33715
|
close();
|
|
32688
33716
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
32689
33717
|
return;
|
|
@@ -32694,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
32694
33722
|
packetId,
|
|
32695
33723
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
32696
33724
|
};
|
|
32697
|
-
const wire = wirePriority(identity,
|
|
32698
|
-
|
|
33725
|
+
const wire = wirePriority(identity, holeClass);
|
|
33726
|
+
if (wire === null) return close();
|
|
33727
|
+
const now = performance.now();
|
|
33728
|
+
const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
|
|
32699
33729
|
if (deadline === null) {
|
|
32700
33730
|
close();
|
|
32701
33731
|
mc.noteRepairSkipped(
|
|
@@ -32707,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32707
33737
|
);
|
|
32708
33738
|
return;
|
|
32709
33739
|
}
|
|
33740
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
32710
33741
|
const entry = {
|
|
32711
33742
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
32712
33743
|
...identity,
|
|
@@ -32714,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32714
33745
|
blockProfile,
|
|
32715
33746
|
priority,
|
|
32716
33747
|
subscriberPriority: wire.subscriberPriority,
|
|
32717
|
-
deadlineAt
|
|
32718
|
-
playoutDeadlineAt:
|
|
33748
|
+
deadlineAt,
|
|
33749
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
32719
33750
|
anchorAt: deadline.anchorAt,
|
|
32720
33751
|
delayMs: deadline.delayMs,
|
|
32721
33752
|
bound: deadline.bound,
|
|
32722
33753
|
order: enqueueOrder++,
|
|
32723
33754
|
attempts: 0,
|
|
32724
33755
|
running: null,
|
|
32725
|
-
range: {
|
|
33756
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33757
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
32726
33758
|
};
|
|
32727
33759
|
const previous = entries.get(entry.key);
|
|
32728
33760
|
if (previous) {
|
|
@@ -32732,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32732
33764
|
entries.set(entry.key, entry);
|
|
32733
33765
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
32734
33766
|
dispatch();
|
|
32735
|
-
});
|
|
33767
|
+
}, onHoleExpired);
|
|
32736
33768
|
}
|
|
32737
33769
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
32738
33770
|
queueMicrotask(() => {
|
|
@@ -32848,6 +33880,7 @@ export {
|
|
|
32848
33880
|
catalogGroupDurationMs,
|
|
32849
33881
|
catalogKeyframeCadenceMs,
|
|
32850
33882
|
catalogRepairTarget,
|
|
33883
|
+
catalogTrackDeliveryMode,
|
|
32851
33884
|
catalogTrackFecUnavailable,
|
|
32852
33885
|
classifyH264Keyframe,
|
|
32853
33886
|
classifyHevcKeyframe,
|
|
@@ -32868,6 +33901,7 @@ export {
|
|
|
32868
33901
|
detectH264Keyframe,
|
|
32869
33902
|
detectHevcKeyframe,
|
|
32870
33903
|
driftCheck,
|
|
33904
|
+
encodeCmcdRecord,
|
|
32871
33905
|
encodeDataSegment,
|
|
32872
33906
|
encodeFecSourceInfo,
|
|
32873
33907
|
encodeLocTimestamp,
|