@blockcast/mmt-container 2.2.0-main.efb4fa4521af → 2.2.0-main.fb5c7dfbe2de
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -17
- package/dist/browser-runtime.js +1336 -302
- package/dist/browser-runtime.js.map +3 -3
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/cmcd-producer.d.ts +60 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +207 -26
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +209 -29
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +206 -26
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +34 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +555 -79
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +13 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +980 -146
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +28 -18
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +206 -26
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/repair-layer-subscriptions.d.ts +6 -2
- package/dist/repair-layer-subscriptions.d.ts.map +1 -1
- package/dist/repair-layer-subscriptions.js +6 -3
- package/dist/repair-layer-subscriptions.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +206 -26
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +189 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +9 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-router.ts +548 -38
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +13 -5
- package/src/track-session.ts +4 -1
|
@@ -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
|
|
@@ -1210,7 +1219,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1210
1219
|
#retentionBlocks;
|
|
1211
1220
|
#heldEvicted;
|
|
1212
1221
|
#newest;
|
|
1222
|
+
#newestSource;
|
|
1213
1223
|
#holes;
|
|
1224
|
+
#expired;
|
|
1225
|
+
#newlyExpired;
|
|
1226
|
+
#closedBehind;
|
|
1214
1227
|
static #checkedRetention(retentionBlocks) {
|
|
1215
1228
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
1216
1229
|
throw new Error(
|
|
@@ -1247,6 +1260,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1247
1260
|
observe(position) {
|
|
1248
1261
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
1249
1262
|
}
|
|
1263
|
+
/**
|
|
1264
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
1265
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
1266
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
1267
|
+
* reordering.
|
|
1268
|
+
*/
|
|
1269
|
+
get newestSource() {
|
|
1270
|
+
return this.#newestSource;
|
|
1271
|
+
}
|
|
1272
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
1273
|
+
noteSource(ssId) {
|
|
1274
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
1275
|
+
this.#newestSource = ssId;
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1250
1278
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
1251
1279
|
get heldEvicted() {
|
|
1252
1280
|
return this.#heldEvicted;
|
|
@@ -1265,17 +1293,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1265
1293
|
}
|
|
1266
1294
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
1267
1295
|
locate(ssId) {
|
|
1268
|
-
for (
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
+
}
|
|
1272
1302
|
}
|
|
1273
1303
|
}
|
|
1274
1304
|
return null;
|
|
1275
1305
|
}
|
|
1276
|
-
/** Number of pending
|
|
1306
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
1277
1307
|
get holeCount() {
|
|
1278
|
-
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);
|
|
1279
1319
|
}
|
|
1280
1320
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
1281
1321
|
blockEndingAt(end) {
|
|
@@ -1331,9 +1371,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1331
1371
|
/**
|
|
1332
1372
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
1333
1373
|
* block states its range and places every held source in it; later repair
|
|
1334
|
-
* symbols of the same block change nothing. A range
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
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.
|
|
1337
1379
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
1338
1380
|
* A block so far past `newest` that stating it would retire the position
|
|
1339
1381
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -1352,7 +1394,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1352
1394
|
const known = this.blockAt(start);
|
|
1353
1395
|
if (known !== void 0) {
|
|
1354
1396
|
if (known.span) {
|
|
1355
|
-
|
|
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}` };
|
|
1356
1407
|
}
|
|
1357
1408
|
if (known.length !== length) {
|
|
1358
1409
|
return {
|
|
@@ -1420,18 +1471,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1420
1471
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
1421
1472
|
}
|
|
1422
1473
|
/**
|
|
1423
|
-
* Turn every
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
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.
|
|
1426
1480
|
*/
|
|
1427
1481
|
closeHolesWithin(start, length) {
|
|
1428
1482
|
const closed = [];
|
|
1429
|
-
for (const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
+
}
|
|
1435
1496
|
}
|
|
1436
1497
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
1437
1498
|
}
|
|
@@ -1445,15 +1506,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1445
1506
|
return void 0;
|
|
1446
1507
|
}
|
|
1447
1508
|
/**
|
|
1448
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
1449
|
-
* lies more than `retentionBlocks` blocks behind
|
|
1450
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
1451
|
-
* 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.
|
|
1452
1514
|
*/
|
|
1453
1515
|
retireBehind(position) {
|
|
1454
1516
|
this.observe(position);
|
|
1455
1517
|
const horizon = this.#retentionBlocks;
|
|
1456
1518
|
const retired = [];
|
|
1519
|
+
this.#closedBehind.length = 0;
|
|
1457
1520
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
1458
1521
|
const block = this.#blocks[i];
|
|
1459
1522
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -1465,10 +1528,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1465
1528
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
1466
1529
|
const hole = this.#holes[i];
|
|
1467
1530
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
1468
|
-
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);
|
|
1469
1535
|
}
|
|
1470
1536
|
for (const ssId of [...this.#held.keys()]) {
|
|
1471
|
-
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
|
+
}
|
|
1472
1540
|
}
|
|
1473
1541
|
return retired;
|
|
1474
1542
|
}
|
|
@@ -1476,9 +1544,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1476
1544
|
reset() {
|
|
1477
1545
|
this.#blocks.length = 0;
|
|
1478
1546
|
this.#holes.length = 0;
|
|
1547
|
+
this.#expired.length = 0;
|
|
1548
|
+
this.#newlyExpired.length = 0;
|
|
1549
|
+
this.#closedBehind.length = 0;
|
|
1479
1550
|
this.#held.clear();
|
|
1480
1551
|
this.#coverageEnd = void 0;
|
|
1481
1552
|
this.#newest = void 0;
|
|
1553
|
+
this.#newestSource = void 0;
|
|
1482
1554
|
}
|
|
1483
1555
|
#claim(block) {
|
|
1484
1556
|
const placed = [];
|
|
@@ -2067,6 +2139,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
2067
2139
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
2068
2140
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
2069
2141
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
2142
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
2070
2143
|
function isMulticastSymbolDelivery(value) {
|
|
2071
2144
|
return value === "datagram" || value === "ordered-relay";
|
|
2072
2145
|
}
|
|
@@ -2160,10 +2233,18 @@ var FECManager = class _FECManager {
|
|
|
2160
2233
|
// 'pending' forever, which would park every later frame of its stream in
|
|
2161
2234
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
2162
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
|
+
*/
|
|
2163
2240
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
2164
2241
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
2165
2242
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
2166
2243
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
2244
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
2245
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
2246
|
+
// not network arrivals and never make a block clean.
|
|
2247
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
2167
2248
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
2168
2249
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
2169
2250
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -2172,6 +2253,9 @@ var FECManager = class _FECManager {
|
|
|
2172
2253
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
2173
2254
|
// symbols; waiting for K fails every one of them.
|
|
2174
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();
|
|
2175
2259
|
this.#ssbLengthConflictCount = 0;
|
|
2176
2260
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
2177
2261
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -2292,6 +2376,10 @@ var FECManager = class _FECManager {
|
|
|
2292
2376
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
2293
2377
|
// One classifier per physical stream, built from that stream's geometry.
|
|
2294
2378
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
2379
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
2380
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
2381
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
2382
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
2295
2383
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
2296
2384
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
2297
2385
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -2365,7 +2453,10 @@ var FECManager = class _FECManager {
|
|
|
2365
2453
|
// arrives via onBlockRecovered callback.
|
|
2366
2454
|
this.onBlockStarted = null;
|
|
2367
2455
|
this.onBlockComplete = null;
|
|
2368
|
-
/**
|
|
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
|
+
*/
|
|
2369
2460
|
this.onBlockFailed = null;
|
|
2370
2461
|
/**
|
|
2371
2462
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -2407,6 +2498,8 @@ var FECManager = class _FECManager {
|
|
|
2407
2498
|
* for it, which it must be.
|
|
2408
2499
|
*/
|
|
2409
2500
|
this.#onUndelimitedLoss = null;
|
|
2501
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
2502
|
+
this.#onIsoHoleExpired = null;
|
|
2410
2503
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
2411
2504
|
this.onBlockVanished = null;
|
|
2412
2505
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -2449,7 +2542,9 @@ var FECManager = class _FECManager {
|
|
|
2449
2542
|
#pendingWorkerCleanupBounds;
|
|
2450
2543
|
#blockSourceCounts;
|
|
2451
2544
|
#blockSourceEsis;
|
|
2545
|
+
#blockRebuiltEsis;
|
|
2452
2546
|
#blockAuthoredLengths;
|
|
2547
|
+
#isoSpanKeys;
|
|
2453
2548
|
#ssbLengthConflictCount;
|
|
2454
2549
|
#blockRepairRsIds;
|
|
2455
2550
|
#blockRsbLengths;
|
|
@@ -2491,6 +2586,7 @@ var FECManager = class _FECManager {
|
|
|
2491
2586
|
#orderedFrontiers;
|
|
2492
2587
|
#orderedPendingBlocks;
|
|
2493
2588
|
#classifiers;
|
|
2589
|
+
#noSpanRaps;
|
|
2494
2590
|
#streamGeometries;
|
|
2495
2591
|
#failedBlocks;
|
|
2496
2592
|
#resolvedBlockClasses;
|
|
@@ -2515,6 +2611,7 @@ var FECManager = class _FECManager {
|
|
|
2515
2611
|
#decoderUnavailableEvents;
|
|
2516
2612
|
#repairSymbolSizeMismatchCount;
|
|
2517
2613
|
#onUndelimitedLoss;
|
|
2614
|
+
#onIsoHoleExpired;
|
|
2518
2615
|
#encodeTrackName(catalogTrackName) {
|
|
2519
2616
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
2520
2617
|
if (encoded === void 0) {
|
|
@@ -2986,7 +3083,10 @@ var FECManager = class _FECManager {
|
|
|
2986
3083
|
timer.handle = null;
|
|
2987
3084
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
2988
3085
|
};
|
|
2989
|
-
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
|
+
);
|
|
2990
3090
|
this.#repairDeadlines.set(key, timer);
|
|
2991
3091
|
}
|
|
2992
3092
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -3130,8 +3230,14 @@ var FECManager = class _FECManager {
|
|
|
3130
3230
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
3131
3231
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
3132
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).
|
|
3133
3239
|
*/
|
|
3134
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
3240
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
3135
3241
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3136
3242
|
const now = performance.now();
|
|
3137
3243
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -3152,6 +3258,7 @@ var FECManager = class _FECManager {
|
|
|
3152
3258
|
};
|
|
3153
3259
|
const discontinuity = () => {
|
|
3154
3260
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
3261
|
+
this.#noSpanRaps.delete(streamKey);
|
|
3155
3262
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
3156
3263
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
3157
3264
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -3169,7 +3276,7 @@ var FECManager = class _FECManager {
|
|
|
3169
3276
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
3170
3277
|
};
|
|
3171
3278
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
3172
|
-
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))) {
|
|
3173
3280
|
discontinuity();
|
|
3174
3281
|
return;
|
|
3175
3282
|
}
|
|
@@ -3431,17 +3538,21 @@ var FECManager = class _FECManager {
|
|
|
3431
3538
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
3432
3539
|
this.#repairHoldUntilMs.delete(key);
|
|
3433
3540
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
3434
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
3541
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
3435
3542
|
};
|
|
3436
|
-
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
|
+
);
|
|
3437
3547
|
this.#activeBlocks.set(key, timer);
|
|
3438
3548
|
this.#indexBlockKey(key, sbn, packetId);
|
|
3439
3549
|
}
|
|
3440
3550
|
/**
|
|
3441
3551
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
3442
|
-
* 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.
|
|
3443
3554
|
*/
|
|
3444
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
3555
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
3445
3556
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3446
3557
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
3447
3558
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -3457,7 +3568,7 @@ var FECManager = class _FECManager {
|
|
|
3457
3568
|
if (workerClient) {
|
|
3458
3569
|
this.#pendingWorkerCleanups.add(key);
|
|
3459
3570
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
3460
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
3571
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
3461
3572
|
workerClient.cleanup(sbn);
|
|
3462
3573
|
} else {
|
|
3463
3574
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -3564,6 +3675,9 @@ var FECManager = class _FECManager {
|
|
|
3564
3675
|
}
|
|
3565
3676
|
if (policyChanged) {
|
|
3566
3677
|
this.#bumpEpoch(packetId);
|
|
3678
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
3679
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
3680
|
+
}
|
|
3567
3681
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
3568
3682
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
3569
3683
|
const stream = this.#parseStreamKey(key);
|
|
@@ -3766,7 +3880,9 @@ var FECManager = class _FECManager {
|
|
|
3766
3880
|
}
|
|
3767
3881
|
resetMap(this.#blockSourceCounts);
|
|
3768
3882
|
resetMap(this.#blockSourceEsis);
|
|
3883
|
+
resetMap(this.#blockRebuiltEsis);
|
|
3769
3884
|
resetMap(this.#blockAuthoredLengths);
|
|
3885
|
+
resetSet(this.#isoSpanKeys);
|
|
3770
3886
|
resetMap(this.#blockRepairRsIds);
|
|
3771
3887
|
resetMap(this.#blockRsbLengths);
|
|
3772
3888
|
resetMap(this.#heldLayerStates);
|
|
@@ -3799,6 +3915,7 @@ var FECManager = class _FECManager {
|
|
|
3799
3915
|
resetMap(this.#isoLocators);
|
|
3800
3916
|
resetMap(this.#sourceAnchors);
|
|
3801
3917
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
3918
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
3802
3919
|
this.#bumpEpoch(packetId);
|
|
3803
3920
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
3804
3921
|
for (const key of keys.keys()) {
|
|
@@ -3889,7 +4006,9 @@ var FECManager = class _FECManager {
|
|
|
3889
4006
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
3890
4007
|
dropMap(this.#blockSourceCounts);
|
|
3891
4008
|
dropMap(this.#blockSourceEsis);
|
|
4009
|
+
dropMap(this.#blockRebuiltEsis);
|
|
3892
4010
|
dropMap(this.#blockAuthoredLengths);
|
|
4011
|
+
dropSet(this.#isoSpanKeys);
|
|
3893
4012
|
dropMap(this.#blockRepairRsIds);
|
|
3894
4013
|
dropMap(this.#blockRsbLengths);
|
|
3895
4014
|
dropMap(this.#blockPathCounts);
|
|
@@ -3905,6 +4024,7 @@ var FECManager = class _FECManager {
|
|
|
3905
4024
|
dropMap(this.#isoLocators);
|
|
3906
4025
|
dropMap(this.#sourceAnchors);
|
|
3907
4026
|
dropMap(this.#classifiers);
|
|
4027
|
+
dropMap(this.#noSpanRaps);
|
|
3908
4028
|
dropMap(this.#streamGeometries);
|
|
3909
4029
|
dropSet(this.#pendingWorkerCleanups);
|
|
3910
4030
|
dropSet(this.#completedBlocks);
|
|
@@ -4050,6 +4170,9 @@ var FECManager = class _FECManager {
|
|
|
4050
4170
|
this.#unindexBlockKey(key);
|
|
4051
4171
|
}
|
|
4052
4172
|
}
|
|
4173
|
+
if (isSourceSymbol && source === "multicast") {
|
|
4174
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
4175
|
+
}
|
|
4053
4176
|
return true;
|
|
4054
4177
|
}
|
|
4055
4178
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -4228,8 +4351,9 @@ var FECManager = class _FECManager {
|
|
|
4228
4351
|
return true;
|
|
4229
4352
|
}
|
|
4230
4353
|
/**
|
|
4231
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
4232
|
-
*
|
|
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.
|
|
4233
4357
|
*/
|
|
4234
4358
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
4235
4359
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -4367,6 +4491,8 @@ var FECManager = class _FECManager {
|
|
|
4367
4491
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
4368
4492
|
const evictedBefore = locator.heldEvicted;
|
|
4369
4493
|
const held = locator.hold(ssId, source);
|
|
4494
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
4495
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
4370
4496
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
4371
4497
|
if (evicted > 0) {
|
|
4372
4498
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -4387,17 +4513,20 @@ var FECManager = class _FECManager {
|
|
|
4387
4513
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
4388
4514
|
*/
|
|
4389
4515
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
4390
|
-
const
|
|
4516
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
4517
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
4518
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
4391
4519
|
if (placement.status !== "placed") {
|
|
4392
4520
|
this.#isoRepairRefusedCount++;
|
|
4393
4521
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
4394
4522
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
4395
4523
|
}
|
|
4524
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
4396
4525
|
return placement;
|
|
4397
4526
|
}
|
|
4527
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
4398
4528
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
4399
4529
|
if (placement.hole !== void 0) {
|
|
4400
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
4401
4530
|
const hole = placement.hole.block;
|
|
4402
4531
|
const previous = locator.blockEndingAt(hole.start);
|
|
4403
4532
|
const loss = {
|
|
@@ -4418,17 +4547,29 @@ var FECManager = class _FECManager {
|
|
|
4418
4547
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
4419
4548
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
4420
4549
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
4421
|
-
* 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.
|
|
4422
4554
|
*/
|
|
4423
|
-
setUndelimitedLossHandler(handler) {
|
|
4555
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
4424
4556
|
this.#onUndelimitedLoss = handler;
|
|
4557
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
4425
4558
|
this.#deferIsoGaps = handler !== null;
|
|
4426
4559
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
4427
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
|
+
}
|
|
4428
4568
|
/**
|
|
4429
|
-
* The range repair for a hole ended: every hole
|
|
4430
|
-
*
|
|
4431
|
-
* 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).
|
|
4432
4573
|
*/
|
|
4433
4574
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
4434
4575
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -4447,7 +4588,10 @@ var FECManager = class _FECManager {
|
|
|
4447
4588
|
*/
|
|
4448
4589
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
4449
4590
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
4450
|
-
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
|
+
}
|
|
4451
4595
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
4452
4596
|
}
|
|
4453
4597
|
/**
|
|
@@ -4535,11 +4679,14 @@ var FECManager = class _FECManager {
|
|
|
4535
4679
|
/**
|
|
4536
4680
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
4537
4681
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
4538
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
4682
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
4683
|
+
* and neither has a stream without catalog timing until it releases two
|
|
4684
|
+
* RAPs inside one MPU.
|
|
4539
4685
|
*/
|
|
4540
4686
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
4541
|
-
|
|
4542
|
-
return this.#
|
|
4687
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
4688
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
4689
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
4543
4690
|
}
|
|
4544
4691
|
/** A block's class now, or frozen at its resolution. */
|
|
4545
4692
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -4565,7 +4712,14 @@ var FECManager = class _FECManager {
|
|
|
4565
4712
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
4566
4713
|
*/
|
|
4567
4714
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
4568
|
-
if (!this.#trackConfigs.has(packetId)
|
|
4715
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
4716
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
4717
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
4718
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
4719
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
4720
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4569
4723
|
this.#applyClassifierChanges(
|
|
4570
4724
|
packetId,
|
|
4571
4725
|
catalogTrackName,
|
|
@@ -4778,11 +4932,14 @@ var FECManager = class _FECManager {
|
|
|
4778
4932
|
});
|
|
4779
4933
|
client.onCleanupComplete((sbn) => {
|
|
4780
4934
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
4781
|
-
const
|
|
4935
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
4936
|
+
const cleanupKeys = owed.filter((key) => {
|
|
4782
4937
|
const block = this.#parseBlockKey(key);
|
|
4783
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
4938
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
4784
4939
|
});
|
|
4785
|
-
|
|
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");
|
|
4786
4943
|
});
|
|
4787
4944
|
client.onError((msg, code) => {
|
|
4788
4945
|
const requestedKind = code ?? msg;
|
|
@@ -4852,9 +5009,11 @@ var FECManager = class _FECManager {
|
|
|
4852
5009
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
4853
5010
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
4854
5011
|
*/
|
|
4855
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
5012
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
4856
5013
|
this.#clearOwedCleanupBound(key);
|
|
4857
5014
|
const bound = {
|
|
5015
|
+
dueAtMs,
|
|
5016
|
+
askedAtMs: performance.now(),
|
|
4858
5017
|
handle: setTimeout(() => {
|
|
4859
5018
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
4860
5019
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -4952,7 +5111,15 @@ var FECManager = class _FECManager {
|
|
|
4952
5111
|
this.#isoSourceOversizeCount++;
|
|
4953
5112
|
return null;
|
|
4954
5113
|
}
|
|
4955
|
-
|
|
5114
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
5117
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
5118
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
5119
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
5120
|
+
* stated for `ssId`.
|
|
5121
|
+
*/
|
|
5122
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
4956
5123
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
4957
5124
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
4958
5125
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -4981,6 +5148,74 @@ var FECManager = class _FECManager {
|
|
|
4981
5148
|
}
|
|
4982
5149
|
return recovered;
|
|
4983
5150
|
}
|
|
5151
|
+
/**
|
|
5152
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
5153
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
5154
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
5155
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
5156
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
5157
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
5158
|
+
* no deadline refresh, and it never makes a block clean.
|
|
5159
|
+
*
|
|
5160
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
5161
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
5162
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
5163
|
+
* feedSourceSymbol takes it.
|
|
5164
|
+
*
|
|
5165
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
5166
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
5167
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
5168
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
5169
|
+
* block as clean on its own side), the block is completed here as
|
|
5170
|
+
* recovered, with nothing left to route.
|
|
5171
|
+
*/
|
|
5172
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
5173
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
5174
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
5175
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
5176
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
5177
|
+
const sbn = located.block.start;
|
|
5178
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
5179
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
5180
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
5181
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
5182
|
+
if (rebuilt === void 0) {
|
|
5183
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
5184
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
5185
|
+
}
|
|
5186
|
+
const config = this.#trackConfigs.get(packetId);
|
|
5187
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
5188
|
+
rebuilt.add(located.esi);
|
|
5189
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
5190
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
5191
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
5192
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
return { taken: true, recovered };
|
|
5196
|
+
}
|
|
5197
|
+
/**
|
|
5198
|
+
* A block that holds every one of its `length` sources, received or
|
|
5199
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
5200
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
5201
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
5202
|
+
*/
|
|
5203
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
5204
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
5205
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
5206
|
+
let present = rebuilt.size;
|
|
5207
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
5208
|
+
if (present < length) return false;
|
|
5209
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
5210
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
5211
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
5212
|
+
if (catalogTrackName === void 0) {
|
|
5213
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
5214
|
+
} else {
|
|
5215
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
5216
|
+
}
|
|
5217
|
+
return true;
|
|
5218
|
+
}
|
|
4984
5219
|
/**
|
|
4985
5220
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
4986
5221
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -5183,8 +5418,75 @@ var FECManager = class _FECManager {
|
|
|
5183
5418
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
5184
5419
|
return null;
|
|
5185
5420
|
}
|
|
5421
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
5186
5422
|
return recovered;
|
|
5187
5423
|
}
|
|
5424
|
+
/**
|
|
5425
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
5426
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
5427
|
+
* position past a block leaves the sources that block still misses
|
|
5428
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
5429
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
5430
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
5431
|
+
* sent before it.
|
|
5432
|
+
*/
|
|
5433
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
5434
|
+
const previous = locator.newestSource;
|
|
5435
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
5436
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
5437
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
5438
|
+
this.#shortenUnrecoverableWait(
|
|
5439
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
5440
|
+
block.start,
|
|
5441
|
+
packetId,
|
|
5442
|
+
catalogTrackName
|
|
5443
|
+
);
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
/**
|
|
5447
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
5448
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
5449
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
5450
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
5451
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
5452
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
5453
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
5454
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
5455
|
+
* block.
|
|
5456
|
+
*/
|
|
5457
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
5458
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
5459
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
5460
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
5461
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
5462
|
+
const received = this.#blockSourceEsis.get(key);
|
|
5463
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
5464
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
5465
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
5466
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
5467
|
+
return void 0;
|
|
5468
|
+
}
|
|
5469
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
5470
|
+
}
|
|
5471
|
+
/**
|
|
5472
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
5473
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
5474
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
5475
|
+
*/
|
|
5476
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
5477
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
5478
|
+
const config = this.#trackConfigs.get(packetId);
|
|
5479
|
+
if (config === void 0) return;
|
|
5480
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
5481
|
+
if (waitMs === void 0) return;
|
|
5482
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
5483
|
+
this.#armBlockTimeout(
|
|
5484
|
+
sbn,
|
|
5485
|
+
packetId,
|
|
5486
|
+
catalogTrackName,
|
|
5487
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
5488
|
+
);
|
|
5489
|
+
}
|
|
5188
5490
|
/**
|
|
5189
5491
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
5190
5492
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -5282,12 +5584,13 @@ var FECManager = class _FECManager {
|
|
|
5282
5584
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
5283
5585
|
* classification evidence, and marks the block clean when all of its
|
|
5284
5586
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
5285
|
-
* otherwise K.
|
|
5587
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
5588
|
+
* placed only now (see #noteSbnSeen).
|
|
5286
5589
|
*
|
|
5287
5590
|
* Throws for a packetId that was never configured, so a classified block
|
|
5288
5591
|
* never reads the manager-global K or timeout.
|
|
5289
5592
|
*/
|
|
5290
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
5593
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
5291
5594
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
5292
5595
|
if (trackConfig === void 0) {
|
|
5293
5596
|
throw new Error(
|
|
@@ -5303,7 +5606,8 @@ var FECManager = class _FECManager {
|
|
|
5303
5606
|
catalogTrackName,
|
|
5304
5607
|
"source",
|
|
5305
5608
|
evidence.packetSequenceNumber,
|
|
5306
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
5609
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
5610
|
+
held
|
|
5307
5611
|
);
|
|
5308
5612
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
5309
5613
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -5339,6 +5643,15 @@ var FECManager = class _FECManager {
|
|
|
5339
5643
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
5340
5644
|
return;
|
|
5341
5645
|
}
|
|
5646
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
5647
|
+
key,
|
|
5648
|
+
sbn,
|
|
5649
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
5650
|
+
packetId,
|
|
5651
|
+
catalogTrackName
|
|
5652
|
+
)) {
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
5342
5655
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
5343
5656
|
}
|
|
5344
5657
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -5347,7 +5660,9 @@ var FECManager = class _FECManager {
|
|
|
5347
5660
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
5348
5661
|
this.#blockSourceCounts.delete(key);
|
|
5349
5662
|
this.#blockSourceEsis.delete(key);
|
|
5663
|
+
this.#blockRebuiltEsis.delete(key);
|
|
5350
5664
|
this.#blockAuthoredLengths.delete(key);
|
|
5665
|
+
this.#isoSpanKeys.delete(key);
|
|
5351
5666
|
this.#blockRepairRsIds.delete(key);
|
|
5352
5667
|
this.#blockRsbLengths.delete(key);
|
|
5353
5668
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -5378,6 +5693,19 @@ var FECManager = class _FECManager {
|
|
|
5378
5693
|
#receiptNow() {
|
|
5379
5694
|
return this.#receiptAtMs ?? performance.now();
|
|
5380
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
|
+
}
|
|
5381
5709
|
/**
|
|
5382
5710
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
5383
5711
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -5413,10 +5741,20 @@ var FECManager = class _FECManager {
|
|
|
5413
5741
|
this.#repairDeadlineMs(trackConfig)
|
|
5414
5742
|
);
|
|
5415
5743
|
}
|
|
5416
|
-
this.#
|
|
5744
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
5745
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
5748
|
+
this.#armBlockTimeout(
|
|
5749
|
+
sbn,
|
|
5750
|
+
packetId,
|
|
5751
|
+
catalogTrackName,
|
|
5752
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
5753
|
+
);
|
|
5417
5754
|
}
|
|
5418
5755
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
5419
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;
|
|
5420
5758
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
5421
5759
|
this.#clearRepairDeadline(key);
|
|
5422
5760
|
this.#clearBlockTimer(key);
|
|
@@ -5426,6 +5764,7 @@ var FECManager = class _FECManager {
|
|
|
5426
5764
|
}
|
|
5427
5765
|
this.#blockSourceCounts.delete(key);
|
|
5428
5766
|
this.#blockSourceEsis.delete(key);
|
|
5767
|
+
this.#blockRebuiltEsis.delete(key);
|
|
5429
5768
|
this.#blockAuthoredLengths.delete(key);
|
|
5430
5769
|
this.#blockRepairRsIds.delete(key);
|
|
5431
5770
|
this.#blockRsbLengths.delete(key);
|
|
@@ -5435,7 +5774,8 @@ var FECManager = class _FECManager {
|
|
|
5435
5774
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
5436
5775
|
this.#fecStats.blocksFailed++;
|
|
5437
5776
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
5438
|
-
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);
|
|
5439
5779
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
5440
5780
|
}
|
|
5441
5781
|
/**
|
|
@@ -5464,7 +5804,9 @@ var FECManager = class _FECManager {
|
|
|
5464
5804
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
5465
5805
|
this.#blockSourceCounts.delete(key);
|
|
5466
5806
|
this.#blockSourceEsis.delete(key);
|
|
5807
|
+
this.#blockRebuiltEsis.delete(key);
|
|
5467
5808
|
this.#blockAuthoredLengths.delete(key);
|
|
5809
|
+
this.#isoSpanKeys.delete(key);
|
|
5468
5810
|
this.#blockRepairRsIds.delete(key);
|
|
5469
5811
|
this.#blockRsbLengths.delete(key);
|
|
5470
5812
|
this.#clearBlockTimer(key);
|
|
@@ -5555,7 +5897,9 @@ var FECManager = class _FECManager {
|
|
|
5555
5897
|
this.#blockKeysBySbn.clear();
|
|
5556
5898
|
this.#blockSourceCounts.clear();
|
|
5557
5899
|
this.#blockSourceEsis.clear();
|
|
5900
|
+
this.#blockRebuiltEsis.clear();
|
|
5558
5901
|
this.#blockAuthoredLengths.clear();
|
|
5902
|
+
this.#isoSpanKeys.clear();
|
|
5559
5903
|
this.#blockRepairRsIds.clear();
|
|
5560
5904
|
this.#blockRsbLengths.clear();
|
|
5561
5905
|
this.#heldLayerStates.clear();
|
|
@@ -5580,6 +5924,7 @@ var FECManager = class _FECManager {
|
|
|
5580
5924
|
this.#blockAnchorsMs.clear();
|
|
5581
5925
|
this.#blockCatalogTrackNames.clear();
|
|
5582
5926
|
this.#classifiers.clear();
|
|
5927
|
+
this.#noSpanRaps.clear();
|
|
5583
5928
|
this.#streamGeometries.clear();
|
|
5584
5929
|
this.#failedBlocks.clear();
|
|
5585
5930
|
this.#resolvedBlockClasses.clear();
|
|
@@ -6933,9 +7278,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
|
6933
7278
|
} catch (error) {
|
|
6934
7279
|
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
6935
7280
|
}
|
|
6936
|
-
if (
|
|
7281
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
6937
7282
|
await rx.close();
|
|
6938
|
-
throw unsupported(`integrity scheme ${String(
|
|
7283
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
6939
7284
|
}
|
|
6940
7285
|
const events = new EventTarget();
|
|
6941
7286
|
let state = "connected";
|
|
@@ -7054,14 +7399,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
7054
7399
|
});
|
|
7055
7400
|
|
|
7056
7401
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
7402
|
+
var MAX_PENDING_PACKETS = 256;
|
|
7057
7403
|
var AMTGatewayManager = class {
|
|
7058
7404
|
subscriptions = /* @__PURE__ */ new Map();
|
|
7059
7405
|
messageHandler;
|
|
7406
|
+
messageListenerRegistered = false;
|
|
7060
7407
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
7061
7408
|
constructor() {
|
|
7062
7409
|
this.messageHandler = (event) => {
|
|
7063
7410
|
const data = event.data;
|
|
7064
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
7065
7411
|
if (data.type === "UDP_PACKET") {
|
|
7066
7412
|
this.handlePacket(data);
|
|
7067
7413
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -7070,12 +7416,22 @@ var AMTGatewayManager = class {
|
|
|
7070
7416
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
7071
7417
|
}
|
|
7072
7418
|
};
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
console.warn("[AMT] Service worker not available");
|
|
7419
|
+
}
|
|
7420
|
+
ensureMessageListener() {
|
|
7421
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
7422
|
+
return;
|
|
7078
7423
|
}
|
|
7424
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
7425
|
+
this.messageListenerRegistered = true;
|
|
7426
|
+
console.log("[AMT] Registered service worker message handler");
|
|
7427
|
+
}
|
|
7428
|
+
removeMessageListener() {
|
|
7429
|
+
if (!this.messageListenerRegistered)
|
|
7430
|
+
return;
|
|
7431
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
7432
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
7433
|
+
}
|
|
7434
|
+
this.messageListenerRegistered = false;
|
|
7079
7435
|
}
|
|
7080
7436
|
/**
|
|
7081
7437
|
* Subscribe via AMT tunnel
|
|
@@ -7085,6 +7441,7 @@ var AMTGatewayManager = class {
|
|
|
7085
7441
|
if (config.protected) {
|
|
7086
7442
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
7087
7443
|
}
|
|
7444
|
+
this.ensureMessageListener();
|
|
7088
7445
|
const keyStr = this.getKeyString(key);
|
|
7089
7446
|
if (this.subscriptions.has(keyStr)) {
|
|
7090
7447
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -7120,6 +7477,8 @@ var AMTGatewayManager = class {
|
|
|
7120
7477
|
this.subscriptions.delete(keyStr);
|
|
7121
7478
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
7122
7479
|
}
|
|
7480
|
+
if (this.subscriptions.size === 0)
|
|
7481
|
+
this.removeMessageListener();
|
|
7123
7482
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
7124
7483
|
}
|
|
7125
7484
|
}
|
|
@@ -7141,6 +7500,8 @@ var AMTGatewayManager = class {
|
|
|
7141
7500
|
} finally {
|
|
7142
7501
|
this.subscriptions.delete(handle);
|
|
7143
7502
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
7503
|
+
if (this.subscriptions.size === 0)
|
|
7504
|
+
this.removeMessageListener();
|
|
7144
7505
|
}
|
|
7145
7506
|
}
|
|
7146
7507
|
/**
|
|
@@ -7157,14 +7518,12 @@ var AMTGatewayManager = class {
|
|
|
7157
7518
|
if (!subscriptionId)
|
|
7158
7519
|
return;
|
|
7159
7520
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
7160
|
-
if (!subscription)
|
|
7161
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
7521
|
+
if (!subscription)
|
|
7162
7522
|
return;
|
|
7163
|
-
}
|
|
7164
7523
|
if (!subscription.handshakeComplete) {
|
|
7165
7524
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
7166
7525
|
const packetData2 = data;
|
|
7167
|
-
if (queue) {
|
|
7526
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
7168
7527
|
queue.push({
|
|
7169
7528
|
data: packetData2.data,
|
|
7170
7529
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -7190,19 +7549,15 @@ var AMTGatewayManager = class {
|
|
|
7190
7549
|
if (!subscriptionId)
|
|
7191
7550
|
return;
|
|
7192
7551
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
7193
|
-
if (!subscription)
|
|
7194
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
7552
|
+
if (!subscription)
|
|
7195
7553
|
return;
|
|
7196
|
-
}
|
|
7197
7554
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
7198
7555
|
}
|
|
7199
7556
|
/**
|
|
7200
7557
|
* Cleanup on destroy
|
|
7201
7558
|
*/
|
|
7202
7559
|
destroy() {
|
|
7203
|
-
|
|
7204
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
7205
|
-
}
|
|
7560
|
+
this.removeMessageListener();
|
|
7206
7561
|
const handles = Array.from(this.subscriptions.keys());
|
|
7207
7562
|
for (const handle of handles) {
|
|
7208
7563
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -13111,9 +13466,21 @@ var IsoBmffTimingResolver = class {
|
|
|
13111
13466
|
}
|
|
13112
13467
|
#track = null;
|
|
13113
13468
|
#fragments = /* @__PURE__ */ new Map();
|
|
13469
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
13470
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
13471
|
+
/**
|
|
13472
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
13473
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
13474
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
13475
|
+
*/
|
|
13476
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
13114
13477
|
#pendingSamples = 0;
|
|
13115
13478
|
#evictedSamples = 0;
|
|
13116
13479
|
#lastMovieFragmentRejection = null;
|
|
13480
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
13481
|
+
get initialized() {
|
|
13482
|
+
return this.#track !== null;
|
|
13483
|
+
}
|
|
13117
13484
|
get pendingSampleCount() {
|
|
13118
13485
|
return this.#pendingSamples;
|
|
13119
13486
|
}
|
|
@@ -13137,7 +13504,14 @@ var IsoBmffTimingResolver = class {
|
|
|
13137
13504
|
this.#track = timing;
|
|
13138
13505
|
return true;
|
|
13139
13506
|
}
|
|
13140
|
-
|
|
13507
|
+
/**
|
|
13508
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
13509
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
13510
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
13511
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
13512
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
13513
|
+
*/
|
|
13514
|
+
addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
13141
13515
|
this.#lastMovieFragmentRejection = null;
|
|
13142
13516
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
13143
13517
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -13163,6 +13537,8 @@ var IsoBmffTimingResolver = class {
|
|
|
13163
13537
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
13164
13538
|
);
|
|
13165
13539
|
}
|
|
13540
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
13541
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
13166
13542
|
let freed = 0;
|
|
13167
13543
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
13168
13544
|
freed += this.#evictOldestFragment();
|
|
@@ -13176,6 +13552,7 @@ var IsoBmffTimingResolver = class {
|
|
|
13176
13552
|
return false;
|
|
13177
13553
|
}
|
|
13178
13554
|
this.#fragments.set(key, [...parsed.samples]);
|
|
13555
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
13179
13556
|
this.#pendingSamples += parsed.samples.length;
|
|
13180
13557
|
return true;
|
|
13181
13558
|
}
|
|
@@ -13233,6 +13610,7 @@ var IsoBmffTimingResolver = class {
|
|
|
13233
13610
|
let live = 0;
|
|
13234
13611
|
for (const sample of samples) if (sample !== null) live++;
|
|
13235
13612
|
this.#fragments.delete(oldest);
|
|
13613
|
+
this.#firstSamples.delete(oldest);
|
|
13236
13614
|
this.#pendingSamples -= live;
|
|
13237
13615
|
this.#evictedSamples += live;
|
|
13238
13616
|
return live;
|
|
@@ -13248,12 +13626,23 @@ var IsoBmffTimingResolver = class {
|
|
|
13248
13626
|
if (!samples || !samples[index]) return false;
|
|
13249
13627
|
samples[index] = null;
|
|
13250
13628
|
this.#pendingSamples--;
|
|
13251
|
-
if (samples.every((value) => value === null))
|
|
13629
|
+
if (samples.every((value) => value === null)) {
|
|
13630
|
+
this.#fragments.delete(key);
|
|
13631
|
+
const first = this.#firstSamples.get(key);
|
|
13632
|
+
this.#firstSamples.delete(key);
|
|
13633
|
+
this.#delivered.delete(key);
|
|
13634
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
13635
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
13636
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
13637
|
+
}
|
|
13638
|
+
}
|
|
13252
13639
|
return true;
|
|
13253
13640
|
}
|
|
13254
13641
|
reset() {
|
|
13255
13642
|
this.#track = null;
|
|
13256
13643
|
this.#fragments.clear();
|
|
13644
|
+
this.#firstSamples.clear();
|
|
13645
|
+
this.#delivered.clear();
|
|
13257
13646
|
this.#pendingSamples = 0;
|
|
13258
13647
|
}
|
|
13259
13648
|
};
|
|
@@ -13423,6 +13812,9 @@ function authoredGap(before, after) {
|
|
|
13423
13812
|
function ssIdAfter(a, b) {
|
|
13424
13813
|
return (a - b | 0) > 0;
|
|
13425
13814
|
}
|
|
13815
|
+
function holdsRebuilt(stream) {
|
|
13816
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
13817
|
+
}
|
|
13426
13818
|
var FecReleaseSequencer = class {
|
|
13427
13819
|
#streams = /* @__PURE__ */ new Map();
|
|
13428
13820
|
#gate;
|
|
@@ -13485,6 +13877,7 @@ var FecReleaseSequencer = class {
|
|
|
13485
13877
|
stream.lastSbn = void 0;
|
|
13486
13878
|
stream.discontinuity = false;
|
|
13487
13879
|
stream.rapSsId = void 0;
|
|
13880
|
+
stream.sourced = false;
|
|
13488
13881
|
}
|
|
13489
13882
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
13490
13883
|
this.#recordStat("droppedStale");
|
|
@@ -13511,6 +13904,7 @@ var FecReleaseSequencer = class {
|
|
|
13511
13904
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
13512
13905
|
else hi = mid;
|
|
13513
13906
|
}
|
|
13907
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
13514
13908
|
stream.buf.splice(lo, 0, frame);
|
|
13515
13909
|
this.#drainUnlessDeferred(stream);
|
|
13516
13910
|
}
|
|
@@ -13546,6 +13940,41 @@ var FecReleaseSequencer = class {
|
|
|
13546
13940
|
}
|
|
13547
13941
|
return false;
|
|
13548
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
|
+
}
|
|
13549
13978
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
13550
13979
|
poke(streamKey) {
|
|
13551
13980
|
if (streamKey !== void 0) {
|
|
@@ -13637,16 +14066,32 @@ var FecReleaseSequencer = class {
|
|
|
13637
14066
|
last: null,
|
|
13638
14067
|
lastSbn: void 0,
|
|
13639
14068
|
discontinuity: false,
|
|
13640
|
-
rapSsId: void 0
|
|
14069
|
+
rapSsId: void 0,
|
|
14070
|
+
sourced: false,
|
|
14071
|
+
draining: 0
|
|
13641
14072
|
};
|
|
13642
14073
|
this.#streams.set(streamKey, stream);
|
|
13643
14074
|
}
|
|
13644
14075
|
return stream;
|
|
13645
14076
|
}
|
|
13646
|
-
#
|
|
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) {
|
|
13647
14087
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
13648
14088
|
const head = stream.buf[0];
|
|
13649
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
|
+
}
|
|
13650
14095
|
if (stream.discontinuity) {
|
|
13651
14096
|
stream.discontinuity = false;
|
|
13652
14097
|
stream.buf.shift();
|
|
@@ -13660,9 +14105,10 @@ var FecReleaseSequencer = class {
|
|
|
13660
14105
|
}
|
|
13661
14106
|
const headSbn = head.meta.fecSbn;
|
|
13662
14107
|
const from = stream.lastSbn ?? headSbn;
|
|
13663
|
-
const to = headSbn ?? stream.lastSbn;
|
|
14108
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
13664
14109
|
let lost = [];
|
|
13665
14110
|
const unanchored = from === void 0 && to === void 0;
|
|
14111
|
+
let unresolved = false;
|
|
13666
14112
|
if (from !== void 0 && to !== void 0) {
|
|
13667
14113
|
const resolved = this.#gate.resolvedLoss(
|
|
13668
14114
|
stream.packetId,
|
|
@@ -13671,7 +14117,8 @@ var FecReleaseSequencer = class {
|
|
|
13671
14117
|
Math.max(from, to),
|
|
13672
14118
|
from
|
|
13673
14119
|
);
|
|
13674
|
-
|
|
14120
|
+
unresolved = resolved === null;
|
|
14121
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
13675
14122
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
13676
14123
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
13677
14124
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -13679,22 +14126,56 @@ var FecReleaseSequencer = class {
|
|
|
13679
14126
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
13680
14127
|
continue;
|
|
13681
14128
|
}
|
|
13682
|
-
lost = resolved;
|
|
14129
|
+
lost = resolved ?? [];
|
|
13683
14130
|
}
|
|
13684
14131
|
stream.buf.shift();
|
|
13685
14132
|
this.#release(
|
|
13686
14133
|
stream,
|
|
13687
14134
|
head,
|
|
13688
|
-
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
|
|
13689
14136
|
);
|
|
13690
14137
|
}
|
|
13691
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
|
+
}
|
|
13692
14168
|
#release(stream, frame, decision) {
|
|
14169
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
14170
|
+
if (stale) this.#recordStat("droppedStale");
|
|
13693
14171
|
if (decision) frame.meta.gapBefore = decision;
|
|
13694
14172
|
stream.last = frame;
|
|
13695
|
-
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;
|
|
13696
14177
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
13697
|
-
this.#emit(frame.data, frame.meta);
|
|
14178
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
13698
14179
|
}
|
|
13699
14180
|
};
|
|
13700
14181
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -13735,6 +14216,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13735
14216
|
this.#reclaimedTimingSamples = 0;
|
|
13736
14217
|
this.#evictedTimingSamples = 0;
|
|
13737
14218
|
this.#rejectedMovieFragments = 0;
|
|
14219
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
14220
|
+
held: 0,
|
|
14221
|
+
expired: 0,
|
|
14222
|
+
discarded: 0,
|
|
14223
|
+
superseded: 0
|
|
14224
|
+
};
|
|
13738
14225
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
13739
14226
|
this.#fecGate = null;
|
|
13740
14227
|
this.#fecSequencer = null;
|
|
@@ -13843,6 +14330,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13843
14330
|
#reclaimedTimingSamples;
|
|
13844
14331
|
#evictedTimingSamples;
|
|
13845
14332
|
#rejectedMovieFragments;
|
|
14333
|
+
#recoveredHeadsAwaitingTiming;
|
|
13846
14334
|
#sequencers;
|
|
13847
14335
|
#fecGate;
|
|
13848
14336
|
#fecSequencer;
|
|
@@ -13948,11 +14436,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13948
14436
|
/**
|
|
13949
14437
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
13950
14438
|
*
|
|
13951
|
-
*
|
|
13952
|
-
*
|
|
13953
|
-
*
|
|
13954
|
-
*
|
|
13955
|
-
*
|
|
14439
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
14440
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
14441
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
14442
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
14443
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
14444
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
13956
14445
|
*/
|
|
13957
14446
|
get timingBudgetStats() {
|
|
13958
14447
|
return {
|
|
@@ -13963,6 +14452,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13963
14452
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
13964
14453
|
};
|
|
13965
14454
|
}
|
|
14455
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
14456
|
+
get recoveredHeadsAwaitingTiming() {
|
|
14457
|
+
let pending = 0;
|
|
14458
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14459
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
14460
|
+
}
|
|
14461
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
14462
|
+
}
|
|
13966
14463
|
get reorderStats() {
|
|
13967
14464
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
13968
14465
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -14004,6 +14501,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14004
14501
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
14005
14502
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
14006
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
|
+
}
|
|
14007
14512
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
14008
14513
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
14009
14514
|
if (!this.#fecSequencer) return;
|
|
@@ -14136,7 +14641,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14136
14641
|
const now = Date.now();
|
|
14137
14642
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14138
14643
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
14139
|
-
awaitingTiming.shift();
|
|
14644
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
14140
14645
|
}
|
|
14141
14646
|
}
|
|
14142
14647
|
}
|
|
@@ -14254,11 +14759,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14254
14759
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
14255
14760
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
14256
14761
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
14257
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
14762
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
14258
14763
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
14259
14764
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
14260
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
14261
|
-
const admitted = resolver2.addMovieFragment(
|
|
14765
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
14766
|
+
const admitted = resolver2.addMovieFragment(
|
|
14767
|
+
mpu.mpuSequenceNumber,
|
|
14768
|
+
data,
|
|
14769
|
+
remaining,
|
|
14770
|
+
options?.recovered === true
|
|
14771
|
+
);
|
|
14262
14772
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
14263
14773
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
14264
14774
|
if (!admitted) {
|
|
@@ -14278,7 +14788,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14278
14788
|
};
|
|
14279
14789
|
}
|
|
14280
14790
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
14281
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
14791
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
14282
14792
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
14283
14793
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
14284
14794
|
return this.#drop("mfu-empty-payload");
|
|
@@ -14299,6 +14809,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14299
14809
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
14300
14810
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
14301
14811
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
14812
|
+
sequenceNumber: info.sequenceNumber,
|
|
14302
14813
|
arrivedAt: Date.now()
|
|
14303
14814
|
});
|
|
14304
14815
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -14324,7 +14835,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14324
14835
|
info,
|
|
14325
14836
|
resolver,
|
|
14326
14837
|
options?.recovered === true,
|
|
14327
|
-
options?.fecSbn
|
|
14838
|
+
options?.fecSbn,
|
|
14839
|
+
options?.fecSbnFirst
|
|
14328
14840
|
);
|
|
14329
14841
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
14330
14842
|
}
|
|
@@ -14417,7 +14929,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14417
14929
|
info,
|
|
14418
14930
|
resolver,
|
|
14419
14931
|
options?.recovered === true,
|
|
14420
|
-
options?.fecSbn
|
|
14932
|
+
options?.fecSbn,
|
|
14933
|
+
options?.fecSbnFirst
|
|
14421
14934
|
);
|
|
14422
14935
|
if (outcome.record !== void 0) return outcome.record;
|
|
14423
14936
|
return this.#drop(outcome.reason);
|
|
@@ -14437,7 +14950,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14437
14950
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
14438
14951
|
* is what keeps both true.
|
|
14439
14952
|
*/
|
|
14440
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
14953
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
14441
14954
|
const timing = resolver.sampleTiming(
|
|
14442
14955
|
combined.mpuSequenceNumber,
|
|
14443
14956
|
combined.movieFragmentSequenceNumber,
|
|
@@ -14454,6 +14967,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14454
14967
|
);
|
|
14455
14968
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
14456
14969
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
14970
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
14457
14971
|
if (!resolver.consumeSample(
|
|
14458
14972
|
combined.mpuSequenceNumber,
|
|
14459
14973
|
combined.movieFragmentSequenceNumber,
|
|
@@ -14622,6 +15136,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14622
15136
|
}
|
|
14623
15137
|
this.#emitFrame(data, meta);
|
|
14624
15138
|
}
|
|
15139
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
15140
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
15141
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
15142
|
+
}
|
|
14625
15143
|
/**
|
|
14626
15144
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
14627
15145
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -14683,6 +15201,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14683
15201
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
14684
15202
|
this.#sequencers.clear();
|
|
14685
15203
|
this.#fecSequencer?.discard();
|
|
15204
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
15205
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
15206
|
+
}
|
|
14686
15207
|
this.#timingStreams.clear();
|
|
14687
15208
|
this.#pendingTimingSamples = 0;
|
|
14688
15209
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -14701,10 +15222,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14701
15222
|
return resolver;
|
|
14702
15223
|
}
|
|
14703
15224
|
#awaitTiming(streamKey, head) {
|
|
14704
|
-
const
|
|
14705
|
-
if (!
|
|
15225
|
+
const state = this.#timingStreams.get(streamKey);
|
|
15226
|
+
if (!state) return;
|
|
15227
|
+
if (!head.options?.recovered) {
|
|
15228
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
15229
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
15230
|
+
state.awaitingTiming = kept;
|
|
15231
|
+
}
|
|
15232
|
+
const awaitingTiming = state.awaitingTiming;
|
|
14706
15233
|
awaitingTiming.push(head);
|
|
14707
|
-
if (
|
|
15234
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
15235
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
15236
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
15237
|
+
}
|
|
15238
|
+
}
|
|
15239
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
15240
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
15241
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
14708
15242
|
}
|
|
14709
15243
|
/**
|
|
14710
15244
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -14732,6 +15266,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14732
15266
|
const state = this.#timingStreams.get(streamKey);
|
|
14733
15267
|
if (!state) return;
|
|
14734
15268
|
this.#timingStreams.delete(streamKey);
|
|
15269
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
14735
15270
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
14736
15271
|
if (this.#pendingTimingSamples < 0) {
|
|
14737
15272
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -17937,7 +18472,18 @@ function keyframeOverlaySsId(header) {
|
|
|
17937
18472
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
17938
18473
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
17939
18474
|
}
|
|
18475
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
18476
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
18477
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
18478
|
+
}
|
|
18479
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
18480
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
18481
|
+
return headerPsn + offset >>> 0;
|
|
18482
|
+
}
|
|
18483
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
18484
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
17940
18485
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
18486
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
17941
18487
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
17942
18488
|
function assertStreamIdentity(role, stream) {
|
|
17943
18489
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -18048,6 +18594,7 @@ var MmtpRouter = class {
|
|
|
18048
18594
|
#provenance = /* @__PURE__ */ new Map();
|
|
18049
18595
|
#onFlowProtected = null;
|
|
18050
18596
|
#unadmittedListedDrops = 0;
|
|
18597
|
+
#admittedSourceDatagrams = 0;
|
|
18051
18598
|
#unlistedRepairSetAside = 0;
|
|
18052
18599
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
18053
18600
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -18104,6 +18651,26 @@ var MmtpRouter = class {
|
|
|
18104
18651
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
18105
18652
|
*/
|
|
18106
18653
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
18654
|
+
/**
|
|
18655
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
18656
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
18657
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
18658
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
18659
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
18660
|
+
* an MPU no member of which arrived).
|
|
18661
|
+
*/
|
|
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();
|
|
18107
18674
|
/**
|
|
18108
18675
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
18109
18676
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -18173,7 +18740,9 @@ var MmtpRouter = class {
|
|
|
18173
18740
|
recovered: 0,
|
|
18174
18741
|
restarts: 0,
|
|
18175
18742
|
unsequencedDropped: 0,
|
|
18176
|
-
protectedDropped: 0
|
|
18743
|
+
protectedDropped: 0,
|
|
18744
|
+
baseFed: 0,
|
|
18745
|
+
baseUndetermined: 0
|
|
18177
18746
|
};
|
|
18178
18747
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
18179
18748
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -18346,7 +18915,7 @@ var MmtpRouter = class {
|
|
|
18346
18915
|
const cb = this.#getFrameCallback(packetId);
|
|
18347
18916
|
if (isVideo && !assembler) {
|
|
18348
18917
|
if (frameMeta.recovered) {
|
|
18349
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
18918
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
18350
18919
|
} else {
|
|
18351
18920
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
18352
18921
|
}
|
|
@@ -18379,13 +18948,18 @@ var MmtpRouter = class {
|
|
|
18379
18948
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
18380
18949
|
}
|
|
18381
18950
|
};
|
|
18382
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
18951
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
18383
18952
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
18384
18953
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
18385
18954
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
18386
18955
|
}
|
|
18387
|
-
|
|
18388
|
-
|
|
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
|
+
}
|
|
18389
18963
|
};
|
|
18390
18964
|
}
|
|
18391
18965
|
}
|
|
@@ -18430,8 +19004,9 @@ var MmtpRouter = class {
|
|
|
18430
19004
|
}
|
|
18431
19005
|
/**
|
|
18432
19006
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
18433
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
18434
|
-
*
|
|
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.
|
|
18435
19010
|
*/
|
|
18436
19011
|
get restampedRecoveredSequenceCount() {
|
|
18437
19012
|
return this.#restampedRecoveredSeqs;
|
|
@@ -18610,6 +19185,7 @@ var MmtpRouter = class {
|
|
|
18610
19185
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
18611
19186
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
18612
19187
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
19188
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
18613
19189
|
if (previous) {
|
|
18614
19190
|
previous.flush();
|
|
18615
19191
|
previous.dispose();
|
|
@@ -18736,6 +19312,10 @@ var MmtpRouter = class {
|
|
|
18736
19312
|
get keyframeOverlayStats() {
|
|
18737
19313
|
return { ...this.#overlayStats };
|
|
18738
19314
|
}
|
|
19315
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
19316
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19317
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
19318
|
+
}
|
|
18739
19319
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
18740
19320
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
18741
19321
|
}
|
|
@@ -18756,7 +19336,8 @@ var MmtpRouter = class {
|
|
|
18756
19336
|
fec,
|
|
18757
19337
|
k,
|
|
18758
19338
|
offsets: /* @__PURE__ */ new Map(),
|
|
18759
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
19339
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
19340
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
18760
19341
|
};
|
|
18761
19342
|
this.#overlays.set(repairPacketId, inst);
|
|
18762
19343
|
return () => {
|
|
@@ -18786,12 +19367,18 @@ var MmtpRouter = class {
|
|
|
18786
19367
|
*/
|
|
18787
19368
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
18788
19369
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
19370
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
19371
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
19372
|
+
stream.overlayToBase.routed.clear();
|
|
19373
|
+
stream.overlayToBase.pending.clear();
|
|
19374
|
+
}
|
|
18789
19375
|
}
|
|
18790
19376
|
#resetOverlay(inst) {
|
|
18791
19377
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
18792
19378
|
inst.last = void 0;
|
|
18793
19379
|
inst.offsets.clear();
|
|
18794
19380
|
inst.moqBlocks.clear();
|
|
19381
|
+
inst.rebuilt.clear();
|
|
18795
19382
|
}
|
|
18796
19383
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
18797
19384
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -18842,18 +19429,33 @@ var MmtpRouter = class {
|
|
|
18842
19429
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
18843
19430
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
18844
19431
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
18845
|
-
* included (7.1, 7.4.3).
|
|
18846
|
-
|
|
18847
|
-
|
|
19432
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
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).
|
|
19441
|
+
*/
|
|
19442
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
18848
19443
|
if (this.#overlays.size === 0) return;
|
|
18849
19444
|
const ssId = keyframeOverlaySsId(header);
|
|
18850
19445
|
if (ssId === void 0) return;
|
|
19446
|
+
let rebuiltPsn;
|
|
18851
19447
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
18852
19448
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
19449
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
19450
|
+
inst.rebuilt.delete(ssId);
|
|
18853
19451
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
18854
19452
|
if (identity !== null) {
|
|
18855
19453
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
18856
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
19454
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
19455
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
19456
|
+
overlaySsId: ssId,
|
|
19457
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
19458
|
+
});
|
|
18857
19459
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
18858
19460
|
}
|
|
18859
19461
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -18875,6 +19477,14 @@ var MmtpRouter = class {
|
|
|
18875
19477
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
18876
19478
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
18877
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
|
+
}
|
|
18878
19488
|
}
|
|
18879
19489
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
18880
19490
|
inst.moqBlocks.add(blockStart);
|
|
@@ -18908,11 +19518,16 @@ var MmtpRouter = class {
|
|
|
18908
19518
|
}
|
|
18909
19519
|
/**
|
|
18910
19520
|
* Route an overlay-recovered block's packets as source media of the
|
|
18911
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
18912
|
-
*
|
|
18913
|
-
*
|
|
18914
|
-
*
|
|
18915
|
-
*
|
|
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.
|
|
18916
19531
|
*/
|
|
18917
19532
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
18918
19533
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -18932,22 +19547,139 @@ var MmtpRouter = class {
|
|
|
18932
19547
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
18933
19548
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
18934
19549
|
const restored = /* @__PURE__ */ new Set();
|
|
19550
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
19551
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
18935
19552
|
const symbols = result.symbols.flatMap((sym) => {
|
|
18936
|
-
const
|
|
18937
|
-
|
|
18938
|
-
|
|
19553
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
19554
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
19555
|
+
if (anchor !== void 0) {
|
|
19556
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
18939
19557
|
restored.add(packetSequenceNumber);
|
|
18940
|
-
|
|
19558
|
+
const out = { ...sym, packetSequenceNumber };
|
|
19559
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
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
|
+
}
|
|
19567
|
+
return [out];
|
|
19568
|
+
}
|
|
19569
|
+
if (!restamped) {
|
|
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
|
+
}
|
|
19580
|
+
return [sym];
|
|
18941
19581
|
}
|
|
18942
|
-
if (!restamped) return [sym];
|
|
18943
19582
|
this.#overlayStats.unsequencedDropped++;
|
|
18944
19583
|
return [];
|
|
18945
19584
|
});
|
|
18946
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
|
+
}
|
|
18947
19597
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
18948
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
19598
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
19599
|
+
baseSsIds,
|
|
19600
|
+
firstSsIds
|
|
18949
19601
|
});
|
|
18950
19602
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
19603
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
19604
|
+
}
|
|
19605
|
+
/**
|
|
19606
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
19607
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
19608
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
19609
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
19610
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
19611
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
19612
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
19613
|
+
* A base block no repair has stated yet takes it when one does
|
|
19614
|
+
* (#feedPendingRebuilt).
|
|
19615
|
+
*
|
|
19616
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
19617
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
19618
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
19619
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
19620
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
19621
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
19622
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
19623
|
+
* block's own arrivals cannot show.
|
|
19624
|
+
*/
|
|
19625
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
19626
|
+
const fec = this.#fecManager;
|
|
19627
|
+
const owner = inst.leg.sourcePacketId;
|
|
19628
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
19629
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
19630
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
19631
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
19632
|
+
const feeds = [];
|
|
19633
|
+
for (const sym of symbols) {
|
|
19634
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
19635
|
+
const ssId = baseSsIds.get(sym.index);
|
|
19636
|
+
if (ssId === void 0) {
|
|
19637
|
+
this.#overlayStats.baseUndetermined++;
|
|
19638
|
+
continue;
|
|
19639
|
+
}
|
|
19640
|
+
routed.delete(ssId);
|
|
19641
|
+
routed.add(ssId);
|
|
19642
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
19643
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
19644
|
+
}
|
|
19645
|
+
for (const [ssId, symbolData] of feeds) {
|
|
19646
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
19647
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
19648
|
+
}
|
|
19649
|
+
}
|
|
19650
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
19651
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
19652
|
+
const fec = this.#fecManager;
|
|
19653
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
19654
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
19655
|
+
if (coords == null) {
|
|
19656
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
19657
|
+
pending.delete(ssId);
|
|
19658
|
+
pending.set(ssId, symbolData.slice());
|
|
19659
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
19660
|
+
return;
|
|
19661
|
+
}
|
|
19662
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
19663
|
+
if (!fed.taken) return;
|
|
19664
|
+
this.#overlayStats.baseFed++;
|
|
19665
|
+
if (fed.recovered === null) return;
|
|
19666
|
+
const sbn = coords.sbn;
|
|
19667
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
19668
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
19669
|
+
fec.countRecovery(owner, sbn, name);
|
|
19670
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
19671
|
+
});
|
|
19672
|
+
}
|
|
19673
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
19674
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
19675
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
19676
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
19677
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
19678
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
19679
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
19680
|
+
pending.delete(ssId);
|
|
19681
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
19682
|
+
}
|
|
18951
19683
|
}
|
|
18952
19684
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
18953
19685
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -19087,6 +19819,10 @@ var MmtpRouter = class {
|
|
|
19087
19819
|
get unadmittedListedDropCount() {
|
|
19088
19820
|
return this.#unadmittedListedDrops;
|
|
19089
19821
|
}
|
|
19822
|
+
/** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
|
|
19823
|
+
get admittedSourceDatagramCount() {
|
|
19824
|
+
return this.#admittedSourceDatagrams;
|
|
19825
|
+
}
|
|
19090
19826
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
19091
19827
|
get unlistedRepairSetAsideCount() {
|
|
19092
19828
|
return this.#unlistedRepairSetAside;
|
|
@@ -19573,6 +20309,8 @@ var MmtpRouter = class {
|
|
|
19573
20309
|
)) return;
|
|
19574
20310
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
19575
20311
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
20312
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
20313
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
19576
20314
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
19577
20315
|
const recovered = fec.feedRepairSymbol(
|
|
19578
20316
|
ownerPacketId,
|
|
@@ -19623,7 +20361,7 @@ var MmtpRouter = class {
|
|
|
19623
20361
|
catalogTrackName
|
|
19624
20362
|
)) continue;
|
|
19625
20363
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
19626
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
20364
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
19627
20365
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
19628
20366
|
}
|
|
19629
20367
|
if (payload.oversized) continue;
|
|
@@ -19710,6 +20448,8 @@ var MmtpRouter = class {
|
|
|
19710
20448
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
19711
20449
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
19712
20450
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
20451
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
20452
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
19713
20453
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
19714
20454
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
19715
20455
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -19836,6 +20576,12 @@ var MmtpRouter = class {
|
|
|
19836
20576
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
19837
20577
|
* own RAP cadence, so no timer is involved.
|
|
19838
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
|
+
*
|
|
19839
20585
|
* The commit is one synchronous transition inside the frame path: the
|
|
19840
20586
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
19841
20587
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -19886,6 +20632,9 @@ var MmtpRouter = class {
|
|
|
19886
20632
|
droppedIncomingFrames: 0
|
|
19887
20633
|
};
|
|
19888
20634
|
this.#staged = state;
|
|
20635
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
20636
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
20637
|
+
}
|
|
19889
20638
|
return {
|
|
19890
20639
|
cancel: () => {
|
|
19891
20640
|
if (this.#staged !== state) return false;
|
|
@@ -19968,7 +20717,17 @@ var MmtpRouter = class {
|
|
|
19968
20717
|
*/
|
|
19969
20718
|
#routeParsedFrame(data, meta) {
|
|
19970
20719
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
19971
|
-
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
|
+
}
|
|
19972
20731
|
this.#deliverParsedFrame(data, meta);
|
|
19973
20732
|
}
|
|
19974
20733
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -20120,12 +20879,16 @@ var MmtpRouter = class {
|
|
|
20120
20879
|
this.#parser.deferFecRelease(work);
|
|
20121
20880
|
} finally {
|
|
20122
20881
|
this.#fecPokeDepth--;
|
|
20123
|
-
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
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);
|
|
20129
20892
|
}
|
|
20130
20893
|
}
|
|
20131
20894
|
}
|
|
@@ -20203,6 +20966,14 @@ var MmtpRouter = class {
|
|
|
20203
20966
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
20204
20967
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
20205
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
|
+
},
|
|
20206
20977
|
onReleased: (data, meta) => {
|
|
20207
20978
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
20208
20979
|
const timing = meta.authoredTiming;
|
|
@@ -20344,6 +21115,7 @@ var MmtpRouter = class {
|
|
|
20344
21115
|
const parsed = this.#parser.parsePacket(packet);
|
|
20345
21116
|
if (!parsed) return;
|
|
20346
21117
|
const info = parsed.info;
|
|
21118
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
20347
21119
|
if (source === "multicast" && !manifestAdmitted) {
|
|
20348
21120
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
20349
21121
|
}
|
|
@@ -20625,7 +21397,8 @@ var MmtpRouter = class {
|
|
|
20625
21397
|
parsed.header,
|
|
20626
21398
|
info.sequenceNumber,
|
|
20627
21399
|
info.rapFlag,
|
|
20628
|
-
source
|
|
21400
|
+
source,
|
|
21401
|
+
fecPayloadId?.ssId
|
|
20629
21402
|
);
|
|
20630
21403
|
}
|
|
20631
21404
|
}
|
|
@@ -20651,7 +21424,8 @@ var MmtpRouter = class {
|
|
|
20651
21424
|
source: parserOptions?.source,
|
|
20652
21425
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
20653
21426
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
20654
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
21427
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
21428
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
20655
21429
|
};
|
|
20656
21430
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
20657
21431
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -20775,7 +21549,8 @@ var MmtpRouter = class {
|
|
|
20775
21549
|
if (stream === void 0) {
|
|
20776
21550
|
stream = {
|
|
20777
21551
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
20778
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
21552
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
21553
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
20779
21554
|
};
|
|
20780
21555
|
byName.set(catalogTrackName, stream);
|
|
20781
21556
|
}
|
|
@@ -20890,8 +21665,16 @@ var MmtpRouter = class {
|
|
|
20890
21665
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
20891
21666
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
20892
21667
|
}
|
|
20893
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
21668
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
20894
21669
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
21670
|
+
if (this.#isFecTrack(packetId)) {
|
|
21671
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
21672
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
21673
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
21674
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
21675
|
+
return;
|
|
21676
|
+
}
|
|
21677
|
+
}
|
|
20895
21678
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
20896
21679
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
20897
21680
|
this.#drainDirectRecoveredVideo(
|
|
@@ -21045,6 +21828,13 @@ var MmtpRouter = class {
|
|
|
21045
21828
|
* frames by epoch only if pre-protection input ever matters.
|
|
21046
21829
|
*/
|
|
21047
21830
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
21831
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
21832
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
21833
|
+
result = {
|
|
21834
|
+
...result,
|
|
21835
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
21836
|
+
};
|
|
21837
|
+
}
|
|
21048
21838
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
21049
21839
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
21050
21840
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -21101,7 +21891,12 @@ var MmtpRouter = class {
|
|
|
21101
21891
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
21102
21892
|
if (ownerPacketId == null) return;
|
|
21103
21893
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
21104
|
-
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
|
+
};
|
|
21105
21900
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
21106
21901
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
21107
21902
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -21109,7 +21904,10 @@ var MmtpRouter = class {
|
|
|
21109
21904
|
...result,
|
|
21110
21905
|
symbols: result.symbols.map((sym) => {
|
|
21111
21906
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
21112
|
-
if (wire === sym.packetSequenceNumber)
|
|
21907
|
+
if (wire === sym.packetSequenceNumber) {
|
|
21908
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
21909
|
+
return sym;
|
|
21910
|
+
}
|
|
21113
21911
|
this.#restampedRecoveredSeqs++;
|
|
21114
21912
|
this.#restampedStreams.add(streamKey);
|
|
21115
21913
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -21145,6 +21943,29 @@ var MmtpRouter = class {
|
|
|
21145
21943
|
}
|
|
21146
21944
|
);
|
|
21147
21945
|
}
|
|
21946
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
21947
|
+
ownerPacketId,
|
|
21948
|
+
sym.fragment.mpuSequenceNumber,
|
|
21949
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
21950
|
+
sym.fragment.sampleNumber,
|
|
21951
|
+
catalogTrackName
|
|
21952
|
+
);
|
|
21953
|
+
for (const sym of result.symbols) {
|
|
21954
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
21955
|
+
this.#feedViaParser(
|
|
21956
|
+
sym.rawSymbol,
|
|
21957
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
21958
|
+
void 0,
|
|
21959
|
+
{
|
|
21960
|
+
skipFecPayloadId: true,
|
|
21961
|
+
recovered: true,
|
|
21962
|
+
deliveryProvenance,
|
|
21963
|
+
...fecSbnOf(sym.index),
|
|
21964
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
21965
|
+
}
|
|
21966
|
+
);
|
|
21967
|
+
}
|
|
21968
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
21148
21969
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
21149
21970
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
21150
21971
|
if (assembler) {
|
|
@@ -22621,6 +23442,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
22621
23442
|
function isSymbolCounts(value) {
|
|
22622
23443
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
22623
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
|
+
}
|
|
22624
23451
|
function isNullableNonNegative(value) {
|
|
22625
23452
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
22626
23453
|
}
|
|
@@ -22640,7 +23467,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
22640
23467
|
case "recovered":
|
|
22641
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"]);
|
|
22642
23469
|
case "failed":
|
|
22643
|
-
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"]));
|
|
22644
23474
|
case "repair-preempted":
|
|
22645
23475
|
case "repair-deferred":
|
|
22646
23476
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -22906,7 +23736,7 @@ var MulticastManager = class {
|
|
|
22906
23736
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
22907
23737
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
22908
23738
|
},
|
|
22909
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23739
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
22910
23740
|
const pathCount = this.#fec.blockPathCount(
|
|
22911
23741
|
sbn,
|
|
22912
23742
|
packetId,
|
|
@@ -22924,7 +23754,8 @@ var MulticastManager = class {
|
|
|
22924
23754
|
symbols: {
|
|
22925
23755
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
22926
23756
|
moq: pathCount?.moqSymbols ?? 0
|
|
22927
|
-
}
|
|
23757
|
+
},
|
|
23758
|
+
...span === void 0 ? {} : { span }
|
|
22928
23759
|
});
|
|
22929
23760
|
},
|
|
22930
23761
|
onFrameGap: (decision) => {
|
|
@@ -24304,10 +25135,12 @@ var MulticastManager = class {
|
|
|
24304
25135
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
24305
25136
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
24306
25137
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
24307
|
-
* 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.
|
|
24308
25141
|
*/
|
|
24309
|
-
onUndelimitedLoss(cb) {
|
|
24310
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
25142
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
25143
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
24311
25144
|
}
|
|
24312
25145
|
/**
|
|
24313
25146
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -24543,7 +25376,8 @@ var MulticastManager = class {
|
|
|
24543
25376
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
24544
25377
|
...event,
|
|
24545
25378
|
block: Object.freeze({ ...event.block }),
|
|
24546
|
-
..."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 }) } : {}
|
|
24547
25381
|
}
|
|
24548
25382
|
);
|
|
24549
25383
|
if (this.#logRepairLifecycle) {
|