@blockcast/mmt-container 0.1.0-main.fcefcc30604d → 0.1.0-main.ffdace838275
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-runtime.js +859 -115
- package/dist/browser-runtime.js.map +3 -3
- package/dist/carp-catalog.d.ts +1 -2
- package/dist/carp-catalog.d.ts.map +1 -1
- package/dist/carp-catalog.js +32 -23
- package/dist/carp-catalog.js.map +2 -2
- package/dist/catalog-fec-params.d.ts +2 -4
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +3 -4
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/delivery-gap.d.ts +75 -0
- package/dist/delivery-gap.d.ts.map +1 -0
- package/dist/delivery-gap.js +88 -0
- package/dist/delivery-gap.js.map +7 -0
- package/dist/fec-manager.d.ts +80 -2
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +528 -56
- package/dist/fec-manager.js.map +3 -3
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/media-router.d.ts +12 -4
- package/dist/media-router.d.ts.map +1 -1
- package/dist/media-router.js +22 -4
- package/dist/media-router.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +32 -23
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-router.d.ts +15 -4
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +51 -4
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +48 -6
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +710 -72
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +34 -5
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +87 -11
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +32 -23
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-fetch.d.ts +51 -2
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +76 -3
- package/dist/repair-fetch.js.map +2 -2
- package/dist/track-session.d.ts +15 -9
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +15 -9
- package/dist/track-session.js.map +2 -2
- package/package.json +6 -6
- package/src/carp-catalog.ts +34 -29
- package/src/catalog-fec-params.ts +6 -9
- package/src/delivery-gap.ts +147 -0
- package/src/fec-manager.ts +587 -67
- package/src/index.ts +3 -0
- package/src/media-router.ts +36 -8
- package/src/mmtp-router.ts +81 -8
- package/src/multicast-manager.ts +78 -3
- package/src/multicast-transport.ts +104 -14
- package/src/repair-fetch.ts +170 -8
- package/src/track-session.ts +15 -9
- package/test-vectors/catalog-validation.json +469 -25
package/dist/browser-runtime.js
CHANGED
|
@@ -6308,10 +6308,15 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
6308
6308
|
if (!Array.isArray(catalog["tracks"])) {
|
|
6309
6309
|
catalogError("catalog_tracks", "$.tracks", "tracks must be an array");
|
|
6310
6310
|
}
|
|
6311
|
-
|
|
6311
|
+
if ("commonTrackFields" in catalog) {
|
|
6312
|
+
catalogError(
|
|
6313
|
+
"catalog_common_track_fields",
|
|
6314
|
+
"$.commonTrackFields",
|
|
6315
|
+
"commonTrackFields is retired; every track carries its own fields"
|
|
6316
|
+
);
|
|
6317
|
+
}
|
|
6312
6318
|
const tracks = catalog["tracks"];
|
|
6313
6319
|
const trackRecords = [];
|
|
6314
|
-
const effective = (track, field) => track[field] ?? common[field];
|
|
6315
6320
|
const positiveInteger2 = (input) => Number.isSafeInteger(input) && input > 0;
|
|
6316
6321
|
const positiveNumber = (input) => typeof input === "number" && Number.isFinite(input) && input > 0;
|
|
6317
6322
|
for (let index = 0; index < tracks.length; index++) {
|
|
@@ -6326,28 +6331,32 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
6326
6331
|
catalogError("track_name", `${path}.name`, "name must be a non-empty string");
|
|
6327
6332
|
}
|
|
6328
6333
|
trackRecords.push(track);
|
|
6329
|
-
const packaging =
|
|
6334
|
+
const packaging = track["packaging"];
|
|
6330
6335
|
if (typeof packaging !== "string" || packaging.length === 0) {
|
|
6331
6336
|
catalogError("track_packaging", `${path}.packaging`, "packaging must be a non-empty string");
|
|
6332
6337
|
}
|
|
6333
|
-
const
|
|
6334
|
-
const
|
|
6335
|
-
const isLive = trackHasIsLive ? track["isLive"] : common["isLive"];
|
|
6338
|
+
const hasIsLive = Object.prototype.hasOwnProperty.call(track, "isLive");
|
|
6339
|
+
const isLive = track["isLive"];
|
|
6336
6340
|
if (hasIsLive && typeof isLive !== "boolean") {
|
|
6337
6341
|
catalogError("track_is_live", `${path}.isLive`, "isLive must be a boolean");
|
|
6338
6342
|
}
|
|
6339
6343
|
track["isLive"] = isLive ?? true;
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6344
|
+
if ("selectionParams" in track) {
|
|
6345
|
+
catalogError(
|
|
6346
|
+
"track_selection_params",
|
|
6347
|
+
`${path}.selectionParams`,
|
|
6348
|
+
"selectionParams is retired; codec and the other selection fields are flat on the track"
|
|
6349
|
+
);
|
|
6350
|
+
}
|
|
6351
|
+
const codec = track["codec"];
|
|
6343
6352
|
if (packaging !== "timeline" && packaging !== "mediatimeline" && packaging !== "eventtimeline" && packaging !== "fec-repair" && (typeof codec !== "string" || codec.length === 0)) {
|
|
6344
6353
|
catalogError("track_codec", `${path}.codec`, `codec is required for packaging=${packaging}`);
|
|
6345
6354
|
}
|
|
6346
|
-
const timescale =
|
|
6347
|
-
const groupDurationMs =
|
|
6348
|
-
const groupDurationTicks =
|
|
6349
|
-
const keyframeIntervalMs =
|
|
6350
|
-
const keyframeIntervalTicks =
|
|
6355
|
+
const timescale = track["timescale"];
|
|
6356
|
+
const groupDurationMs = track["groupDurationMs"];
|
|
6357
|
+
const groupDurationTicks = track["groupDurationTicks"];
|
|
6358
|
+
const keyframeIntervalMs = track["keyframeIntervalMs"];
|
|
6359
|
+
const keyframeIntervalTicks = track["keyframeIntervalTicks"];
|
|
6351
6360
|
if (groupDurationMs !== void 0 && !positiveInteger2(groupDurationMs)) {
|
|
6352
6361
|
catalogError("track_group_duration_ms", `${path}.groupDurationMs`, "must be a positive integer");
|
|
6353
6362
|
}
|
|
@@ -6364,7 +6373,7 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
6364
6373
|
rejectRepairFieldsOnMediaTrack(path, track, catalogError);
|
|
6365
6374
|
}
|
|
6366
6375
|
if (packaging === "mmtp") {
|
|
6367
|
-
const mmtpMode =
|
|
6376
|
+
const mmtpMode = track["mmtpMode"];
|
|
6368
6377
|
if (mmtpMode !== "mpu" && mmtpMode !== "mfu") {
|
|
6369
6378
|
catalogError("track_mmtp_mode", `${path}.mmtpMode`, "must be mpu or mfu");
|
|
6370
6379
|
}
|
|
@@ -6418,10 +6427,10 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
6418
6427
|
}
|
|
6419
6428
|
}
|
|
6420
6429
|
const repairDescriptors = trackRecords.flatMap(
|
|
6421
|
-
(track, index) =>
|
|
6430
|
+
(track, index) => track["packaging"] === "fec-repair" ? [describeRepairTrack(track["name"], `$.tracks[${index}]`, track)] : []
|
|
6422
6431
|
);
|
|
6423
6432
|
const reachability = repairReachability(
|
|
6424
|
-
trackRecords.filter((track) =>
|
|
6433
|
+
trackRecords.filter((track) => track["packaging"] !== "fec-repair").map((track) => track["fec"])
|
|
6425
6434
|
);
|
|
6426
6435
|
for (const descriptor of repairDescriptors) {
|
|
6427
6436
|
assertRepairTrackReachable(descriptor, reachability, catalogError);
|
|
@@ -6429,7 +6438,7 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
6429
6438
|
if (target === void 0) continue;
|
|
6430
6439
|
const source = trackRecords.find((candidate) => candidate["name"] === target);
|
|
6431
6440
|
const sourceFec = source?.["fec"];
|
|
6432
|
-
if (!source ||
|
|
6441
|
+
if (!source || source["packaging"] !== "mmtp" || !sourceFec || sourceFec["algorithm"] === "none") {
|
|
6433
6442
|
catalogError(
|
|
6434
6443
|
"track_repair_depends",
|
|
6435
6444
|
`${descriptor.path}.depends`,
|
|
@@ -6458,12 +6467,12 @@ function validateCmsfMmtpCatalog(value) {
|
|
|
6458
6467
|
const switchingSets = /* @__PURE__ */ new Map();
|
|
6459
6468
|
for (let index = 0; index < trackRecords.length; index++) {
|
|
6460
6469
|
const track = trackRecords[index];
|
|
6461
|
-
if (
|
|
6462
|
-
const altGroup =
|
|
6470
|
+
if (track["packaging"] !== "mmtp") continue;
|
|
6471
|
+
const altGroup = track["altGroup"];
|
|
6463
6472
|
if (!Number.isSafeInteger(altGroup)) continue;
|
|
6464
|
-
const timescale = BigInt(
|
|
6465
|
-
const durationTicks =
|
|
6466
|
-
const ticks = durationTicks !== void 0 ? BigInt(durationTicks) : BigInt(
|
|
6473
|
+
const timescale = BigInt(track["timescale"]);
|
|
6474
|
+
const durationTicks = track["groupDurationTicks"];
|
|
6475
|
+
const ticks = durationTicks !== void 0 ? BigInt(durationTicks) : BigInt(track["groupDurationMs"]) * timescale / 1000n;
|
|
6467
6476
|
const anchor = switchingSets.get(altGroup);
|
|
6468
6477
|
if (anchor && ticks * anchor.timescale !== anchor.ticks * timescale) {
|
|
6469
6478
|
catalogError("track_switching_set", `$.tracks[${index}]`, `group duration does not match ${JSON.stringify(anchor.name)} in altGroup ${altGroup}`);
|
|
@@ -9522,15 +9531,21 @@ var SessionManager = class {
|
|
|
9522
9531
|
* deadline before the subscription existed and abandoned the join on the
|
|
9523
9532
|
* happy path. BLO-34289.
|
|
9524
9533
|
*
|
|
9525
|
-
*
|
|
9526
|
-
*
|
|
9527
|
-
*
|
|
9528
|
-
*
|
|
9529
|
-
*
|
|
9530
|
-
*
|
|
9531
|
-
*
|
|
9532
|
-
*
|
|
9533
|
-
*
|
|
9534
|
+
* The deadline is a media budget (group cadence plus FEC delivery window) and
|
|
9535
|
+
* budgets nothing for reaching the stream, so ideally it starts at
|
|
9536
|
+
* MulticastTransport.onDataPlaneLive, the join's first accepted packet. Over
|
|
9537
|
+
* AMT that is measurably later than a settled subscribe: the subscribe
|
|
9538
|
+
* settles once the gateway has sent its Membership Update, and the relay's
|
|
9539
|
+
* upstream join plus a path RTT (0.4-0.8 s measured to the prod relay) still
|
|
9540
|
+
* precede the first packet. But RFC 7450 gives a gateway no signal that the
|
|
9541
|
+
* relay has traffic for its (S,G), so a join whose source never sends never
|
|
9542
|
+
* reaches that edge. Starting the deadline there is therefore sound only when
|
|
9543
|
+
* the provider bounds the join phase itself, with a typed subscription error
|
|
9544
|
+
* for a join that delivers nothing (MulticastTransport forwards it to
|
|
9545
|
+
* onFailback). A host whose provider makes no such guarantee flips this true
|
|
9546
|
+
* when the subscribe settles and accepts that join latency is charged to the
|
|
9547
|
+
* media budget; holding it until the first packet would park a silent join
|
|
9548
|
+
* in `joining` forever.
|
|
9534
9549
|
*
|
|
9535
9550
|
* The flag tracks the host's primary join, not per-track transport state. A
|
|
9536
9551
|
* track that fails over to the next transport in its own chain while this is
|
|
@@ -9767,6 +9782,91 @@ var SessionManager = class {
|
|
|
9767
9782
|
}
|
|
9768
9783
|
};
|
|
9769
9784
|
|
|
9785
|
+
// src/delivery-gap.ts
|
|
9786
|
+
var DeliveryGapEstimator = class {
|
|
9787
|
+
/** Most recent arrival, or undefined before the first one since reset. */
|
|
9788
|
+
#lastArrivalMs;
|
|
9789
|
+
/** Last MMTP packet_sequence_number seen per packet_id. */
|
|
9790
|
+
#lastSequence = /* @__PURE__ */ new Map();
|
|
9791
|
+
/**
|
|
9792
|
+
* Monotonic deque of (arrivalMs, gapMs): gaps strictly decrease from head to
|
|
9793
|
+
* tail, so the head is the window maximum. Amortized O(1) per arrival.
|
|
9794
|
+
*/
|
|
9795
|
+
#times = [];
|
|
9796
|
+
#gaps = [];
|
|
9797
|
+
#head = 0;
|
|
9798
|
+
#windowMs;
|
|
9799
|
+
/**
|
|
9800
|
+
* @param windowMs trailing window in ms, which also caps the reported gap,
|
|
9801
|
+
* or undefined when the catalog gives none (no gap is reported).
|
|
9802
|
+
*/
|
|
9803
|
+
setWindowMs(windowMs) {
|
|
9804
|
+
if (windowMs !== void 0 && !(Number.isFinite(windowMs) && windowMs > 0)) {
|
|
9805
|
+
throw new Error(
|
|
9806
|
+
`[DeliveryGap] window must be a finite positive ms value or undefined (got ${windowMs})`
|
|
9807
|
+
);
|
|
9808
|
+
}
|
|
9809
|
+
this.#windowMs = windowMs;
|
|
9810
|
+
}
|
|
9811
|
+
get windowMs() {
|
|
9812
|
+
return this.#windowMs;
|
|
9813
|
+
}
|
|
9814
|
+
/**
|
|
9815
|
+
* Record one multicast datagram arrival at receiver time `nowMs`, carrying
|
|
9816
|
+
* MMTP `packetSequenceNumber` (32-bit, per packet_id) on `packetId`.
|
|
9817
|
+
*/
|
|
9818
|
+
noteArrival(nowMs, packetId, packetSequenceNumber) {
|
|
9819
|
+
const last = this.#lastArrivalMs;
|
|
9820
|
+
const previousSequence = this.#lastSequence.get(packetId);
|
|
9821
|
+
this.#lastArrivalMs = nowMs;
|
|
9822
|
+
this.#lastSequence.set(packetId, packetSequenceNumber);
|
|
9823
|
+
if (last === void 0 || previousSequence === void 0) return;
|
|
9824
|
+
if (this.#windowMs === void 0) return;
|
|
9825
|
+
if (previousSequence + 1 >>> 0 !== packetSequenceNumber >>> 0) return;
|
|
9826
|
+
const gap = nowMs - last;
|
|
9827
|
+
this.#evict(nowMs);
|
|
9828
|
+
while (this.#gaps.length > this.#head && this.#gaps[this.#gaps.length - 1] <= gap) {
|
|
9829
|
+
this.#gaps.pop();
|
|
9830
|
+
this.#times.pop();
|
|
9831
|
+
}
|
|
9832
|
+
this.#gaps.push(gap);
|
|
9833
|
+
this.#times.push(nowMs);
|
|
9834
|
+
this.#compact();
|
|
9835
|
+
}
|
|
9836
|
+
/**
|
|
9837
|
+
* Largest gap observed within the window ending at `nowMs`, at most the
|
|
9838
|
+
* window itself; 0 if none or when no window is set.
|
|
9839
|
+
*/
|
|
9840
|
+
peakGapMs(nowMs) {
|
|
9841
|
+
const windowMs = this.#windowMs;
|
|
9842
|
+
if (windowMs === void 0) return 0;
|
|
9843
|
+
this.#evict(nowMs);
|
|
9844
|
+
return this.#gaps.length > this.#head ? Math.min(this.#gaps[this.#head], windowMs) : 0;
|
|
9845
|
+
}
|
|
9846
|
+
/** Forget everything: a new delivery path starts with no measurement. */
|
|
9847
|
+
reset() {
|
|
9848
|
+
this.#lastArrivalMs = void 0;
|
|
9849
|
+
this.#lastSequence.clear();
|
|
9850
|
+
this.#times = [];
|
|
9851
|
+
this.#gaps = [];
|
|
9852
|
+
this.#head = 0;
|
|
9853
|
+
}
|
|
9854
|
+
#evict(nowMs) {
|
|
9855
|
+
const windowMs = this.#windowMs;
|
|
9856
|
+
if (windowMs === void 0) return;
|
|
9857
|
+
while (this.#times.length > this.#head && this.#times[this.#head] < nowMs - windowMs) {
|
|
9858
|
+
this.#head++;
|
|
9859
|
+
}
|
|
9860
|
+
}
|
|
9861
|
+
#compact() {
|
|
9862
|
+
if (this.#head > 0 && this.#head * 2 >= this.#times.length) {
|
|
9863
|
+
this.#times = this.#times.slice(this.#head);
|
|
9864
|
+
this.#gaps = this.#gaps.slice(this.#head);
|
|
9865
|
+
this.#head = 0;
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9868
|
+
};
|
|
9869
|
+
|
|
9770
9870
|
// src/block-policy.ts
|
|
9771
9871
|
function repairPriorityForClass(blockClass) {
|
|
9772
9872
|
switch (blockClass) {
|
|
@@ -10231,6 +10331,9 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
10231
10331
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
10232
10332
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
10233
10333
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
10334
|
+
function isMulticastSymbolDelivery(value) {
|
|
10335
|
+
return value === "datagram" || value === "ordered-relay";
|
|
10336
|
+
}
|
|
10234
10337
|
function newFecCounters() {
|
|
10235
10338
|
return {
|
|
10236
10339
|
sourcePackets: 0,
|
|
@@ -10296,6 +10399,17 @@ var FECManager = class {
|
|
|
10296
10399
|
// deadline; the timer re-checks it when it fires (see DeadlineTimer).
|
|
10297
10400
|
this.#activeBlocks = /* @__PURE__ */ new Map();
|
|
10298
10401
|
this.#activeBlockDeadlinesMs = /* @__PURE__ */ new Map();
|
|
10402
|
+
// The two terms #activeBlockDeadlinesMs is the later of (BLO-35597):
|
|
10403
|
+
// - the multicast late-symbol wait, refreshed by every accepted symbol;
|
|
10404
|
+
// - a D-28 repair hold, set by the repair scheduler only while a repair
|
|
10405
|
+
// FETCH it admitted can still land before the block's presentation
|
|
10406
|
+
// deadline. The multicast span elapsing never ends a fetch the scheduler
|
|
10407
|
+
// judged able to arrive in time.
|
|
10408
|
+
this.#blockWaitDeadlinesMs = /* @__PURE__ */ new Map();
|
|
10409
|
+
this.#repairHoldUntilMs = /* @__PURE__ */ new Map();
|
|
10410
|
+
// Receiver-measured multicast delivery gap: the late-symbol wait's jitter
|
|
10411
|
+
// term (see DeliveryGapEstimator for the statistic and why).
|
|
10412
|
+
this.#deliveryGaps = new DeliveryGapEstimator();
|
|
10299
10413
|
this.#lifecycleTrace = [];
|
|
10300
10414
|
// Worker-backed timeouts enqueue cleanup behind all prior symbol messages.
|
|
10301
10415
|
// Failure remains pending until cleanupComplete acknowledges that queue drain.
|
|
@@ -10353,6 +10467,9 @@ var FECManager = class {
|
|
|
10353
10467
|
this.#lastStreamKey = "";
|
|
10354
10468
|
this.#seenSymbols = /* @__PURE__ */ new Set();
|
|
10355
10469
|
this.#blockPathCounts = /* @__PURE__ */ new Map();
|
|
10470
|
+
// Receiver-local first-accepted-symbol time per block, bounded like
|
|
10471
|
+
// #blockPathCounts (see blockAnchorAtMs).
|
|
10472
|
+
this.#blockAnchorsMs = /* @__PURE__ */ new Map();
|
|
10356
10473
|
// Exact catalog identity observed on accepted symbols for each FEC block.
|
|
10357
10474
|
// Conflicting identities collapse to null so recovery fails closed.
|
|
10358
10475
|
this.#blockCatalogTrackNames = /* @__PURE__ */ new Map();
|
|
@@ -10400,6 +10517,19 @@ var FECManager = class {
|
|
|
10400
10517
|
// Worker recovery emits all K source symbols. Track recovery once per block
|
|
10401
10518
|
// while routing only ESI slots that were absent from the direct source path.
|
|
10402
10519
|
this.#workerRecoveredBlocks = /* @__PURE__ */ new Set();
|
|
10520
|
+
// How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
|
|
10521
|
+
// provider declares it per transport join; absence keeps datagram semantics.
|
|
10522
|
+
this.#multicastSymbolDelivery = "datagram";
|
|
10523
|
+
// ordered-relay: per stream, the highest SBN any multicast symbol carried.
|
|
10524
|
+
// The relay preserves the receiving socket's order, so every block more than
|
|
10525
|
+
// the decoder's interleave horizon behind this frontier has received every
|
|
10526
|
+
// symbol it will ever receive over multicast.
|
|
10527
|
+
this.#orderedFrontiers = /* @__PURE__ */ new Map();
|
|
10528
|
+
// ordered-relay: blocks with multicast symbols that are neither terminal nor
|
|
10529
|
+
// handed to a decoder cleanup, per stream, keyed by SBN. They carry no
|
|
10530
|
+
// timer; the frontier settles them. At most one interleave horizon of blocks
|
|
10531
|
+
// per stream is ever here.
|
|
10532
|
+
this.#orderedPendingBlocks = /* @__PURE__ */ new Map();
|
|
10403
10533
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
10404
10534
|
// One classifier per physical stream, built from that stream's geometry.
|
|
10405
10535
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
@@ -10506,6 +10636,9 @@ var FECManager = class {
|
|
|
10506
10636
|
#fecStatsObservers;
|
|
10507
10637
|
#activeBlocks;
|
|
10508
10638
|
#activeBlockDeadlinesMs;
|
|
10639
|
+
#blockWaitDeadlinesMs;
|
|
10640
|
+
#repairHoldUntilMs;
|
|
10641
|
+
#deliveryGaps;
|
|
10509
10642
|
#lifecycleTrace;
|
|
10510
10643
|
#pendingWorkerCleanups;
|
|
10511
10644
|
#pendingWorkerCleanupClients;
|
|
@@ -10529,6 +10662,7 @@ var FECManager = class {
|
|
|
10529
10662
|
#lastStreamKey;
|
|
10530
10663
|
#seenSymbols;
|
|
10531
10664
|
#blockPathCounts;
|
|
10665
|
+
#blockAnchorsMs;
|
|
10532
10666
|
#blockCatalogTrackNames;
|
|
10533
10667
|
#startupAlignedPackets;
|
|
10534
10668
|
#startupPartialBlocks;
|
|
@@ -10541,6 +10675,9 @@ var FECManager = class {
|
|
|
10541
10675
|
#repairDeadlines;
|
|
10542
10676
|
#repairDeadlineAtMs;
|
|
10543
10677
|
#workerRecoveredBlocks;
|
|
10678
|
+
#multicastSymbolDelivery;
|
|
10679
|
+
#orderedFrontiers;
|
|
10680
|
+
#orderedPendingBlocks;
|
|
10544
10681
|
#classifiers;
|
|
10545
10682
|
#streamGeometries;
|
|
10546
10683
|
#failedBlocks;
|
|
@@ -10640,6 +10777,7 @@ var FECManager = class {
|
|
|
10640
10777
|
#markTerminal(key, sbn, packetId, clean) {
|
|
10641
10778
|
const nowMs = performance.now();
|
|
10642
10779
|
this.#retireTerminalBlocks(packetId, nowMs);
|
|
10780
|
+
this.#forgetOrderedPending(key);
|
|
10643
10781
|
this.#completedBlocks.add(key);
|
|
10644
10782
|
if (clean) this.#cleanlyCompletedBlocks.add(key);
|
|
10645
10783
|
this.#indexBlockKey(key, sbn, packetId);
|
|
@@ -10657,8 +10795,10 @@ var FECManager = class {
|
|
|
10657
10795
|
if (!queue) return;
|
|
10658
10796
|
for (let record = queue.peek(); record !== void 0; record = queue.peek()) {
|
|
10659
10797
|
if (record.expiresAtMs > nowMs) return;
|
|
10798
|
+
const current = this.#terminalExpiryMs.get(record.key) === record.expiresAtMs;
|
|
10799
|
+
if (current && this.#withinOrderedHorizon(record.key)) return;
|
|
10660
10800
|
queue.shift();
|
|
10661
|
-
if (
|
|
10801
|
+
if (!current) continue;
|
|
10662
10802
|
this.#terminalExpiryMs.delete(record.key);
|
|
10663
10803
|
this.#completedBlocks.delete(record.key);
|
|
10664
10804
|
this.#cleanlyCompletedBlocks.delete(record.key);
|
|
@@ -10669,12 +10809,143 @@ var FECManager = class {
|
|
|
10669
10809
|
this.#unindexBlockKey(record.key);
|
|
10670
10810
|
}
|
|
10671
10811
|
}
|
|
10812
|
+
// -----------------------------------------------------------------------
|
|
10813
|
+
// Ordered-relay settlement (MulticastSymbolDelivery 'ordered-relay')
|
|
10814
|
+
// -----------------------------------------------------------------------
|
|
10815
|
+
/**
|
|
10816
|
+
* The decoder's interleave horizon for one stream, in blocks: the same
|
|
10817
|
+
* `interleaveDepth` the Worker and the WASM decoder are configured with
|
|
10818
|
+
* (fec-worker-setup derives it from the catalog interleaveDepthMs and the
|
|
10819
|
+
* media-unit duration). The decoder already retires every block more than
|
|
10820
|
+
* this many SBNs behind the newest one it saw (`cleanup_by_sbn`), so it is
|
|
10821
|
+
* the stream-order reorder allowance the receiver has already committed to.
|
|
10822
|
+
*/
|
|
10823
|
+
#orderedHorizonBlocksFor(packetId, config) {
|
|
10824
|
+
const horizon = this.#derivableOrderedHorizon(config);
|
|
10825
|
+
if (horizon === void 0) {
|
|
10826
|
+
throw new Error(
|
|
10827
|
+
`[FECManager] packetId=${packetId}: ordered-relay settlement derives the decoder's interleave horizon from the catalog interleaveDepthMs (>= ${MIN_INTERLEAVE_DEPTH_MS}ms) and the media-unit rate; got interleaveDepthMs=${config.interleaveDepthMs} fps=${String(config.fps)}.`
|
|
10828
|
+
);
|
|
10829
|
+
}
|
|
10830
|
+
return horizon;
|
|
10831
|
+
}
|
|
10832
|
+
#derivableOrderedHorizon(config) {
|
|
10833
|
+
if (!(config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) || config.fps === void 0 || !Number.isFinite(config.fps) || config.fps <= 0) {
|
|
10834
|
+
return void 0;
|
|
10835
|
+
}
|
|
10836
|
+
return deriveFecInterleaveDepth(config.interleaveDepthMs, 1e3 / config.fps);
|
|
10837
|
+
}
|
|
10838
|
+
/**
|
|
10839
|
+
* The stream's interleave horizon, or undefined when its geometry cannot
|
|
10840
|
+
* define one. Explicitly configured tracks are refused that case at config
|
|
10841
|
+
* time (#upsertTrackConfig, setMulticastSymbolDelivery); only a packetId that
|
|
10842
|
+
* was never configured, and so resolves to the manager defaults, reaches it
|
|
10843
|
+
* here, and such a stream keeps datagram semantics rather than throwing from
|
|
10844
|
+
* the packet-feed path. Those defaults can change under a stream that already
|
|
10845
|
+
* holds blocks, so a caller cannot assume a horizon that was defined when a
|
|
10846
|
+
* block was held is still defined (see #advanceOrderedFrontier).
|
|
10847
|
+
*/
|
|
10848
|
+
#orderedHorizonBlocks(packetId) {
|
|
10849
|
+
return this.#derivableOrderedHorizon(this.#resolveTrackConfig(packetId));
|
|
10850
|
+
}
|
|
10851
|
+
/** True while a block of an ordered-relay stream can still receive symbols. */
|
|
10852
|
+
#withinOrderedHorizon(key) {
|
|
10853
|
+
if (this.#multicastSymbolDelivery !== "ordered-relay") return false;
|
|
10854
|
+
const block = this.#parseBlockKey(key);
|
|
10855
|
+
const frontier = this.#orderedFrontiers.get(
|
|
10856
|
+
this.#streamKey(block.packetId, block.catalogTrackName)
|
|
10857
|
+
);
|
|
10858
|
+
if (frontier === void 0) return false;
|
|
10859
|
+
const horizon = this.#orderedHorizonBlocks(block.packetId);
|
|
10860
|
+
if (horizon === void 0) return false;
|
|
10861
|
+
return block.sbn >= frontier - horizon;
|
|
10862
|
+
}
|
|
10863
|
+
/** Whether a block's fate is decided by stream order rather than a timer. */
|
|
10864
|
+
#settlesByOrder(key) {
|
|
10865
|
+
return this.#multicastSymbolDelivery === "ordered-relay" && (this.#blockPathCounts.get(key)?.multicastSymbols ?? 0) > 0 && this.#orderedHorizonBlocks(this.#parseBlockKey(key).packetId) !== void 0;
|
|
10866
|
+
}
|
|
10867
|
+
#forgetOrderedPending(key) {
|
|
10868
|
+
if (this.#orderedPendingBlocks.size === 0) return;
|
|
10869
|
+
const block = this.#parseBlockKey(key);
|
|
10870
|
+
const streamKey = this.#streamKey(block.packetId, block.catalogTrackName);
|
|
10871
|
+
const pending = this.#orderedPendingBlocks.get(streamKey);
|
|
10872
|
+
if (pending?.get(block.sbn) !== key) return;
|
|
10873
|
+
pending.delete(block.sbn);
|
|
10874
|
+
if (pending.size === 0) this.#orderedPendingBlocks.delete(streamKey);
|
|
10875
|
+
}
|
|
10876
|
+
/**
|
|
10877
|
+
* Record the stream position of a multicast symbol and settle every pending
|
|
10878
|
+
* block it proves final: a block more than the interleave horizon behind the
|
|
10879
|
+
* frontier has already been handed every symbol the relay will deliver.
|
|
10880
|
+
*/
|
|
10881
|
+
#advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName) {
|
|
10882
|
+
const previous = this.#orderedFrontiers.get(streamKey);
|
|
10883
|
+
const frontier = previous === void 0 || sbn > previous ? sbn : previous;
|
|
10884
|
+
if (frontier !== previous) this.#orderedFrontiers.set(streamKey, frontier);
|
|
10885
|
+
const pending = this.#orderedPendingBlocks.get(streamKey);
|
|
10886
|
+
if (!pending) return;
|
|
10887
|
+
const blocks = this.#orderedHorizonBlocks(packetId);
|
|
10888
|
+
if (blocks === void 0) {
|
|
10889
|
+
this.#orderedPendingBlocks.delete(streamKey);
|
|
10890
|
+
for (const pendingSbn of pending.keys()) {
|
|
10891
|
+
this.noteBlockActivity(pendingSbn, packetId, catalogTrackName);
|
|
10892
|
+
}
|
|
10893
|
+
return;
|
|
10894
|
+
}
|
|
10895
|
+
const horizon = frontier - blocks;
|
|
10896
|
+
for (const [pendingSbn, key] of pending) {
|
|
10897
|
+
if (pendingSbn >= horizon) continue;
|
|
10898
|
+
pending.delete(pendingSbn);
|
|
10899
|
+
this.#settleOrderedBlock(key, pendingSbn, packetId, catalogTrackName);
|
|
10900
|
+
}
|
|
10901
|
+
if (pending.size === 0 && this.#orderedPendingBlocks.get(streamKey) === pending) {
|
|
10902
|
+
this.#orderedPendingBlocks.delete(streamKey);
|
|
10903
|
+
}
|
|
10904
|
+
}
|
|
10905
|
+
/**
|
|
10906
|
+
* Decide a block the ordered relay has finished delivering. Its accounting
|
|
10907
|
+
* is final: SSB_length (from any repair symbol) against the source and
|
|
10908
|
+
* repair symbols it holds says whether the decoder can still recover it.
|
|
10909
|
+
* A block short of that asks for repair when D-28 is wired, under the same
|
|
10910
|
+
* post-request fetch budget the datagram path grants; otherwise, and for a
|
|
10911
|
+
* block the decoder holds enough symbols for, the decoder finalizes it
|
|
10912
|
+
* exactly as a datagram deadline would (a Worker acknowledges the cleanup
|
|
10913
|
+
* only after every earlier symbol, so an in-flight recovery still wins).
|
|
10914
|
+
*/
|
|
10915
|
+
#settleOrderedBlock(key, sbn, packetId, catalogTrackName) {
|
|
10916
|
+
if (this.#completedBlocks.has(key) || this.#pendingWorkerCleanups.has(key) || this.#repairRequestedBlocks.has(key)) return;
|
|
10917
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
10918
|
+
const received = (this.#blockSourceCounts.get(key) ?? 0) + (this.#blockRepairRsIds.get(key)?.size ?? 0);
|
|
10919
|
+
if (this.onRepairNeeded && (authoredLength === void 0 || received < authoredLength)) {
|
|
10920
|
+
this.#requestRepair(sbn, packetId, catalogTrackName, true);
|
|
10921
|
+
return;
|
|
10922
|
+
}
|
|
10923
|
+
this.#traceLifecycle("horizon-settled", sbn, packetId, catalogTrackName);
|
|
10924
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
10925
|
+
}
|
|
10926
|
+
/** Register a block whose multicast symbols arrive over an ordered relay. */
|
|
10927
|
+
#holdForOrderedSettlement(key, sbn, packetId, catalogTrackName) {
|
|
10928
|
+
this.#clearRepairDeadline(key);
|
|
10929
|
+
if (this.#activeBlocks.has(key)) {
|
|
10930
|
+
this.#clearBlockTimer(key);
|
|
10931
|
+
this.#unindexBlockKey(key);
|
|
10932
|
+
}
|
|
10933
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
10934
|
+
let pending = this.#orderedPendingBlocks.get(streamKey);
|
|
10935
|
+
if (!pending) {
|
|
10936
|
+
pending = /* @__PURE__ */ new Map();
|
|
10937
|
+
this.#orderedPendingBlocks.set(streamKey, pending);
|
|
10938
|
+
}
|
|
10939
|
+
pending.set(sbn, key);
|
|
10940
|
+
}
|
|
10672
10941
|
/** Cancel and forget a block's inactivity timer and deadline. */
|
|
10673
10942
|
#clearBlockTimer(key) {
|
|
10674
10943
|
const timer = this.#activeBlocks.get(key);
|
|
10675
10944
|
if (timer?.handle != null) clearTimeout(timer.handle);
|
|
10676
10945
|
this.#activeBlocks.delete(key);
|
|
10677
10946
|
this.#activeBlockDeadlinesMs.delete(key);
|
|
10947
|
+
this.#blockWaitDeadlinesMs.delete(key);
|
|
10948
|
+
this.#repairHoldUntilMs.delete(key);
|
|
10678
10949
|
}
|
|
10679
10950
|
#parseBlockKey(key) {
|
|
10680
10951
|
const [packetIdText, sbnText, ...catalogParts] = key.split(":");
|
|
@@ -10799,7 +11070,7 @@ var FECManager = class {
|
|
|
10799
11070
|
timer.handle = setTimeout(onFire, delayMs);
|
|
10800
11071
|
this.#repairDeadlines.set(key, timer);
|
|
10801
11072
|
}
|
|
10802
|
-
#requestRepair(sbn, packetId, catalogTrackName) {
|
|
11073
|
+
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
10803
11074
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
10804
11075
|
if (this.#completedBlocks.has(key) || !this.onRepairNeeded || this.#repairRequestedBlocks.has(key)) return;
|
|
10805
11076
|
const deadline = this.#repairDeadlines.get(key);
|
|
@@ -10807,16 +11078,7 @@ var FECManager = class {
|
|
|
10807
11078
|
this.#repairDeadlines.set(key, null);
|
|
10808
11079
|
this.#repairDeadlineAtMs.delete(key);
|
|
10809
11080
|
this.#repairRequestedBlocks.add(key);
|
|
10810
|
-
|
|
10811
|
-
const graceMs = this.#repairGraceMs(config);
|
|
10812
|
-
const now = performance.now();
|
|
10813
|
-
const currentDeadline = this.#activeBlockDeadlinesMs.get(key) ?? now;
|
|
10814
|
-
this.#armBlockTimeout(
|
|
10815
|
-
sbn,
|
|
10816
|
-
packetId,
|
|
10817
|
-
catalogTrackName,
|
|
10818
|
-
Math.max(1, Math.ceil(Math.max(now, currentDeadline) + graceMs - now))
|
|
10819
|
-
);
|
|
11081
|
+
if (settledByOrder) this.#armBlockTimeout(sbn, packetId, catalogTrackName, 0);
|
|
10820
11082
|
this.#traceLifecycle("repair-requested", sbn, packetId, catalogTrackName);
|
|
10821
11083
|
this.onRepairNeeded(
|
|
10822
11084
|
sbn,
|
|
@@ -11062,8 +11324,55 @@ var FECManager = class {
|
|
|
11062
11324
|
}
|
|
11063
11325
|
return this.#fecTimeoutMs;
|
|
11064
11326
|
}
|
|
11065
|
-
|
|
11066
|
-
|
|
11327
|
+
/**
|
|
11328
|
+
* The delivery-gap window is the longest catalog keyframe interval among
|
|
11329
|
+
* the configured video streams. A failed video block costs every frame up
|
|
11330
|
+
* to the next keyframe, so a receiver must remember a stall for at least
|
|
11331
|
+
* that long: forgetting it sooner would let the next recurrence fail a
|
|
11332
|
+
* block whose damage outlives the memory of the stall that caused it. It
|
|
11333
|
+
* comes from the catalog's `keyframeIntervalTicks`/`keyframeIntervalMs`
|
|
11334
|
+
* (via the classifier geometry), never from a constant. It also caps the
|
|
11335
|
+
* gap the wait may absorb; with no catalog cadence no gap is reported and
|
|
11336
|
+
* the catalog tolerance alone applies (see DeliveryGapEstimator).
|
|
11337
|
+
*/
|
|
11338
|
+
#refreshDeliveryGapWindow() {
|
|
11339
|
+
let windowMs;
|
|
11340
|
+
const consider = (geometry) => {
|
|
11341
|
+
if (geometry?.trackKind !== "video" || geometry.mode !== "classifying") return;
|
|
11342
|
+
const { ticks, timescale } = geometry.keyframeInterval;
|
|
11343
|
+
const ms = ticks / timescale * 1e3;
|
|
11344
|
+
if (Number.isFinite(ms) && ms > 0 && (windowMs === void 0 || ms > windowMs)) windowMs = ms;
|
|
11345
|
+
};
|
|
11346
|
+
for (const geometry of this.#streamGeometries.values()) consider(geometry);
|
|
11347
|
+
for (const config of this.#trackConfigs.values()) consider(config.geometry);
|
|
11348
|
+
this.#deliveryGaps.setWindowMs(windowMs);
|
|
11349
|
+
}
|
|
11350
|
+
/**
|
|
11351
|
+
* The multicast late-symbol wait: the block span plus the larger of the
|
|
11352
|
+
* catalog's signaled tolerance and the receiver-measured delivery gap.
|
|
11353
|
+
*
|
|
11354
|
+
* `config.timeoutMs` already is span + `fec.reorderToleranceMs` (or the
|
|
11355
|
+
* wider envelope configure() applies), so it stays the floor; the measured
|
|
11356
|
+
* gap widens it only when the path stalls for longer than the catalog
|
|
11357
|
+
* allowed for. Datagram delivery only: an ordered relay settles blocks by
|
|
11358
|
+
* stream order and never feeds the estimator.
|
|
11359
|
+
*/
|
|
11360
|
+
#multicastWaitMs(config) {
|
|
11361
|
+
return this.#waitForGapMs(config, this.#deliveryGaps.peakGapMs(performance.now()));
|
|
11362
|
+
}
|
|
11363
|
+
/**
|
|
11364
|
+
* The widest #multicastWaitMs can become: the estimator reports at most its
|
|
11365
|
+
* catalog window, so this is a catalog quantity that no measurement moves.
|
|
11366
|
+
*/
|
|
11367
|
+
#maxMulticastWaitMs(config) {
|
|
11368
|
+
return this.#waitForGapMs(config, this.#deliveryGaps.windowMs ?? 0);
|
|
11369
|
+
}
|
|
11370
|
+
#waitForGapMs(config, gapMs) {
|
|
11371
|
+
if (!(gapMs > 0) || !(config.interleaveDepthMs > 0)) return config.timeoutMs;
|
|
11372
|
+
return Math.max(config.timeoutMs, Math.ceil(config.interleaveDepthMs + gapMs));
|
|
11373
|
+
}
|
|
11374
|
+
#repairDeadlineMs(config, waitMs = this.#multicastWaitMs(config)) {
|
|
11375
|
+
const halfFailBoundMs = Math.ceil(waitMs / 2);
|
|
11067
11376
|
if (config.k > 0 && config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS && config.fps !== void 0 && Number.isFinite(config.fps) && config.fps > 0) {
|
|
11068
11377
|
const mediaUnitMs = 1e3 / config.fps;
|
|
11069
11378
|
const interleaveUnits = deriveFecInterleaveDepth(
|
|
@@ -11071,21 +11380,52 @@ var FECManager = class {
|
|
|
11071
11380
|
mediaUnitMs
|
|
11072
11381
|
);
|
|
11073
11382
|
const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs);
|
|
11074
|
-
if (sourceHorizonMs <
|
|
11383
|
+
if (sourceHorizonMs < waitMs) return sourceHorizonMs;
|
|
11075
11384
|
}
|
|
11076
11385
|
return halfFailBoundMs;
|
|
11077
11386
|
}
|
|
11078
|
-
/**
|
|
11387
|
+
/**
|
|
11388
|
+
* One block lifetime: the inactivity fail bound plus the D-28 repair grace,
|
|
11389
|
+
* at the widest wait the catalog allows (#maxMulticastWaitMs). Everything
|
|
11390
|
+
* derived from it (terminal retention, the release gate's hold ceiling, the
|
|
11391
|
+
* duplicate ledgers, the owed-cleanup bound) is compared across time, so it
|
|
11392
|
+
* must not follow the live measurement.
|
|
11393
|
+
*/
|
|
11079
11394
|
#blockLifetimeMs(config) {
|
|
11080
|
-
|
|
11395
|
+
const waitMs = this.#maxMulticastWaitMs(config);
|
|
11396
|
+
return waitMs + this.#repairGraceMs(config, waitMs);
|
|
11081
11397
|
}
|
|
11082
|
-
|
|
11083
|
-
|
|
11398
|
+
/**
|
|
11399
|
+
* The lifetime at the live wait, capped at #blockLifetimeMs: lifetime is not
|
|
11400
|
+
* monotone in the wait (the repair deadline switches from half the wait to
|
|
11401
|
+
* the source horizon), so the cap is what keeps it inside the retention.
|
|
11402
|
+
*/
|
|
11403
|
+
#liveBlockLifetimeMs(config) {
|
|
11404
|
+
const waitMs = this.#multicastWaitMs(config);
|
|
11405
|
+
return Math.min(waitMs + this.#repairGraceMs(config, waitMs), this.#blockLifetimeMs(config));
|
|
11406
|
+
}
|
|
11407
|
+
#repairGraceMs(config, waitMs) {
|
|
11408
|
+
return Math.max(1, waitMs - this.#repairDeadlineMs(config, waitMs));
|
|
11084
11409
|
}
|
|
11085
11410
|
#armBlockTimeout(sbn, packetId, catalogTrackName, delayMs) {
|
|
11086
11411
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
11087
|
-
const
|
|
11088
|
-
const
|
|
11412
|
+
const waitAtMs = performance.now() + Math.max(1, Math.ceil(delayMs));
|
|
11413
|
+
const holdAtMs = this.#repairHoldUntilMs.get(key);
|
|
11414
|
+
this.#blockWaitDeadlinesMs.set(key, holdAtMs === void 0 ? waitAtMs : Math.min(waitAtMs, holdAtMs));
|
|
11415
|
+
this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
|
|
11416
|
+
}
|
|
11417
|
+
/**
|
|
11418
|
+
* Arm (or move) a block's one fail timer at the later of its multicast
|
|
11419
|
+
* wait and its repair hold. Moving it later costs no timer operation: the
|
|
11420
|
+
* armed timer wakes no later than the new deadline and re-arms for the
|
|
11421
|
+
* remainder.
|
|
11422
|
+
*/
|
|
11423
|
+
#applyBlockDeadline(key, sbn, packetId, catalogTrackName) {
|
|
11424
|
+
const nowMs = performance.now();
|
|
11425
|
+
const deadlineMs = Math.max(
|
|
11426
|
+
this.#blockWaitDeadlinesMs.get(key) ?? nowMs + 1,
|
|
11427
|
+
this.#repairHoldUntilMs.get(key) ?? -Infinity
|
|
11428
|
+
);
|
|
11089
11429
|
this.#activeBlockDeadlinesMs.set(key, deadlineMs);
|
|
11090
11430
|
const armed = this.#activeBlocks.get(key);
|
|
11091
11431
|
if (armed?.handle != null) {
|
|
@@ -11096,45 +11436,55 @@ var FECManager = class {
|
|
|
11096
11436
|
const onFire = () => {
|
|
11097
11437
|
if (this.#activeBlocks.get(key) !== timer) return;
|
|
11098
11438
|
const currentDeadlineMs = this.#activeBlockDeadlinesMs.get(key);
|
|
11099
|
-
const
|
|
11100
|
-
if (currentDeadlineMs !== void 0 && currentDeadlineMs >
|
|
11439
|
+
const firedAtMs = performance.now();
|
|
11440
|
+
if (currentDeadlineMs !== void 0 && currentDeadlineMs > firedAtMs) {
|
|
11101
11441
|
timer.fireAtMs = currentDeadlineMs;
|
|
11102
|
-
timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs -
|
|
11442
|
+
timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - firedAtMs));
|
|
11103
11443
|
return;
|
|
11104
11444
|
}
|
|
11105
11445
|
this.#activeBlocks.delete(key);
|
|
11106
11446
|
this.#activeBlockDeadlinesMs.delete(key);
|
|
11447
|
+
this.#blockWaitDeadlinesMs.delete(key);
|
|
11448
|
+
this.#repairHoldUntilMs.delete(key);
|
|
11107
11449
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
11108
|
-
|
|
11109
|
-
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11110
|
-
return;
|
|
11111
|
-
}
|
|
11112
|
-
const syncOwner = this.#syncDecoderBlockOwners.get(key);
|
|
11113
|
-
if (syncOwner) {
|
|
11114
|
-
this.#routeSyncCleanup(syncOwner, sbn, packetId);
|
|
11115
|
-
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11116
|
-
return;
|
|
11117
|
-
}
|
|
11118
|
-
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
11119
|
-
if (workerClient) {
|
|
11120
|
-
this.#pendingWorkerCleanups.add(key);
|
|
11121
|
-
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
11122
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
11123
|
-
workerClient.cleanup(sbn);
|
|
11124
|
-
} else {
|
|
11125
|
-
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
11126
|
-
if (decoder) {
|
|
11127
|
-
this.#routeSyncCleanup(decoder, sbn, packetId);
|
|
11128
|
-
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11129
|
-
} else {
|
|
11130
|
-
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11131
|
-
}
|
|
11132
|
-
}
|
|
11450
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
11133
11451
|
};
|
|
11134
|
-
timer.handle = setTimeout(onFire,
|
|
11452
|
+
timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
|
|
11135
11453
|
this.#activeBlocks.set(key, timer);
|
|
11136
11454
|
this.#indexBlockKey(key, sbn, packetId);
|
|
11137
11455
|
}
|
|
11456
|
+
/**
|
|
11457
|
+
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
11458
|
+
* datagram deadline fired, or the ordered relay moved past it.
|
|
11459
|
+
*/
|
|
11460
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
11461
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
11462
|
+
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
11463
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11464
|
+
return;
|
|
11465
|
+
}
|
|
11466
|
+
const syncOwner = this.#syncDecoderBlockOwners.get(key);
|
|
11467
|
+
if (syncOwner) {
|
|
11468
|
+
this.#routeSyncCleanup(syncOwner, sbn, packetId);
|
|
11469
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11470
|
+
return;
|
|
11471
|
+
}
|
|
11472
|
+
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
11473
|
+
if (workerClient) {
|
|
11474
|
+
this.#pendingWorkerCleanups.add(key);
|
|
11475
|
+
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
11476
|
+
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
11477
|
+
workerClient.cleanup(sbn);
|
|
11478
|
+
} else {
|
|
11479
|
+
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
11480
|
+
if (decoder) {
|
|
11481
|
+
this.#routeSyncCleanup(decoder, sbn, packetId);
|
|
11482
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11483
|
+
} else {
|
|
11484
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
11485
|
+
}
|
|
11486
|
+
}
|
|
11487
|
+
}
|
|
11138
11488
|
/**
|
|
11139
11489
|
* Upsert one packetId's track configuration. With `catalogTrackName`, the
|
|
11140
11490
|
* classifier geometry in `config` is that stream's own and is kept per
|
|
@@ -11159,12 +11509,17 @@ var FECManager = class {
|
|
|
11159
11509
|
...(config.geometry ?? previous.geometry) === void 0 ? {} : { geometry: config.geometry ?? previous.geometry }
|
|
11160
11510
|
};
|
|
11161
11511
|
next.timeoutMs = this.#computeTimeout(next);
|
|
11512
|
+
if (this.#multicastSymbolDelivery === "ordered-relay") {
|
|
11513
|
+
this.#orderedHorizonBlocksFor(packetId, next);
|
|
11514
|
+
}
|
|
11162
11515
|
this.#trackConfigs.set(packetId, next);
|
|
11516
|
+
this.#refreshDeliveryGapWindow();
|
|
11163
11517
|
let policyChanged = config.geometry !== void 0 && JSON.stringify(config.geometry) !== JSON.stringify(previous.geometry) || config.trackKind !== void 0 && config.trackKind !== previous.trackKind;
|
|
11164
11518
|
if (streamScopedGeometry !== void 0) {
|
|
11165
11519
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
11166
11520
|
if (JSON.stringify(streamScopedGeometry) !== JSON.stringify(this.#streamGeometries.get(streamKey))) {
|
|
11167
11521
|
this.#streamGeometries.set(streamKey, streamScopedGeometry);
|
|
11522
|
+
this.#refreshDeliveryGapWindow();
|
|
11168
11523
|
policyChanged = true;
|
|
11169
11524
|
}
|
|
11170
11525
|
}
|
|
@@ -11231,6 +11586,9 @@ var FECManager = class {
|
|
|
11231
11586
|
pendingWorkerCleanups: this.#pendingWorkerCleanups.size,
|
|
11232
11587
|
decoderUnavailableEvents: this.#decoderUnavailableEvents,
|
|
11233
11588
|
repairTrackedBlocks: this.#blockRepairRsIds.size,
|
|
11589
|
+
multicastSymbolDelivery: this.#multicastSymbolDelivery,
|
|
11590
|
+
orderedFrontiers: Object.fromEntries(this.#orderedFrontiers),
|
|
11591
|
+
orderedPendingBlocks: [...this.#orderedPendingBlocks.values()].reduce((total, pending) => total + pending.size, 0),
|
|
11234
11592
|
lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry }))
|
|
11235
11593
|
};
|
|
11236
11594
|
}
|
|
@@ -11289,6 +11647,7 @@ var FECManager = class {
|
|
|
11289
11647
|
}
|
|
11290
11648
|
/** Start a new multicast transport join without discarding track/decoder state. */
|
|
11291
11649
|
beginMulticastJoin() {
|
|
11650
|
+
this.#orderedFrontiers.clear();
|
|
11292
11651
|
this.#startupAlignedPackets.clear();
|
|
11293
11652
|
this.#startupPartialBlocks.clear();
|
|
11294
11653
|
this.#startupRequireRepairReady = true;
|
|
@@ -11296,6 +11655,45 @@ var FECManager = class {
|
|
|
11296
11655
|
this.#startupRepairBoundaryCounts.clear();
|
|
11297
11656
|
this.#startupRepairFailOpenPackets.clear();
|
|
11298
11657
|
}
|
|
11658
|
+
/** How the multicast leg currently delivers symbols. */
|
|
11659
|
+
get multicastSymbolDelivery() {
|
|
11660
|
+
return this.#multicastSymbolDelivery;
|
|
11661
|
+
}
|
|
11662
|
+
/**
|
|
11663
|
+
* Declare how the multicast leg delivers symbols (see
|
|
11664
|
+
* MulticastSymbolDelivery). The multicast provider owns this fact:
|
|
11665
|
+
* MulticastManager applies what the window.multicast subscription declares
|
|
11666
|
+
* on every join, and a consumer feeding packets itself declares its own leg.
|
|
11667
|
+
*
|
|
11668
|
+
* Switching to 'ordered-relay' requires every configured track to define
|
|
11669
|
+
* its interleave horizon and throws otherwise. Switching back to 'datagram'
|
|
11670
|
+
* gives every block awaiting ordered settlement the datagram inactivity
|
|
11671
|
+
* deadline from now.
|
|
11672
|
+
*/
|
|
11673
|
+
setMulticastSymbolDelivery(delivery) {
|
|
11674
|
+
if (!isMulticastSymbolDelivery(delivery)) {
|
|
11675
|
+
throw new Error(
|
|
11676
|
+
`[FECManager] multicast symbol delivery must be 'datagram' or 'ordered-relay', got ${String(delivery)}`
|
|
11677
|
+
);
|
|
11678
|
+
}
|
|
11679
|
+
if (delivery === this.#multicastSymbolDelivery) return;
|
|
11680
|
+
this.#deliveryGaps.reset();
|
|
11681
|
+
if (delivery === "ordered-relay") {
|
|
11682
|
+
for (const [packetId, config] of this.#trackConfigs) {
|
|
11683
|
+
this.#orderedHorizonBlocksFor(packetId, config);
|
|
11684
|
+
}
|
|
11685
|
+
this.#multicastSymbolDelivery = delivery;
|
|
11686
|
+
return;
|
|
11687
|
+
}
|
|
11688
|
+
this.#multicastSymbolDelivery = delivery;
|
|
11689
|
+
const pending = [...this.#orderedPendingBlocks.values()].flatMap((blocks) => [...blocks.values()]);
|
|
11690
|
+
this.#orderedPendingBlocks.clear();
|
|
11691
|
+
this.#orderedFrontiers.clear();
|
|
11692
|
+
for (const key of pending) {
|
|
11693
|
+
const block = this.#parseBlockKey(key);
|
|
11694
|
+
this.noteBlockActivity(block.sbn, block.packetId, block.catalogTrackName);
|
|
11695
|
+
}
|
|
11696
|
+
}
|
|
11299
11697
|
/** Begin a fresh decode epoch for one physical stream. */
|
|
11300
11698
|
resetTrack(packetId, catalogTrackName) {
|
|
11301
11699
|
if (!Number.isInteger(packetId) || packetId < 0) {
|
|
@@ -11325,6 +11723,8 @@ var FECManager = class {
|
|
|
11325
11723
|
resetTimerMap(this.#activeBlocks);
|
|
11326
11724
|
resetTimerMap(this.#repairDeadlines);
|
|
11327
11725
|
resetMap(this.#activeBlockDeadlinesMs);
|
|
11726
|
+
resetMap(this.#blockWaitDeadlinesMs);
|
|
11727
|
+
resetMap(this.#repairHoldUntilMs);
|
|
11328
11728
|
resetMap(this.#repairDeadlineAtMs);
|
|
11329
11729
|
resetMap(this.#terminalExpiryMs);
|
|
11330
11730
|
resetMap(this.#pendingWorkerCleanupClients);
|
|
@@ -11336,9 +11736,12 @@ var FECManager = class {
|
|
|
11336
11736
|
resetMap(this.#blockAuthoredLengths);
|
|
11337
11737
|
resetMap(this.#blockRepairRsIds);
|
|
11338
11738
|
resetMap(this.#blockPathCounts);
|
|
11739
|
+
resetMap(this.#blockAnchorsMs);
|
|
11339
11740
|
resetMap(this.#blockCatalogTrackNames);
|
|
11340
11741
|
resetMap(this.#syncDecoderBlockOwners);
|
|
11341
11742
|
resetMap(this.#startupRepairBoundaryCounts);
|
|
11743
|
+
resetMap(this.#orderedFrontiers);
|
|
11744
|
+
resetMap(this.#orderedPendingBlocks);
|
|
11342
11745
|
resetSet(this.#pendingWorkerCleanups);
|
|
11343
11746
|
resetSet(this.#completedBlocks);
|
|
11344
11747
|
resetSet(this.#cleanlyCompletedBlocks);
|
|
@@ -11398,6 +11801,9 @@ var FECManager = class {
|
|
|
11398
11801
|
feedSymbolFromSource(packetId, sbn, esi, _data, source, catalogTrackName) {
|
|
11399
11802
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
11400
11803
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
11804
|
+
if (source === "multicast" && this.#multicastSymbolDelivery === "ordered-relay") {
|
|
11805
|
+
this.#advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName);
|
|
11806
|
+
}
|
|
11401
11807
|
const k = this.kForPacket(packetId);
|
|
11402
11808
|
const isSourceSymbol = esi >= 0 && esi < k;
|
|
11403
11809
|
const isStartupPartial = this.#startupPartialBlocks.has(blockKey);
|
|
@@ -11452,6 +11858,7 @@ var FECManager = class {
|
|
|
11452
11858
|
moqSymbols: 0,
|
|
11453
11859
|
complete: false
|
|
11454
11860
|
};
|
|
11861
|
+
if (!this.#blockAnchorsMs.has(blockKey)) this.#blockAnchorsMs.set(blockKey, performance.now());
|
|
11455
11862
|
if (source === "multicast") counts.multicastSymbols++;
|
|
11456
11863
|
else counts.moqSymbols++;
|
|
11457
11864
|
this.#blockPathCounts.set(blockKey, counts);
|
|
@@ -11462,6 +11869,9 @@ var FECManager = class {
|
|
|
11462
11869
|
if (this.#blockPathCounts.size > 512) {
|
|
11463
11870
|
dropOldest(this.#blockPathCounts, this.#blockPathCounts.size - 256);
|
|
11464
11871
|
}
|
|
11872
|
+
if (this.#blockAnchorsMs.size > 512) {
|
|
11873
|
+
dropOldest(this.#blockAnchorsMs, this.#blockAnchorsMs.size - 256);
|
|
11874
|
+
}
|
|
11465
11875
|
if (this.#blockCatalogTrackNames.size > 512) {
|
|
11466
11876
|
for (const key of dropOldest(
|
|
11467
11877
|
this.#blockCatalogTrackNames,
|
|
@@ -11550,9 +11960,75 @@ var FECManager = class {
|
|
|
11550
11960
|
interleaveDepthMsForPacket(packetId) {
|
|
11551
11961
|
return this.#resolveTrackConfig(packetId).interleaveDepthMs;
|
|
11552
11962
|
}
|
|
11553
|
-
/**
|
|
11963
|
+
/**
|
|
11964
|
+
* Record one multicast MMTP packet arrival (every packet of the leg, FEC
|
|
11965
|
+
* symbol or not) for the delivery-gap measurement. `packetSequenceNumber`
|
|
11966
|
+
* is the MMTP packet_sequence_number, which increments by one per packet of
|
|
11967
|
+
* a packet_id: it is what separates a stall from a loss (see
|
|
11968
|
+
* DeliveryGapEstimator). Datagram delivery only.
|
|
11969
|
+
*/
|
|
11970
|
+
noteMulticastArrival(packetId, packetSequenceNumber) {
|
|
11971
|
+
if (this.#multicastSymbolDelivery !== "datagram") return;
|
|
11972
|
+
this.#deliveryGaps.noteArrival(performance.now(), packetId, packetSequenceNumber);
|
|
11973
|
+
}
|
|
11974
|
+
/**
|
|
11975
|
+
* Resolved per-track multicast late-symbol wait in milliseconds: the span
|
|
11976
|
+
* plus the larger of the catalog tolerance and the measured delivery gap.
|
|
11977
|
+
*/
|
|
11554
11978
|
timeoutMsForPacket(packetId) {
|
|
11555
|
-
return this.#resolveTrackConfig(packetId)
|
|
11979
|
+
return this.#multicastWaitMs(this.#resolveTrackConfig(packetId));
|
|
11980
|
+
}
|
|
11981
|
+
/**
|
|
11982
|
+
* Largest multicast delivery gap measured over the current window (ms), and
|
|
11983
|
+
* that window (undefined: held since the last reset). Diagnostics.
|
|
11984
|
+
*/
|
|
11985
|
+
multicastDeliveryGap() {
|
|
11986
|
+
return {
|
|
11987
|
+
peakGapMs: this.#deliveryGaps.peakGapMs(performance.now()),
|
|
11988
|
+
windowMs: this.#deliveryGaps.windowMs
|
|
11989
|
+
};
|
|
11990
|
+
}
|
|
11991
|
+
/**
|
|
11992
|
+
* Receiver-local time of a block's first accepted symbol, or undefined for
|
|
11993
|
+
* a block no symbol reached (one materialized because a later SBN arrived).
|
|
11994
|
+
*/
|
|
11995
|
+
blockAnchorAtMs(sbn, packetId, catalogTrackName) {
|
|
11996
|
+
return this.#blockAnchorsMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
11997
|
+
}
|
|
11998
|
+
/** Absolute fail deadline of a still-open block, or undefined. */
|
|
11999
|
+
blockDeadlineAtMs(sbn, packetId, catalogTrackName) {
|
|
12000
|
+
return this.#activeBlockDeadlinesMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
12001
|
+
}
|
|
12002
|
+
/**
|
|
12003
|
+
* Keep a block open until `untilAtMs` while a repair FETCH the scheduler
|
|
12004
|
+
* admitted is in flight. Never shortens the multicast wait already armed;
|
|
12005
|
+
* while set, the hold is the block's whole deadline, so no later symbol
|
|
12006
|
+
* extends it. Returns false when the block is no longer open (resolved, or
|
|
12007
|
+
* not yet tracked).
|
|
12008
|
+
*/
|
|
12009
|
+
holdBlockForRepair(sbn, packetId, catalogTrackName, untilAtMs) {
|
|
12010
|
+
if (!Number.isFinite(untilAtMs)) {
|
|
12011
|
+
throw new Error(`[FECManager] repair hold must be a finite time (got ${untilAtMs})`);
|
|
12012
|
+
}
|
|
12013
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
12014
|
+
if (!this.#activeBlocks.has(key)) return false;
|
|
12015
|
+
const previous = this.#repairHoldUntilMs.get(key);
|
|
12016
|
+
if (previous !== void 0 && previous >= untilAtMs) return true;
|
|
12017
|
+
this.#repairHoldUntilMs.set(
|
|
12018
|
+
key,
|
|
12019
|
+
Math.max(untilAtMs, this.#blockWaitDeadlinesMs.get(key) ?? -Infinity)
|
|
12020
|
+
);
|
|
12021
|
+
this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
|
|
12022
|
+
return true;
|
|
12023
|
+
}
|
|
12024
|
+
/**
|
|
12025
|
+
* Drop a block's repair hold: its FETCH ended without delivering. The block
|
|
12026
|
+
* falls back to its multicast wait, which may already have passed.
|
|
12027
|
+
*/
|
|
12028
|
+
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
12029
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
12030
|
+
if (!this.#repairHoldUntilMs.delete(key) || !this.#activeBlocks.has(key)) return;
|
|
12031
|
+
this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
|
|
11556
12032
|
}
|
|
11557
12033
|
/** Resolved per-track D-28 repair request deadline in milliseconds. */
|
|
11558
12034
|
repairDeadlineMsForPacket(packetId) {
|
|
@@ -11921,7 +12397,7 @@ var FECManager = class {
|
|
|
11921
12397
|
return;
|
|
11922
12398
|
}
|
|
11923
12399
|
this.#settleOwedCleanup(key, "cleanup-ack-timeout");
|
|
11924
|
-
}, Math.ceil(this.#
|
|
12400
|
+
}, Math.ceil(this.#liveBlockLifetimeMs(this.#resolveTrackConfig(packetId))))
|
|
11925
12401
|
};
|
|
11926
12402
|
this.#pendingWorkerCleanupBounds.set(key, bound);
|
|
11927
12403
|
}
|
|
@@ -12239,14 +12715,19 @@ var FECManager = class {
|
|
|
12239
12715
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
12240
12716
|
if (this.#completedBlocks.has(key)) return;
|
|
12241
12717
|
if (this.#pendingWorkerCleanups.has(key)) return;
|
|
12242
|
-
|
|
12718
|
+
const repairRequested = this.#repairRequestedBlocks.has(key);
|
|
12719
|
+
if (repairRequested && this.#settlesByOrder(key)) return;
|
|
12720
|
+
if (this.#settlesByOrder(key)) {
|
|
12721
|
+
this.#holdForOrderedSettlement(key, sbn, packetId, catalogTrackName);
|
|
12722
|
+
return;
|
|
12723
|
+
}
|
|
12243
12724
|
const trackConfig = this.#resolveTrackConfig(packetId);
|
|
12244
|
-
const timeoutMs = trackConfig
|
|
12725
|
+
const timeoutMs = this.#multicastWaitMs(trackConfig);
|
|
12245
12726
|
if (!(timeoutMs > 0)) {
|
|
12246
12727
|
throw new Error(`[FECManager] packetId=${packetId} has no positive fail bound (timeoutMs=${timeoutMs})`);
|
|
12247
12728
|
}
|
|
12248
12729
|
const repairDeadline = this.#repairDeadlines.get(key);
|
|
12249
|
-
if (this.onRepairNeeded && (repairDeadline === void 0 || sourceProgress)) {
|
|
12730
|
+
if (this.onRepairNeeded && !repairRequested && (repairDeadline === void 0 || sourceProgress)) {
|
|
12250
12731
|
this.#armRepairDeadline(
|
|
12251
12732
|
key,
|
|
12252
12733
|
sbn,
|
|
@@ -12377,6 +12858,9 @@ var FECManager = class {
|
|
|
12377
12858
|
}
|
|
12378
12859
|
this.#activeBlocks.clear();
|
|
12379
12860
|
this.#activeBlockDeadlinesMs.clear();
|
|
12861
|
+
this.#blockWaitDeadlinesMs.clear();
|
|
12862
|
+
this.#repairHoldUntilMs.clear();
|
|
12863
|
+
this.#deliveryGaps.reset();
|
|
12380
12864
|
this.#pendingWorkerCleanups.clear();
|
|
12381
12865
|
this.#pendingWorkerCleanupClients.clear();
|
|
12382
12866
|
for (const bound of this.#pendingWorkerCleanupBounds.values()) clearTimeout(bound.handle);
|
|
@@ -12404,8 +12888,11 @@ var FECManager = class {
|
|
|
12404
12888
|
this.#startupRepairReadyPackets.clear();
|
|
12405
12889
|
this.#startupRepairBoundaryCounts.clear();
|
|
12406
12890
|
this.#startupRepairFailOpenPackets.clear();
|
|
12891
|
+
this.#orderedFrontiers.clear();
|
|
12892
|
+
this.#orderedPendingBlocks.clear();
|
|
12407
12893
|
this.#seenSymbols.clear();
|
|
12408
12894
|
this.#blockPathCounts.clear();
|
|
12895
|
+
this.#blockAnchorsMs.clear();
|
|
12409
12896
|
this.#blockCatalogTrackNames.clear();
|
|
12410
12897
|
this.#classifiers.clear();
|
|
12411
12898
|
this.#streamGeometries.clear();
|
|
@@ -14798,6 +15285,7 @@ var StreamDuplicateLedgers = class {
|
|
|
14798
15285
|
|
|
14799
15286
|
// src/mmtp-router.ts
|
|
14800
15287
|
var KEYFRAME_LOST_TIMEOUT_SLACK = 1.5;
|
|
15288
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
14801
15289
|
function isRouterDebugEnabled() {
|
|
14802
15290
|
const globalDebug = globalThis.__MMTP_ROUTER_DEBUG;
|
|
14803
15291
|
if (globalDebug === true || globalDebug === "true" || globalDebug === "1") return true;
|
|
@@ -14967,8 +15455,10 @@ var MmtpRouter = class {
|
|
|
14967
15455
|
#moqRepairOwnerCounts = {};
|
|
14968
15456
|
#parserDiagnostics = /* @__PURE__ */ new Map();
|
|
14969
15457
|
// ── Keyframe loss detection state ───────────────────────────────
|
|
14970
|
-
/** Catalog-derived keyframe loss timeout (ms). null = not configured (
|
|
15458
|
+
/** Catalog-derived keyframe loss timeout (ms), or a reader evaluated at each arm. null = not configured (warns on video track registration; Phase-13 keyframe repair stays off). */
|
|
14971
15459
|
#keyframeLostTimeoutMs;
|
|
15460
|
+
/** Why the last reader evaluation left the timer unarmed; null while arming. Dedups the warning. */
|
|
15461
|
+
#keyframeLostUnarmedReason = null;
|
|
14972
15462
|
/** Active keyframe loss timer handle. Single timer per video track — cleared on each rapFlag and on dispose(). */
|
|
14973
15463
|
#keyframeLostTimer = null;
|
|
14974
15464
|
/** Monotonic counter incremented on each emitted video keyframe. Tracks current GOP position. */
|
|
@@ -15229,11 +15719,19 @@ var MmtpRouter = class {
|
|
|
15229
15719
|
/**
|
|
15230
15720
|
* Set the keyframe loss timeout after construction.
|
|
15231
15721
|
* Must be called BEFORE setPacketIdType('video').
|
|
15232
|
-
*
|
|
15722
|
+
* A fixed timeout derives from explicit keyframe/group duration:
|
|
15233
15723
|
* Math.ceil(groupDurationMs * KEYFRAME_LOST_TIMEOUT_SLACK).
|
|
15724
|
+
* A timeout whose inputs are measured after configuration (e.g. keyframe
|
|
15725
|
+
* cadence plus measured playout lateness) is passed as a reader, evaluated
|
|
15726
|
+
* each time the timer is armed. Both forms pass the same check at each arm:
|
|
15727
|
+
* a throw, or a value that is not finite, positive and within the timer
|
|
15728
|
+
* maximum, leaves that keyframe unarmed (fail closed per keyframe, not per
|
|
15729
|
+
* session). A fixed NaN, e.g. derived from a catalog without a group
|
|
15730
|
+
* duration, therefore never reaches setTimeout.
|
|
15234
15731
|
*/
|
|
15235
15732
|
setKeyframeLostTimeoutMs(ms) {
|
|
15236
15733
|
this.#keyframeLostTimeoutMs = ms;
|
|
15734
|
+
this.#keyframeLostUnarmedReason = null;
|
|
15237
15735
|
}
|
|
15238
15736
|
/** Update MFU reassembly timeout. Source and FEC-recovery paths share the
|
|
15239
15737
|
* parser's single reassembler (BLO-8151), so one update covers both. */
|
|
@@ -15727,16 +16225,49 @@ var MmtpRouter = class {
|
|
|
15727
16225
|
// ── Private: keyframe loss detection ────────────────────────────
|
|
15728
16226
|
/** Arm (or re-arm) the keyframe loss timer.
|
|
15729
16227
|
* Clears any existing timer and sets a new one for keyframeLostTimeoutMs.
|
|
15730
|
-
* On timeout, fires onKeyframeLost with the next expected groupId.
|
|
16228
|
+
* On timeout, fires onKeyframeLost with the next expected groupId.
|
|
16229
|
+
* A reader without a valid value leaves the timer cleared and unarmed. */
|
|
15731
16230
|
#armKeyframeLostTimer() {
|
|
15732
16231
|
if (this.#keyframeLostTimer) {
|
|
15733
16232
|
clearTimeout(this.#keyframeLostTimer);
|
|
16233
|
+
this.#keyframeLostTimer = null;
|
|
15734
16234
|
}
|
|
16235
|
+
const timeoutMs = this.#readKeyframeLostTimeoutMs();
|
|
16236
|
+
if (timeoutMs === null) return;
|
|
15735
16237
|
const expectedNext = this.#lastKeyframeGroupId + 1;
|
|
15736
16238
|
this.#keyframeLostTimer = setTimeout(() => {
|
|
15737
16239
|
this.#keyframeLostTimer = null;
|
|
15738
16240
|
this.#onKeyframeLost?.(expectedNext);
|
|
15739
|
-
},
|
|
16241
|
+
}, timeoutMs);
|
|
16242
|
+
}
|
|
16243
|
+
/** The configured number, or the reader's current value; null when the
|
|
16244
|
+
* reader throws or either form yields a value no timer can honour. Warns
|
|
16245
|
+
* on entering that state or when its cause changes (a different exception,
|
|
16246
|
+
* or a different class of bad value), not once per keyframe. */
|
|
16247
|
+
#readKeyframeLostTimeoutMs() {
|
|
16248
|
+
const timeout = this.#keyframeLostTimeoutMs;
|
|
16249
|
+
let reason;
|
|
16250
|
+
let detail;
|
|
16251
|
+
try {
|
|
16252
|
+
const ms = typeof timeout === "function" ? timeout() : timeout;
|
|
16253
|
+
if (Number.isFinite(ms) && ms > 0 && ms <= MAX_TIMER_DELAY_MS) {
|
|
16254
|
+
this.#keyframeLostUnarmedReason = null;
|
|
16255
|
+
return ms;
|
|
16256
|
+
}
|
|
16257
|
+
reason = typeof ms !== "number" ? "yielded a non-number" : Number.isNaN(ms) ? "yielded NaN" : !Number.isFinite(ms) ? "yielded an infinite value" : ms <= 0 ? "yielded a non-positive value" : `exceeds the ${MAX_TIMER_DELAY_MS} ms timer maximum`;
|
|
16258
|
+
detail = ms;
|
|
16259
|
+
} catch (error) {
|
|
16260
|
+
reason = `threw ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}`;
|
|
16261
|
+
detail = error;
|
|
16262
|
+
}
|
|
16263
|
+
if (reason !== this.#keyframeLostUnarmedReason) {
|
|
16264
|
+
this.#keyframeLostUnarmedReason = reason;
|
|
16265
|
+
console.warn(
|
|
16266
|
+
`[MmtpRouter] keyframeLostTimeoutMs ${reason} \u2014 keyframe loss timer not armed until it yields a finite positive value:`,
|
|
16267
|
+
detail
|
|
16268
|
+
);
|
|
16269
|
+
}
|
|
16270
|
+
return null;
|
|
15740
16271
|
}
|
|
15741
16272
|
#noteEmittedKeyframe(packetId, keyframe) {
|
|
15742
16273
|
if (!keyframe) return;
|
|
@@ -15950,6 +16481,9 @@ var MmtpRouter = class {
|
|
|
15950
16481
|
const parsed = this.#parser.parsePacket(packet);
|
|
15951
16482
|
if (!parsed) return;
|
|
15952
16483
|
const info = parsed.info;
|
|
16484
|
+
if (source === "multicast" && !manifestAdmitted) {
|
|
16485
|
+
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
16486
|
+
}
|
|
15953
16487
|
if (source === "multicast" && !ingressHasPacketId(ingress, info.packetId)) {
|
|
15954
16488
|
debugRouter(
|
|
15955
16489
|
`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: absent from ingress tracks`
|
|
@@ -18901,6 +19435,8 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
18901
19435
|
#dataPlaneLive = false;
|
|
18902
19436
|
#onDataPlaneLive = null;
|
|
18903
19437
|
#errorListener = null;
|
|
19438
|
+
#onDelivery = null;
|
|
19439
|
+
#delivery;
|
|
18904
19440
|
#disposed = false;
|
|
18905
19441
|
#stats = {
|
|
18906
19442
|
packetsReceived: 0,
|
|
@@ -18910,6 +19446,11 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
18910
19446
|
packetsDroppedSignaling: 0
|
|
18911
19447
|
};
|
|
18912
19448
|
#droppedPidCounts = /* @__PURE__ */ new Map();
|
|
19449
|
+
// Accepted bytes per MMTP packet_id; see bytesReceivedForPacketIds. Each
|
|
19450
|
+
// value is a mutable cell, so the per-packet update is one lookup and an
|
|
19451
|
+
// in-place add: no insertion once the key exists, and no re-boxed number
|
|
19452
|
+
// per packet once a count outgrows the small-integer range.
|
|
19453
|
+
#acceptedBytesByPacketId = /* @__PURE__ */ new Map();
|
|
18913
19454
|
#packetIds = null;
|
|
18914
19455
|
#joinGeneration = 0;
|
|
18915
19456
|
constructor(onPacket) {
|
|
@@ -18971,7 +19512,22 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
18971
19512
|
this.#subscription = subscription;
|
|
18972
19513
|
this.#packetListener = packetListener;
|
|
18973
19514
|
this.#packetIds = packetIds;
|
|
19515
|
+
if (packetIds) {
|
|
19516
|
+
for (const packetId of packetIds) {
|
|
19517
|
+
if (!this.#acceptedBytesByPacketId.has(packetId)) {
|
|
19518
|
+
this.#acceptedBytesByPacketId.set(packetId, { bytes: 0 });
|
|
19519
|
+
}
|
|
19520
|
+
}
|
|
19521
|
+
}
|
|
18974
19522
|
this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
19523
|
+
const delivery = subscription.delivery ?? "datagram";
|
|
19524
|
+
if (delivery !== "datagram" && delivery !== "ordered-relay") {
|
|
19525
|
+
throw new Error(
|
|
19526
|
+
`window.multicast subscription declared unknown delivery ${String(delivery)}`
|
|
19527
|
+
);
|
|
19528
|
+
}
|
|
19529
|
+
this.#delivery = delivery;
|
|
19530
|
+
this.#onDelivery?.(delivery);
|
|
18975
19531
|
const onError = (event) => {
|
|
18976
19532
|
if (this.#disposed || generation !== this.#joinGeneration) return;
|
|
18977
19533
|
this.#failBackOnSubscriptionError(event.detail);
|
|
@@ -19005,8 +19561,6 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
19005
19561
|
return false;
|
|
19006
19562
|
}
|
|
19007
19563
|
if (!this.#acceptPacket(packet)) return false;
|
|
19008
|
-
this.#stats.packetsReceived++;
|
|
19009
|
-
this.#stats.bytesReceived += packet.byteLength;
|
|
19010
19564
|
this.#onPacket(packet, context);
|
|
19011
19565
|
return true;
|
|
19012
19566
|
}
|
|
@@ -19052,8 +19606,6 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
19052
19606
|
if (this.#disposed) return;
|
|
19053
19607
|
const packet = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
19054
19608
|
if (!this.#acceptPacket(packet)) return;
|
|
19055
|
-
this.#stats.packetsReceived++;
|
|
19056
|
-
this.#stats.bytesReceived += packet.byteLength;
|
|
19057
19609
|
this.#onPacket(packet, context);
|
|
19058
19610
|
this.#resetWatchdog();
|
|
19059
19611
|
}
|
|
@@ -19071,14 +19623,25 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
19071
19623
|
onFailback(cb) {
|
|
19072
19624
|
this.#onFailback = cb;
|
|
19073
19625
|
}
|
|
19626
|
+
/**
|
|
19627
|
+
* Called on every successful join, before the joined subscription can
|
|
19628
|
+
* deliver a packet, with how that subscription delivers packets.
|
|
19629
|
+
*/
|
|
19630
|
+
onDelivery(cb) {
|
|
19631
|
+
this.#onDelivery = cb;
|
|
19632
|
+
}
|
|
19633
|
+
/** How the current subscription delivers packets, once joined. */
|
|
19634
|
+
get delivery() {
|
|
19635
|
+
return this.#delivery;
|
|
19636
|
+
}
|
|
19074
19637
|
/**
|
|
19075
19638
|
* Fires once per join, at the first packet the join accepts (after the PID
|
|
19076
19639
|
* filter; a rejected packet is not liveness). This is the data-plane-live
|
|
19077
|
-
* edge, where this transport's silence window starts.
|
|
19078
|
-
*
|
|
19079
|
-
*
|
|
19080
|
-
*
|
|
19081
|
-
*
|
|
19640
|
+
* edge, where this transport's silence window starts. A join whose source
|
|
19641
|
+
* never sends never reaches it, so a host may start a media-arrival budget
|
|
19642
|
+
* here only if the provider bounds the join phase with a typed subscription
|
|
19643
|
+
* error (see SessionManager.joinActive); otherwise the host starts it when
|
|
19644
|
+
* the subscribe settles.
|
|
19082
19645
|
*/
|
|
19083
19646
|
onDataPlaneLive(cb) {
|
|
19084
19647
|
this.#onDataPlaneLive = cb;
|
|
@@ -19103,6 +19666,33 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
19103
19666
|
get stats() {
|
|
19104
19667
|
return this.#stats;
|
|
19105
19668
|
}
|
|
19669
|
+
/**
|
|
19670
|
+
* Accepted bytes summed over `packetIds`: the `bytesReceived` share of those
|
|
19671
|
+
* MMTP packet_ids. Counted by the same rule, on every path that counts
|
|
19672
|
+
* `bytesReceived` (only packets that pass the ingress filter; packet_id 0
|
|
19673
|
+
* signaling never), and cumulative for the life of the transport, across
|
|
19674
|
+
* rejoins, as `bytesReceived` is. An id never accepted contributes 0. An
|
|
19675
|
+
* unparseable packet admitted by a join without an allow-list has no
|
|
19676
|
+
* packet_id, so it counts toward `bytesReceived` only.
|
|
19677
|
+
*
|
|
19678
|
+
* Use it to scope an ABR throughput sample to the active tier's source and
|
|
19679
|
+
* repair packet_ids when the join admits more than the active tier (a
|
|
19680
|
+
* shared group joined across rendition switches); `bytesReceived` is then
|
|
19681
|
+
* the whole allow-list's arrival, not the active tier's. A Set, so an id
|
|
19682
|
+
* shared by two tracks of the tier is counted once.
|
|
19683
|
+
*
|
|
19684
|
+
* Monotonic only for a fixed set: each id has counted since it was first
|
|
19685
|
+
* admitted, so after a tier switch the new set's sum is not a continuation
|
|
19686
|
+
* of the old one. A cumulative consumer takes a fresh baseline for the new
|
|
19687
|
+
* set and never differences across sets.
|
|
19688
|
+
*/
|
|
19689
|
+
bytesReceivedForPacketIds(packetIds) {
|
|
19690
|
+
let bytes = 0;
|
|
19691
|
+
for (const packetId of packetIds) {
|
|
19692
|
+
bytes += this.#acceptedBytesByPacketId.get(packetId)?.bytes ?? 0;
|
|
19693
|
+
}
|
|
19694
|
+
return bytes;
|
|
19695
|
+
}
|
|
19106
19696
|
/**
|
|
19107
19697
|
* Per-PID drop breakdown, newest-capped at MAX_TRACKED_DROPPED_PIDS distinct
|
|
19108
19698
|
* PIDs. This is what names the offender: `packetsDroppedUnregisteredPid`
|
|
@@ -19138,12 +19728,17 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
19138
19728
|
this.#packetListener = null;
|
|
19139
19729
|
this.#errorListener = null;
|
|
19140
19730
|
this.#resolvedTransport = void 0;
|
|
19731
|
+
this.#delivery = void 0;
|
|
19141
19732
|
this.#packetIds = null;
|
|
19142
19733
|
}
|
|
19734
|
+
/** The ingress filter; an accepted packet is counted here, once. */
|
|
19143
19735
|
#acceptPacket(packet) {
|
|
19144
19736
|
const packetId = readMMTPPacketId(packet);
|
|
19145
19737
|
if (packetId === null) {
|
|
19146
|
-
if (!this.#packetIds)
|
|
19738
|
+
if (!this.#packetIds) {
|
|
19739
|
+
this.#countAccepted(packet, null);
|
|
19740
|
+
return true;
|
|
19741
|
+
}
|
|
19147
19742
|
this.#stats.packetsDroppedUnparseable++;
|
|
19148
19743
|
return false;
|
|
19149
19744
|
}
|
|
@@ -19151,10 +19746,25 @@ var MulticastTransport = class _MulticastTransport {
|
|
|
19151
19746
|
this.#stats.packetsDroppedSignaling++;
|
|
19152
19747
|
return false;
|
|
19153
19748
|
}
|
|
19154
|
-
if (!this.#packetIds || this.#packetIds.has(packetId))
|
|
19749
|
+
if (!this.#packetIds || this.#packetIds.has(packetId)) {
|
|
19750
|
+
this.#countAccepted(packet, packetId);
|
|
19751
|
+
return true;
|
|
19752
|
+
}
|
|
19155
19753
|
this.#recordUnregisteredPidDrop(packetId);
|
|
19156
19754
|
return false;
|
|
19157
19755
|
}
|
|
19756
|
+
/** Receive counters for one accepted packet; `packetId` is null when it has none. */
|
|
19757
|
+
#countAccepted(packet, packetId) {
|
|
19758
|
+
this.#stats.packetsReceived++;
|
|
19759
|
+
this.#stats.bytesReceived += packet.byteLength;
|
|
19760
|
+
if (packetId === null) return;
|
|
19761
|
+
const counter = this.#acceptedBytesByPacketId.get(packetId);
|
|
19762
|
+
if (counter !== void 0) {
|
|
19763
|
+
counter.bytes += packet.byteLength;
|
|
19764
|
+
} else {
|
|
19765
|
+
this.#acceptedBytesByPacketId.set(packetId, { bytes: packet.byteLength });
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19158
19768
|
#recordUnregisteredPidDrop(packetId) {
|
|
19159
19769
|
this.#stats.packetsDroppedUnregisteredPid++;
|
|
19160
19770
|
const seen = this.#droppedPidCounts.get(packetId);
|
|
@@ -19240,6 +19850,7 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
|
|
|
19240
19850
|
deltaBlocksRecoveredQuic: 0,
|
|
19241
19851
|
deltaRepairsPreempted: 0,
|
|
19242
19852
|
deltaRepairsDeferred: 0,
|
|
19853
|
+
repairsSkipped: 0,
|
|
19243
19854
|
deltaBlocksFailed: 0,
|
|
19244
19855
|
audioBlocksRecoveredInband: 0,
|
|
19245
19856
|
audioBlocksRecoveredQuic: 0,
|
|
@@ -19630,6 +20241,9 @@ var MulticastManager = class {
|
|
|
19630
20241
|
this.#transport = new MulticastTransport((packet, ingress) => {
|
|
19631
20242
|
this.#router.feedPacket(packet, "multicast", void 0, ingress);
|
|
19632
20243
|
});
|
|
20244
|
+
this.#transport.onDelivery((delivery) => {
|
|
20245
|
+
this.#fec.setMulticastSymbolDelivery(delivery);
|
|
20246
|
+
});
|
|
19633
20247
|
this.#fec.onBlockRecovered = (sbn, packetId, data, meta, recoveredTrackName) => {
|
|
19634
20248
|
const recovered = new Uint8Array(data);
|
|
19635
20249
|
const catalogTrackName = recoveredTrackName ?? this.#fec.catalogTrackNameForBlock(sbn, packetId);
|
|
@@ -19736,6 +20350,16 @@ var MulticastManager = class {
|
|
|
19736
20350
|
get stats() {
|
|
19737
20351
|
return this.#transport.stats;
|
|
19738
20352
|
}
|
|
20353
|
+
/**
|
|
20354
|
+
* Accepted bytes for these MMTP packet_ids, cumulative like
|
|
20355
|
+
* `stats.bytesReceived`; see MulticastTransport.bytesReceivedForPacketIds.
|
|
20356
|
+
* Scopes an ABR throughput sample to the active tier when the join admits
|
|
20357
|
+
* more packet_ids than that tier's. Monotonic only while the set is fixed:
|
|
20358
|
+
* re-baseline when the queried set changes (e.g. on a tier switch).
|
|
20359
|
+
*/
|
|
20360
|
+
bytesReceivedForPacketIds(packetIds) {
|
|
20361
|
+
return this.#transport.bytesReceivedForPacketIds(packetIds);
|
|
20362
|
+
}
|
|
19739
20363
|
get fecStats() {
|
|
19740
20364
|
return this.#fec.fecStats;
|
|
19741
20365
|
}
|
|
@@ -20011,7 +20635,8 @@ var MulticastManager = class {
|
|
|
20011
20635
|
* Applies only to tracks WITHOUT FEC: an FEC track never arms the timer,
|
|
20012
20636
|
* because its vanished blocks resolve through catalog-derived block
|
|
20013
20637
|
* deadlines and the release gate's stall decision.
|
|
20014
|
-
* Must be called BEFORE setPacketIdType('video').
|
|
20638
|
+
* Must be called BEFORE setPacketIdType('video'). A reader is passed
|
|
20639
|
+
* through; see MmtpRouter.setKeyframeLostTimeoutMs.
|
|
20015
20640
|
*/
|
|
20016
20641
|
setKeyframeLostTimeoutMs(ms) {
|
|
20017
20642
|
this.#router.setKeyframeLostTimeoutMs(ms);
|
|
@@ -20115,6 +20740,14 @@ var MulticastManager = class {
|
|
|
20115
20740
|
groupAddress: config.groupAddress,
|
|
20116
20741
|
port: config.port,
|
|
20117
20742
|
mode: config.mode,
|
|
20743
|
+
// Precedence between two sources, not a tolerant alias: `amtRelay*` is
|
|
20744
|
+
// the player/UI override and `relay*` is the catalog endpoint's relay
|
|
20745
|
+
// (REQUIRED for `protocol: "amt"`; see multicast-input.ts). The override
|
|
20746
|
+
// wins, else the catalog value. This is the only inlet for the
|
|
20747
|
+
// endpoint-level catalog relay (`relayAddress` / `relayPort`); a catalog
|
|
20748
|
+
// `multicast.networkSource` AMT entry reaches the transport separately,
|
|
20749
|
+
// through `findAmtRelay`. Retiring `relayAddress` therefore belongs to
|
|
20750
|
+
// the coordinated `networkSource` migration.
|
|
20118
20751
|
amtRelay: config.amtRelay ?? config.relayAddress,
|
|
20119
20752
|
amtRelayPort: config.amtRelayPort ?? config.relayPort,
|
|
20120
20753
|
packetIds: config.packetIds,
|
|
@@ -20133,6 +20766,14 @@ var MulticastManager = class {
|
|
|
20133
20766
|
feedPacket(data, ingress) {
|
|
20134
20767
|
this.#transport.feedPacket(data, ingress);
|
|
20135
20768
|
}
|
|
20769
|
+
/**
|
|
20770
|
+
* Declare how packets given to feedPacket() reach this manager. A consumer
|
|
20771
|
+
* that owns the multicast socket itself declares its own leg; join()
|
|
20772
|
+
* applies what the window.multicast subscription declares instead.
|
|
20773
|
+
*/
|
|
20774
|
+
setMulticastSymbolDelivery(delivery) {
|
|
20775
|
+
this.#fec.setMulticastSymbolDelivery(delivery);
|
|
20776
|
+
}
|
|
20136
20777
|
/** Process a repair packet from MoQ (reliable QUIC). */
|
|
20137
20778
|
processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
|
|
20138
20779
|
this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
|
|
@@ -20349,6 +20990,18 @@ var MulticastManager = class {
|
|
|
20349
20990
|
reason
|
|
20350
20991
|
});
|
|
20351
20992
|
}
|
|
20993
|
+
/** Record a repair request the scheduler sent no FETCH for. */
|
|
20994
|
+
noteRepairSkipped(reason, measurement, sbn, packetId, catalogTrackName) {
|
|
20995
|
+
this.#statsFor(packetId, catalogTrackName).repairsSkipped++;
|
|
20996
|
+
this.#emitRepairLifecycle({
|
|
20997
|
+
version: 2,
|
|
20998
|
+
type: "repair-skipped",
|
|
20999
|
+
block: repairBlockIdentity(packetId, sbn, catalogTrackName),
|
|
21000
|
+
reason,
|
|
21001
|
+
rttMs: measurement.rttMs,
|
|
21002
|
+
budgetMs: measurement.budgetMs
|
|
21003
|
+
});
|
|
21004
|
+
}
|
|
20352
21005
|
/**
|
|
20353
21006
|
* Record a repair-scheduler decision about a delta block: preempted for
|
|
20354
21007
|
* keyframe/audio work, or never dispatched before its block resolved.
|
|
@@ -20622,7 +21275,7 @@ var MediaRouter = class {
|
|
|
20622
21275
|
// ── Keyframe repair state ────────────────────────────────────────
|
|
20623
21276
|
/** Groups currently being repaired — idempotent guard per Pitfall 2 (T-13-01 mitigation) */
|
|
20624
21277
|
#repairInFlight = /* @__PURE__ */ new Set();
|
|
20625
|
-
/** Max concurrent repairs — prevents relay SUBSCRIBE flood (T-13-01 mitigation) */
|
|
21278
|
+
/** Max concurrent repairs — prevents relay SUBSCRIBE flood (T-13-01 mitigation). A reader is evaluated per decision. */
|
|
20626
21279
|
#maxRepairInFlight;
|
|
20627
21280
|
/** Callback to request MoQ subscribe for a specific group (wired by player) */
|
|
20628
21281
|
#onRepairRequest = null;
|
|
@@ -20668,7 +21321,7 @@ var MediaRouter = class {
|
|
|
20668
21321
|
this.#crossfadeFrames = options?.crossfadeFrames ?? DEFAULT_CROSSFADE_FRAMES;
|
|
20669
21322
|
this.#maxOverlayBuffer = options?.maxOverlayBuffer ?? DEFAULT_MAX_OVERLAY_BUFFER;
|
|
20670
21323
|
const maxRepairInFlight = options?.maxRepairInFlight;
|
|
20671
|
-
if (maxRepairInFlight !== void 0 && (!Number.isSafeInteger(maxRepairInFlight) || maxRepairInFlight < 1)) {
|
|
21324
|
+
if (maxRepairInFlight !== void 0 && typeof maxRepairInFlight !== "function" && (!Number.isSafeInteger(maxRepairInFlight) || maxRepairInFlight < 1)) {
|
|
20672
21325
|
throw new Error(
|
|
20673
21326
|
`[MediaRouter] maxRepairInFlight=${maxRepairInFlight} must be a positive integer`
|
|
20674
21327
|
);
|
|
@@ -20704,6 +21357,8 @@ var MediaRouter = class {
|
|
|
20704
21357
|
/**
|
|
20705
21358
|
* Request repair for a lost keyframe group. Only fires when activeSource is 'multicast'.
|
|
20706
21359
|
* Idempotent per groupId — duplicate calls for the same group are suppressed.
|
|
21360
|
+
* A reader `maxRepairInFlight` is evaluated here; without a valid value the
|
|
21361
|
+
* group is skipped and a later call reads it again.
|
|
20707
21362
|
* @param expectedGroupId - The group ID expected but not received
|
|
20708
21363
|
* @param trackName - Track name for the SUBSCRIBE (e.g. 'video'). Default: 'video'.
|
|
20709
21364
|
*/
|
|
@@ -20711,9 +21366,25 @@ var MediaRouter = class {
|
|
|
20711
21366
|
if (this.#active !== "multicast") return;
|
|
20712
21367
|
if (this.#maxRepairInFlight === null) return;
|
|
20713
21368
|
if (this.#repairInFlight.has(expectedGroupId)) return;
|
|
20714
|
-
|
|
21369
|
+
let maxRepairInFlight;
|
|
21370
|
+
try {
|
|
21371
|
+
maxRepairInFlight = typeof this.#maxRepairInFlight === "function" ? this.#maxRepairInFlight() : this.#maxRepairInFlight;
|
|
21372
|
+
} catch (error) {
|
|
21373
|
+
console.warn(
|
|
21374
|
+
`[MediaRouter] maxRepairInFlight unmeasured, skipping repair for group ${expectedGroupId}:`,
|
|
21375
|
+
error
|
|
21376
|
+
);
|
|
21377
|
+
return;
|
|
21378
|
+
}
|
|
21379
|
+
if (!Number.isSafeInteger(maxRepairInFlight) || maxRepairInFlight < 1) {
|
|
21380
|
+
console.warn(
|
|
21381
|
+
`[MediaRouter] maxRepairInFlight=${maxRepairInFlight} is not a positive integer, skipping repair for group ${expectedGroupId}`
|
|
21382
|
+
);
|
|
21383
|
+
return;
|
|
21384
|
+
}
|
|
21385
|
+
if (this.#repairInFlight.size >= maxRepairInFlight) {
|
|
20715
21386
|
console.warn(
|
|
20716
|
-
`[MediaRouter] max repair in flight (${
|
|
21387
|
+
`[MediaRouter] max repair in flight (${maxRepairInFlight}), dropping repair for group ${expectedGroupId}`
|
|
20717
21388
|
);
|
|
20718
21389
|
return;
|
|
20719
21390
|
}
|
|
@@ -22193,8 +22864,7 @@ function recordOf(value) {
|
|
|
22193
22864
|
function msfTracks(catalog) {
|
|
22194
22865
|
const raw = catalog.tracks;
|
|
22195
22866
|
if (!Array.isArray(raw)) return void 0;
|
|
22196
|
-
|
|
22197
|
-
return { tracks: raw.map((t) => recordOf(t) ?? {}), common };
|
|
22867
|
+
return { tracks: raw.map((t) => recordOf(t) ?? {}) };
|
|
22198
22868
|
}
|
|
22199
22869
|
function resolveRepairLayers(catalog, source) {
|
|
22200
22870
|
if (!catalog) return void 0;
|
|
@@ -22250,7 +22920,7 @@ function resolveRepairLayers(catalog, source) {
|
|
|
22250
22920
|
if (!msf || msfIndex < 0) return void 0;
|
|
22251
22921
|
const track = msf.tracks[msfIndex];
|
|
22252
22922
|
const fec = recordOf(track["fec"]);
|
|
22253
|
-
const packagingOf = (t) => t["packaging"]
|
|
22923
|
+
const packagingOf = (t) => t["packaging"];
|
|
22254
22924
|
const fecOfTrack = (t) => {
|
|
22255
22925
|
const trackFec = recordOf(t?.["fec"]);
|
|
22256
22926
|
return trackFec !== void 0 && trackFec["algorithm"] !== "none" ? trackFec : void 0;
|
|
@@ -22296,7 +22966,7 @@ function catalogRepairTarget(catalog, source) {
|
|
|
22296
22966
|
const sourceName = typeof source === "string" ? source : source.name;
|
|
22297
22967
|
const msf = msfTracks(catalog);
|
|
22298
22968
|
const entry = msf?.tracks.find((t) => t["name"] === sourceName);
|
|
22299
|
-
const sourcePriority = entry === void 0 ? null : moqtPriority2(entry["priority"]
|
|
22969
|
+
const sourcePriority = entry === void 0 ? null : moqtPriority2(entry["priority"]);
|
|
22300
22970
|
return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
|
|
22301
22971
|
}
|
|
22302
22972
|
function geometryOf(name, path, fec) {
|
|
@@ -22430,6 +23100,12 @@ function wireRepairFetch(mc, opts) {
|
|
|
22430
23100
|
`[RepairFetch] attemptLimit=${String(opts.attemptLimit)} must be a positive integer`
|
|
22431
23101
|
);
|
|
22432
23102
|
}
|
|
23103
|
+
if (typeof opts.reliableRttMs !== "function") {
|
|
23104
|
+
throw new Error("[RepairFetch] reliableRttMs is required: repair admission needs the measured reliable-path RTT");
|
|
23105
|
+
}
|
|
23106
|
+
if (typeof opts.presentationDelayMs !== "function") {
|
|
23107
|
+
throw new Error("[RepairFetch] presentationDelayMs is required: a repair FETCH is due by the block's presentation deadline");
|
|
23108
|
+
}
|
|
22433
23109
|
const entries = /* @__PURE__ */ new Map();
|
|
22434
23110
|
const warnedNoTarget = /* @__PURE__ */ new Set();
|
|
22435
23111
|
let disposed = false;
|
|
@@ -22443,12 +23119,41 @@ function wireRepairFetch(mc, opts) {
|
|
|
22443
23119
|
const note = (type, entry) => {
|
|
22444
23120
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
22445
23121
|
};
|
|
23122
|
+
const releaseHold = (entry) => {
|
|
23123
|
+
if (entry.catalogTrackName === void 0) mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId);
|
|
23124
|
+
else mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
23125
|
+
};
|
|
22446
23126
|
const stop = (entry, reason) => {
|
|
22447
23127
|
const run = entry.running;
|
|
22448
23128
|
if (!run) return;
|
|
22449
23129
|
entry.running = null;
|
|
22450
23130
|
run.stopped = reason;
|
|
22451
23131
|
run.controller.abort();
|
|
23132
|
+
if (reason === "preempted") releaseHold(entry);
|
|
23133
|
+
};
|
|
23134
|
+
const readRtt = () => {
|
|
23135
|
+
try {
|
|
23136
|
+
const rtt = opts.reliableRttMs();
|
|
23137
|
+
return Number.isFinite(rtt) && rtt > 0 ? rtt : null;
|
|
23138
|
+
} catch {
|
|
23139
|
+
return null;
|
|
23140
|
+
}
|
|
23141
|
+
};
|
|
23142
|
+
const admit = (entry, now) => {
|
|
23143
|
+
const budgetMs = entry.deadlineAt - now;
|
|
23144
|
+
const rttMs = readRtt();
|
|
23145
|
+
if (rttMs !== null && rttMs < budgetMs) return true;
|
|
23146
|
+
retire(entry);
|
|
23147
|
+
releaseHold(entry);
|
|
23148
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
23149
|
+
mc.noteRepairSkipped(
|
|
23150
|
+
rttMs === null ? "rtt-unmeasured" : "rtt-exceeds-budget",
|
|
23151
|
+
{ rttMs, budgetMs },
|
|
23152
|
+
entry.sbn,
|
|
23153
|
+
entry.packetId,
|
|
23154
|
+
entry.catalogTrackName
|
|
23155
|
+
);
|
|
23156
|
+
return false;
|
|
22452
23157
|
};
|
|
22453
23158
|
const retire = (entry) => {
|
|
22454
23159
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
@@ -22480,9 +23185,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
22480
23185
|
return derived.kind === "omit" ? { subscriberPriority: void 0 } : null;
|
|
22481
23186
|
};
|
|
22482
23187
|
const start = (entry, counted = true) => {
|
|
23188
|
+
if (!admit(entry, performance.now())) return;
|
|
22483
23189
|
if (counted) entry.attempts++;
|
|
22484
23190
|
const run = { controller: new AbortController(), stopped: null };
|
|
22485
23191
|
entry.running = run;
|
|
23192
|
+
mc.fecManager.holdBlockForRepair(
|
|
23193
|
+
entry.sbn,
|
|
23194
|
+
entry.packetId,
|
|
23195
|
+
entry.catalogTrackName,
|
|
23196
|
+
entry.deadlineAt
|
|
23197
|
+
);
|
|
22486
23198
|
void execute(entry, run);
|
|
22487
23199
|
};
|
|
22488
23200
|
const dispatch = () => {
|
|
@@ -22519,7 +23231,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
22519
23231
|
entry.running = null;
|
|
22520
23232
|
if (error === void 0) {
|
|
22521
23233
|
retire(entry);
|
|
22522
|
-
if (fed === 0)
|
|
23234
|
+
if (fed === 0) {
|
|
23235
|
+
releaseHold(entry);
|
|
23236
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
23237
|
+
}
|
|
22523
23238
|
dispatch();
|
|
22524
23239
|
return;
|
|
22525
23240
|
}
|
|
@@ -22529,6 +23244,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
22529
23244
|
return;
|
|
22530
23245
|
}
|
|
22531
23246
|
retire(entry);
|
|
23247
|
+
if (fed === 0) releaseHold(entry);
|
|
22532
23248
|
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
22533
23249
|
console.warn(
|
|
22534
23250
|
`[RepairFetch] fetch failed pid=${entry.packetId} sbn=${entry.sbn} track=${entry.repairTrack}:`,
|
|
@@ -22599,6 +23315,20 @@ function wireRepairFetch(mc, opts) {
|
|
|
22599
23315
|
else if (entry.priority === 2 && entry.attempts === 0) note("repair-deferred", entry);
|
|
22600
23316
|
dispatch();
|
|
22601
23317
|
});
|
|
23318
|
+
const presentationDeadlineAt = (sbn, packetId, catalogTrackName, now) => {
|
|
23319
|
+
const waitDeadlineAt = now + mc.fecManager.repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName);
|
|
23320
|
+
const anchor = mc.fecManager.blockAnchorAtMs(sbn, packetId, catalogTrackName);
|
|
23321
|
+
if (anchor === void 0) return waitDeadlineAt;
|
|
23322
|
+
let delayMs;
|
|
23323
|
+
try {
|
|
23324
|
+
delayMs = opts.presentationDelayMs(packetId, catalogTrackName);
|
|
23325
|
+
} catch {
|
|
23326
|
+
return null;
|
|
23327
|
+
}
|
|
23328
|
+
if (!(Number.isFinite(delayMs) && delayMs > 0)) return null;
|
|
23329
|
+
const ceilingAt = anchor + mc.fecManager.terminalRetentionMsForPacket(packetId);
|
|
23330
|
+
return Math.max(waitDeadlineAt, Math.min(anchor + delayMs, ceilingAt));
|
|
23331
|
+
};
|
|
22602
23332
|
mc.onRepairSubscribe((sbn, packetId, catalogTrackName, blockClass) => {
|
|
22603
23333
|
if (disposed) {
|
|
22604
23334
|
clearRepair(sbn, packetId, catalogTrackName);
|
|
@@ -22633,14 +23363,26 @@ function wireRepairFetch(mc, opts) {
|
|
|
22633
23363
|
clearRepair(sbn, packetId, catalogTrackName);
|
|
22634
23364
|
return;
|
|
22635
23365
|
}
|
|
22636
|
-
const
|
|
23366
|
+
const now = performance.now();
|
|
23367
|
+
const deadlineAt = presentationDeadlineAt(sbn, packetId, catalogTrackName, now);
|
|
23368
|
+
if (deadlineAt === null) {
|
|
23369
|
+
clearRepair(sbn, packetId, catalogTrackName);
|
|
23370
|
+
mc.noteRepairSkipped(
|
|
23371
|
+
"presentation-delay-unavailable",
|
|
23372
|
+
{ rttMs: readRtt(), budgetMs: 0 },
|
|
23373
|
+
sbn,
|
|
23374
|
+
packetId,
|
|
23375
|
+
catalogTrackName
|
|
23376
|
+
);
|
|
23377
|
+
return;
|
|
23378
|
+
}
|
|
22637
23379
|
const entry = {
|
|
22638
23380
|
key,
|
|
22639
23381
|
...identity,
|
|
22640
23382
|
repairTrack: target.repairTrack,
|
|
22641
23383
|
priority: repairPriorityForClass(blockClass),
|
|
22642
23384
|
subscriberPriority: wire.subscriberPriority,
|
|
22643
|
-
deadlineAt
|
|
23385
|
+
deadlineAt,
|
|
22644
23386
|
order: enqueueOrder++,
|
|
22645
23387
|
attempts: 0,
|
|
22646
23388
|
running: null
|
|
@@ -22655,6 +23397,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
22655
23397
|
removeLifecycleObserver();
|
|
22656
23398
|
for (const entry of [...entries.values()]) {
|
|
22657
23399
|
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
23400
|
+
if (entry.running) releaseHold(entry);
|
|
22658
23401
|
stop(entry, "disposed");
|
|
22659
23402
|
}
|
|
22660
23403
|
entries.clear();
|
|
@@ -22670,6 +23413,7 @@ export {
|
|
|
22670
23413
|
CmcdProducer,
|
|
22671
23414
|
CmsfCatalogValidationError,
|
|
22672
23415
|
DEFAULT_REPAIR_PRIORITY,
|
|
23416
|
+
DeliveryGapEstimator,
|
|
22673
23417
|
FECManager,
|
|
22674
23418
|
FEC_ESI_MAX,
|
|
22675
23419
|
FEC_SBN_MAX,
|