@blockcast/mmt-container 2.2.0-main.75b39a2cdddf → 2.2.0-main.7e8a9d13c01c
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 +634 -129
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +37 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +181 -52
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +39 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +80 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +21 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +114 -11
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +113 -12
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +114 -11
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +18 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +254 -44
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +13 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +455 -103
- package/dist/multicast-manager.js.map +2 -2
- package/dist/no-modules.js +114 -11
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +114 -11
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +156 -26
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +114 -31
- package/src/mmtp-container-parser.ts +181 -10
- package/src/mmtp-router.ts +231 -58
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/track-session.ts +3 -0
|
@@ -1189,6 +1189,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1189
1189
|
this.#heldEvicted = 0;
|
|
1190
1190
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
1191
1191
|
this.#holes = [];
|
|
1192
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
1193
|
+
this.#expired = [];
|
|
1194
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
1195
|
+
this.#newlyExpired = [];
|
|
1196
|
+
/**
|
|
1197
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
1198
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
1199
|
+
*/
|
|
1200
|
+
this.#closedBehind = [];
|
|
1192
1201
|
/**
|
|
1193
1202
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
1194
1203
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -1212,6 +1221,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1212
1221
|
#newest;
|
|
1213
1222
|
#newestSource;
|
|
1214
1223
|
#holes;
|
|
1224
|
+
#expired;
|
|
1225
|
+
#newlyExpired;
|
|
1226
|
+
#closedBehind;
|
|
1215
1227
|
static #checkedRetention(retentionBlocks) {
|
|
1216
1228
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
1217
1229
|
throw new Error(
|
|
@@ -1281,17 +1293,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1281
1293
|
}
|
|
1282
1294
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
1283
1295
|
locate(ssId) {
|
|
1284
|
-
for (
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1296
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
1297
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
1298
|
+
const block = blocks[i];
|
|
1299
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
1300
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
1301
|
+
}
|
|
1288
1302
|
}
|
|
1289
1303
|
}
|
|
1290
1304
|
return null;
|
|
1291
1305
|
}
|
|
1292
|
-
/** Number of pending
|
|
1306
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
1293
1307
|
get holeCount() {
|
|
1294
|
-
return this.#holes.length;
|
|
1308
|
+
return this.#holes.length + this.#expired.length;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
1312
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
1313
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
1314
|
+
* release gate does not wait for it. Its owner must close it
|
|
1315
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
1316
|
+
*/
|
|
1317
|
+
takeExpiredHoles() {
|
|
1318
|
+
return this.#newlyExpired.splice(0);
|
|
1295
1319
|
}
|
|
1296
1320
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
1297
1321
|
blockEndingAt(end) {
|
|
@@ -1347,9 +1371,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1347
1371
|
/**
|
|
1348
1372
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
1349
1373
|
* block states its range and places every held source in it; later repair
|
|
1350
|
-
* symbols of the same block change nothing. A range
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1374
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
1375
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
1376
|
+
* a known block (a different length, or an overlap with another block or
|
|
1377
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
1378
|
+
* block changes.
|
|
1353
1379
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
1354
1380
|
* A block so far past `newest` that stating it would retire the position
|
|
1355
1381
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -1368,7 +1394,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1368
1394
|
const known = this.blockAt(start);
|
|
1369
1395
|
if (known !== void 0) {
|
|
1370
1396
|
if (known.span) {
|
|
1371
|
-
|
|
1397
|
+
if (known.length === length) {
|
|
1398
|
+
const block2 = { start, length, span: false };
|
|
1399
|
+
this.#blocks[this.#blocks.indexOf(known)] = block2;
|
|
1400
|
+
return { status: "placed", block: block2, placed: [] };
|
|
1401
|
+
}
|
|
1402
|
+
return length < known.length ? {
|
|
1403
|
+
status: "overlap",
|
|
1404
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
1405
|
+
ofSpan: known
|
|
1406
|
+
} : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
|
|
1372
1407
|
}
|
|
1373
1408
|
if (known.length !== length) {
|
|
1374
1409
|
return {
|
|
@@ -1436,18 +1471,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1436
1471
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
1437
1472
|
}
|
|
1438
1473
|
/**
|
|
1439
|
-
* Turn every
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1474
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
1475
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
1476
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
1477
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
1478
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
1479
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
1442
1480
|
*/
|
|
1443
1481
|
closeHolesWithin(start, length) {
|
|
1444
1482
|
const closed = [];
|
|
1445
|
-
for (const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1483
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
1484
|
+
for (const hole of [...holes]) {
|
|
1485
|
+
if (!isoBlockContains(start, length, hole.start)) continue;
|
|
1486
|
+
holes.splice(holes.indexOf(hole), 1);
|
|
1487
|
+
const spanBlock = { start: hole.start, length: hole.length, span: true };
|
|
1488
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
|
|
1489
|
+
if (holes === this.#holes) {
|
|
1490
|
+
this.#blocks.push(spanBlock);
|
|
1491
|
+
} else {
|
|
1492
|
+
this.#closedBehind.push(spanBlock);
|
|
1493
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1451
1496
|
}
|
|
1452
1497
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
1453
1498
|
}
|
|
@@ -1461,15 +1506,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1461
1506
|
return void 0;
|
|
1462
1507
|
}
|
|
1463
1508
|
/**
|
|
1464
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
1465
|
-
* lies more than `retentionBlocks` blocks behind
|
|
1466
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
1467
|
-
* horizon uses
|
|
1509
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
1510
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
1511
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
1512
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
1513
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
1468
1514
|
*/
|
|
1469
1515
|
retireBehind(position) {
|
|
1470
1516
|
this.observe(position);
|
|
1471
1517
|
const horizon = this.#retentionBlocks;
|
|
1472
1518
|
const retired = [];
|
|
1519
|
+
this.#closedBehind.length = 0;
|
|
1473
1520
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
1474
1521
|
const block = this.#blocks[i];
|
|
1475
1522
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -1481,10 +1528,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1481
1528
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
1482
1529
|
const hole = this.#holes[i];
|
|
1483
1530
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
1484
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
1531
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
|
|
1532
|
+
this.#holes.splice(i, 1);
|
|
1533
|
+
this.#expired.push(hole);
|
|
1534
|
+
this.#newlyExpired.push(hole);
|
|
1485
1535
|
}
|
|
1486
1536
|
for (const ssId of [...this.#held.keys()]) {
|
|
1487
|
-
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#
|
|
1537
|
+
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))) {
|
|
1538
|
+
this.#held.delete(ssId);
|
|
1539
|
+
}
|
|
1488
1540
|
}
|
|
1489
1541
|
return retired;
|
|
1490
1542
|
}
|
|
@@ -1492,6 +1544,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1492
1544
|
reset() {
|
|
1493
1545
|
this.#blocks.length = 0;
|
|
1494
1546
|
this.#holes.length = 0;
|
|
1547
|
+
this.#expired.length = 0;
|
|
1548
|
+
this.#newlyExpired.length = 0;
|
|
1549
|
+
this.#closedBehind.length = 0;
|
|
1495
1550
|
this.#held.clear();
|
|
1496
1551
|
this.#coverageEnd = void 0;
|
|
1497
1552
|
this.#newest = void 0;
|
|
@@ -2178,6 +2233,10 @@ var FECManager = class _FECManager {
|
|
|
2178
2233
|
// 'pending' forever, which would park every later frame of its stream in
|
|
2179
2234
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
2180
2235
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
2236
|
+
/**
|
|
2237
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
2238
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
2239
|
+
*/
|
|
2181
2240
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
2182
2241
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
2183
2242
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
@@ -2194,6 +2253,9 @@ var FECManager = class _FECManager {
|
|
|
2194
2253
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
2195
2254
|
// symbols; waiting for K fails every one of them.
|
|
2196
2255
|
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
2256
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
2257
|
+
// a repair states the block there or the span resolves.
|
|
2258
|
+
this.#isoSpanKeys = /* @__PURE__ */ new Set();
|
|
2197
2259
|
this.#ssbLengthConflictCount = 0;
|
|
2198
2260
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
2199
2261
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -2391,7 +2453,10 @@ var FECManager = class _FECManager {
|
|
|
2391
2453
|
// arrives via onBlockRecovered callback.
|
|
2392
2454
|
this.onBlockStarted = null;
|
|
2393
2455
|
this.onBlockComplete = null;
|
|
2394
|
-
/**
|
|
2456
|
+
/**
|
|
2457
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
2458
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
2459
|
+
*/
|
|
2395
2460
|
this.onBlockFailed = null;
|
|
2396
2461
|
/**
|
|
2397
2462
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -2433,6 +2498,8 @@ var FECManager = class _FECManager {
|
|
|
2433
2498
|
* for it, which it must be.
|
|
2434
2499
|
*/
|
|
2435
2500
|
this.#onUndelimitedLoss = null;
|
|
2501
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
2502
|
+
this.#onIsoHoleExpired = null;
|
|
2436
2503
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
2437
2504
|
this.onBlockVanished = null;
|
|
2438
2505
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -2477,6 +2544,7 @@ var FECManager = class _FECManager {
|
|
|
2477
2544
|
#blockSourceEsis;
|
|
2478
2545
|
#blockRebuiltEsis;
|
|
2479
2546
|
#blockAuthoredLengths;
|
|
2547
|
+
#isoSpanKeys;
|
|
2480
2548
|
#ssbLengthConflictCount;
|
|
2481
2549
|
#blockRepairRsIds;
|
|
2482
2550
|
#blockRsbLengths;
|
|
@@ -2543,6 +2611,7 @@ var FECManager = class _FECManager {
|
|
|
2543
2611
|
#decoderUnavailableEvents;
|
|
2544
2612
|
#repairSymbolSizeMismatchCount;
|
|
2545
2613
|
#onUndelimitedLoss;
|
|
2614
|
+
#onIsoHoleExpired;
|
|
2546
2615
|
#encodeTrackName(catalogTrackName) {
|
|
2547
2616
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
2548
2617
|
if (encoded === void 0) {
|
|
@@ -3014,7 +3083,10 @@ var FECManager = class _FECManager {
|
|
|
3014
3083
|
timer.handle = null;
|
|
3015
3084
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
3016
3085
|
};
|
|
3017
|
-
timer.handle = setTimeout(
|
|
3086
|
+
timer.handle = setTimeout(
|
|
3087
|
+
onFire,
|
|
3088
|
+
this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
|
|
3089
|
+
);
|
|
3018
3090
|
this.#repairDeadlines.set(key, timer);
|
|
3019
3091
|
}
|
|
3020
3092
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -3158,8 +3230,14 @@ var FECManager = class _FECManager {
|
|
|
3158
3230
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
3159
3231
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
3160
3232
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
3233
|
+
*
|
|
3234
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
3235
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
3236
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
3237
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
3238
|
+
* it (BLO-52085).
|
|
3161
3239
|
*/
|
|
3162
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
3240
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
3163
3241
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3164
3242
|
const now = performance.now();
|
|
3165
3243
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -3198,7 +3276,7 @@ var FECManager = class _FECManager {
|
|
|
3198
3276
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
3199
3277
|
};
|
|
3200
3278
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
3201
|
-
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
3279
|
+
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
3202
3280
|
discontinuity();
|
|
3203
3281
|
return;
|
|
3204
3282
|
}
|
|
@@ -3460,17 +3538,21 @@ var FECManager = class _FECManager {
|
|
|
3460
3538
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
3461
3539
|
this.#repairHoldUntilMs.delete(key);
|
|
3462
3540
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
3463
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
3541
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
3464
3542
|
};
|
|
3465
|
-
timer.handle = setTimeout(
|
|
3543
|
+
timer.handle = setTimeout(
|
|
3544
|
+
onFire,
|
|
3545
|
+
this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
|
|
3546
|
+
);
|
|
3466
3547
|
this.#activeBlocks.set(key, timer);
|
|
3467
3548
|
this.#indexBlockKey(key, sbn, packetId);
|
|
3468
3549
|
}
|
|
3469
3550
|
/**
|
|
3470
3551
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
3471
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
3552
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
3553
|
+
* is when it was due: its deadline, or now.
|
|
3472
3554
|
*/
|
|
3473
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
3555
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
3474
3556
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3475
3557
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
3476
3558
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -3486,7 +3568,7 @@ var FECManager = class _FECManager {
|
|
|
3486
3568
|
if (workerClient) {
|
|
3487
3569
|
this.#pendingWorkerCleanups.add(key);
|
|
3488
3570
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
3489
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
3571
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
3490
3572
|
workerClient.cleanup(sbn);
|
|
3491
3573
|
} else {
|
|
3492
3574
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -3800,6 +3882,7 @@ var FECManager = class _FECManager {
|
|
|
3800
3882
|
resetMap(this.#blockSourceEsis);
|
|
3801
3883
|
resetMap(this.#blockRebuiltEsis);
|
|
3802
3884
|
resetMap(this.#blockAuthoredLengths);
|
|
3885
|
+
resetSet(this.#isoSpanKeys);
|
|
3803
3886
|
resetMap(this.#blockRepairRsIds);
|
|
3804
3887
|
resetMap(this.#blockRsbLengths);
|
|
3805
3888
|
resetMap(this.#heldLayerStates);
|
|
@@ -3925,6 +4008,7 @@ var FECManager = class _FECManager {
|
|
|
3925
4008
|
dropMap(this.#blockSourceEsis);
|
|
3926
4009
|
dropMap(this.#blockRebuiltEsis);
|
|
3927
4010
|
dropMap(this.#blockAuthoredLengths);
|
|
4011
|
+
dropSet(this.#isoSpanKeys);
|
|
3928
4012
|
dropMap(this.#blockRepairRsIds);
|
|
3929
4013
|
dropMap(this.#blockRsbLengths);
|
|
3930
4014
|
dropMap(this.#blockPathCounts);
|
|
@@ -4267,8 +4351,9 @@ var FECManager = class _FECManager {
|
|
|
4267
4351
|
return true;
|
|
4268
4352
|
}
|
|
4269
4353
|
/**
|
|
4270
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
4271
|
-
*
|
|
4354
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
4355
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
4356
|
+
* multicast wait, which may already have passed.
|
|
4272
4357
|
*/
|
|
4273
4358
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
4274
4359
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -4406,6 +4491,7 @@ var FECManager = class _FECManager {
|
|
|
4406
4491
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
4407
4492
|
const evictedBefore = locator.heldEvicted;
|
|
4408
4493
|
const held = locator.hold(ssId, source);
|
|
4494
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
4409
4495
|
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
4410
4496
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
4411
4497
|
if (evicted > 0) {
|
|
@@ -4427,17 +4513,20 @@ var FECManager = class _FECManager {
|
|
|
4427
4513
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
4428
4514
|
*/
|
|
4429
4515
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
4430
|
-
const
|
|
4516
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
4517
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
4518
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
4431
4519
|
if (placement.status !== "placed") {
|
|
4432
4520
|
this.#isoRepairRefusedCount++;
|
|
4433
4521
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
4434
4522
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
4435
4523
|
}
|
|
4524
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
4436
4525
|
return placement;
|
|
4437
4526
|
}
|
|
4527
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
4438
4528
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
4439
4529
|
if (placement.hole !== void 0) {
|
|
4440
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
4441
4530
|
const hole = placement.hole.block;
|
|
4442
4531
|
const previous = locator.blockEndingAt(hole.start);
|
|
4443
4532
|
const loss = {
|
|
@@ -4458,17 +4547,29 @@ var FECManager = class _FECManager {
|
|
|
4458
4547
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
4459
4548
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
4460
4549
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
4461
|
-
* nobody to close it.
|
|
4550
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
4551
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
4552
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
4553
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
4462
4554
|
*/
|
|
4463
|
-
setUndelimitedLossHandler(handler) {
|
|
4555
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
4464
4556
|
this.#onUndelimitedLoss = handler;
|
|
4557
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
4465
4558
|
this.#deferIsoGaps = handler !== null;
|
|
4466
4559
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
4467
4560
|
}
|
|
4561
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
4562
|
+
#settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
|
|
4563
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
4564
|
+
const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
|
|
4565
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4468
4568
|
/**
|
|
4469
|
-
* The range repair for a hole ended: every hole
|
|
4470
|
-
*
|
|
4471
|
-
* with the held sources they placed, for the caller to feed
|
|
4569
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
4570
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
4571
|
+
* spans with the held sources they placed, for the caller to feed
|
|
4572
|
+
* (MmtpRouter).
|
|
4472
4573
|
*/
|
|
4473
4574
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
4474
4575
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -4487,7 +4588,10 @@ var FECManager = class _FECManager {
|
|
|
4487
4588
|
*/
|
|
4488
4589
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
4489
4590
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
4490
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
4591
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
4592
|
+
this.#blockAuthoredLengths.set(key, span.block.length);
|
|
4593
|
+
this.#isoSpanKeys.add(key);
|
|
4594
|
+
}
|
|
4491
4595
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
4492
4596
|
}
|
|
4493
4597
|
/**
|
|
@@ -4828,11 +4932,14 @@ var FECManager = class _FECManager {
|
|
|
4828
4932
|
});
|
|
4829
4933
|
client.onCleanupComplete((sbn) => {
|
|
4830
4934
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
4831
|
-
const
|
|
4935
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
4936
|
+
const cleanupKeys = owed.filter((key) => {
|
|
4832
4937
|
const block = this.#parseBlockKey(key);
|
|
4833
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
4938
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
4834
4939
|
});
|
|
4835
|
-
|
|
4940
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
|
|
4941
|
+
const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
|
|
4942
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
4836
4943
|
});
|
|
4837
4944
|
client.onError((msg, code) => {
|
|
4838
4945
|
const requestedKind = code ?? msg;
|
|
@@ -4902,9 +5009,11 @@ var FECManager = class _FECManager {
|
|
|
4902
5009
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
4903
5010
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
4904
5011
|
*/
|
|
4905
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
5012
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
4906
5013
|
this.#clearOwedCleanupBound(key);
|
|
4907
5014
|
const bound = {
|
|
5015
|
+
dueAtMs,
|
|
5016
|
+
askedAtMs: performance.now(),
|
|
4908
5017
|
handle: setTimeout(() => {
|
|
4909
5018
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
4910
5019
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -5475,12 +5584,13 @@ var FECManager = class _FECManager {
|
|
|
5475
5584
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
5476
5585
|
* classification evidence, and marks the block clean when all of its
|
|
5477
5586
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
5478
|
-
* otherwise K.
|
|
5587
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
5588
|
+
* placed only now (see #noteSbnSeen).
|
|
5479
5589
|
*
|
|
5480
5590
|
* Throws for a packetId that was never configured, so a classified block
|
|
5481
5591
|
* never reads the manager-global K or timeout.
|
|
5482
5592
|
*/
|
|
5483
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
5593
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
5484
5594
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
5485
5595
|
if (trackConfig === void 0) {
|
|
5486
5596
|
throw new Error(
|
|
@@ -5496,7 +5606,8 @@ var FECManager = class _FECManager {
|
|
|
5496
5606
|
catalogTrackName,
|
|
5497
5607
|
"source",
|
|
5498
5608
|
evidence.packetSequenceNumber,
|
|
5499
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
5609
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
5610
|
+
held
|
|
5500
5611
|
);
|
|
5501
5612
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
5502
5613
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -5551,6 +5662,7 @@ var FECManager = class _FECManager {
|
|
|
5551
5662
|
this.#blockSourceEsis.delete(key);
|
|
5552
5663
|
this.#blockRebuiltEsis.delete(key);
|
|
5553
5664
|
this.#blockAuthoredLengths.delete(key);
|
|
5665
|
+
this.#isoSpanKeys.delete(key);
|
|
5554
5666
|
this.#blockRepairRsIds.delete(key);
|
|
5555
5667
|
this.#blockRsbLengths.delete(key);
|
|
5556
5668
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -5581,6 +5693,19 @@ var FECManager = class _FECManager {
|
|
|
5581
5693
|
#receiptNow() {
|
|
5582
5694
|
return this.#receiptAtMs ?? performance.now();
|
|
5583
5695
|
}
|
|
5696
|
+
/**
|
|
5697
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
5698
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
5699
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
5700
|
+
* that timer would have, never later than a delay counted from now, and
|
|
5701
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
5702
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
5703
|
+
* from another fraction of a ms (BLO-52085).
|
|
5704
|
+
*/
|
|
5705
|
+
#receiptTimerDelayMs(deadlineMs, minMs) {
|
|
5706
|
+
const receivedAtMs = this.#receiptAtMs;
|
|
5707
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
|
|
5708
|
+
}
|
|
5584
5709
|
/**
|
|
5585
5710
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
5586
5711
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -5629,6 +5754,7 @@ var FECManager = class _FECManager {
|
|
|
5629
5754
|
}
|
|
5630
5755
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
5631
5756
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
5757
|
+
const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
|
|
5632
5758
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
5633
5759
|
this.#clearRepairDeadline(key);
|
|
5634
5760
|
this.#clearBlockTimer(key);
|
|
@@ -5648,7 +5774,8 @@ var FECManager = class _FECManager {
|
|
|
5648
5774
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
5649
5775
|
this.#fecStats.blocksFailed++;
|
|
5650
5776
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
5651
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
5777
|
+
if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
5778
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
5652
5779
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
5653
5780
|
}
|
|
5654
5781
|
/**
|
|
@@ -5679,6 +5806,7 @@ var FECManager = class _FECManager {
|
|
|
5679
5806
|
this.#blockSourceEsis.delete(key);
|
|
5680
5807
|
this.#blockRebuiltEsis.delete(key);
|
|
5681
5808
|
this.#blockAuthoredLengths.delete(key);
|
|
5809
|
+
this.#isoSpanKeys.delete(key);
|
|
5682
5810
|
this.#blockRepairRsIds.delete(key);
|
|
5683
5811
|
this.#blockRsbLengths.delete(key);
|
|
5684
5812
|
this.#clearBlockTimer(key);
|
|
@@ -5771,6 +5899,7 @@ var FECManager = class _FECManager {
|
|
|
5771
5899
|
this.#blockSourceEsis.clear();
|
|
5772
5900
|
this.#blockRebuiltEsis.clear();
|
|
5773
5901
|
this.#blockAuthoredLengths.clear();
|
|
5902
|
+
this.#isoSpanKeys.clear();
|
|
5774
5903
|
this.#blockRepairRsIds.clear();
|
|
5775
5904
|
this.#blockRsbLengths.clear();
|
|
5776
5905
|
this.#heldLayerStates.clear();
|
|
@@ -13683,6 +13812,9 @@ function authoredGap(before, after) {
|
|
|
13683
13812
|
function ssIdAfter(a, b) {
|
|
13684
13813
|
return (a - b | 0) > 0;
|
|
13685
13814
|
}
|
|
13815
|
+
function holdsRebuilt(stream) {
|
|
13816
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
13817
|
+
}
|
|
13686
13818
|
var FecReleaseSequencer = class {
|
|
13687
13819
|
#streams = /* @__PURE__ */ new Map();
|
|
13688
13820
|
#gate;
|
|
@@ -13745,6 +13877,7 @@ var FecReleaseSequencer = class {
|
|
|
13745
13877
|
stream.lastSbn = void 0;
|
|
13746
13878
|
stream.discontinuity = false;
|
|
13747
13879
|
stream.rapSsId = void 0;
|
|
13880
|
+
stream.sourced = false;
|
|
13748
13881
|
}
|
|
13749
13882
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
13750
13883
|
this.#recordStat("droppedStale");
|
|
@@ -13771,6 +13904,7 @@ var FecReleaseSequencer = class {
|
|
|
13771
13904
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
13772
13905
|
else hi = mid;
|
|
13773
13906
|
}
|
|
13907
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
13774
13908
|
stream.buf.splice(lo, 0, frame);
|
|
13775
13909
|
this.#drainUnlessDeferred(stream);
|
|
13776
13910
|
}
|
|
@@ -13806,6 +13940,41 @@ var FecReleaseSequencer = class {
|
|
|
13806
13940
|
}
|
|
13807
13941
|
return false;
|
|
13808
13942
|
}
|
|
13943
|
+
/**
|
|
13944
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
13945
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
13946
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
13947
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
13948
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
13949
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
13950
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
13951
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
13952
|
+
* released.
|
|
13953
|
+
*/
|
|
13954
|
+
unsource(packetId, catalogTrackName) {
|
|
13955
|
+
for (const stream of this.#streams.values()) {
|
|
13956
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
13957
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
13958
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
13959
|
+
else this.#drain(stream, true);
|
|
13960
|
+
}
|
|
13961
|
+
stream.sourced = false;
|
|
13962
|
+
}
|
|
13963
|
+
}
|
|
13964
|
+
}
|
|
13965
|
+
/**
|
|
13966
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
13967
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
13968
|
+
* packetId.
|
|
13969
|
+
*/
|
|
13970
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
13971
|
+
for (const stream of this.#streams.values()) {
|
|
13972
|
+
if (stream.packetId !== packetId) continue;
|
|
13973
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
13974
|
+
if (holdsRebuilt(stream)) return true;
|
|
13975
|
+
}
|
|
13976
|
+
return false;
|
|
13977
|
+
}
|
|
13809
13978
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
13810
13979
|
poke(streamKey) {
|
|
13811
13980
|
if (streamKey !== void 0) {
|
|
@@ -13897,16 +14066,32 @@ var FecReleaseSequencer = class {
|
|
|
13897
14066
|
last: null,
|
|
13898
14067
|
lastSbn: void 0,
|
|
13899
14068
|
discontinuity: false,
|
|
13900
|
-
rapSsId: void 0
|
|
14069
|
+
rapSsId: void 0,
|
|
14070
|
+
sourced: false,
|
|
14071
|
+
draining: 0
|
|
13901
14072
|
};
|
|
13902
14073
|
this.#streams.set(streamKey, stream);
|
|
13903
14074
|
}
|
|
13904
14075
|
return stream;
|
|
13905
14076
|
}
|
|
13906
|
-
#
|
|
14077
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
14078
|
+
#drain(stream, asUnanchored = false) {
|
|
14079
|
+
stream.draining++;
|
|
14080
|
+
try {
|
|
14081
|
+
this.#drainLoop(stream, asUnanchored);
|
|
14082
|
+
} finally {
|
|
14083
|
+
stream.draining--;
|
|
14084
|
+
}
|
|
14085
|
+
}
|
|
14086
|
+
#drainLoop(stream, asUnanchored) {
|
|
13907
14087
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
13908
14088
|
const head = stream.buf[0];
|
|
13909
14089
|
const last = stream.last;
|
|
14090
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
14091
|
+
stream.buf.shift();
|
|
14092
|
+
this.#release(stream, head, void 0);
|
|
14093
|
+
continue;
|
|
14094
|
+
}
|
|
13910
14095
|
if (stream.discontinuity) {
|
|
13911
14096
|
stream.discontinuity = false;
|
|
13912
14097
|
stream.buf.shift();
|
|
@@ -13920,9 +14105,10 @@ var FecReleaseSequencer = class {
|
|
|
13920
14105
|
}
|
|
13921
14106
|
const headSbn = head.meta.fecSbn;
|
|
13922
14107
|
const from = stream.lastSbn ?? headSbn;
|
|
13923
|
-
const to = headSbn ?? stream.lastSbn;
|
|
14108
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
13924
14109
|
let lost = [];
|
|
13925
14110
|
const unanchored = from === void 0 && to === void 0;
|
|
14111
|
+
let unresolved = false;
|
|
13926
14112
|
if (from !== void 0 && to !== void 0) {
|
|
13927
14113
|
const resolved = this.#gate.resolvedLoss(
|
|
13928
14114
|
stream.packetId,
|
|
@@ -13931,7 +14117,8 @@ var FecReleaseSequencer = class {
|
|
|
13931
14117
|
Math.max(from, to),
|
|
13932
14118
|
from
|
|
13933
14119
|
);
|
|
13934
|
-
|
|
14120
|
+
unresolved = resolved === null;
|
|
14121
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
13935
14122
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
13936
14123
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
13937
14124
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -13939,22 +14126,56 @@ var FecReleaseSequencer = class {
|
|
|
13939
14126
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
13940
14127
|
continue;
|
|
13941
14128
|
}
|
|
13942
|
-
lost = resolved;
|
|
14129
|
+
lost = resolved ?? [];
|
|
13943
14130
|
}
|
|
13944
14131
|
stream.buf.shift();
|
|
13945
14132
|
this.#release(
|
|
13946
14133
|
stream,
|
|
13947
14134
|
head,
|
|
13948
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
14135
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
13949
14136
|
);
|
|
13950
14137
|
}
|
|
13951
14138
|
}
|
|
14139
|
+
/**
|
|
14140
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
14141
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
14142
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
14143
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
14144
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
14145
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
14146
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
14147
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
14148
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
14149
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
14150
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
14151
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
14152
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
14153
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
14154
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
14155
|
+
*/
|
|
14156
|
+
#cursorPassed(stream, asUnanchored) {
|
|
14157
|
+
let at = -1;
|
|
14158
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
14159
|
+
const frame = stream.buf[i];
|
|
14160
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
14161
|
+
frame.passed = true;
|
|
14162
|
+
if (at < 0) at = i;
|
|
14163
|
+
} else if (frame.passed !== true) break;
|
|
14164
|
+
}
|
|
14165
|
+
if (at < 0) return false;
|
|
14166
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
14167
|
+
}
|
|
13952
14168
|
#release(stream, frame, decision) {
|
|
14169
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
14170
|
+
if (stale) this.#recordStat("droppedStale");
|
|
13953
14171
|
if (decision) frame.meta.gapBefore = decision;
|
|
13954
14172
|
stream.last = frame;
|
|
13955
|
-
if (frame.meta.fecSbn !== void 0
|
|
14173
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
14174
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
14175
|
+
}
|
|
14176
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
13956
14177
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
13957
|
-
this.#emit(frame.data, frame.meta);
|
|
14178
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
13958
14179
|
}
|
|
13959
14180
|
};
|
|
13960
14181
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -14280,6 +14501,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14280
14501
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
14281
14502
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
14282
14503
|
}
|
|
14504
|
+
/** See FecReleaseSequencer.unsource. */
|
|
14505
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
14506
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
14507
|
+
}
|
|
14508
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
14509
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
14510
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
14511
|
+
}
|
|
14283
14512
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
14284
14513
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
14285
14514
|
if (!this.#fecSequencer) return;
|
|
@@ -14606,7 +14835,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14606
14835
|
info,
|
|
14607
14836
|
resolver,
|
|
14608
14837
|
options?.recovered === true,
|
|
14609
|
-
options?.fecSbn
|
|
14838
|
+
options?.fecSbn,
|
|
14839
|
+
options?.fecSbnFirst
|
|
14610
14840
|
);
|
|
14611
14841
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
14612
14842
|
}
|
|
@@ -14699,7 +14929,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14699
14929
|
info,
|
|
14700
14930
|
resolver,
|
|
14701
14931
|
options?.recovered === true,
|
|
14702
|
-
options?.fecSbn
|
|
14932
|
+
options?.fecSbn,
|
|
14933
|
+
options?.fecSbnFirst
|
|
14703
14934
|
);
|
|
14704
14935
|
if (outcome.record !== void 0) return outcome.record;
|
|
14705
14936
|
return this.#drop(outcome.reason);
|
|
@@ -14719,7 +14950,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14719
14950
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
14720
14951
|
* is what keeps both true.
|
|
14721
14952
|
*/
|
|
14722
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
14953
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
14723
14954
|
const timing = resolver.sampleTiming(
|
|
14724
14955
|
combined.mpuSequenceNumber,
|
|
14725
14956
|
combined.movieFragmentSequenceNumber,
|
|
@@ -14736,6 +14967,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14736
14967
|
);
|
|
14737
14968
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
14738
14969
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
14970
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
14739
14971
|
if (!resolver.consumeSample(
|
|
14740
14972
|
combined.mpuSequenceNumber,
|
|
14741
14973
|
combined.movieFragmentSequenceNumber,
|
|
@@ -18251,6 +18483,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
|
18251
18483
|
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
18252
18484
|
var OVERLAY_PENDING_LIMIT = 256;
|
|
18253
18485
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
18486
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
18254
18487
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
18255
18488
|
function assertStreamIdentity(role, stream) {
|
|
18256
18489
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -18427,6 +18660,17 @@ var MmtpRouter = class {
|
|
|
18427
18660
|
* an MPU no member of which arrived).
|
|
18428
18661
|
*/
|
|
18429
18662
|
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
18663
|
+
/**
|
|
18664
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
18665
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
18666
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
18667
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
18668
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
18669
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
18670
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
18671
|
+
* this offset did.
|
|
18672
|
+
*/
|
|
18673
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
18430
18674
|
/**
|
|
18431
18675
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
18432
18676
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -18704,13 +18948,18 @@ var MmtpRouter = class {
|
|
|
18704
18948
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
18705
18949
|
}
|
|
18706
18950
|
};
|
|
18707
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
18951
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
18708
18952
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
18709
18953
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
18710
18954
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
18711
18955
|
}
|
|
18712
|
-
|
|
18713
|
-
|
|
18956
|
+
if (span === void 0) {
|
|
18957
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
18958
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
18959
|
+
} else {
|
|
18960
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
18961
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
18962
|
+
}
|
|
18714
18963
|
};
|
|
18715
18964
|
}
|
|
18716
18965
|
}
|
|
@@ -18755,8 +19004,9 @@ var MmtpRouter = class {
|
|
|
18755
19004
|
}
|
|
18756
19005
|
/**
|
|
18757
19006
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
18758
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
18759
|
-
*
|
|
19007
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
19008
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
19009
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
18760
19010
|
*/
|
|
18761
19011
|
get restampedRecoveredSequenceCount() {
|
|
18762
19012
|
return this.#restampedRecoveredSeqs;
|
|
@@ -18935,6 +19185,7 @@ var MmtpRouter = class {
|
|
|
18935
19185
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
18936
19186
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
18937
19187
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
19188
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
18938
19189
|
if (previous) {
|
|
18939
19190
|
previous.flush();
|
|
18940
19191
|
previous.dispose();
|
|
@@ -19085,7 +19336,8 @@ var MmtpRouter = class {
|
|
|
19085
19336
|
fec,
|
|
19086
19337
|
k,
|
|
19087
19338
|
offsets: /* @__PURE__ */ new Map(),
|
|
19088
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
19339
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
19340
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
19089
19341
|
};
|
|
19090
19342
|
this.#overlays.set(repairPacketId, inst);
|
|
19091
19343
|
return () => {
|
|
@@ -19126,6 +19378,7 @@ var MmtpRouter = class {
|
|
|
19126
19378
|
inst.last = void 0;
|
|
19127
19379
|
inst.offsets.clear();
|
|
19128
19380
|
inst.moqBlocks.clear();
|
|
19381
|
+
inst.rebuilt.clear();
|
|
19129
19382
|
}
|
|
19130
19383
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
19131
19384
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -19178,13 +19431,23 @@ var MmtpRouter = class {
|
|
|
19178
19431
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
19179
19432
|
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
19180
19433
|
* FEC reads it.
|
|
19434
|
+
*
|
|
19435
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
19436
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
19437
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
19438
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
19439
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
19440
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
19181
19441
|
*/
|
|
19182
19442
|
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
19183
19443
|
if (this.#overlays.size === 0) return;
|
|
19184
19444
|
const ssId = keyframeOverlaySsId(header);
|
|
19185
19445
|
if (ssId === void 0) return;
|
|
19446
|
+
let rebuiltPsn;
|
|
19186
19447
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
19187
19448
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
19449
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
19450
|
+
inst.rebuilt.delete(ssId);
|
|
19188
19451
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
19189
19452
|
if (identity !== null) {
|
|
19190
19453
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
@@ -19214,6 +19477,14 @@ var MmtpRouter = class {
|
|
|
19214
19477
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
19215
19478
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
19216
19479
|
}
|
|
19480
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
19481
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
19482
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
19483
|
+
this.#restampedRecoveredSeqs++;
|
|
19484
|
+
} else if (baseSsId !== void 0) {
|
|
19485
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
19486
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
19487
|
+
}
|
|
19217
19488
|
}
|
|
19218
19489
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
19219
19490
|
inst.moqBlocks.add(blockStart);
|
|
@@ -19247,11 +19518,16 @@ var MmtpRouter = class {
|
|
|
19247
19518
|
}
|
|
19248
19519
|
/**
|
|
19249
19520
|
* Route an overlay-recovered block's packets as source media of the
|
|
19250
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
19251
|
-
*
|
|
19252
|
-
*
|
|
19253
|
-
*
|
|
19254
|
-
*
|
|
19521
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
19522
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
19523
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
19524
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
19525
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
19526
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
19527
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
19528
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
19529
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
19530
|
+
* header's sequence and any other drops the packet.
|
|
19255
19531
|
*/
|
|
19256
19532
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
19257
19533
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -19272,40 +19548,65 @@ var MmtpRouter = class {
|
|
|
19272
19548
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
19273
19549
|
const restored = /* @__PURE__ */ new Set();
|
|
19274
19550
|
const baseSsIds = /* @__PURE__ */ new Map();
|
|
19551
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
19275
19552
|
const symbols = result.symbols.flatMap((sym) => {
|
|
19553
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
19276
19554
|
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
19277
19555
|
if (anchor !== void 0) {
|
|
19278
|
-
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
19279
19556
|
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
19280
19557
|
restored.add(packetSequenceNumber);
|
|
19281
19558
|
const out = { ...sym, packetSequenceNumber };
|
|
19282
19559
|
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
19283
|
-
if (baseSsId !== void 0)
|
|
19560
|
+
if (baseSsId !== void 0) {
|
|
19561
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
19562
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
19563
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
19564
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
19565
|
+
}
|
|
19566
|
+
}
|
|
19284
19567
|
return [out];
|
|
19285
19568
|
}
|
|
19286
19569
|
if (!restamped) {
|
|
19287
|
-
const
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
|
|
19570
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
19571
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
19572
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
19573
|
+
if (baseSsId !== void 0) {
|
|
19574
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
19575
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
19576
|
+
} else {
|
|
19577
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
19578
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
19579
|
+
}
|
|
19293
19580
|
return [sym];
|
|
19294
19581
|
}
|
|
19295
19582
|
this.#overlayStats.unsequencedDropped++;
|
|
19296
19583
|
return [];
|
|
19297
19584
|
});
|
|
19298
19585
|
this.#overlayStats.recovered++;
|
|
19586
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
19587
|
+
for (const sym of symbols) {
|
|
19588
|
+
const ssId = baseSsIds.get(sym.index);
|
|
19589
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
19590
|
+
const first = mpuFirst.get(mpu);
|
|
19591
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
19592
|
+
}
|
|
19593
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
19594
|
+
for (const sym of symbols) {
|
|
19595
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
19596
|
+
}
|
|
19299
19597
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
19300
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
19598
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
19599
|
+
baseSsIds,
|
|
19600
|
+
firstSsIds
|
|
19301
19601
|
});
|
|
19302
19602
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
19303
|
-
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
19603
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
19304
19604
|
}
|
|
19305
19605
|
/**
|
|
19306
19606
|
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
19307
19607
|
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
19308
|
-
* from a received member of its MPU, else unstampedBaseSsId
|
|
19608
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
19609
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
19309
19610
|
* so it is fed to the base decoder as a rebuilt source
|
|
19310
19611
|
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
19311
19612
|
* fewer to solve, and its repair can recover the block's other packets.
|
|
@@ -19331,7 +19632,7 @@ var MmtpRouter = class {
|
|
|
19331
19632
|
const feeds = [];
|
|
19332
19633
|
for (const sym of symbols) {
|
|
19333
19634
|
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
19334
|
-
const ssId = baseSsIds.get(sym);
|
|
19635
|
+
const ssId = baseSsIds.get(sym.index);
|
|
19335
19636
|
if (ssId === void 0) {
|
|
19336
19637
|
this.#overlayStats.baseUndetermined++;
|
|
19337
19638
|
continue;
|
|
@@ -20060,7 +20361,7 @@ var MmtpRouter = class {
|
|
|
20060
20361
|
catalogTrackName
|
|
20061
20362
|
)) continue;
|
|
20062
20363
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
20063
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
20364
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
20064
20365
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
20065
20366
|
}
|
|
20066
20367
|
if (payload.oversized) continue;
|
|
@@ -20148,6 +20449,7 @@ var MmtpRouter = class {
|
|
|
20148
20449
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
20149
20450
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
20150
20451
|
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
20452
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
20151
20453
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
20152
20454
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
20153
20455
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -20274,6 +20576,12 @@ var MmtpRouter = class {
|
|
|
20274
20576
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
20275
20577
|
* own RAP cadence, so no timer is involved.
|
|
20276
20578
|
*
|
|
20579
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
20580
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
20581
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
20582
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
20583
|
+
* frames of the outgoing stream.
|
|
20584
|
+
*
|
|
20277
20585
|
* The commit is one synchronous transition inside the frame path: the
|
|
20278
20586
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
20279
20587
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -20324,6 +20632,9 @@ var MmtpRouter = class {
|
|
|
20324
20632
|
droppedIncomingFrames: 0
|
|
20325
20633
|
};
|
|
20326
20634
|
this.#staged = state;
|
|
20635
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
20636
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
20637
|
+
}
|
|
20327
20638
|
return {
|
|
20328
20639
|
cancel: () => {
|
|
20329
20640
|
if (this.#staged !== state) return false;
|
|
@@ -20406,7 +20717,17 @@ var MmtpRouter = class {
|
|
|
20406
20717
|
*/
|
|
20407
20718
|
#routeParsedFrame(data, meta) {
|
|
20408
20719
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
20409
|
-
if (this.#staged !== null
|
|
20720
|
+
if (this.#staged !== null) {
|
|
20721
|
+
let consumed = false;
|
|
20722
|
+
this.#fecPokeDepth++;
|
|
20723
|
+
try {
|
|
20724
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
20725
|
+
} finally {
|
|
20726
|
+
this.#fecPokeDepth--;
|
|
20727
|
+
this.#flushPendingFecPokes();
|
|
20728
|
+
}
|
|
20729
|
+
if (consumed) return;
|
|
20730
|
+
}
|
|
20410
20731
|
this.#deliverParsedFrame(data, meta);
|
|
20411
20732
|
}
|
|
20412
20733
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -20558,12 +20879,16 @@ var MmtpRouter = class {
|
|
|
20558
20879
|
this.#parser.deferFecRelease(work);
|
|
20559
20880
|
} finally {
|
|
20560
20881
|
this.#fecPokeDepth--;
|
|
20561
|
-
|
|
20562
|
-
|
|
20563
|
-
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20882
|
+
this.#flushPendingFecPokes();
|
|
20883
|
+
}
|
|
20884
|
+
}
|
|
20885
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
20886
|
+
#flushPendingFecPokes() {
|
|
20887
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
20888
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
20889
|
+
this.#pendingFecPokes.clear();
|
|
20890
|
+
for (const poke of pokes) {
|
|
20891
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
20567
20892
|
}
|
|
20568
20893
|
}
|
|
20569
20894
|
}
|
|
@@ -20641,6 +20966,14 @@ var MmtpRouter = class {
|
|
|
20641
20966
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
20642
20967
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
20643
20968
|
},
|
|
20969
|
+
// A staged switch commits on when each side's frames reach it
|
|
20970
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
20971
|
+
// they did unanchored.
|
|
20972
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
20973
|
+
const staged = this.#staged;
|
|
20974
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
20975
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
20976
|
+
},
|
|
20644
20977
|
onReleased: (data, meta) => {
|
|
20645
20978
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
20646
20979
|
const timing = meta.authoredTiming;
|
|
@@ -21091,7 +21424,8 @@ var MmtpRouter = class {
|
|
|
21091
21424
|
source: parserOptions?.source,
|
|
21092
21425
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
21093
21426
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
21094
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
21427
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
21428
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
21095
21429
|
};
|
|
21096
21430
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
21097
21431
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -21557,7 +21891,12 @@ var MmtpRouter = class {
|
|
|
21557
21891
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
21558
21892
|
if (ownerPacketId == null) return;
|
|
21559
21893
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
21560
|
-
const fecSbnOf = (index) =>
|
|
21894
|
+
const fecSbnOf = (index) => {
|
|
21895
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
21896
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
21897
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
21898
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
21899
|
+
};
|
|
21561
21900
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
21562
21901
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
21563
21902
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -23103,6 +23442,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
23103
23442
|
function isSymbolCounts(value) {
|
|
23104
23443
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
23105
23444
|
}
|
|
23445
|
+
function isSpanFailure(value) {
|
|
23446
|
+
if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
|
|
23447
|
+
const length = value["length"];
|
|
23448
|
+
const sources = value["sources"];
|
|
23449
|
+
return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
|
|
23450
|
+
}
|
|
23106
23451
|
function isNullableNonNegative(value) {
|
|
23107
23452
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
23108
23453
|
}
|
|
@@ -23122,7 +23467,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
23122
23467
|
case "recovered":
|
|
23123
23468
|
return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
|
|
23124
23469
|
case "failed":
|
|
23125
|
-
return hasExactKeys(
|
|
23470
|
+
return hasExactKeys(
|
|
23471
|
+
value,
|
|
23472
|
+
"span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
|
|
23473
|
+
) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
|
|
23126
23474
|
case "repair-preempted":
|
|
23127
23475
|
case "repair-deferred":
|
|
23128
23476
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -23388,7 +23736,7 @@ var MulticastManager = class {
|
|
|
23388
23736
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
23389
23737
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
23390
23738
|
},
|
|
23391
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23739
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23392
23740
|
const pathCount = this.#fec.blockPathCount(
|
|
23393
23741
|
sbn,
|
|
23394
23742
|
packetId,
|
|
@@ -23406,7 +23754,8 @@ var MulticastManager = class {
|
|
|
23406
23754
|
symbols: {
|
|
23407
23755
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
23408
23756
|
moq: pathCount?.moqSymbols ?? 0
|
|
23409
|
-
}
|
|
23757
|
+
},
|
|
23758
|
+
...span === void 0 ? {} : { span }
|
|
23410
23759
|
});
|
|
23411
23760
|
},
|
|
23412
23761
|
onFrameGap: (decision) => {
|
|
@@ -24786,10 +25135,12 @@ var MulticastManager = class {
|
|
|
24786
25135
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
24787
25136
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
24788
25137
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
24789
|
-
* once its range FETCH ends (or none is sent).
|
|
25138
|
+
* once its range FETCH ends (or none is sent). `onExpired` is told of a
|
|
25139
|
+
* pending hole the stream runs past the locator's horizon: returning true,
|
|
25140
|
+
* the caller closes it (`closeIsoHoles`); otherwise it is discarded.
|
|
24790
25141
|
*/
|
|
24791
|
-
onUndelimitedLoss(cb) {
|
|
24792
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
25142
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
25143
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
24793
25144
|
}
|
|
24794
25145
|
/**
|
|
24795
25146
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -25025,7 +25376,8 @@ var MulticastManager = class {
|
|
|
25025
25376
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
25026
25377
|
...event,
|
|
25027
25378
|
block: Object.freeze({ ...event.block }),
|
|
25028
|
-
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
|
|
25379
|
+
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
|
|
25380
|
+
..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
|
|
25029
25381
|
}
|
|
25030
25382
|
);
|
|
25031
25383
|
if (this.#logRepairLifecycle) {
|