@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +1949 -374
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- 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/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -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-state.js.map +1 -1
- 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/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +336 -59
- package/dist/no-modules.js.map +3 -3
- 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 +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.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-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -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-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -1
package/dist/fec-manager.js
CHANGED
|
@@ -327,6 +327,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
327
327
|
this.#heldEvicted = 0;
|
|
328
328
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
329
329
|
this.#holes = [];
|
|
330
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
331
|
+
this.#expired = [];
|
|
332
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
333
|
+
this.#newlyExpired = [];
|
|
334
|
+
/**
|
|
335
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
336
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
337
|
+
*/
|
|
338
|
+
this.#closedBehind = [];
|
|
330
339
|
/**
|
|
331
340
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
332
341
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -348,7 +357,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
348
357
|
#retentionBlocks;
|
|
349
358
|
#heldEvicted;
|
|
350
359
|
#newest;
|
|
360
|
+
#newestSource;
|
|
351
361
|
#holes;
|
|
362
|
+
#expired;
|
|
363
|
+
#newlyExpired;
|
|
364
|
+
#closedBehind;
|
|
352
365
|
static #checkedRetention(retentionBlocks) {
|
|
353
366
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
354
367
|
throw new Error(
|
|
@@ -385,6 +398,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
385
398
|
observe(position) {
|
|
386
399
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
387
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
403
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
404
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
405
|
+
* reordering.
|
|
406
|
+
*/
|
|
407
|
+
get newestSource() {
|
|
408
|
+
return this.#newestSource;
|
|
409
|
+
}
|
|
410
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
411
|
+
noteSource(ssId) {
|
|
412
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
413
|
+
this.#newestSource = ssId;
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
388
416
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
389
417
|
get heldEvicted() {
|
|
390
418
|
return this.#heldEvicted;
|
|
@@ -403,17 +431,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
403
431
|
}
|
|
404
432
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
405
433
|
locate(ssId) {
|
|
406
|
-
for (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
434
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
435
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
436
|
+
const block = blocks[i];
|
|
437
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
438
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
439
|
+
}
|
|
410
440
|
}
|
|
411
441
|
}
|
|
412
442
|
return null;
|
|
413
443
|
}
|
|
414
|
-
/** Number of pending
|
|
444
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
415
445
|
get holeCount() {
|
|
416
|
-
return this.#holes.length;
|
|
446
|
+
return this.#holes.length + this.#expired.length;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
450
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
451
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
452
|
+
* release gate does not wait for it. Its owner must close it
|
|
453
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
454
|
+
*/
|
|
455
|
+
takeExpiredHoles() {
|
|
456
|
+
return this.#newlyExpired.splice(0);
|
|
417
457
|
}
|
|
418
458
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
419
459
|
blockEndingAt(end) {
|
|
@@ -469,9 +509,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
469
509
|
/**
|
|
470
510
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
471
511
|
* block states its range and places every held source in it; later repair
|
|
472
|
-
* symbols of the same block change nothing. A range
|
|
473
|
-
*
|
|
474
|
-
*
|
|
512
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
513
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
514
|
+
* a known block (a different length, or an overlap with another block or
|
|
515
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
516
|
+
* block changes.
|
|
475
517
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
476
518
|
* A block so far past `newest` that stating it would retire the position
|
|
477
519
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -490,7 +532,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
490
532
|
const known = this.blockAt(start);
|
|
491
533
|
if (known !== void 0) {
|
|
492
534
|
if (known.span) {
|
|
493
|
-
|
|
535
|
+
if (known.length === length) {
|
|
536
|
+
const block2 = { start, length, span: false };
|
|
537
|
+
this.#blocks[this.#blocks.indexOf(known)] = block2;
|
|
538
|
+
return { status: "placed", block: block2, placed: [] };
|
|
539
|
+
}
|
|
540
|
+
return length < known.length ? {
|
|
541
|
+
status: "overlap",
|
|
542
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
543
|
+
ofSpan: known
|
|
544
|
+
} : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
|
|
494
545
|
}
|
|
495
546
|
if (known.length !== length) {
|
|
496
547
|
return {
|
|
@@ -558,18 +609,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
558
609
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
559
610
|
}
|
|
560
611
|
/**
|
|
561
|
-
* Turn every
|
|
562
|
-
*
|
|
563
|
-
*
|
|
612
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
613
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
614
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
615
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
616
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
617
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
564
618
|
*/
|
|
565
619
|
closeHolesWithin(start, length) {
|
|
566
620
|
const closed = [];
|
|
567
|
-
for (const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
621
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
622
|
+
for (const hole of [...holes]) {
|
|
623
|
+
if (!isoBlockContains(start, length, hole.start)) continue;
|
|
624
|
+
holes.splice(holes.indexOf(hole), 1);
|
|
625
|
+
const spanBlock = { start: hole.start, length: hole.length, span: true };
|
|
626
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
|
|
627
|
+
if (holes === this.#holes) {
|
|
628
|
+
this.#blocks.push(spanBlock);
|
|
629
|
+
} else {
|
|
630
|
+
this.#closedBehind.push(spanBlock);
|
|
631
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
573
634
|
}
|
|
574
635
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
575
636
|
}
|
|
@@ -583,15 +644,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
583
644
|
return void 0;
|
|
584
645
|
}
|
|
585
646
|
/**
|
|
586
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
587
|
-
* lies more than `retentionBlocks` blocks behind
|
|
588
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
589
|
-
* horizon uses
|
|
647
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
648
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
649
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
650
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
651
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
590
652
|
*/
|
|
591
653
|
retireBehind(position) {
|
|
592
654
|
this.observe(position);
|
|
593
655
|
const horizon = this.#retentionBlocks;
|
|
594
656
|
const retired = [];
|
|
657
|
+
this.#closedBehind.length = 0;
|
|
595
658
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
596
659
|
const block = this.#blocks[i];
|
|
597
660
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -603,10 +666,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
603
666
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
604
667
|
const hole = this.#holes[i];
|
|
605
668
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
606
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
669
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
|
|
670
|
+
this.#holes.splice(i, 1);
|
|
671
|
+
this.#expired.push(hole);
|
|
672
|
+
this.#newlyExpired.push(hole);
|
|
607
673
|
}
|
|
608
674
|
for (const ssId of [...this.#held.keys()]) {
|
|
609
|
-
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#
|
|
675
|
+
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))) {
|
|
676
|
+
this.#held.delete(ssId);
|
|
677
|
+
}
|
|
610
678
|
}
|
|
611
679
|
return retired;
|
|
612
680
|
}
|
|
@@ -614,9 +682,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
614
682
|
reset() {
|
|
615
683
|
this.#blocks.length = 0;
|
|
616
684
|
this.#holes.length = 0;
|
|
685
|
+
this.#expired.length = 0;
|
|
686
|
+
this.#newlyExpired.length = 0;
|
|
687
|
+
this.#closedBehind.length = 0;
|
|
617
688
|
this.#held.clear();
|
|
618
689
|
this.#coverageEnd = void 0;
|
|
619
690
|
this.#newest = void 0;
|
|
691
|
+
this.#newestSource = void 0;
|
|
620
692
|
}
|
|
621
693
|
#claim(block) {
|
|
622
694
|
const placed = [];
|
|
@@ -1081,6 +1153,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
1081
1153
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
1082
1154
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
1083
1155
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
1156
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
1084
1157
|
function isMulticastSymbolDelivery(value) {
|
|
1085
1158
|
return value === "datagram" || value === "ordered-relay";
|
|
1086
1159
|
}
|
|
@@ -1174,10 +1247,18 @@ var FECManager = class _FECManager {
|
|
|
1174
1247
|
// 'pending' forever, which would park every later frame of its stream in
|
|
1175
1248
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
1176
1249
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
1250
|
+
/**
|
|
1251
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
1252
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
1253
|
+
*/
|
|
1177
1254
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
1178
1255
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
1179
1256
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
1180
1257
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
1258
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
1259
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
1260
|
+
// not network arrivals and never make a block clean.
|
|
1261
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
1181
1262
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
1182
1263
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
1183
1264
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -1186,6 +1267,9 @@ var FECManager = class _FECManager {
|
|
|
1186
1267
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
1187
1268
|
// symbols; waiting for K fails every one of them.
|
|
1188
1269
|
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
1270
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
1271
|
+
// a repair states the block there or the span resolves.
|
|
1272
|
+
this.#isoSpanKeys = /* @__PURE__ */ new Set();
|
|
1189
1273
|
this.#ssbLengthConflictCount = 0;
|
|
1190
1274
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
1191
1275
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -1306,6 +1390,10 @@ var FECManager = class _FECManager {
|
|
|
1306
1390
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
1307
1391
|
// One classifier per physical stream, built from that stream's geometry.
|
|
1308
1392
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
1393
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
1394
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
1395
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
1396
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
1309
1397
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
1310
1398
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
1311
1399
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -1379,7 +1467,10 @@ var FECManager = class _FECManager {
|
|
|
1379
1467
|
// arrives via onBlockRecovered callback.
|
|
1380
1468
|
this.onBlockStarted = null;
|
|
1381
1469
|
this.onBlockComplete = null;
|
|
1382
|
-
/**
|
|
1470
|
+
/**
|
|
1471
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
1472
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
1473
|
+
*/
|
|
1383
1474
|
this.onBlockFailed = null;
|
|
1384
1475
|
/**
|
|
1385
1476
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -1421,6 +1512,8 @@ var FECManager = class _FECManager {
|
|
|
1421
1512
|
* for it, which it must be.
|
|
1422
1513
|
*/
|
|
1423
1514
|
this.#onUndelimitedLoss = null;
|
|
1515
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
1516
|
+
this.#onIsoHoleExpired = null;
|
|
1424
1517
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
1425
1518
|
this.onBlockVanished = null;
|
|
1426
1519
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -1463,7 +1556,9 @@ var FECManager = class _FECManager {
|
|
|
1463
1556
|
#pendingWorkerCleanupBounds;
|
|
1464
1557
|
#blockSourceCounts;
|
|
1465
1558
|
#blockSourceEsis;
|
|
1559
|
+
#blockRebuiltEsis;
|
|
1466
1560
|
#blockAuthoredLengths;
|
|
1561
|
+
#isoSpanKeys;
|
|
1467
1562
|
#ssbLengthConflictCount;
|
|
1468
1563
|
#blockRepairRsIds;
|
|
1469
1564
|
#blockRsbLengths;
|
|
@@ -1505,6 +1600,7 @@ var FECManager = class _FECManager {
|
|
|
1505
1600
|
#orderedFrontiers;
|
|
1506
1601
|
#orderedPendingBlocks;
|
|
1507
1602
|
#classifiers;
|
|
1603
|
+
#noSpanRaps;
|
|
1508
1604
|
#streamGeometries;
|
|
1509
1605
|
#failedBlocks;
|
|
1510
1606
|
#resolvedBlockClasses;
|
|
@@ -1529,6 +1625,7 @@ var FECManager = class _FECManager {
|
|
|
1529
1625
|
#decoderUnavailableEvents;
|
|
1530
1626
|
#repairSymbolSizeMismatchCount;
|
|
1531
1627
|
#onUndelimitedLoss;
|
|
1628
|
+
#onIsoHoleExpired;
|
|
1532
1629
|
#encodeTrackName(catalogTrackName) {
|
|
1533
1630
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
1534
1631
|
if (encoded === void 0) {
|
|
@@ -2000,7 +2097,10 @@ var FECManager = class _FECManager {
|
|
|
2000
2097
|
timer.handle = null;
|
|
2001
2098
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
2002
2099
|
};
|
|
2003
|
-
timer.handle = setTimeout(
|
|
2100
|
+
timer.handle = setTimeout(
|
|
2101
|
+
onFire,
|
|
2102
|
+
this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
|
|
2103
|
+
);
|
|
2004
2104
|
this.#repairDeadlines.set(key, timer);
|
|
2005
2105
|
}
|
|
2006
2106
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -2144,8 +2244,14 @@ var FECManager = class _FECManager {
|
|
|
2144
2244
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
2145
2245
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
2146
2246
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
2247
|
+
*
|
|
2248
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
2249
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
2250
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
2251
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
2252
|
+
* it (BLO-52085).
|
|
2147
2253
|
*/
|
|
2148
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
2254
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
2149
2255
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
2150
2256
|
const now = performance.now();
|
|
2151
2257
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -2166,6 +2272,7 @@ var FECManager = class _FECManager {
|
|
|
2166
2272
|
};
|
|
2167
2273
|
const discontinuity = () => {
|
|
2168
2274
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
2275
|
+
this.#noSpanRaps.delete(streamKey);
|
|
2169
2276
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
2170
2277
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
2171
2278
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -2183,7 +2290,7 @@ var FECManager = class _FECManager {
|
|
|
2183
2290
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
2184
2291
|
};
|
|
2185
2292
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
2186
|
-
if (symbol === "source" && window !== 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))) {
|
|
2293
|
+
if (symbol === "source" && window !== 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))) {
|
|
2187
2294
|
discontinuity();
|
|
2188
2295
|
return;
|
|
2189
2296
|
}
|
|
@@ -2445,17 +2552,21 @@ var FECManager = class _FECManager {
|
|
|
2445
2552
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
2446
2553
|
this.#repairHoldUntilMs.delete(key);
|
|
2447
2554
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
2448
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
2555
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
2449
2556
|
};
|
|
2450
|
-
timer.handle = setTimeout(
|
|
2557
|
+
timer.handle = setTimeout(
|
|
2558
|
+
onFire,
|
|
2559
|
+
this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
|
|
2560
|
+
);
|
|
2451
2561
|
this.#activeBlocks.set(key, timer);
|
|
2452
2562
|
this.#indexBlockKey(key, sbn, packetId);
|
|
2453
2563
|
}
|
|
2454
2564
|
/**
|
|
2455
2565
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
2456
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
2566
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
2567
|
+
* is when it was due: its deadline, or now.
|
|
2457
2568
|
*/
|
|
2458
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
2569
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
2459
2570
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
2460
2571
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
2461
2572
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -2471,7 +2582,7 @@ var FECManager = class _FECManager {
|
|
|
2471
2582
|
if (workerClient) {
|
|
2472
2583
|
this.#pendingWorkerCleanups.add(key);
|
|
2473
2584
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
2474
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
2585
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
2475
2586
|
workerClient.cleanup(sbn);
|
|
2476
2587
|
} else {
|
|
2477
2588
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -2578,6 +2689,9 @@ var FECManager = class _FECManager {
|
|
|
2578
2689
|
}
|
|
2579
2690
|
if (policyChanged) {
|
|
2580
2691
|
this.#bumpEpoch(packetId);
|
|
2692
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
2693
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
2694
|
+
}
|
|
2581
2695
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
2582
2696
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
2583
2697
|
const stream = this.#parseStreamKey(key);
|
|
@@ -2780,7 +2894,9 @@ var FECManager = class _FECManager {
|
|
|
2780
2894
|
}
|
|
2781
2895
|
resetMap(this.#blockSourceCounts);
|
|
2782
2896
|
resetMap(this.#blockSourceEsis);
|
|
2897
|
+
resetMap(this.#blockRebuiltEsis);
|
|
2783
2898
|
resetMap(this.#blockAuthoredLengths);
|
|
2899
|
+
resetSet(this.#isoSpanKeys);
|
|
2784
2900
|
resetMap(this.#blockRepairRsIds);
|
|
2785
2901
|
resetMap(this.#blockRsbLengths);
|
|
2786
2902
|
resetMap(this.#heldLayerStates);
|
|
@@ -2813,6 +2929,7 @@ var FECManager = class _FECManager {
|
|
|
2813
2929
|
resetMap(this.#isoLocators);
|
|
2814
2930
|
resetMap(this.#sourceAnchors);
|
|
2815
2931
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
2932
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
2816
2933
|
this.#bumpEpoch(packetId);
|
|
2817
2934
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
2818
2935
|
for (const key of keys.keys()) {
|
|
@@ -2903,7 +3020,9 @@ var FECManager = class _FECManager {
|
|
|
2903
3020
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
2904
3021
|
dropMap(this.#blockSourceCounts);
|
|
2905
3022
|
dropMap(this.#blockSourceEsis);
|
|
3023
|
+
dropMap(this.#blockRebuiltEsis);
|
|
2906
3024
|
dropMap(this.#blockAuthoredLengths);
|
|
3025
|
+
dropSet(this.#isoSpanKeys);
|
|
2907
3026
|
dropMap(this.#blockRepairRsIds);
|
|
2908
3027
|
dropMap(this.#blockRsbLengths);
|
|
2909
3028
|
dropMap(this.#blockPathCounts);
|
|
@@ -2919,6 +3038,7 @@ var FECManager = class _FECManager {
|
|
|
2919
3038
|
dropMap(this.#isoLocators);
|
|
2920
3039
|
dropMap(this.#sourceAnchors);
|
|
2921
3040
|
dropMap(this.#classifiers);
|
|
3041
|
+
dropMap(this.#noSpanRaps);
|
|
2922
3042
|
dropMap(this.#streamGeometries);
|
|
2923
3043
|
dropSet(this.#pendingWorkerCleanups);
|
|
2924
3044
|
dropSet(this.#completedBlocks);
|
|
@@ -3064,6 +3184,9 @@ var FECManager = class _FECManager {
|
|
|
3064
3184
|
this.#unindexBlockKey(key);
|
|
3065
3185
|
}
|
|
3066
3186
|
}
|
|
3187
|
+
if (isSourceSymbol && source === "multicast") {
|
|
3188
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
3189
|
+
}
|
|
3067
3190
|
return true;
|
|
3068
3191
|
}
|
|
3069
3192
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -3242,8 +3365,9 @@ var FECManager = class _FECManager {
|
|
|
3242
3365
|
return true;
|
|
3243
3366
|
}
|
|
3244
3367
|
/**
|
|
3245
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
3246
|
-
*
|
|
3368
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
3369
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
3370
|
+
* multicast wait, which may already have passed.
|
|
3247
3371
|
*/
|
|
3248
3372
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
3249
3373
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -3381,6 +3505,8 @@ var FECManager = class _FECManager {
|
|
|
3381
3505
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
3382
3506
|
const evictedBefore = locator.heldEvicted;
|
|
3383
3507
|
const held = locator.hold(ssId, source);
|
|
3508
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
3509
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
3384
3510
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
3385
3511
|
if (evicted > 0) {
|
|
3386
3512
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -3401,17 +3527,20 @@ var FECManager = class _FECManager {
|
|
|
3401
3527
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
3402
3528
|
*/
|
|
3403
3529
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
3404
|
-
const
|
|
3530
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
3531
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
3532
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
3405
3533
|
if (placement.status !== "placed") {
|
|
3406
3534
|
this.#isoRepairRefusedCount++;
|
|
3407
3535
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
3408
3536
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
3409
3537
|
}
|
|
3538
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
3410
3539
|
return placement;
|
|
3411
3540
|
}
|
|
3541
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
3412
3542
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
3413
3543
|
if (placement.hole !== void 0) {
|
|
3414
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
3415
3544
|
const hole = placement.hole.block;
|
|
3416
3545
|
const previous = locator.blockEndingAt(hole.start);
|
|
3417
3546
|
const loss = {
|
|
@@ -3432,17 +3561,29 @@ var FECManager = class _FECManager {
|
|
|
3432
3561
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
3433
3562
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
3434
3563
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
3435
|
-
* nobody to close it.
|
|
3564
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
3565
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
3566
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
3567
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
3436
3568
|
*/
|
|
3437
|
-
setUndelimitedLossHandler(handler) {
|
|
3569
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
3438
3570
|
this.#onUndelimitedLoss = handler;
|
|
3571
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
3439
3572
|
this.#deferIsoGaps = handler !== null;
|
|
3440
3573
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
3441
3574
|
}
|
|
3575
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
3576
|
+
#settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
|
|
3577
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
3578
|
+
const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
|
|
3579
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3442
3582
|
/**
|
|
3443
|
-
* The range repair for a hole ended: every hole
|
|
3444
|
-
*
|
|
3445
|
-
* with the held sources they placed, for the caller to feed
|
|
3583
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
3584
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
3585
|
+
* spans with the held sources they placed, for the caller to feed
|
|
3586
|
+
* (MmtpRouter).
|
|
3446
3587
|
*/
|
|
3447
3588
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
3448
3589
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -3461,7 +3602,10 @@ var FECManager = class _FECManager {
|
|
|
3461
3602
|
*/
|
|
3462
3603
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
3463
3604
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
3464
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
3605
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
3606
|
+
this.#blockAuthoredLengths.set(key, span.block.length);
|
|
3607
|
+
this.#isoSpanKeys.add(key);
|
|
3608
|
+
}
|
|
3465
3609
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
3466
3610
|
}
|
|
3467
3611
|
/**
|
|
@@ -3549,11 +3693,14 @@ var FECManager = class _FECManager {
|
|
|
3549
3693
|
/**
|
|
3550
3694
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
3551
3695
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
3552
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
3696
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
3697
|
+
* and neither has a stream without catalog timing until it releases two
|
|
3698
|
+
* RAPs inside one MPU.
|
|
3553
3699
|
*/
|
|
3554
3700
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
3555
|
-
|
|
3556
|
-
return this.#
|
|
3701
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3702
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
3703
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
3557
3704
|
}
|
|
3558
3705
|
/** A block's class now, or frozen at its resolution. */
|
|
3559
3706
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -3579,7 +3726,14 @@ var FECManager = class _FECManager {
|
|
|
3579
3726
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
3580
3727
|
*/
|
|
3581
3728
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
3582
|
-
if (!this.#trackConfigs.has(packetId)
|
|
3729
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
3730
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
3731
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3732
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
3733
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
3734
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3583
3737
|
this.#applyClassifierChanges(
|
|
3584
3738
|
packetId,
|
|
3585
3739
|
catalogTrackName,
|
|
@@ -3792,11 +3946,14 @@ var FECManager = class _FECManager {
|
|
|
3792
3946
|
});
|
|
3793
3947
|
client.onCleanupComplete((sbn) => {
|
|
3794
3948
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
3795
|
-
const
|
|
3949
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
3950
|
+
const cleanupKeys = owed.filter((key) => {
|
|
3796
3951
|
const block = this.#parseBlockKey(key);
|
|
3797
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
3952
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
3798
3953
|
});
|
|
3799
|
-
|
|
3954
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
|
|
3955
|
+
const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
|
|
3956
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
3800
3957
|
});
|
|
3801
3958
|
client.onError((msg, code) => {
|
|
3802
3959
|
const requestedKind = code ?? msg;
|
|
@@ -3866,9 +4023,11 @@ var FECManager = class _FECManager {
|
|
|
3866
4023
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
3867
4024
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
3868
4025
|
*/
|
|
3869
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
4026
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
3870
4027
|
this.#clearOwedCleanupBound(key);
|
|
3871
4028
|
const bound = {
|
|
4029
|
+
dueAtMs,
|
|
4030
|
+
askedAtMs: performance.now(),
|
|
3872
4031
|
handle: setTimeout(() => {
|
|
3873
4032
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
3874
4033
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -3966,7 +4125,15 @@ var FECManager = class _FECManager {
|
|
|
3966
4125
|
this.#isoSourceOversizeCount++;
|
|
3967
4126
|
return null;
|
|
3968
4127
|
}
|
|
3969
|
-
|
|
4128
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
4132
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
4133
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
4134
|
+
* stated for `ssId`.
|
|
4135
|
+
*/
|
|
4136
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
3970
4137
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3971
4138
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
3972
4139
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -3995,6 +4162,74 @@ var FECManager = class _FECManager {
|
|
|
3995
4162
|
}
|
|
3996
4163
|
return recovered;
|
|
3997
4164
|
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
4167
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
4168
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
4169
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
4170
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
4171
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
4172
|
+
* no deadline refresh, and it never makes a block clean.
|
|
4173
|
+
*
|
|
4174
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
4175
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
4176
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
4177
|
+
* feedSourceSymbol takes it.
|
|
4178
|
+
*
|
|
4179
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
4180
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
4181
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
4182
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
4183
|
+
* block as clean on its own side), the block is completed here as
|
|
4184
|
+
* recovered, with nothing left to route.
|
|
4185
|
+
*/
|
|
4186
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
4187
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
4188
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
4189
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
4190
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
4191
|
+
const sbn = located.block.start;
|
|
4192
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
4193
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
4194
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
4195
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
4196
|
+
if (rebuilt === void 0) {
|
|
4197
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
4198
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
4199
|
+
}
|
|
4200
|
+
const config = this.#trackConfigs.get(packetId);
|
|
4201
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
4202
|
+
rebuilt.add(located.esi);
|
|
4203
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
4204
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
4205
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
4206
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return { taken: true, recovered };
|
|
4210
|
+
}
|
|
4211
|
+
/**
|
|
4212
|
+
* A block that holds every one of its `length` sources, received or
|
|
4213
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
4214
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
4215
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
4216
|
+
*/
|
|
4217
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
4218
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
4219
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
4220
|
+
let present = rebuilt.size;
|
|
4221
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
4222
|
+
if (present < length) return false;
|
|
4223
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
4224
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
4225
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
4226
|
+
if (catalogTrackName === void 0) {
|
|
4227
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
4228
|
+
} else {
|
|
4229
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
4230
|
+
}
|
|
4231
|
+
return true;
|
|
4232
|
+
}
|
|
3998
4233
|
/**
|
|
3999
4234
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
4000
4235
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -4197,8 +4432,75 @@ var FECManager = class _FECManager {
|
|
|
4197
4432
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
4198
4433
|
return null;
|
|
4199
4434
|
}
|
|
4435
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
4200
4436
|
return recovered;
|
|
4201
4437
|
}
|
|
4438
|
+
/**
|
|
4439
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
4440
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
4441
|
+
* position past a block leaves the sources that block still misses
|
|
4442
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
4443
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
4444
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
4445
|
+
* sent before it.
|
|
4446
|
+
*/
|
|
4447
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
4448
|
+
const previous = locator.newestSource;
|
|
4449
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
4450
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
4451
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
4452
|
+
this.#shortenUnrecoverableWait(
|
|
4453
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
4454
|
+
block.start,
|
|
4455
|
+
packetId,
|
|
4456
|
+
catalogTrackName
|
|
4457
|
+
);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
/**
|
|
4461
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
4462
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
4463
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
4464
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
4465
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
4466
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
4467
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
4468
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
4469
|
+
* block.
|
|
4470
|
+
*/
|
|
4471
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
4472
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
4473
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
4474
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
4475
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
4476
|
+
const received = this.#blockSourceEsis.get(key);
|
|
4477
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
4478
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
4479
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
4480
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
4481
|
+
return void 0;
|
|
4482
|
+
}
|
|
4483
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
4484
|
+
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
4487
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
4488
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
4489
|
+
*/
|
|
4490
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
4491
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
4492
|
+
const config = this.#trackConfigs.get(packetId);
|
|
4493
|
+
if (config === void 0) return;
|
|
4494
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
4495
|
+
if (waitMs === void 0) return;
|
|
4496
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
4497
|
+
this.#armBlockTimeout(
|
|
4498
|
+
sbn,
|
|
4499
|
+
packetId,
|
|
4500
|
+
catalogTrackName,
|
|
4501
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
4502
|
+
);
|
|
4503
|
+
}
|
|
4202
4504
|
/**
|
|
4203
4505
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
4204
4506
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -4296,12 +4598,13 @@ var FECManager = class _FECManager {
|
|
|
4296
4598
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
4297
4599
|
* classification evidence, and marks the block clean when all of its
|
|
4298
4600
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
4299
|
-
* otherwise K.
|
|
4601
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
4602
|
+
* placed only now (see #noteSbnSeen).
|
|
4300
4603
|
*
|
|
4301
4604
|
* Throws for a packetId that was never configured, so a classified block
|
|
4302
4605
|
* never reads the manager-global K or timeout.
|
|
4303
4606
|
*/
|
|
4304
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
4607
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
4305
4608
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
4306
4609
|
if (trackConfig === void 0) {
|
|
4307
4610
|
throw new Error(
|
|
@@ -4317,7 +4620,8 @@ var FECManager = class _FECManager {
|
|
|
4317
4620
|
catalogTrackName,
|
|
4318
4621
|
"source",
|
|
4319
4622
|
evidence.packetSequenceNumber,
|
|
4320
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
4623
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
4624
|
+
held
|
|
4321
4625
|
);
|
|
4322
4626
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
4323
4627
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -4353,6 +4657,15 @@ var FECManager = class _FECManager {
|
|
|
4353
4657
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
4354
4658
|
return;
|
|
4355
4659
|
}
|
|
4660
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
4661
|
+
key,
|
|
4662
|
+
sbn,
|
|
4663
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
4664
|
+
packetId,
|
|
4665
|
+
catalogTrackName
|
|
4666
|
+
)) {
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4356
4669
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
4357
4670
|
}
|
|
4358
4671
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -4361,7 +4674,9 @@ var FECManager = class _FECManager {
|
|
|
4361
4674
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
4362
4675
|
this.#blockSourceCounts.delete(key);
|
|
4363
4676
|
this.#blockSourceEsis.delete(key);
|
|
4677
|
+
this.#blockRebuiltEsis.delete(key);
|
|
4364
4678
|
this.#blockAuthoredLengths.delete(key);
|
|
4679
|
+
this.#isoSpanKeys.delete(key);
|
|
4365
4680
|
this.#blockRepairRsIds.delete(key);
|
|
4366
4681
|
this.#blockRsbLengths.delete(key);
|
|
4367
4682
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -4392,6 +4707,19 @@ var FECManager = class _FECManager {
|
|
|
4392
4707
|
#receiptNow() {
|
|
4393
4708
|
return this.#receiptAtMs ?? performance.now();
|
|
4394
4709
|
}
|
|
4710
|
+
/**
|
|
4711
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
4712
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
4713
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
4714
|
+
* that timer would have, never later than a delay counted from now, and
|
|
4715
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
4716
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
4717
|
+
* from another fraction of a ms (BLO-52085).
|
|
4718
|
+
*/
|
|
4719
|
+
#receiptTimerDelayMs(deadlineMs, minMs) {
|
|
4720
|
+
const receivedAtMs = this.#receiptAtMs;
|
|
4721
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
|
|
4722
|
+
}
|
|
4395
4723
|
/**
|
|
4396
4724
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
4397
4725
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -4427,10 +4755,20 @@ var FECManager = class _FECManager {
|
|
|
4427
4755
|
this.#repairDeadlineMs(trackConfig)
|
|
4428
4756
|
);
|
|
4429
4757
|
}
|
|
4430
|
-
this.#
|
|
4758
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
4759
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
this.#armBlockTimeout(
|
|
4763
|
+
sbn,
|
|
4764
|
+
packetId,
|
|
4765
|
+
catalogTrackName,
|
|
4766
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
4767
|
+
);
|
|
4431
4768
|
}
|
|
4432
4769
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
4433
4770
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
4771
|
+
const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
|
|
4434
4772
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
4435
4773
|
this.#clearRepairDeadline(key);
|
|
4436
4774
|
this.#clearBlockTimer(key);
|
|
@@ -4440,6 +4778,7 @@ var FECManager = class _FECManager {
|
|
|
4440
4778
|
}
|
|
4441
4779
|
this.#blockSourceCounts.delete(key);
|
|
4442
4780
|
this.#blockSourceEsis.delete(key);
|
|
4781
|
+
this.#blockRebuiltEsis.delete(key);
|
|
4443
4782
|
this.#blockAuthoredLengths.delete(key);
|
|
4444
4783
|
this.#blockRepairRsIds.delete(key);
|
|
4445
4784
|
this.#blockRsbLengths.delete(key);
|
|
@@ -4449,7 +4788,8 @@ var FECManager = class _FECManager {
|
|
|
4449
4788
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
4450
4789
|
this.#fecStats.blocksFailed++;
|
|
4451
4790
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
4452
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
4791
|
+
if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
4792
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
4453
4793
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
4454
4794
|
}
|
|
4455
4795
|
/**
|
|
@@ -4478,7 +4818,9 @@ var FECManager = class _FECManager {
|
|
|
4478
4818
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
4479
4819
|
this.#blockSourceCounts.delete(key);
|
|
4480
4820
|
this.#blockSourceEsis.delete(key);
|
|
4821
|
+
this.#blockRebuiltEsis.delete(key);
|
|
4481
4822
|
this.#blockAuthoredLengths.delete(key);
|
|
4823
|
+
this.#isoSpanKeys.delete(key);
|
|
4482
4824
|
this.#blockRepairRsIds.delete(key);
|
|
4483
4825
|
this.#blockRsbLengths.delete(key);
|
|
4484
4826
|
this.#clearBlockTimer(key);
|
|
@@ -4569,7 +4911,9 @@ var FECManager = class _FECManager {
|
|
|
4569
4911
|
this.#blockKeysBySbn.clear();
|
|
4570
4912
|
this.#blockSourceCounts.clear();
|
|
4571
4913
|
this.#blockSourceEsis.clear();
|
|
4914
|
+
this.#blockRebuiltEsis.clear();
|
|
4572
4915
|
this.#blockAuthoredLengths.clear();
|
|
4916
|
+
this.#isoSpanKeys.clear();
|
|
4573
4917
|
this.#blockRepairRsIds.clear();
|
|
4574
4918
|
this.#blockRsbLengths.clear();
|
|
4575
4919
|
this.#heldLayerStates.clear();
|
|
@@ -4594,6 +4938,7 @@ var FECManager = class _FECManager {
|
|
|
4594
4938
|
this.#blockAnchorsMs.clear();
|
|
4595
4939
|
this.#blockCatalogTrackNames.clear();
|
|
4596
4940
|
this.#classifiers.clear();
|
|
4941
|
+
this.#noSpanRaps.clear();
|
|
4597
4942
|
this.#streamGeometries.clear();
|
|
4598
4943
|
this.#failedBlocks.clear();
|
|
4599
4944
|
this.#resolvedBlockClasses.clear();
|