@blockcast/mmt-container 2.2.0-main.f480a7ede8b1 → 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/dist/browser-runtime.js +891 -141
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +67 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +283 -54
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +39 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +80 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +42 -8
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +121 -17
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +120 -18
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +121 -17
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +28 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +407 -52
- 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 +712 -115
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +2 -2
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +121 -17
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +121 -17
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +304 -28
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +114 -31
- package/src/mmtp-container-parser.ts +210 -21
- package/src/mmtp-router.ts +488 -45
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/track-session.ts +3 -0
package/dist/browser-runtime.js
CHANGED
|
@@ -851,6 +851,17 @@ function readRepairFecPayloadIdFields(mmtpPacket, parsedHeader) {
|
|
|
851
851
|
dataOffset: off + REPAIR_FEC_PID_SIZE
|
|
852
852
|
};
|
|
853
853
|
}
|
|
854
|
+
function extractRepairFecPayloadId(mmtpPacket) {
|
|
855
|
+
const fields = readRepairFecPayloadIdFields(mmtpPacket);
|
|
856
|
+
if (!fields)
|
|
857
|
+
return null;
|
|
858
|
+
return {
|
|
859
|
+
ssStart: fields.ssStart,
|
|
860
|
+
rsbLength: fields.rsbLength,
|
|
861
|
+
rsId: fields.rsId,
|
|
862
|
+
ssbLength: fields.ssbLength
|
|
863
|
+
};
|
|
864
|
+
}
|
|
854
865
|
function parseMoqRepairFrame(frame) {
|
|
855
866
|
return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame));
|
|
856
867
|
}
|
|
@@ -8625,9 +8636,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
|
8625
8636
|
} catch (error) {
|
|
8626
8637
|
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8627
8638
|
}
|
|
8628
|
-
if (
|
|
8639
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8629
8640
|
await rx.close();
|
|
8630
|
-
throw unsupported(`integrity scheme ${String(
|
|
8641
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8631
8642
|
}
|
|
8632
8643
|
const events = new EventTarget();
|
|
8633
8644
|
let state = "connected";
|
|
@@ -12435,6 +12446,7 @@ var TrackSession = class {
|
|
|
12435
12446
|
this.#state = "idle";
|
|
12436
12447
|
this.#subscription = null;
|
|
12437
12448
|
this.#disarmFec();
|
|
12449
|
+
this.#unsuppress();
|
|
12438
12450
|
}
|
|
12439
12451
|
// -----------------------------------------------------------------------
|
|
12440
12452
|
// Internal
|
|
@@ -12928,6 +12940,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12928
12940
|
this.#heldEvicted = 0;
|
|
12929
12941
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
12930
12942
|
this.#holes = [];
|
|
12943
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
12944
|
+
this.#expired = [];
|
|
12945
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
12946
|
+
this.#newlyExpired = [];
|
|
12947
|
+
/**
|
|
12948
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
12949
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
12950
|
+
*/
|
|
12951
|
+
this.#closedBehind = [];
|
|
12931
12952
|
/**
|
|
12932
12953
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
12933
12954
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -12951,6 +12972,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12951
12972
|
#newest;
|
|
12952
12973
|
#newestSource;
|
|
12953
12974
|
#holes;
|
|
12975
|
+
#expired;
|
|
12976
|
+
#newlyExpired;
|
|
12977
|
+
#closedBehind;
|
|
12954
12978
|
static #checkedRetention(retentionBlocks) {
|
|
12955
12979
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
12956
12980
|
throw new Error(
|
|
@@ -13020,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13020
13044
|
}
|
|
13021
13045
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
13022
13046
|
locate(ssId) {
|
|
13023
|
-
for (
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13047
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
13048
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
13049
|
+
const block = blocks[i];
|
|
13050
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
13051
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
13052
|
+
}
|
|
13027
13053
|
}
|
|
13028
13054
|
}
|
|
13029
13055
|
return null;
|
|
13030
13056
|
}
|
|
13031
|
-
/** Number of pending
|
|
13057
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
13032
13058
|
get holeCount() {
|
|
13033
|
-
return this.#holes.length;
|
|
13059
|
+
return this.#holes.length + this.#expired.length;
|
|
13060
|
+
}
|
|
13061
|
+
/**
|
|
13062
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
13063
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
13064
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
13065
|
+
* release gate does not wait for it. Its owner must close it
|
|
13066
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
13067
|
+
*/
|
|
13068
|
+
takeExpiredHoles() {
|
|
13069
|
+
return this.#newlyExpired.splice(0);
|
|
13034
13070
|
}
|
|
13035
13071
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
13036
13072
|
blockEndingAt(end) {
|
|
@@ -13086,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13086
13122
|
/**
|
|
13087
13123
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
13088
13124
|
* block states its range and places every held source in it; later repair
|
|
13089
|
-
* symbols of the same block change nothing. A range
|
|
13090
|
-
*
|
|
13091
|
-
*
|
|
13125
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
13126
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
13127
|
+
* a known block (a different length, or an overlap with another block or
|
|
13128
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
13129
|
+
* block changes.
|
|
13092
13130
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
13093
13131
|
* A block so far past `newest` that stating it would retire the position
|
|
13094
13132
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -13107,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13107
13145
|
const known = this.blockAt(start);
|
|
13108
13146
|
if (known !== void 0) {
|
|
13109
13147
|
if (known.span) {
|
|
13110
|
-
|
|
13148
|
+
if (known.length === length) {
|
|
13149
|
+
const block2 = { start, length, span: false };
|
|
13150
|
+
this.#blocks[this.#blocks.indexOf(known)] = block2;
|
|
13151
|
+
return { status: "placed", block: block2, placed: [] };
|
|
13152
|
+
}
|
|
13153
|
+
return length < known.length ? {
|
|
13154
|
+
status: "overlap",
|
|
13155
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
13156
|
+
ofSpan: known
|
|
13157
|
+
} : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
|
|
13111
13158
|
}
|
|
13112
13159
|
if (known.length !== length) {
|
|
13113
13160
|
return {
|
|
@@ -13175,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13175
13222
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
13176
13223
|
}
|
|
13177
13224
|
/**
|
|
13178
|
-
* Turn every
|
|
13179
|
-
*
|
|
13180
|
-
*
|
|
13225
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
13226
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
13227
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
13228
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
13229
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
13230
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
13181
13231
|
*/
|
|
13182
13232
|
closeHolesWithin(start, length) {
|
|
13183
13233
|
const closed = [];
|
|
13184
|
-
for (const
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13234
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
13235
|
+
for (const hole of [...holes]) {
|
|
13236
|
+
if (!isoBlockContains(start, length, hole.start)) continue;
|
|
13237
|
+
holes.splice(holes.indexOf(hole), 1);
|
|
13238
|
+
const spanBlock = { start: hole.start, length: hole.length, span: true };
|
|
13239
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
|
|
13240
|
+
if (holes === this.#holes) {
|
|
13241
|
+
this.#blocks.push(spanBlock);
|
|
13242
|
+
} else {
|
|
13243
|
+
this.#closedBehind.push(spanBlock);
|
|
13244
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
|
|
13245
|
+
}
|
|
13246
|
+
}
|
|
13190
13247
|
}
|
|
13191
13248
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
13192
13249
|
}
|
|
@@ -13200,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13200
13257
|
return void 0;
|
|
13201
13258
|
}
|
|
13202
13259
|
/**
|
|
13203
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
13204
|
-
* lies more than `retentionBlocks` blocks behind
|
|
13205
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13206
|
-
* horizon uses
|
|
13260
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
13261
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
13262
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13263
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
13264
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
13207
13265
|
*/
|
|
13208
13266
|
retireBehind(position) {
|
|
13209
13267
|
this.observe(position);
|
|
13210
13268
|
const horizon = this.#retentionBlocks;
|
|
13211
13269
|
const retired = [];
|
|
13270
|
+
this.#closedBehind.length = 0;
|
|
13212
13271
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
13213
13272
|
const block = this.#blocks[i];
|
|
13214
13273
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -13220,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13220
13279
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
13221
13280
|
const hole = this.#holes[i];
|
|
13222
13281
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
13223
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
13282
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
|
|
13283
|
+
this.#holes.splice(i, 1);
|
|
13284
|
+
this.#expired.push(hole);
|
|
13285
|
+
this.#newlyExpired.push(hole);
|
|
13224
13286
|
}
|
|
13225
13287
|
for (const ssId of [...this.#held.keys()]) {
|
|
13226
|
-
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#
|
|
13288
|
+
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon && !this.#holes.some((hole) => isoBlockContains(hole.start, hole.length, ssId)) && !this.#expired.some((hole) => isoBlockContains(hole.start, hole.length, ssId))) {
|
|
13289
|
+
this.#held.delete(ssId);
|
|
13290
|
+
}
|
|
13227
13291
|
}
|
|
13228
13292
|
return retired;
|
|
13229
13293
|
}
|
|
@@ -13231,6 +13295,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13231
13295
|
reset() {
|
|
13232
13296
|
this.#blocks.length = 0;
|
|
13233
13297
|
this.#holes.length = 0;
|
|
13298
|
+
this.#expired.length = 0;
|
|
13299
|
+
this.#newlyExpired.length = 0;
|
|
13300
|
+
this.#closedBehind.length = 0;
|
|
13234
13301
|
this.#held.clear();
|
|
13235
13302
|
this.#coverageEnd = void 0;
|
|
13236
13303
|
this.#newest = void 0;
|
|
@@ -13854,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13854
13921
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13855
13922
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13856
13923
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13924
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13857
13925
|
function isMulticastSymbolDelivery(value) {
|
|
13858
13926
|
return value === "datagram" || value === "ordered-relay";
|
|
13859
13927
|
}
|
|
@@ -13947,10 +14015,18 @@ var FECManager = class _FECManager {
|
|
|
13947
14015
|
// 'pending' forever, which would park every later frame of its stream in
|
|
13948
14016
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
13949
14017
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
14018
|
+
/**
|
|
14019
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
14020
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
14021
|
+
*/
|
|
13950
14022
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
13951
14023
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13952
14024
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13953
14025
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
14026
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
14027
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
14028
|
+
// not network arrivals and never make a block clean.
|
|
14029
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13954
14030
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13955
14031
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13956
14032
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -13959,6 +14035,9 @@ var FECManager = class _FECManager {
|
|
|
13959
14035
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
13960
14036
|
// symbols; waiting for K fails every one of them.
|
|
13961
14037
|
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
14038
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
14039
|
+
// a repair states the block there or the span resolves.
|
|
14040
|
+
this.#isoSpanKeys = /* @__PURE__ */ new Set();
|
|
13962
14041
|
this.#ssbLengthConflictCount = 0;
|
|
13963
14042
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
13964
14043
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -14156,7 +14235,10 @@ var FECManager = class _FECManager {
|
|
|
14156
14235
|
// arrives via onBlockRecovered callback.
|
|
14157
14236
|
this.onBlockStarted = null;
|
|
14158
14237
|
this.onBlockComplete = null;
|
|
14159
|
-
/**
|
|
14238
|
+
/**
|
|
14239
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
14240
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
14241
|
+
*/
|
|
14160
14242
|
this.onBlockFailed = null;
|
|
14161
14243
|
/**
|
|
14162
14244
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -14198,6 +14280,8 @@ var FECManager = class _FECManager {
|
|
|
14198
14280
|
* for it, which it must be.
|
|
14199
14281
|
*/
|
|
14200
14282
|
this.#onUndelimitedLoss = null;
|
|
14283
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
14284
|
+
this.#onIsoHoleExpired = null;
|
|
14201
14285
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
14202
14286
|
this.onBlockVanished = null;
|
|
14203
14287
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -14240,7 +14324,9 @@ var FECManager = class _FECManager {
|
|
|
14240
14324
|
#pendingWorkerCleanupBounds;
|
|
14241
14325
|
#blockSourceCounts;
|
|
14242
14326
|
#blockSourceEsis;
|
|
14327
|
+
#blockRebuiltEsis;
|
|
14243
14328
|
#blockAuthoredLengths;
|
|
14329
|
+
#isoSpanKeys;
|
|
14244
14330
|
#ssbLengthConflictCount;
|
|
14245
14331
|
#blockRepairRsIds;
|
|
14246
14332
|
#blockRsbLengths;
|
|
@@ -14307,6 +14393,7 @@ var FECManager = class _FECManager {
|
|
|
14307
14393
|
#decoderUnavailableEvents;
|
|
14308
14394
|
#repairSymbolSizeMismatchCount;
|
|
14309
14395
|
#onUndelimitedLoss;
|
|
14396
|
+
#onIsoHoleExpired;
|
|
14310
14397
|
#encodeTrackName(catalogTrackName) {
|
|
14311
14398
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
14312
14399
|
if (encoded === void 0) {
|
|
@@ -14778,7 +14865,10 @@ var FECManager = class _FECManager {
|
|
|
14778
14865
|
timer.handle = null;
|
|
14779
14866
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
14780
14867
|
};
|
|
14781
|
-
timer.handle = setTimeout(
|
|
14868
|
+
timer.handle = setTimeout(
|
|
14869
|
+
onFire,
|
|
14870
|
+
this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
|
|
14871
|
+
);
|
|
14782
14872
|
this.#repairDeadlines.set(key, timer);
|
|
14783
14873
|
}
|
|
14784
14874
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -14922,8 +15012,14 @@ var FECManager = class _FECManager {
|
|
|
14922
15012
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
14923
15013
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
14924
15014
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
15015
|
+
*
|
|
15016
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
15017
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
15018
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
15019
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
15020
|
+
* it (BLO-52085).
|
|
14925
15021
|
*/
|
|
14926
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
15022
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
14927
15023
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
14928
15024
|
const now = performance.now();
|
|
14929
15025
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -14962,7 +15058,7 @@ var FECManager = class _FECManager {
|
|
|
14962
15058
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
14963
15059
|
};
|
|
14964
15060
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
14965
|
-
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
15061
|
+
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
14966
15062
|
discontinuity();
|
|
14967
15063
|
return;
|
|
14968
15064
|
}
|
|
@@ -15224,17 +15320,21 @@ var FECManager = class _FECManager {
|
|
|
15224
15320
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
15225
15321
|
this.#repairHoldUntilMs.delete(key);
|
|
15226
15322
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
15227
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
15323
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
15228
15324
|
};
|
|
15229
|
-
timer.handle = setTimeout(
|
|
15325
|
+
timer.handle = setTimeout(
|
|
15326
|
+
onFire,
|
|
15327
|
+
this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
|
|
15328
|
+
);
|
|
15230
15329
|
this.#activeBlocks.set(key, timer);
|
|
15231
15330
|
this.#indexBlockKey(key, sbn, packetId);
|
|
15232
15331
|
}
|
|
15233
15332
|
/**
|
|
15234
15333
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
15235
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
15334
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
15335
|
+
* is when it was due: its deadline, or now.
|
|
15236
15336
|
*/
|
|
15237
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
15337
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
15238
15338
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
15239
15339
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
15240
15340
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -15250,7 +15350,7 @@ var FECManager = class _FECManager {
|
|
|
15250
15350
|
if (workerClient) {
|
|
15251
15351
|
this.#pendingWorkerCleanups.add(key);
|
|
15252
15352
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
15253
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
15353
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
15254
15354
|
workerClient.cleanup(sbn);
|
|
15255
15355
|
} else {
|
|
15256
15356
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -15562,7 +15662,9 @@ var FECManager = class _FECManager {
|
|
|
15562
15662
|
}
|
|
15563
15663
|
resetMap(this.#blockSourceCounts);
|
|
15564
15664
|
resetMap(this.#blockSourceEsis);
|
|
15665
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15565
15666
|
resetMap(this.#blockAuthoredLengths);
|
|
15667
|
+
resetSet(this.#isoSpanKeys);
|
|
15566
15668
|
resetMap(this.#blockRepairRsIds);
|
|
15567
15669
|
resetMap(this.#blockRsbLengths);
|
|
15568
15670
|
resetMap(this.#heldLayerStates);
|
|
@@ -15686,7 +15788,9 @@ var FECManager = class _FECManager {
|
|
|
15686
15788
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15687
15789
|
dropMap(this.#blockSourceCounts);
|
|
15688
15790
|
dropMap(this.#blockSourceEsis);
|
|
15791
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15689
15792
|
dropMap(this.#blockAuthoredLengths);
|
|
15793
|
+
dropSet(this.#isoSpanKeys);
|
|
15690
15794
|
dropMap(this.#blockRepairRsIds);
|
|
15691
15795
|
dropMap(this.#blockRsbLengths);
|
|
15692
15796
|
dropMap(this.#blockPathCounts);
|
|
@@ -16029,8 +16133,9 @@ var FECManager = class _FECManager {
|
|
|
16029
16133
|
return true;
|
|
16030
16134
|
}
|
|
16031
16135
|
/**
|
|
16032
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
16033
|
-
*
|
|
16136
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
16137
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
16138
|
+
* multicast wait, which may already have passed.
|
|
16034
16139
|
*/
|
|
16035
16140
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
16036
16141
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -16168,6 +16273,7 @@ var FECManager = class _FECManager {
|
|
|
16168
16273
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16169
16274
|
const evictedBefore = locator.heldEvicted;
|
|
16170
16275
|
const held = locator.hold(ssId, source);
|
|
16276
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16171
16277
|
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16172
16278
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16173
16279
|
if (evicted > 0) {
|
|
@@ -16189,17 +16295,20 @@ var FECManager = class _FECManager {
|
|
|
16189
16295
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
16190
16296
|
*/
|
|
16191
16297
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
16192
|
-
const
|
|
16298
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16299
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
16300
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16193
16301
|
if (placement.status !== "placed") {
|
|
16194
16302
|
this.#isoRepairRefusedCount++;
|
|
16195
16303
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
16196
16304
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
16197
16305
|
}
|
|
16306
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
16198
16307
|
return placement;
|
|
16199
16308
|
}
|
|
16309
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
16200
16310
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
16201
16311
|
if (placement.hole !== void 0) {
|
|
16202
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
16203
16312
|
const hole = placement.hole.block;
|
|
16204
16313
|
const previous = locator.blockEndingAt(hole.start);
|
|
16205
16314
|
const loss = {
|
|
@@ -16220,17 +16329,29 @@ var FECManager = class _FECManager {
|
|
|
16220
16329
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
16221
16330
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
16222
16331
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
16223
|
-
* nobody to close it.
|
|
16332
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
16333
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
16334
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
16335
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
16224
16336
|
*/
|
|
16225
|
-
setUndelimitedLossHandler(handler) {
|
|
16337
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
16226
16338
|
this.#onUndelimitedLoss = handler;
|
|
16339
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
16227
16340
|
this.#deferIsoGaps = handler !== null;
|
|
16228
16341
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
16229
16342
|
}
|
|
16343
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
16344
|
+
#settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
|
|
16345
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
16346
|
+
const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
|
|
16347
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
|
|
16348
|
+
}
|
|
16349
|
+
}
|
|
16230
16350
|
/**
|
|
16231
|
-
* The range repair for a hole ended: every hole
|
|
16232
|
-
*
|
|
16233
|
-
* with the held sources they placed, for the caller to feed
|
|
16351
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
16352
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
16353
|
+
* spans with the held sources they placed, for the caller to feed
|
|
16354
|
+
* (MmtpRouter).
|
|
16234
16355
|
*/
|
|
16235
16356
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
16236
16357
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -16249,7 +16370,10 @@ var FECManager = class _FECManager {
|
|
|
16249
16370
|
*/
|
|
16250
16371
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
16251
16372
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
16252
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
16373
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
16374
|
+
this.#blockAuthoredLengths.set(key, span.block.length);
|
|
16375
|
+
this.#isoSpanKeys.add(key);
|
|
16376
|
+
}
|
|
16253
16377
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
16254
16378
|
}
|
|
16255
16379
|
/**
|
|
@@ -16590,11 +16714,14 @@ var FECManager = class _FECManager {
|
|
|
16590
16714
|
});
|
|
16591
16715
|
client.onCleanupComplete((sbn) => {
|
|
16592
16716
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
16593
|
-
const
|
|
16717
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
16718
|
+
const cleanupKeys = owed.filter((key) => {
|
|
16594
16719
|
const block = this.#parseBlockKey(key);
|
|
16595
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
16720
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
16596
16721
|
});
|
|
16597
|
-
|
|
16722
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
|
|
16723
|
+
const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
|
|
16724
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
16598
16725
|
});
|
|
16599
16726
|
client.onError((msg, code) => {
|
|
16600
16727
|
const requestedKind = code ?? msg;
|
|
@@ -16664,9 +16791,11 @@ var FECManager = class _FECManager {
|
|
|
16664
16791
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
16665
16792
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
16666
16793
|
*/
|
|
16667
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
16794
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
16668
16795
|
this.#clearOwedCleanupBound(key);
|
|
16669
16796
|
const bound = {
|
|
16797
|
+
dueAtMs,
|
|
16798
|
+
askedAtMs: performance.now(),
|
|
16670
16799
|
handle: setTimeout(() => {
|
|
16671
16800
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
16672
16801
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -16764,7 +16893,15 @@ var FECManager = class _FECManager {
|
|
|
16764
16893
|
this.#isoSourceOversizeCount++;
|
|
16765
16894
|
return null;
|
|
16766
16895
|
}
|
|
16767
|
-
|
|
16896
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16897
|
+
}
|
|
16898
|
+
/**
|
|
16899
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16900
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16901
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16902
|
+
* stated for `ssId`.
|
|
16903
|
+
*/
|
|
16904
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16768
16905
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16769
16906
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16770
16907
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16793,6 +16930,74 @@ var FECManager = class _FECManager {
|
|
|
16793
16930
|
}
|
|
16794
16931
|
return recovered;
|
|
16795
16932
|
}
|
|
16933
|
+
/**
|
|
16934
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16935
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16936
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16937
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16938
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16939
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16940
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16941
|
+
*
|
|
16942
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16943
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16944
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16945
|
+
* feedSourceSymbol takes it.
|
|
16946
|
+
*
|
|
16947
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16948
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16949
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16950
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16951
|
+
* block as clean on its own side), the block is completed here as
|
|
16952
|
+
* recovered, with nothing left to route.
|
|
16953
|
+
*/
|
|
16954
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16955
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16956
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16957
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16958
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16959
|
+
const sbn = located.block.start;
|
|
16960
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16961
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16962
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16963
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16964
|
+
if (rebuilt === void 0) {
|
|
16965
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16966
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16967
|
+
}
|
|
16968
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16969
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16970
|
+
rebuilt.add(located.esi);
|
|
16971
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16972
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
16973
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
16974
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16977
|
+
return { taken: true, recovered };
|
|
16978
|
+
}
|
|
16979
|
+
/**
|
|
16980
|
+
* A block that holds every one of its `length` sources, received or
|
|
16981
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16982
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16983
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16984
|
+
*/
|
|
16985
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16986
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16987
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16988
|
+
let present = rebuilt.size;
|
|
16989
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16990
|
+
if (present < length) return false;
|
|
16991
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16992
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16993
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16994
|
+
if (catalogTrackName === void 0) {
|
|
16995
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16996
|
+
} else {
|
|
16997
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16998
|
+
}
|
|
16999
|
+
return true;
|
|
17000
|
+
}
|
|
16796
17001
|
/**
|
|
16797
17002
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16798
17003
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -17035,7 +17240,10 @@ var FECManager = class _FECManager {
|
|
|
17035
17240
|
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17036
17241
|
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17037
17242
|
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17038
|
-
|
|
17243
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
17244
|
+
const received = this.#blockSourceEsis.get(key);
|
|
17245
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
17246
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
17039
17247
|
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17040
17248
|
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17041
17249
|
return void 0;
|
|
@@ -17158,12 +17366,13 @@ var FECManager = class _FECManager {
|
|
|
17158
17366
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
17159
17367
|
* classification evidence, and marks the block clean when all of its
|
|
17160
17368
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
17161
|
-
* otherwise K.
|
|
17369
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
17370
|
+
* placed only now (see #noteSbnSeen).
|
|
17162
17371
|
*
|
|
17163
17372
|
* Throws for a packetId that was never configured, so a classified block
|
|
17164
17373
|
* never reads the manager-global K or timeout.
|
|
17165
17374
|
*/
|
|
17166
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
17375
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
17167
17376
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
17168
17377
|
if (trackConfig === void 0) {
|
|
17169
17378
|
throw new Error(
|
|
@@ -17179,7 +17388,8 @@ var FECManager = class _FECManager {
|
|
|
17179
17388
|
catalogTrackName,
|
|
17180
17389
|
"source",
|
|
17181
17390
|
evidence.packetSequenceNumber,
|
|
17182
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
17391
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
17392
|
+
held
|
|
17183
17393
|
);
|
|
17184
17394
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
17185
17395
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -17215,6 +17425,15 @@ var FECManager = class _FECManager {
|
|
|
17215
17425
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17216
17426
|
return;
|
|
17217
17427
|
}
|
|
17428
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17429
|
+
key,
|
|
17430
|
+
sbn,
|
|
17431
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17432
|
+
packetId,
|
|
17433
|
+
catalogTrackName
|
|
17434
|
+
)) {
|
|
17435
|
+
return;
|
|
17436
|
+
}
|
|
17218
17437
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17219
17438
|
}
|
|
17220
17439
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17223,7 +17442,9 @@ var FECManager = class _FECManager {
|
|
|
17223
17442
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17224
17443
|
this.#blockSourceCounts.delete(key);
|
|
17225
17444
|
this.#blockSourceEsis.delete(key);
|
|
17445
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17226
17446
|
this.#blockAuthoredLengths.delete(key);
|
|
17447
|
+
this.#isoSpanKeys.delete(key);
|
|
17227
17448
|
this.#blockRepairRsIds.delete(key);
|
|
17228
17449
|
this.#blockRsbLengths.delete(key);
|
|
17229
17450
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -17254,6 +17475,19 @@ var FECManager = class _FECManager {
|
|
|
17254
17475
|
#receiptNow() {
|
|
17255
17476
|
return this.#receiptAtMs ?? performance.now();
|
|
17256
17477
|
}
|
|
17478
|
+
/**
|
|
17479
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
17480
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
17481
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
17482
|
+
* that timer would have, never later than a delay counted from now, and
|
|
17483
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
17484
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
17485
|
+
* from another fraction of a ms (BLO-52085).
|
|
17486
|
+
*/
|
|
17487
|
+
#receiptTimerDelayMs(deadlineMs, minMs) {
|
|
17488
|
+
const receivedAtMs = this.#receiptAtMs;
|
|
17489
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
|
|
17490
|
+
}
|
|
17257
17491
|
/**
|
|
17258
17492
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
17259
17493
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -17302,6 +17536,7 @@ var FECManager = class _FECManager {
|
|
|
17302
17536
|
}
|
|
17303
17537
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17304
17538
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
17539
|
+
const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
|
|
17305
17540
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
17306
17541
|
this.#clearRepairDeadline(key);
|
|
17307
17542
|
this.#clearBlockTimer(key);
|
|
@@ -17311,6 +17546,7 @@ var FECManager = class _FECManager {
|
|
|
17311
17546
|
}
|
|
17312
17547
|
this.#blockSourceCounts.delete(key);
|
|
17313
17548
|
this.#blockSourceEsis.delete(key);
|
|
17549
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17314
17550
|
this.#blockAuthoredLengths.delete(key);
|
|
17315
17551
|
this.#blockRepairRsIds.delete(key);
|
|
17316
17552
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17320,7 +17556,8 @@ var FECManager = class _FECManager {
|
|
|
17320
17556
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
17321
17557
|
this.#fecStats.blocksFailed++;
|
|
17322
17558
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
17323
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17559
|
+
if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17560
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
17324
17561
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
17325
17562
|
}
|
|
17326
17563
|
/**
|
|
@@ -17349,7 +17586,9 @@ var FECManager = class _FECManager {
|
|
|
17349
17586
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17350
17587
|
this.#blockSourceCounts.delete(key);
|
|
17351
17588
|
this.#blockSourceEsis.delete(key);
|
|
17589
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17352
17590
|
this.#blockAuthoredLengths.delete(key);
|
|
17591
|
+
this.#isoSpanKeys.delete(key);
|
|
17353
17592
|
this.#blockRepairRsIds.delete(key);
|
|
17354
17593
|
this.#blockRsbLengths.delete(key);
|
|
17355
17594
|
this.#clearBlockTimer(key);
|
|
@@ -17440,7 +17679,9 @@ var FECManager = class _FECManager {
|
|
|
17440
17679
|
this.#blockKeysBySbn.clear();
|
|
17441
17680
|
this.#blockSourceCounts.clear();
|
|
17442
17681
|
this.#blockSourceEsis.clear();
|
|
17682
|
+
this.#blockRebuiltEsis.clear();
|
|
17443
17683
|
this.#blockAuthoredLengths.clear();
|
|
17684
|
+
this.#isoSpanKeys.clear();
|
|
17444
17685
|
this.#blockRepairRsIds.clear();
|
|
17445
17686
|
this.#blockRsbLengths.clear();
|
|
17446
17687
|
this.#heldLayerStates.clear();
|
|
@@ -18342,6 +18583,9 @@ function authoredGap(before, after) {
|
|
|
18342
18583
|
function ssIdAfter(a, b) {
|
|
18343
18584
|
return (a - b | 0) > 0;
|
|
18344
18585
|
}
|
|
18586
|
+
function holdsRebuilt(stream) {
|
|
18587
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18588
|
+
}
|
|
18345
18589
|
var FecReleaseSequencer = class {
|
|
18346
18590
|
#streams = /* @__PURE__ */ new Map();
|
|
18347
18591
|
#gate;
|
|
@@ -18404,6 +18648,7 @@ var FecReleaseSequencer = class {
|
|
|
18404
18648
|
stream.lastSbn = void 0;
|
|
18405
18649
|
stream.discontinuity = false;
|
|
18406
18650
|
stream.rapSsId = void 0;
|
|
18651
|
+
stream.sourced = false;
|
|
18407
18652
|
}
|
|
18408
18653
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
18409
18654
|
this.#recordStat("droppedStale");
|
|
@@ -18430,6 +18675,7 @@ var FecReleaseSequencer = class {
|
|
|
18430
18675
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18431
18676
|
else hi = mid;
|
|
18432
18677
|
}
|
|
18678
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18433
18679
|
stream.buf.splice(lo, 0, frame);
|
|
18434
18680
|
this.#drainUnlessDeferred(stream);
|
|
18435
18681
|
}
|
|
@@ -18465,6 +18711,41 @@ var FecReleaseSequencer = class {
|
|
|
18465
18711
|
}
|
|
18466
18712
|
return false;
|
|
18467
18713
|
}
|
|
18714
|
+
/**
|
|
18715
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
18716
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
18717
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
18718
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
18719
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
18720
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
18721
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
18722
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
18723
|
+
* released.
|
|
18724
|
+
*/
|
|
18725
|
+
unsource(packetId, catalogTrackName) {
|
|
18726
|
+
for (const stream of this.#streams.values()) {
|
|
18727
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
18728
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
18729
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
18730
|
+
else this.#drain(stream, true);
|
|
18731
|
+
}
|
|
18732
|
+
stream.sourced = false;
|
|
18733
|
+
}
|
|
18734
|
+
}
|
|
18735
|
+
}
|
|
18736
|
+
/**
|
|
18737
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
18738
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
18739
|
+
* packetId.
|
|
18740
|
+
*/
|
|
18741
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
18742
|
+
for (const stream of this.#streams.values()) {
|
|
18743
|
+
if (stream.packetId !== packetId) continue;
|
|
18744
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
18745
|
+
if (holdsRebuilt(stream)) return true;
|
|
18746
|
+
}
|
|
18747
|
+
return false;
|
|
18748
|
+
}
|
|
18468
18749
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18469
18750
|
poke(streamKey) {
|
|
18470
18751
|
if (streamKey !== void 0) {
|
|
@@ -18556,16 +18837,32 @@ var FecReleaseSequencer = class {
|
|
|
18556
18837
|
last: null,
|
|
18557
18838
|
lastSbn: void 0,
|
|
18558
18839
|
discontinuity: false,
|
|
18559
|
-
rapSsId: void 0
|
|
18840
|
+
rapSsId: void 0,
|
|
18841
|
+
sourced: false,
|
|
18842
|
+
draining: 0
|
|
18560
18843
|
};
|
|
18561
18844
|
this.#streams.set(streamKey, stream);
|
|
18562
18845
|
}
|
|
18563
18846
|
return stream;
|
|
18564
18847
|
}
|
|
18565
|
-
#
|
|
18848
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
18849
|
+
#drain(stream, asUnanchored = false) {
|
|
18850
|
+
stream.draining++;
|
|
18851
|
+
try {
|
|
18852
|
+
this.#drainLoop(stream, asUnanchored);
|
|
18853
|
+
} finally {
|
|
18854
|
+
stream.draining--;
|
|
18855
|
+
}
|
|
18856
|
+
}
|
|
18857
|
+
#drainLoop(stream, asUnanchored) {
|
|
18566
18858
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18567
18859
|
const head = stream.buf[0];
|
|
18568
18860
|
const last = stream.last;
|
|
18861
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
18862
|
+
stream.buf.shift();
|
|
18863
|
+
this.#release(stream, head, void 0);
|
|
18864
|
+
continue;
|
|
18865
|
+
}
|
|
18569
18866
|
if (stream.discontinuity) {
|
|
18570
18867
|
stream.discontinuity = false;
|
|
18571
18868
|
stream.buf.shift();
|
|
@@ -18579,9 +18876,10 @@ var FecReleaseSequencer = class {
|
|
|
18579
18876
|
}
|
|
18580
18877
|
const headSbn = head.meta.fecSbn;
|
|
18581
18878
|
const from = stream.lastSbn ?? headSbn;
|
|
18582
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18879
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18583
18880
|
let lost = [];
|
|
18584
18881
|
const unanchored = from === void 0 && to === void 0;
|
|
18882
|
+
let unresolved = false;
|
|
18585
18883
|
if (from !== void 0 && to !== void 0) {
|
|
18586
18884
|
const resolved = this.#gate.resolvedLoss(
|
|
18587
18885
|
stream.packetId,
|
|
@@ -18590,7 +18888,8 @@ var FecReleaseSequencer = class {
|
|
|
18590
18888
|
Math.max(from, to),
|
|
18591
18889
|
from
|
|
18592
18890
|
);
|
|
18593
|
-
|
|
18891
|
+
unresolved = resolved === null;
|
|
18892
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18594
18893
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18595
18894
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18596
18895
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18598,22 +18897,56 @@ var FecReleaseSequencer = class {
|
|
|
18598
18897
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18599
18898
|
continue;
|
|
18600
18899
|
}
|
|
18601
|
-
lost = resolved;
|
|
18900
|
+
lost = resolved ?? [];
|
|
18602
18901
|
}
|
|
18603
18902
|
stream.buf.shift();
|
|
18604
18903
|
this.#release(
|
|
18605
18904
|
stream,
|
|
18606
18905
|
head,
|
|
18607
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18906
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18608
18907
|
);
|
|
18609
18908
|
}
|
|
18610
18909
|
}
|
|
18910
|
+
/**
|
|
18911
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
18912
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
18913
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
18914
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
18915
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
18916
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
18917
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
18918
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
18919
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
18920
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
18921
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
18922
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
18923
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
18924
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
18925
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
18926
|
+
*/
|
|
18927
|
+
#cursorPassed(stream, asUnanchored) {
|
|
18928
|
+
let at2 = -1;
|
|
18929
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
18930
|
+
const frame = stream.buf[i];
|
|
18931
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
18932
|
+
frame.passed = true;
|
|
18933
|
+
if (at2 < 0) at2 = i;
|
|
18934
|
+
} else if (frame.passed !== true) break;
|
|
18935
|
+
}
|
|
18936
|
+
if (at2 < 0) return false;
|
|
18937
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
18938
|
+
}
|
|
18611
18939
|
#release(stream, frame, decision) {
|
|
18940
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18941
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18612
18942
|
if (decision) frame.meta.gapBefore = decision;
|
|
18613
18943
|
stream.last = frame;
|
|
18614
|
-
if (frame.meta.fecSbn !== void 0
|
|
18944
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
18945
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
18946
|
+
}
|
|
18947
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
18615
18948
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18616
|
-
this.#emit(frame.data, frame.meta);
|
|
18949
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18617
18950
|
}
|
|
18618
18951
|
};
|
|
18619
18952
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -18874,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18874
19207
|
/**
|
|
18875
19208
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18876
19209
|
*
|
|
18877
|
-
*
|
|
18878
|
-
*
|
|
18879
|
-
*
|
|
18880
|
-
*
|
|
18881
|
-
*
|
|
19210
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
19211
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
19212
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
19213
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
19214
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
19215
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
18882
19216
|
*/
|
|
18883
19217
|
get timingBudgetStats() {
|
|
18884
19218
|
return {
|
|
@@ -18938,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18938
19272
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18939
19273
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18940
19274
|
}
|
|
19275
|
+
/** See FecReleaseSequencer.unsource. */
|
|
19276
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
19277
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
19278
|
+
}
|
|
19279
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
19280
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
19281
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
19282
|
+
}
|
|
18941
19283
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18942
19284
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18943
19285
|
if (!this.#fecSequencer) return;
|
|
@@ -19191,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19191
19533
|
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19192
19534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19193
19535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19194
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19195
19537
|
const admitted = resolver2.addMovieFragment(
|
|
19196
19538
|
mpu.mpuSequenceNumber,
|
|
19197
19539
|
data,
|
|
@@ -19264,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19264
19606
|
info,
|
|
19265
19607
|
resolver,
|
|
19266
19608
|
options?.recovered === true,
|
|
19267
|
-
options?.fecSbn
|
|
19609
|
+
options?.fecSbn,
|
|
19610
|
+
options?.fecSbnFirst
|
|
19268
19611
|
);
|
|
19269
19612
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19270
19613
|
}
|
|
@@ -19357,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19357
19700
|
info,
|
|
19358
19701
|
resolver,
|
|
19359
19702
|
options?.recovered === true,
|
|
19360
|
-
options?.fecSbn
|
|
19703
|
+
options?.fecSbn,
|
|
19704
|
+
options?.fecSbnFirst
|
|
19361
19705
|
);
|
|
19362
19706
|
if (outcome.record !== void 0) return outcome.record;
|
|
19363
19707
|
return this.#drop(outcome.reason);
|
|
@@ -19377,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19377
19721
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19378
19722
|
* is what keeps both true.
|
|
19379
19723
|
*/
|
|
19380
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19724
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19381
19725
|
const timing = resolver.sampleTiming(
|
|
19382
19726
|
combined.mpuSequenceNumber,
|
|
19383
19727
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19394,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19394
19738
|
);
|
|
19395
19739
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19396
19740
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19741
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19397
19742
|
if (!resolver.consumeSample(
|
|
19398
19743
|
combined.mpuSequenceNumber,
|
|
19399
19744
|
combined.movieFragmentSequenceNumber,
|
|
@@ -22899,7 +23244,18 @@ function keyframeOverlaySsId(header) {
|
|
|
22899
23244
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22900
23245
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22901
23246
|
}
|
|
23247
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23248
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23249
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23250
|
+
}
|
|
23251
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23252
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23253
|
+
return headerPsn + offset >>> 0;
|
|
23254
|
+
}
|
|
23255
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23256
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22902
23257
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23258
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
22903
23259
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22904
23260
|
function assertStreamIdentity(role, stream) {
|
|
22905
23261
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -23010,6 +23366,7 @@ var MmtpRouter = class {
|
|
|
23010
23366
|
#provenance = /* @__PURE__ */ new Map();
|
|
23011
23367
|
#onFlowProtected = null;
|
|
23012
23368
|
#unadmittedListedDrops = 0;
|
|
23369
|
+
#admittedSourceDatagrams = 0;
|
|
23013
23370
|
#unlistedRepairSetAside = 0;
|
|
23014
23371
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
23015
23372
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -23066,6 +23423,26 @@ var MmtpRouter = class {
|
|
|
23066
23423
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
23067
23424
|
*/
|
|
23068
23425
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23426
|
+
/**
|
|
23427
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23428
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23429
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23430
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23431
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23432
|
+
* an MPU no member of which arrived).
|
|
23433
|
+
*/
|
|
23434
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23435
|
+
/**
|
|
23436
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
23437
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
23438
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
23439
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
23440
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
23441
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
23442
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
23443
|
+
* this offset did.
|
|
23444
|
+
*/
|
|
23445
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23069
23446
|
/**
|
|
23070
23447
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
23071
23448
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -23135,7 +23512,9 @@ var MmtpRouter = class {
|
|
|
23135
23512
|
recovered: 0,
|
|
23136
23513
|
restarts: 0,
|
|
23137
23514
|
unsequencedDropped: 0,
|
|
23138
|
-
protectedDropped: 0
|
|
23515
|
+
protectedDropped: 0,
|
|
23516
|
+
baseFed: 0,
|
|
23517
|
+
baseUndetermined: 0
|
|
23139
23518
|
};
|
|
23140
23519
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
23141
23520
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23341,13 +23720,18 @@ var MmtpRouter = class {
|
|
|
23341
23720
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
23342
23721
|
}
|
|
23343
23722
|
};
|
|
23344
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23723
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23345
23724
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
23346
23725
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
23347
23726
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
23348
23727
|
}
|
|
23349
|
-
|
|
23350
|
-
|
|
23728
|
+
if (span === void 0) {
|
|
23729
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23730
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23731
|
+
} else {
|
|
23732
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23733
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23734
|
+
}
|
|
23351
23735
|
};
|
|
23352
23736
|
}
|
|
23353
23737
|
}
|
|
@@ -23392,8 +23776,9 @@ var MmtpRouter = class {
|
|
|
23392
23776
|
}
|
|
23393
23777
|
/**
|
|
23394
23778
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23395
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23396
|
-
*
|
|
23779
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
23780
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
23781
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
23397
23782
|
*/
|
|
23398
23783
|
get restampedRecoveredSequenceCount() {
|
|
23399
23784
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23572,6 +23957,7 @@ var MmtpRouter = class {
|
|
|
23572
23957
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23573
23958
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23574
23959
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23960
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23575
23961
|
if (previous) {
|
|
23576
23962
|
previous.flush();
|
|
23577
23963
|
previous.dispose();
|
|
@@ -23722,7 +24108,8 @@ var MmtpRouter = class {
|
|
|
23722
24108
|
fec,
|
|
23723
24109
|
k,
|
|
23724
24110
|
offsets: /* @__PURE__ */ new Map(),
|
|
23725
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24111
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24112
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23726
24113
|
};
|
|
23727
24114
|
this.#overlays.set(repairPacketId, inst);
|
|
23728
24115
|
return () => {
|
|
@@ -23752,12 +24139,18 @@ var MmtpRouter = class {
|
|
|
23752
24139
|
*/
|
|
23753
24140
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23754
24141
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
24142
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
24143
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
24144
|
+
stream.overlayToBase.routed.clear();
|
|
24145
|
+
stream.overlayToBase.pending.clear();
|
|
24146
|
+
}
|
|
23755
24147
|
}
|
|
23756
24148
|
#resetOverlay(inst) {
|
|
23757
24149
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23758
24150
|
inst.last = void 0;
|
|
23759
24151
|
inst.offsets.clear();
|
|
23760
24152
|
inst.moqBlocks.clear();
|
|
24153
|
+
inst.rebuilt.clear();
|
|
23761
24154
|
}
|
|
23762
24155
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23763
24156
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23808,18 +24201,33 @@ var MmtpRouter = class {
|
|
|
23808
24201
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23809
24202
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23810
24203
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23811
|
-
* included (7.1, 7.4.3).
|
|
23812
|
-
|
|
23813
|
-
|
|
24204
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
24205
|
+
* FEC reads it.
|
|
24206
|
+
*
|
|
24207
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
24208
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
24209
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
24210
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
24211
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
24212
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
24213
|
+
*/
|
|
24214
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23814
24215
|
if (this.#overlays.size === 0) return;
|
|
23815
24216
|
const ssId = keyframeOverlaySsId(header);
|
|
23816
24217
|
if (ssId === void 0) return;
|
|
24218
|
+
let rebuiltPsn;
|
|
23817
24219
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23818
24220
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24221
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24222
|
+
inst.rebuilt.delete(ssId);
|
|
23819
24223
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23820
24224
|
if (identity !== null) {
|
|
23821
24225
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23822
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
24226
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
24227
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
24228
|
+
overlaySsId: ssId,
|
|
24229
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
24230
|
+
});
|
|
23823
24231
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23824
24232
|
}
|
|
23825
24233
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23841,6 +24249,14 @@ var MmtpRouter = class {
|
|
|
23841
24249
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23842
24250
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23843
24251
|
}
|
|
24252
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
24253
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
24254
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
24255
|
+
this.#restampedRecoveredSeqs++;
|
|
24256
|
+
} else if (baseSsId !== void 0) {
|
|
24257
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24258
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24259
|
+
}
|
|
23844
24260
|
}
|
|
23845
24261
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23846
24262
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23874,11 +24290,16 @@ var MmtpRouter = class {
|
|
|
23874
24290
|
}
|
|
23875
24291
|
/**
|
|
23876
24292
|
* Route an overlay-recovered block's packets as source media of the
|
|
23877
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23878
|
-
*
|
|
23879
|
-
*
|
|
23880
|
-
*
|
|
23881
|
-
*
|
|
24293
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
24294
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
24295
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
24296
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
24297
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
24298
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
24299
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
24300
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
24301
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
24302
|
+
* header's sequence and any other drops the packet.
|
|
23882
24303
|
*/
|
|
23883
24304
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23884
24305
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23898,22 +24319,139 @@ var MmtpRouter = class {
|
|
|
23898
24319
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23899
24320
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23900
24321
|
const restored = /* @__PURE__ */ new Set();
|
|
24322
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24323
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
23901
24324
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23902
|
-
const
|
|
23903
|
-
|
|
23904
|
-
|
|
24325
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24326
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24327
|
+
if (anchor !== void 0) {
|
|
24328
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23905
24329
|
restored.add(packetSequenceNumber);
|
|
23906
|
-
|
|
24330
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24331
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24332
|
+
if (baseSsId !== void 0) {
|
|
24333
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24334
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24335
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
24336
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24337
|
+
}
|
|
24338
|
+
}
|
|
24339
|
+
return [out];
|
|
24340
|
+
}
|
|
24341
|
+
if (!restamped) {
|
|
24342
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24343
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24344
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24345
|
+
if (baseSsId !== void 0) {
|
|
24346
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24347
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
24348
|
+
} else {
|
|
24349
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
24350
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
24351
|
+
}
|
|
24352
|
+
return [sym];
|
|
23907
24353
|
}
|
|
23908
|
-
if (!restamped) return [sym];
|
|
23909
24354
|
this.#overlayStats.unsequencedDropped++;
|
|
23910
24355
|
return [];
|
|
23911
24356
|
});
|
|
23912
24357
|
this.#overlayStats.recovered++;
|
|
24358
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
24359
|
+
for (const sym of symbols) {
|
|
24360
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24361
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
24362
|
+
const first = mpuFirst.get(mpu);
|
|
24363
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
24364
|
+
}
|
|
24365
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
24366
|
+
for (const sym of symbols) {
|
|
24367
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
24368
|
+
}
|
|
23913
24369
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23914
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24370
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24371
|
+
baseSsIds,
|
|
24372
|
+
firstSsIds
|
|
23915
24373
|
});
|
|
23916
24374
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24375
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
24376
|
+
}
|
|
24377
|
+
/**
|
|
24378
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24379
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24380
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
24381
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
24382
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24383
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24384
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24385
|
+
* A base block no repair has stated yet takes it when one does
|
|
24386
|
+
* (#feedPendingRebuilt).
|
|
24387
|
+
*
|
|
24388
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24389
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24390
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24391
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24392
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24393
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24394
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24395
|
+
* block's own arrivals cannot show.
|
|
24396
|
+
*/
|
|
24397
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24398
|
+
const fec = this.#fecManager;
|
|
24399
|
+
const owner = inst.leg.sourcePacketId;
|
|
24400
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24401
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24402
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24403
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24404
|
+
const feeds = [];
|
|
24405
|
+
for (const sym of symbols) {
|
|
24406
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24407
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24408
|
+
if (ssId === void 0) {
|
|
24409
|
+
this.#overlayStats.baseUndetermined++;
|
|
24410
|
+
continue;
|
|
24411
|
+
}
|
|
24412
|
+
routed.delete(ssId);
|
|
24413
|
+
routed.add(ssId);
|
|
24414
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24415
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24416
|
+
}
|
|
24417
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24418
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24419
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24420
|
+
}
|
|
24421
|
+
}
|
|
24422
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24423
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24424
|
+
const fec = this.#fecManager;
|
|
24425
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24426
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24427
|
+
if (coords == null) {
|
|
24428
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24429
|
+
pending.delete(ssId);
|
|
24430
|
+
pending.set(ssId, symbolData.slice());
|
|
24431
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24432
|
+
return;
|
|
24433
|
+
}
|
|
24434
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24435
|
+
if (!fed.taken) return;
|
|
24436
|
+
this.#overlayStats.baseFed++;
|
|
24437
|
+
if (fed.recovered === null) return;
|
|
24438
|
+
const sbn = coords.sbn;
|
|
24439
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24440
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24441
|
+
fec.countRecovery(owner, sbn, name);
|
|
24442
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24443
|
+
});
|
|
24444
|
+
}
|
|
24445
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24446
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24447
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24448
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24449
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24450
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24451
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24452
|
+
pending.delete(ssId);
|
|
24453
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24454
|
+
}
|
|
23917
24455
|
}
|
|
23918
24456
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23919
24457
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -24053,6 +24591,10 @@ var MmtpRouter = class {
|
|
|
24053
24591
|
get unadmittedListedDropCount() {
|
|
24054
24592
|
return this.#unadmittedListedDrops;
|
|
24055
24593
|
}
|
|
24594
|
+
/** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
|
|
24595
|
+
get admittedSourceDatagramCount() {
|
|
24596
|
+
return this.#admittedSourceDatagrams;
|
|
24597
|
+
}
|
|
24056
24598
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
24057
24599
|
get unlistedRepairSetAsideCount() {
|
|
24058
24600
|
return this.#unlistedRepairSetAside;
|
|
@@ -24539,6 +25081,8 @@ var MmtpRouter = class {
|
|
|
24539
25081
|
)) return;
|
|
24540
25082
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24541
25083
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
25084
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
25085
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24542
25086
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24543
25087
|
const recovered = fec.feedRepairSymbol(
|
|
24544
25088
|
ownerPacketId,
|
|
@@ -24589,7 +25133,7 @@ var MmtpRouter = class {
|
|
|
24589
25133
|
catalogTrackName
|
|
24590
25134
|
)) continue;
|
|
24591
25135
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24592
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25136
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24593
25137
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24594
25138
|
}
|
|
24595
25139
|
if (payload.oversized) continue;
|
|
@@ -24676,6 +25220,8 @@ var MmtpRouter = class {
|
|
|
24676
25220
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24677
25221
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24678
25222
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
25223
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25224
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24679
25225
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24680
25226
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24681
25227
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24802,6 +25348,12 @@ var MmtpRouter = class {
|
|
|
24802
25348
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24803
25349
|
* own RAP cadence, so no timer is involved.
|
|
24804
25350
|
*
|
|
25351
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
25352
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
25353
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
25354
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
25355
|
+
* frames of the outgoing stream.
|
|
25356
|
+
*
|
|
24805
25357
|
* The commit is one synchronous transition inside the frame path: the
|
|
24806
25358
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24807
25359
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24852,6 +25404,9 @@ var MmtpRouter = class {
|
|
|
24852
25404
|
droppedIncomingFrames: 0
|
|
24853
25405
|
};
|
|
24854
25406
|
this.#staged = state;
|
|
25407
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25408
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25409
|
+
}
|
|
24855
25410
|
return {
|
|
24856
25411
|
cancel: () => {
|
|
24857
25412
|
if (this.#staged !== state) return false;
|
|
@@ -24934,7 +25489,17 @@ var MmtpRouter = class {
|
|
|
24934
25489
|
*/
|
|
24935
25490
|
#routeParsedFrame(data, meta) {
|
|
24936
25491
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
24937
|
-
if (this.#staged !== null
|
|
25492
|
+
if (this.#staged !== null) {
|
|
25493
|
+
let consumed = false;
|
|
25494
|
+
this.#fecPokeDepth++;
|
|
25495
|
+
try {
|
|
25496
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
25497
|
+
} finally {
|
|
25498
|
+
this.#fecPokeDepth--;
|
|
25499
|
+
this.#flushPendingFecPokes();
|
|
25500
|
+
}
|
|
25501
|
+
if (consumed) return;
|
|
25502
|
+
}
|
|
24938
25503
|
this.#deliverParsedFrame(data, meta);
|
|
24939
25504
|
}
|
|
24940
25505
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -25086,12 +25651,16 @@ var MmtpRouter = class {
|
|
|
25086
25651
|
this.#parser.deferFecRelease(work);
|
|
25087
25652
|
} finally {
|
|
25088
25653
|
this.#fecPokeDepth--;
|
|
25089
|
-
|
|
25090
|
-
|
|
25091
|
-
|
|
25092
|
-
|
|
25093
|
-
|
|
25094
|
-
|
|
25654
|
+
this.#flushPendingFecPokes();
|
|
25655
|
+
}
|
|
25656
|
+
}
|
|
25657
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
25658
|
+
#flushPendingFecPokes() {
|
|
25659
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
25660
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
25661
|
+
this.#pendingFecPokes.clear();
|
|
25662
|
+
for (const poke of pokes) {
|
|
25663
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
25095
25664
|
}
|
|
25096
25665
|
}
|
|
25097
25666
|
}
|
|
@@ -25169,6 +25738,14 @@ var MmtpRouter = class {
|
|
|
25169
25738
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
25170
25739
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
25171
25740
|
},
|
|
25741
|
+
// A staged switch commits on when each side's frames reach it
|
|
25742
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
25743
|
+
// they did unanchored.
|
|
25744
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
25745
|
+
const staged = this.#staged;
|
|
25746
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
25747
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
25748
|
+
},
|
|
25172
25749
|
onReleased: (data, meta) => {
|
|
25173
25750
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
25174
25751
|
const timing = meta.authoredTiming;
|
|
@@ -25310,6 +25887,7 @@ var MmtpRouter = class {
|
|
|
25310
25887
|
const parsed = this.#parser.parsePacket(packet);
|
|
25311
25888
|
if (!parsed) return;
|
|
25312
25889
|
const info = parsed.info;
|
|
25890
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25313
25891
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25314
25892
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25315
25893
|
}
|
|
@@ -25591,7 +26169,8 @@ var MmtpRouter = class {
|
|
|
25591
26169
|
parsed.header,
|
|
25592
26170
|
info.sequenceNumber,
|
|
25593
26171
|
info.rapFlag,
|
|
25594
|
-
source
|
|
26172
|
+
source,
|
|
26173
|
+
fecPayloadId?.ssId
|
|
25595
26174
|
);
|
|
25596
26175
|
}
|
|
25597
26176
|
}
|
|
@@ -25617,7 +26196,8 @@ var MmtpRouter = class {
|
|
|
25617
26196
|
source: parserOptions?.source,
|
|
25618
26197
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25619
26198
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25620
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26199
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26200
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25621
26201
|
};
|
|
25622
26202
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25623
26203
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25741,7 +26321,8 @@ var MmtpRouter = class {
|
|
|
25741
26321
|
if (stream === void 0) {
|
|
25742
26322
|
stream = {
|
|
25743
26323
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25744
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26324
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26325
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25745
26326
|
};
|
|
25746
26327
|
byName.set(catalogTrackName, stream);
|
|
25747
26328
|
}
|
|
@@ -26019,6 +26600,13 @@ var MmtpRouter = class {
|
|
|
26019
26600
|
* frames by epoch only if pre-protection input ever matters.
|
|
26020
26601
|
*/
|
|
26021
26602
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26603
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26604
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26605
|
+
result = {
|
|
26606
|
+
...result,
|
|
26607
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26608
|
+
};
|
|
26609
|
+
}
|
|
26022
26610
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
26023
26611
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
26024
26612
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -26075,7 +26663,12 @@ var MmtpRouter = class {
|
|
|
26075
26663
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
26076
26664
|
if (ownerPacketId == null) return;
|
|
26077
26665
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
26078
|
-
const fecSbnOf = (index) =>
|
|
26666
|
+
const fecSbnOf = (index) => {
|
|
26667
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
26668
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
26669
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
26670
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
26671
|
+
};
|
|
26079
26672
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
26080
26673
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
26081
26674
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -26083,7 +26676,10 @@ var MmtpRouter = class {
|
|
|
26083
26676
|
...result,
|
|
26084
26677
|
symbols: result.symbols.map((sym) => {
|
|
26085
26678
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
26086
|
-
if (wire === sym.packetSequenceNumber)
|
|
26679
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26680
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26681
|
+
return sym;
|
|
26682
|
+
}
|
|
26087
26683
|
this.#restampedRecoveredSeqs++;
|
|
26088
26684
|
this.#restampedStreams.add(streamKey);
|
|
26089
26685
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -27868,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
27868
28464
|
function isSymbolCounts(value) {
|
|
27869
28465
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
27870
28466
|
}
|
|
28467
|
+
function isSpanFailure(value) {
|
|
28468
|
+
if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
|
|
28469
|
+
const length = value["length"];
|
|
28470
|
+
const sources = value["sources"];
|
|
28471
|
+
return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
|
|
28472
|
+
}
|
|
27871
28473
|
function isNullableNonNegative(value) {
|
|
27872
28474
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
27873
28475
|
}
|
|
@@ -27887,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
27887
28489
|
case "recovered":
|
|
27888
28490
|
return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
|
|
27889
28491
|
case "failed":
|
|
27890
|
-
return hasExactKeys(
|
|
28492
|
+
return hasExactKeys(
|
|
28493
|
+
value,
|
|
28494
|
+
"span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
|
|
28495
|
+
) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
|
|
27891
28496
|
case "repair-preempted":
|
|
27892
28497
|
case "repair-deferred":
|
|
27893
28498
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -28153,7 +28758,7 @@ var MulticastManager = class {
|
|
|
28153
28758
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
28154
28759
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
28155
28760
|
},
|
|
28156
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28761
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
28157
28762
|
const pathCount = this.#fec.blockPathCount(
|
|
28158
28763
|
sbn,
|
|
28159
28764
|
packetId,
|
|
@@ -28171,7 +28776,8 @@ var MulticastManager = class {
|
|
|
28171
28776
|
symbols: {
|
|
28172
28777
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
28173
28778
|
moq: pathCount?.moqSymbols ?? 0
|
|
28174
|
-
}
|
|
28779
|
+
},
|
|
28780
|
+
...span === void 0 ? {} : { span }
|
|
28175
28781
|
});
|
|
28176
28782
|
},
|
|
28177
28783
|
onFrameGap: (decision) => {
|
|
@@ -29551,10 +30157,12 @@ var MulticastManager = class {
|
|
|
29551
30157
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
29552
30158
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
29553
30159
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
29554
|
-
* once its range FETCH ends (or none is sent).
|
|
30160
|
+
* once its range FETCH ends (or none is sent). `onExpired` is told of a
|
|
30161
|
+
* pending hole the stream runs past the locator's horizon: returning true,
|
|
30162
|
+
* the caller closes it (`closeIsoHoles`); otherwise it is discarded.
|
|
29555
30163
|
*/
|
|
29556
|
-
onUndelimitedLoss(cb) {
|
|
29557
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30164
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30165
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
29558
30166
|
}
|
|
29559
30167
|
/**
|
|
29560
30168
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -29790,7 +30398,8 @@ var MulticastManager = class {
|
|
|
29790
30398
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
29791
30399
|
...event,
|
|
29792
30400
|
block: Object.freeze({ ...event.block }),
|
|
29793
|
-
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
|
|
30401
|
+
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
|
|
30402
|
+
..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
|
|
29794
30403
|
}
|
|
29795
30404
|
);
|
|
29796
30405
|
if (this.#logRepairLifecycle) {
|
|
@@ -32462,6 +33071,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32462
33071
|
|
|
32463
33072
|
// src/repair-fetch.ts
|
|
32464
33073
|
var MOQT_PRIORITY_MAX = 255;
|
|
33074
|
+
var WALK_LEAD_MS = 3;
|
|
32465
33075
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
32466
33076
|
function isMoqtPriority(value) {
|
|
32467
33077
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -32512,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
32512
33122
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
32513
33123
|
};
|
|
32514
33124
|
const clearEntry = (entry) => {
|
|
32515
|
-
|
|
32516
|
-
|
|
33125
|
+
const { range } = entry;
|
|
33126
|
+
if (range) {
|
|
33127
|
+
atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
|
|
33128
|
+
} else {
|
|
33129
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
33130
|
+
}
|
|
33131
|
+
};
|
|
33132
|
+
const atReport = (range, work) => {
|
|
33133
|
+
const fec = mc.fecManager;
|
|
33134
|
+
if (range.groups !== null || fec.withReceiptTime === void 0) work();
|
|
33135
|
+
else fec.withReceiptTime(range.reportedAt, work);
|
|
32517
33136
|
};
|
|
32518
33137
|
const releaseHold = (entry) => {
|
|
32519
33138
|
if (entry.range) return;
|
|
@@ -32580,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
32580
33199
|
const admit = (entry, now) => {
|
|
32581
33200
|
const budgetMs = entry.deadlineAt - now;
|
|
32582
33201
|
const rttMs = readRtt();
|
|
32583
|
-
const reason =
|
|
33202
|
+
const reason = refusal(entry, now, rttMs);
|
|
32584
33203
|
if (reason === null) return rttMs;
|
|
32585
33204
|
skip(entry, reason, rttMs, budgetMs);
|
|
32586
33205
|
return null;
|
|
32587
33206
|
};
|
|
33207
|
+
const refusal = (entry, now, rttMs) => !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < entry.deadlineAt - now ? null : "rtt-exceeds-budget";
|
|
32588
33208
|
const retire = (entry) => {
|
|
32589
33209
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
32590
33210
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
32591
33211
|
};
|
|
32592
|
-
const
|
|
33212
|
+
const holdsBack = (entry, priority, videoWalks = true) => {
|
|
33213
|
+
if (entry.priority >= priority) return false;
|
|
33214
|
+
const { walk } = entry;
|
|
33215
|
+
if (walk === void 0) return true;
|
|
33216
|
+
if (entry.priority !== 0 || !videoWalks) return false;
|
|
33217
|
+
return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
|
|
33218
|
+
};
|
|
33219
|
+
const outstandingBelow = (priority, videoWalks = true) => {
|
|
32593
33220
|
for (const entry of entries.values()) {
|
|
32594
|
-
if (entry
|
|
33221
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
32595
33222
|
}
|
|
32596
33223
|
return false;
|
|
32597
33224
|
};
|
|
33225
|
+
const yieldVideoWalks = (queued) => {
|
|
33226
|
+
const now = performance.now();
|
|
33227
|
+
const rttMs = readRtt();
|
|
33228
|
+
const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
|
|
33229
|
+
if (!due) return;
|
|
33230
|
+
for (const entry of entries.values()) {
|
|
33231
|
+
const walk = entry.walk;
|
|
33232
|
+
if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
|
|
33233
|
+
if (walk.group.controller.signal.aborted) continue;
|
|
33234
|
+
walk.yielded = true;
|
|
33235
|
+
walk.group.controller.abort();
|
|
33236
|
+
}
|
|
33237
|
+
};
|
|
32598
33238
|
const preemptBestEffort = () => {
|
|
32599
33239
|
for (const entry of entries.values()) {
|
|
32600
33240
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -32623,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
32623
33263
|
}
|
|
32624
33264
|
const rttMs = admit(entry, now);
|
|
32625
33265
|
if (rttMs === null) return;
|
|
32626
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33266
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33267
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
32627
33268
|
if (counted) entry.attempts++;
|
|
32628
33269
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
32629
33270
|
rttMs,
|
|
@@ -32645,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
32645
33286
|
}
|
|
32646
33287
|
void execute(entry, run);
|
|
32647
33288
|
};
|
|
33289
|
+
const leaveWalkedGroup = (block) => {
|
|
33290
|
+
for (const entry of entries.values()) {
|
|
33291
|
+
const group = entry.walk?.group;
|
|
33292
|
+
if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
|
|
33293
|
+
group.controller.abort();
|
|
33294
|
+
}
|
|
33295
|
+
}
|
|
33296
|
+
};
|
|
32648
33297
|
const dispatch = () => {
|
|
32649
33298
|
if (disposed) return;
|
|
32650
33299
|
const now = performance.now();
|
|
@@ -32669,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32669
33318
|
for (const entry of queued) {
|
|
32670
33319
|
if (entry.priority === 0) start(entry);
|
|
32671
33320
|
}
|
|
33321
|
+
yieldVideoWalks(queued);
|
|
32672
33322
|
if (outstandingBelow(1)) return;
|
|
32673
33323
|
for (const entry of queued) {
|
|
32674
33324
|
if (entry.priority === 1) start(entry);
|
|
@@ -32695,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32695
33345
|
}
|
|
32696
33346
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
32697
33347
|
};
|
|
33348
|
+
const groupServed = (sbn, packetId, catalogTrackName) => {
|
|
33349
|
+
if (mc.fecManager.blockSymbolCounts === void 0) return false;
|
|
33350
|
+
const counts = symbolCounts(sbn, packetId, catalogTrackName);
|
|
33351
|
+
if (counts === void 0 || counts.authoredLength === void 0) return true;
|
|
33352
|
+
return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
|
|
33353
|
+
};
|
|
33354
|
+
const statedEnd = (sbn, packetId, catalogTrackName) => {
|
|
33355
|
+
const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
|
|
33356
|
+
return length === void 0 ? void 0 : serial32Add(sbn, length);
|
|
33357
|
+
};
|
|
32698
33358
|
const finish = (entry, run, fed, error) => {
|
|
32699
33359
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
32700
33360
|
return;
|
|
@@ -32739,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
32739
33399
|
signal: run.controller.signal,
|
|
32740
33400
|
subscriberPriority: entry.subscriberPriority
|
|
32741
33401
|
};
|
|
33402
|
+
const { range } = entry;
|
|
33403
|
+
const feed = (packet) => {
|
|
33404
|
+
if (entry.catalogTrackName === void 0) {
|
|
33405
|
+
mc.processRepairFromMoQ(packet, entry.packetId);
|
|
33406
|
+
} else {
|
|
33407
|
+
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
33408
|
+
}
|
|
33409
|
+
};
|
|
32742
33410
|
try {
|
|
32743
|
-
const packets = entry.
|
|
33411
|
+
const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
|
|
32744
33412
|
...common,
|
|
32745
|
-
startGroup:
|
|
32746
|
-
endGroup:
|
|
32747
|
-
lostSsId:
|
|
32748
|
-
})
|
|
33413
|
+
startGroup: range.groups.startGroup,
|
|
33414
|
+
endGroup: range.groups.endGroup,
|
|
33415
|
+
lostSsId: range.lostSsId
|
|
33416
|
+
});
|
|
32749
33417
|
for await (const packet of packets) {
|
|
32750
33418
|
if (run.stopped !== null || entry.running !== run) break;
|
|
32751
33419
|
fed++;
|
|
32752
|
-
if (
|
|
32753
|
-
|
|
32754
|
-
} else {
|
|
32755
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
32756
|
-
}
|
|
33420
|
+
if (range === void 0) feed(packet);
|
|
33421
|
+
else atReport(range, () => feed(packet));
|
|
32757
33422
|
}
|
|
32758
33423
|
finish(entry, run, fed, void 0);
|
|
32759
33424
|
} catch (error) {
|
|
32760
33425
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
32761
33426
|
}
|
|
32762
33427
|
};
|
|
33428
|
+
async function* walkHole(entry, walk, hole, common) {
|
|
33429
|
+
const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
|
|
33430
|
+
for (let first = true; !common.signal.aborted; first = false) {
|
|
33431
|
+
if (!first && entry.priority !== 0 && outstandingBelow(1)) {
|
|
33432
|
+
stop(entry, "preempted");
|
|
33433
|
+
note("repair-preempted", entry);
|
|
33434
|
+
return;
|
|
33435
|
+
}
|
|
33436
|
+
if (!first && entry.priority === 0) {
|
|
33437
|
+
dispatch();
|
|
33438
|
+
if (common.signal.aborted) return;
|
|
33439
|
+
}
|
|
33440
|
+
const sbn = walk.next;
|
|
33441
|
+
let next;
|
|
33442
|
+
if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
|
|
33443
|
+
const rsIds = /* @__PURE__ */ new Set();
|
|
33444
|
+
const group = new AbortController();
|
|
33445
|
+
const cancel = () => group.abort();
|
|
33446
|
+
common.signal.addEventListener("abort", cancel, { once: true });
|
|
33447
|
+
walk.group = { sbn, controller: group };
|
|
33448
|
+
try {
|
|
33449
|
+
for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
|
|
33450
|
+
yield packet;
|
|
33451
|
+
const id = extractRepairFecPayloadId(packet);
|
|
33452
|
+
if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
|
|
33453
|
+
next ??= serial32Add(sbn, id.ssbLength);
|
|
33454
|
+
rsIds.add(id.rsId);
|
|
33455
|
+
if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
|
|
33456
|
+
group.abort();
|
|
33457
|
+
break;
|
|
33458
|
+
}
|
|
33459
|
+
}
|
|
33460
|
+
} catch (error) {
|
|
33461
|
+
if (!group.signal.aborted || common.signal.aborted) throw error;
|
|
33462
|
+
} finally {
|
|
33463
|
+
common.signal.removeEventListener("abort", cancel);
|
|
33464
|
+
walk.group = null;
|
|
33465
|
+
}
|
|
33466
|
+
if (walk.yielded) {
|
|
33467
|
+
walk.yielded = false;
|
|
33468
|
+
continue;
|
|
33469
|
+
}
|
|
33470
|
+
}
|
|
33471
|
+
next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
|
|
33472
|
+
if (next === void 0) return;
|
|
33473
|
+
if (serial32Distance(entry.sbn, next) > lostAt) return;
|
|
33474
|
+
walk.next = next;
|
|
33475
|
+
}
|
|
33476
|
+
}
|
|
32763
33477
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
32764
33478
|
for (const entry of [...entries.values()]) {
|
|
32765
33479
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -32955,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
32955
33669
|
throw error;
|
|
32956
33670
|
}
|
|
32957
33671
|
};
|
|
32958
|
-
|
|
33672
|
+
const trackKind = (packetId) => {
|
|
33673
|
+
try {
|
|
33674
|
+
return mc.fecManager.trackKindFor?.(packetId);
|
|
33675
|
+
} catch {
|
|
33676
|
+
return void 0;
|
|
33677
|
+
}
|
|
33678
|
+
};
|
|
33679
|
+
const walkDeadlineAt = (packetId, now, deadlineAt) => {
|
|
33680
|
+
let repairDeadlineMs;
|
|
33681
|
+
try {
|
|
33682
|
+
repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
|
|
33683
|
+
} catch {
|
|
33684
|
+
repairDeadlineMs = void 0;
|
|
33685
|
+
}
|
|
33686
|
+
if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
|
|
33687
|
+
return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
|
|
33688
|
+
};
|
|
33689
|
+
const onHoleExpired = (hole) => {
|
|
33690
|
+
if (disposed) return false;
|
|
33691
|
+
const entry = [...entries.values()].find((candidate) => candidate.walk !== void 0 && candidate.packetId === hole.packetId && candidate.catalogTrackName === hole.catalogTrackName && serial32Distance(candidate.sbn, hole.holeStart) < candidate.range.holeLength);
|
|
33692
|
+
if (entry === void 0) return false;
|
|
33693
|
+
queueMicrotask(() => {
|
|
33694
|
+
if (entries.get(entry.key) !== entry) return;
|
|
33695
|
+
retire(entry);
|
|
33696
|
+
stop(entry, "expired");
|
|
33697
|
+
clearEntry(entry);
|
|
33698
|
+
dispatch();
|
|
33699
|
+
});
|
|
33700
|
+
return true;
|
|
33701
|
+
};
|
|
33702
|
+
if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
|
|
32959
33703
|
mc.onUndelimitedLoss((loss) => {
|
|
32960
33704
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
32961
33705
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -32963,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
32963
33707
|
const blockProfile = blockProfileOf(packetId);
|
|
32964
33708
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
32965
33709
|
if (!target) return close();
|
|
32966
|
-
const
|
|
32967
|
-
const
|
|
32968
|
-
|
|
33710
|
+
const walked = opts.rangeTransport === void 0;
|
|
33711
|
+
const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
|
|
33712
|
+
const priority = repairPriorityForClass(holeClass);
|
|
33713
|
+
const groups = walked ? null : rangeFor(loss, target.repairTrack);
|
|
33714
|
+
if (!walked && groups === null) {
|
|
32969
33715
|
close();
|
|
32970
33716
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
32971
33717
|
return;
|
|
@@ -32976,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
32976
33722
|
packetId,
|
|
32977
33723
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
32978
33724
|
};
|
|
32979
|
-
const wire = wirePriority(identity,
|
|
32980
|
-
|
|
33725
|
+
const wire = wirePriority(identity, holeClass);
|
|
33726
|
+
if (wire === null) return close();
|
|
33727
|
+
const now = performance.now();
|
|
33728
|
+
const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
|
|
32981
33729
|
if (deadline === null) {
|
|
32982
33730
|
close();
|
|
32983
33731
|
mc.noteRepairSkipped(
|
|
@@ -32989,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32989
33737
|
);
|
|
32990
33738
|
return;
|
|
32991
33739
|
}
|
|
33740
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
32992
33741
|
const entry = {
|
|
32993
33742
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
32994
33743
|
...identity,
|
|
@@ -32996,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32996
33745
|
blockProfile,
|
|
32997
33746
|
priority,
|
|
32998
33747
|
subscriberPriority: wire.subscriberPriority,
|
|
32999
|
-
deadlineAt
|
|
33000
|
-
playoutDeadlineAt:
|
|
33748
|
+
deadlineAt,
|
|
33749
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
33001
33750
|
anchorAt: deadline.anchorAt,
|
|
33002
33751
|
delayMs: deadline.delayMs,
|
|
33003
33752
|
bound: deadline.bound,
|
|
33004
33753
|
order: enqueueOrder++,
|
|
33005
33754
|
attempts: 0,
|
|
33006
33755
|
running: null,
|
|
33007
|
-
range: {
|
|
33756
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33757
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
33008
33758
|
};
|
|
33009
33759
|
const previous = entries.get(entry.key);
|
|
33010
33760
|
if (previous) {
|
|
@@ -33014,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
33014
33764
|
entries.set(entry.key, entry);
|
|
33015
33765
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
33016
33766
|
dispatch();
|
|
33017
|
-
});
|
|
33767
|
+
}, onHoleExpired);
|
|
33018
33768
|
}
|
|
33019
33769
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
33020
33770
|
queueMicrotask(() => {
|