@blockcast/mmt-container 2.2.0-main.66f097ed5157 → 2.2.0-main.6a18a111b136
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 +523 -117
- package/dist/browser-runtime.js.map +2 -2
- package/dist/fec-manager.d.ts +37 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +181 -52
- package/dist/fec-manager.js.map +2 -2
- package/dist/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/mmtp-container-parser.d.ts +2 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +52 -7
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +53 -8
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +52 -7
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +12 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +144 -32
- 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 +345 -91
- package/dist/multicast-manager.js.map +2 -2
- package/dist/no-modules.js +52 -7
- 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/wasm-mmtp-container-parser.js +52 -7
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +156 -26
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +114 -31
- package/src/mmtp-container-parser.ts +77 -6
- package/src/mmtp-router.ts +157 -42
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
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
|
}
|
|
@@ -12928,6 +12939,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12928
12939
|
this.#heldEvicted = 0;
|
|
12929
12940
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
12930
12941
|
this.#holes = [];
|
|
12942
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
12943
|
+
this.#expired = [];
|
|
12944
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
12945
|
+
this.#newlyExpired = [];
|
|
12946
|
+
/**
|
|
12947
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
12948
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
12949
|
+
*/
|
|
12950
|
+
this.#closedBehind = [];
|
|
12931
12951
|
/**
|
|
12932
12952
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
12933
12953
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -12951,6 +12971,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12951
12971
|
#newest;
|
|
12952
12972
|
#newestSource;
|
|
12953
12973
|
#holes;
|
|
12974
|
+
#expired;
|
|
12975
|
+
#newlyExpired;
|
|
12976
|
+
#closedBehind;
|
|
12954
12977
|
static #checkedRetention(retentionBlocks) {
|
|
12955
12978
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
12956
12979
|
throw new Error(
|
|
@@ -13020,17 +13043,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13020
13043
|
}
|
|
13021
13044
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
13022
13045
|
locate(ssId) {
|
|
13023
|
-
for (
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13046
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
13047
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
13048
|
+
const block = blocks[i];
|
|
13049
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
13050
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
13051
|
+
}
|
|
13027
13052
|
}
|
|
13028
13053
|
}
|
|
13029
13054
|
return null;
|
|
13030
13055
|
}
|
|
13031
|
-
/** Number of pending
|
|
13056
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
13032
13057
|
get holeCount() {
|
|
13033
|
-
return this.#holes.length;
|
|
13058
|
+
return this.#holes.length + this.#expired.length;
|
|
13059
|
+
}
|
|
13060
|
+
/**
|
|
13061
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
13062
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
13063
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
13064
|
+
* release gate does not wait for it. Its owner must close it
|
|
13065
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
13066
|
+
*/
|
|
13067
|
+
takeExpiredHoles() {
|
|
13068
|
+
return this.#newlyExpired.splice(0);
|
|
13034
13069
|
}
|
|
13035
13070
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
13036
13071
|
blockEndingAt(end) {
|
|
@@ -13086,9 +13121,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13086
13121
|
/**
|
|
13087
13122
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
13088
13123
|
* 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
|
-
*
|
|
13124
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
13125
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
13126
|
+
* a known block (a different length, or an overlap with another block or
|
|
13127
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
13128
|
+
* block changes.
|
|
13092
13129
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
13093
13130
|
* A block so far past `newest` that stating it would retire the position
|
|
13094
13131
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -13107,7 +13144,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13107
13144
|
const known = this.blockAt(start);
|
|
13108
13145
|
if (known !== void 0) {
|
|
13109
13146
|
if (known.span) {
|
|
13110
|
-
|
|
13147
|
+
if (known.length === length) {
|
|
13148
|
+
const block2 = { start, length, span: false };
|
|
13149
|
+
this.#blocks[this.#blocks.indexOf(known)] = block2;
|
|
13150
|
+
return { status: "placed", block: block2, placed: [] };
|
|
13151
|
+
}
|
|
13152
|
+
return length < known.length ? {
|
|
13153
|
+
status: "overlap",
|
|
13154
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
13155
|
+
ofSpan: known
|
|
13156
|
+
} : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
|
|
13111
13157
|
}
|
|
13112
13158
|
if (known.length !== length) {
|
|
13113
13159
|
return {
|
|
@@ -13175,18 +13221,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13175
13221
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
13176
13222
|
}
|
|
13177
13223
|
/**
|
|
13178
|
-
* Turn every
|
|
13179
|
-
*
|
|
13180
|
-
*
|
|
13224
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
13225
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
13226
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
13227
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
13228
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
13229
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
13181
13230
|
*/
|
|
13182
13231
|
closeHolesWithin(start, length) {
|
|
13183
13232
|
const closed = [];
|
|
13184
|
-
for (const
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13233
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
13234
|
+
for (const hole of [...holes]) {
|
|
13235
|
+
if (!isoBlockContains(start, length, hole.start)) continue;
|
|
13236
|
+
holes.splice(holes.indexOf(hole), 1);
|
|
13237
|
+
const spanBlock = { start: hole.start, length: hole.length, span: true };
|
|
13238
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
|
|
13239
|
+
if (holes === this.#holes) {
|
|
13240
|
+
this.#blocks.push(spanBlock);
|
|
13241
|
+
} else {
|
|
13242
|
+
this.#closedBehind.push(spanBlock);
|
|
13243
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
|
|
13244
|
+
}
|
|
13245
|
+
}
|
|
13190
13246
|
}
|
|
13191
13247
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
13192
13248
|
}
|
|
@@ -13200,15 +13256,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13200
13256
|
return void 0;
|
|
13201
13257
|
}
|
|
13202
13258
|
/**
|
|
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
|
|
13259
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
13260
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
13261
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13262
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
13263
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
13207
13264
|
*/
|
|
13208
13265
|
retireBehind(position) {
|
|
13209
13266
|
this.observe(position);
|
|
13210
13267
|
const horizon = this.#retentionBlocks;
|
|
13211
13268
|
const retired = [];
|
|
13269
|
+
this.#closedBehind.length = 0;
|
|
13212
13270
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
13213
13271
|
const block = this.#blocks[i];
|
|
13214
13272
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -13220,10 +13278,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13220
13278
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
13221
13279
|
const hole = this.#holes[i];
|
|
13222
13280
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
13223
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
13281
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
|
|
13282
|
+
this.#holes.splice(i, 1);
|
|
13283
|
+
this.#expired.push(hole);
|
|
13284
|
+
this.#newlyExpired.push(hole);
|
|
13224
13285
|
}
|
|
13225
13286
|
for (const ssId of [...this.#held.keys()]) {
|
|
13226
|
-
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#
|
|
13287
|
+
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))) {
|
|
13288
|
+
this.#held.delete(ssId);
|
|
13289
|
+
}
|
|
13227
13290
|
}
|
|
13228
13291
|
return retired;
|
|
13229
13292
|
}
|
|
@@ -13231,6 +13294,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13231
13294
|
reset() {
|
|
13232
13295
|
this.#blocks.length = 0;
|
|
13233
13296
|
this.#holes.length = 0;
|
|
13297
|
+
this.#expired.length = 0;
|
|
13298
|
+
this.#newlyExpired.length = 0;
|
|
13299
|
+
this.#closedBehind.length = 0;
|
|
13234
13300
|
this.#held.clear();
|
|
13235
13301
|
this.#coverageEnd = void 0;
|
|
13236
13302
|
this.#newest = void 0;
|
|
@@ -13948,6 +14014,10 @@ var FECManager = class _FECManager {
|
|
|
13948
14014
|
// 'pending' forever, which would park every later frame of its stream in
|
|
13949
14015
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
13950
14016
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
14017
|
+
/**
|
|
14018
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
14019
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
14020
|
+
*/
|
|
13951
14021
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
13952
14022
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13953
14023
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
@@ -13964,6 +14034,9 @@ var FECManager = class _FECManager {
|
|
|
13964
14034
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
13965
14035
|
// symbols; waiting for K fails every one of them.
|
|
13966
14036
|
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
14037
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
14038
|
+
// a repair states the block there or the span resolves.
|
|
14039
|
+
this.#isoSpanKeys = /* @__PURE__ */ new Set();
|
|
13967
14040
|
this.#ssbLengthConflictCount = 0;
|
|
13968
14041
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
13969
14042
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -14161,7 +14234,10 @@ var FECManager = class _FECManager {
|
|
|
14161
14234
|
// arrives via onBlockRecovered callback.
|
|
14162
14235
|
this.onBlockStarted = null;
|
|
14163
14236
|
this.onBlockComplete = null;
|
|
14164
|
-
/**
|
|
14237
|
+
/**
|
|
14238
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
14239
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
14240
|
+
*/
|
|
14165
14241
|
this.onBlockFailed = null;
|
|
14166
14242
|
/**
|
|
14167
14243
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -14203,6 +14279,8 @@ var FECManager = class _FECManager {
|
|
|
14203
14279
|
* for it, which it must be.
|
|
14204
14280
|
*/
|
|
14205
14281
|
this.#onUndelimitedLoss = null;
|
|
14282
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
14283
|
+
this.#onIsoHoleExpired = null;
|
|
14206
14284
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
14207
14285
|
this.onBlockVanished = null;
|
|
14208
14286
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -14247,6 +14325,7 @@ var FECManager = class _FECManager {
|
|
|
14247
14325
|
#blockSourceEsis;
|
|
14248
14326
|
#blockRebuiltEsis;
|
|
14249
14327
|
#blockAuthoredLengths;
|
|
14328
|
+
#isoSpanKeys;
|
|
14250
14329
|
#ssbLengthConflictCount;
|
|
14251
14330
|
#blockRepairRsIds;
|
|
14252
14331
|
#blockRsbLengths;
|
|
@@ -14313,6 +14392,7 @@ var FECManager = class _FECManager {
|
|
|
14313
14392
|
#decoderUnavailableEvents;
|
|
14314
14393
|
#repairSymbolSizeMismatchCount;
|
|
14315
14394
|
#onUndelimitedLoss;
|
|
14395
|
+
#onIsoHoleExpired;
|
|
14316
14396
|
#encodeTrackName(catalogTrackName) {
|
|
14317
14397
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
14318
14398
|
if (encoded === void 0) {
|
|
@@ -14784,7 +14864,10 @@ var FECManager = class _FECManager {
|
|
|
14784
14864
|
timer.handle = null;
|
|
14785
14865
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
14786
14866
|
};
|
|
14787
|
-
timer.handle = setTimeout(
|
|
14867
|
+
timer.handle = setTimeout(
|
|
14868
|
+
onFire,
|
|
14869
|
+
this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
|
|
14870
|
+
);
|
|
14788
14871
|
this.#repairDeadlines.set(key, timer);
|
|
14789
14872
|
}
|
|
14790
14873
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -14928,8 +15011,14 @@ var FECManager = class _FECManager {
|
|
|
14928
15011
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
14929
15012
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
14930
15013
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
15014
|
+
*
|
|
15015
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
15016
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
15017
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
15018
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
15019
|
+
* it (BLO-52085).
|
|
14931
15020
|
*/
|
|
14932
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
15021
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
14933
15022
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
14934
15023
|
const now = performance.now();
|
|
14935
15024
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -14968,7 +15057,7 @@ var FECManager = class _FECManager {
|
|
|
14968
15057
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
14969
15058
|
};
|
|
14970
15059
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
14971
|
-
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))) {
|
|
15060
|
+
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))) {
|
|
14972
15061
|
discontinuity();
|
|
14973
15062
|
return;
|
|
14974
15063
|
}
|
|
@@ -15230,17 +15319,21 @@ var FECManager = class _FECManager {
|
|
|
15230
15319
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
15231
15320
|
this.#repairHoldUntilMs.delete(key);
|
|
15232
15321
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
15233
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
15322
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
15234
15323
|
};
|
|
15235
|
-
timer.handle = setTimeout(
|
|
15324
|
+
timer.handle = setTimeout(
|
|
15325
|
+
onFire,
|
|
15326
|
+
this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
|
|
15327
|
+
);
|
|
15236
15328
|
this.#activeBlocks.set(key, timer);
|
|
15237
15329
|
this.#indexBlockKey(key, sbn, packetId);
|
|
15238
15330
|
}
|
|
15239
15331
|
/**
|
|
15240
15332
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
15241
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
15333
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
15334
|
+
* is when it was due: its deadline, or now.
|
|
15242
15335
|
*/
|
|
15243
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
15336
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
15244
15337
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
15245
15338
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
15246
15339
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -15256,7 +15349,7 @@ var FECManager = class _FECManager {
|
|
|
15256
15349
|
if (workerClient) {
|
|
15257
15350
|
this.#pendingWorkerCleanups.add(key);
|
|
15258
15351
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
15259
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
15352
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
15260
15353
|
workerClient.cleanup(sbn);
|
|
15261
15354
|
} else {
|
|
15262
15355
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -15570,6 +15663,7 @@ var FECManager = class _FECManager {
|
|
|
15570
15663
|
resetMap(this.#blockSourceEsis);
|
|
15571
15664
|
resetMap(this.#blockRebuiltEsis);
|
|
15572
15665
|
resetMap(this.#blockAuthoredLengths);
|
|
15666
|
+
resetSet(this.#isoSpanKeys);
|
|
15573
15667
|
resetMap(this.#blockRepairRsIds);
|
|
15574
15668
|
resetMap(this.#blockRsbLengths);
|
|
15575
15669
|
resetMap(this.#heldLayerStates);
|
|
@@ -15695,6 +15789,7 @@ var FECManager = class _FECManager {
|
|
|
15695
15789
|
dropMap(this.#blockSourceEsis);
|
|
15696
15790
|
dropMap(this.#blockRebuiltEsis);
|
|
15697
15791
|
dropMap(this.#blockAuthoredLengths);
|
|
15792
|
+
dropSet(this.#isoSpanKeys);
|
|
15698
15793
|
dropMap(this.#blockRepairRsIds);
|
|
15699
15794
|
dropMap(this.#blockRsbLengths);
|
|
15700
15795
|
dropMap(this.#blockPathCounts);
|
|
@@ -16037,8 +16132,9 @@ var FECManager = class _FECManager {
|
|
|
16037
16132
|
return true;
|
|
16038
16133
|
}
|
|
16039
16134
|
/**
|
|
16040
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
16041
|
-
*
|
|
16135
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
16136
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
16137
|
+
* multicast wait, which may already have passed.
|
|
16042
16138
|
*/
|
|
16043
16139
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
16044
16140
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -16176,6 +16272,7 @@ var FECManager = class _FECManager {
|
|
|
16176
16272
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16177
16273
|
const evictedBefore = locator.heldEvicted;
|
|
16178
16274
|
const held = locator.hold(ssId, source);
|
|
16275
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16179
16276
|
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16180
16277
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16181
16278
|
if (evicted > 0) {
|
|
@@ -16197,17 +16294,20 @@ var FECManager = class _FECManager {
|
|
|
16197
16294
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
16198
16295
|
*/
|
|
16199
16296
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
16200
|
-
const
|
|
16297
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16298
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
16299
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16201
16300
|
if (placement.status !== "placed") {
|
|
16202
16301
|
this.#isoRepairRefusedCount++;
|
|
16203
16302
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
16204
16303
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
16205
16304
|
}
|
|
16305
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
16206
16306
|
return placement;
|
|
16207
16307
|
}
|
|
16308
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
16208
16309
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
16209
16310
|
if (placement.hole !== void 0) {
|
|
16210
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
16211
16311
|
const hole = placement.hole.block;
|
|
16212
16312
|
const previous = locator.blockEndingAt(hole.start);
|
|
16213
16313
|
const loss = {
|
|
@@ -16228,17 +16328,29 @@ var FECManager = class _FECManager {
|
|
|
16228
16328
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
16229
16329
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
16230
16330
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
16231
|
-
* nobody to close it.
|
|
16331
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
16332
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
16333
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
16334
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
16232
16335
|
*/
|
|
16233
|
-
setUndelimitedLossHandler(handler) {
|
|
16336
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
16234
16337
|
this.#onUndelimitedLoss = handler;
|
|
16338
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
16235
16339
|
this.#deferIsoGaps = handler !== null;
|
|
16236
16340
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
16237
16341
|
}
|
|
16342
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
16343
|
+
#settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
|
|
16344
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
16345
|
+
const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
|
|
16346
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
|
|
16347
|
+
}
|
|
16348
|
+
}
|
|
16238
16349
|
/**
|
|
16239
|
-
* The range repair for a hole ended: every hole
|
|
16240
|
-
*
|
|
16241
|
-
* with the held sources they placed, for the caller to feed
|
|
16350
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
16351
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
16352
|
+
* spans with the held sources they placed, for the caller to feed
|
|
16353
|
+
* (MmtpRouter).
|
|
16242
16354
|
*/
|
|
16243
16355
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
16244
16356
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -16257,7 +16369,10 @@ var FECManager = class _FECManager {
|
|
|
16257
16369
|
*/
|
|
16258
16370
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
16259
16371
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
16260
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
16372
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
16373
|
+
this.#blockAuthoredLengths.set(key, span.block.length);
|
|
16374
|
+
this.#isoSpanKeys.add(key);
|
|
16375
|
+
}
|
|
16261
16376
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
16262
16377
|
}
|
|
16263
16378
|
/**
|
|
@@ -16598,11 +16713,14 @@ var FECManager = class _FECManager {
|
|
|
16598
16713
|
});
|
|
16599
16714
|
client.onCleanupComplete((sbn) => {
|
|
16600
16715
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
16601
|
-
const
|
|
16716
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
16717
|
+
const cleanupKeys = owed.filter((key) => {
|
|
16602
16718
|
const block = this.#parseBlockKey(key);
|
|
16603
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
16719
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
16604
16720
|
});
|
|
16605
|
-
|
|
16721
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
|
|
16722
|
+
const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
|
|
16723
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
16606
16724
|
});
|
|
16607
16725
|
client.onError((msg, code) => {
|
|
16608
16726
|
const requestedKind = code ?? msg;
|
|
@@ -16672,9 +16790,11 @@ var FECManager = class _FECManager {
|
|
|
16672
16790
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
16673
16791
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
16674
16792
|
*/
|
|
16675
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
16793
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
16676
16794
|
this.#clearOwedCleanupBound(key);
|
|
16677
16795
|
const bound = {
|
|
16796
|
+
dueAtMs,
|
|
16797
|
+
askedAtMs: performance.now(),
|
|
16678
16798
|
handle: setTimeout(() => {
|
|
16679
16799
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
16680
16800
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -17245,12 +17365,13 @@ var FECManager = class _FECManager {
|
|
|
17245
17365
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
17246
17366
|
* classification evidence, and marks the block clean when all of its
|
|
17247
17367
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
17248
|
-
* otherwise K.
|
|
17368
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
17369
|
+
* placed only now (see #noteSbnSeen).
|
|
17249
17370
|
*
|
|
17250
17371
|
* Throws for a packetId that was never configured, so a classified block
|
|
17251
17372
|
* never reads the manager-global K or timeout.
|
|
17252
17373
|
*/
|
|
17253
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
17374
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
17254
17375
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
17255
17376
|
if (trackConfig === void 0) {
|
|
17256
17377
|
throw new Error(
|
|
@@ -17266,7 +17387,8 @@ var FECManager = class _FECManager {
|
|
|
17266
17387
|
catalogTrackName,
|
|
17267
17388
|
"source",
|
|
17268
17389
|
evidence.packetSequenceNumber,
|
|
17269
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
17390
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
17391
|
+
held
|
|
17270
17392
|
);
|
|
17271
17393
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
17272
17394
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -17321,6 +17443,7 @@ var FECManager = class _FECManager {
|
|
|
17321
17443
|
this.#blockSourceEsis.delete(key);
|
|
17322
17444
|
this.#blockRebuiltEsis.delete(key);
|
|
17323
17445
|
this.#blockAuthoredLengths.delete(key);
|
|
17446
|
+
this.#isoSpanKeys.delete(key);
|
|
17324
17447
|
this.#blockRepairRsIds.delete(key);
|
|
17325
17448
|
this.#blockRsbLengths.delete(key);
|
|
17326
17449
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -17351,6 +17474,19 @@ var FECManager = class _FECManager {
|
|
|
17351
17474
|
#receiptNow() {
|
|
17352
17475
|
return this.#receiptAtMs ?? performance.now();
|
|
17353
17476
|
}
|
|
17477
|
+
/**
|
|
17478
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
17479
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
17480
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
17481
|
+
* that timer would have, never later than a delay counted from now, and
|
|
17482
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
17483
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
17484
|
+
* from another fraction of a ms (BLO-52085).
|
|
17485
|
+
*/
|
|
17486
|
+
#receiptTimerDelayMs(deadlineMs, minMs) {
|
|
17487
|
+
const receivedAtMs = this.#receiptAtMs;
|
|
17488
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
|
|
17489
|
+
}
|
|
17354
17490
|
/**
|
|
17355
17491
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
17356
17492
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -17399,6 +17535,7 @@ var FECManager = class _FECManager {
|
|
|
17399
17535
|
}
|
|
17400
17536
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17401
17537
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
17538
|
+
const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
|
|
17402
17539
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
17403
17540
|
this.#clearRepairDeadline(key);
|
|
17404
17541
|
this.#clearBlockTimer(key);
|
|
@@ -17418,7 +17555,8 @@ var FECManager = class _FECManager {
|
|
|
17418
17555
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
17419
17556
|
this.#fecStats.blocksFailed++;
|
|
17420
17557
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
17421
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17558
|
+
if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17559
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
17422
17560
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
17423
17561
|
}
|
|
17424
17562
|
/**
|
|
@@ -17449,6 +17587,7 @@ var FECManager = class _FECManager {
|
|
|
17449
17587
|
this.#blockSourceEsis.delete(key);
|
|
17450
17588
|
this.#blockRebuiltEsis.delete(key);
|
|
17451
17589
|
this.#blockAuthoredLengths.delete(key);
|
|
17590
|
+
this.#isoSpanKeys.delete(key);
|
|
17452
17591
|
this.#blockRepairRsIds.delete(key);
|
|
17453
17592
|
this.#blockRsbLengths.delete(key);
|
|
17454
17593
|
this.#clearBlockTimer(key);
|
|
@@ -17541,6 +17680,7 @@ var FECManager = class _FECManager {
|
|
|
17541
17680
|
this.#blockSourceEsis.clear();
|
|
17542
17681
|
this.#blockRebuiltEsis.clear();
|
|
17543
17682
|
this.#blockAuthoredLengths.clear();
|
|
17683
|
+
this.#isoSpanKeys.clear();
|
|
17544
17684
|
this.#blockRepairRsIds.clear();
|
|
17545
17685
|
this.#blockRsbLengths.clear();
|
|
17546
17686
|
this.#heldLayerStates.clear();
|
|
@@ -18442,6 +18582,9 @@ function authoredGap(before, after) {
|
|
|
18442
18582
|
function ssIdAfter(a, b) {
|
|
18443
18583
|
return (a - b | 0) > 0;
|
|
18444
18584
|
}
|
|
18585
|
+
function holdsRebuilt(stream) {
|
|
18586
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18587
|
+
}
|
|
18445
18588
|
var FecReleaseSequencer = class {
|
|
18446
18589
|
#streams = /* @__PURE__ */ new Map();
|
|
18447
18590
|
#gate;
|
|
@@ -18569,19 +18712,39 @@ var FecReleaseSequencer = class {
|
|
|
18569
18712
|
}
|
|
18570
18713
|
/**
|
|
18571
18714
|
* These streams' sink is being replaced by one that has seen no source
|
|
18572
|
-
* frame yet. First
|
|
18573
|
-
*
|
|
18574
|
-
*
|
|
18575
|
-
*
|
|
18715
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
18716
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
18717
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
18718
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
18719
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
18720
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
18721
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
18722
|
+
* released.
|
|
18576
18723
|
*/
|
|
18577
18724
|
unsource(packetId, catalogTrackName) {
|
|
18578
18725
|
for (const stream of this.#streams.values()) {
|
|
18579
18726
|
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
18580
|
-
|
|
18727
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
18728
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
18729
|
+
else this.#drain(stream, true);
|
|
18730
|
+
}
|
|
18581
18731
|
stream.sourced = false;
|
|
18582
18732
|
}
|
|
18583
18733
|
}
|
|
18584
18734
|
}
|
|
18735
|
+
/**
|
|
18736
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
18737
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
18738
|
+
* packetId.
|
|
18739
|
+
*/
|
|
18740
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
18741
|
+
for (const stream of this.#streams.values()) {
|
|
18742
|
+
if (stream.packetId !== packetId) continue;
|
|
18743
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
18744
|
+
if (holdsRebuilt(stream)) return true;
|
|
18745
|
+
}
|
|
18746
|
+
return false;
|
|
18747
|
+
}
|
|
18585
18748
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18586
18749
|
poke(streamKey) {
|
|
18587
18750
|
if (streamKey !== void 0) {
|
|
@@ -18674,7 +18837,8 @@ var FecReleaseSequencer = class {
|
|
|
18674
18837
|
lastSbn: void 0,
|
|
18675
18838
|
discontinuity: false,
|
|
18676
18839
|
rapSsId: void 0,
|
|
18677
|
-
sourced: false
|
|
18840
|
+
sourced: false,
|
|
18841
|
+
draining: 0
|
|
18678
18842
|
};
|
|
18679
18843
|
this.#streams.set(streamKey, stream);
|
|
18680
18844
|
}
|
|
@@ -18682,9 +18846,22 @@ var FecReleaseSequencer = class {
|
|
|
18682
18846
|
}
|
|
18683
18847
|
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
18684
18848
|
#drain(stream, asUnanchored = false) {
|
|
18849
|
+
stream.draining++;
|
|
18850
|
+
try {
|
|
18851
|
+
this.#drainLoop(stream, asUnanchored);
|
|
18852
|
+
} finally {
|
|
18853
|
+
stream.draining--;
|
|
18854
|
+
}
|
|
18855
|
+
}
|
|
18856
|
+
#drainLoop(stream, asUnanchored) {
|
|
18685
18857
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18686
18858
|
const head = stream.buf[0];
|
|
18687
18859
|
const last = stream.last;
|
|
18860
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
18861
|
+
stream.buf.shift();
|
|
18862
|
+
this.#release(stream, head, void 0);
|
|
18863
|
+
continue;
|
|
18864
|
+
}
|
|
18688
18865
|
if (stream.discontinuity) {
|
|
18689
18866
|
stream.discontinuity = false;
|
|
18690
18867
|
stream.buf.shift();
|
|
@@ -18743,6 +18920,8 @@ var FecReleaseSequencer = class {
|
|
|
18743
18920
|
* And it goes at once while a staged switch decides on when frames arrive
|
|
18744
18921
|
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
18745
18922
|
* sink being replaced: unanchored, it had already reached the sink.
|
|
18923
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
18924
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
18746
18925
|
*/
|
|
18747
18926
|
#cursorPassed(stream, asUnanchored) {
|
|
18748
18927
|
let at2 = -1;
|
|
@@ -18757,6 +18936,8 @@ var FecReleaseSequencer = class {
|
|
|
18757
18936
|
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));
|
|
18758
18937
|
}
|
|
18759
18938
|
#release(stream, frame, decision) {
|
|
18939
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18940
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18760
18941
|
if (decision) frame.meta.gapBefore = decision;
|
|
18761
18942
|
stream.last = frame;
|
|
18762
18943
|
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
@@ -18764,7 +18945,7 @@ var FecReleaseSequencer = class {
|
|
|
18764
18945
|
}
|
|
18765
18946
|
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
18766
18947
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18767
|
-
this.#emit(frame.data, frame.meta);
|
|
18948
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18768
18949
|
}
|
|
18769
18950
|
};
|
|
18770
18951
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -19094,6 +19275,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19094
19275
|
unsourceFecRelease(packetId, catalogTrackName) {
|
|
19095
19276
|
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
19096
19277
|
}
|
|
19278
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
19279
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
19280
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
19281
|
+
}
|
|
19097
19282
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
19098
19283
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
19099
19284
|
if (!this.#fecSequencer) return;
|
|
@@ -23069,6 +23254,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
|
23069
23254
|
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23070
23255
|
var OVERLAY_PENDING_LIMIT = 256;
|
|
23071
23256
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23257
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
23072
23258
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
23073
23259
|
function assertStreamIdentity(role, stream) {
|
|
23074
23260
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -23245,6 +23431,17 @@ var MmtpRouter = class {
|
|
|
23245
23431
|
* an MPU no member of which arrived).
|
|
23246
23432
|
*/
|
|
23247
23433
|
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23434
|
+
/**
|
|
23435
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
23436
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
23437
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
23438
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
23439
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
23440
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
23441
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
23442
|
+
* this offset did.
|
|
23443
|
+
*/
|
|
23444
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23248
23445
|
/**
|
|
23249
23446
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
23250
23447
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -23522,13 +23719,18 @@ var MmtpRouter = class {
|
|
|
23522
23719
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
23523
23720
|
}
|
|
23524
23721
|
};
|
|
23525
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23722
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23526
23723
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
23527
23724
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
23528
23725
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
23529
23726
|
}
|
|
23530
|
-
|
|
23531
|
-
|
|
23727
|
+
if (span === void 0) {
|
|
23728
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23729
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23730
|
+
} else {
|
|
23731
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23732
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23733
|
+
}
|
|
23532
23734
|
};
|
|
23533
23735
|
}
|
|
23534
23736
|
}
|
|
@@ -23573,8 +23775,9 @@ var MmtpRouter = class {
|
|
|
23573
23775
|
}
|
|
23574
23776
|
/**
|
|
23575
23777
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23576
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23577
|
-
*
|
|
23778
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
23779
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
23780
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
23578
23781
|
*/
|
|
23579
23782
|
get restampedRecoveredSequenceCount() {
|
|
23580
23783
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23904,7 +24107,8 @@ var MmtpRouter = class {
|
|
|
23904
24107
|
fec,
|
|
23905
24108
|
k,
|
|
23906
24109
|
offsets: /* @__PURE__ */ new Map(),
|
|
23907
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24110
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24111
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23908
24112
|
};
|
|
23909
24113
|
this.#overlays.set(repairPacketId, inst);
|
|
23910
24114
|
return () => {
|
|
@@ -23945,6 +24149,7 @@ var MmtpRouter = class {
|
|
|
23945
24149
|
inst.last = void 0;
|
|
23946
24150
|
inst.offsets.clear();
|
|
23947
24151
|
inst.moqBlocks.clear();
|
|
24152
|
+
inst.rebuilt.clear();
|
|
23948
24153
|
}
|
|
23949
24154
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23950
24155
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23997,13 +24202,23 @@ var MmtpRouter = class {
|
|
|
23997
24202
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23998
24203
|
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
23999
24204
|
* FEC reads it.
|
|
24205
|
+
*
|
|
24206
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
24207
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
24208
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
24209
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
24210
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
24211
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
24000
24212
|
*/
|
|
24001
24213
|
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
24002
24214
|
if (this.#overlays.size === 0) return;
|
|
24003
24215
|
const ssId = keyframeOverlaySsId(header);
|
|
24004
24216
|
if (ssId === void 0) return;
|
|
24217
|
+
let rebuiltPsn;
|
|
24005
24218
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
24006
24219
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24220
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24221
|
+
inst.rebuilt.delete(ssId);
|
|
24007
24222
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
24008
24223
|
if (identity !== null) {
|
|
24009
24224
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
@@ -24033,6 +24248,14 @@ var MmtpRouter = class {
|
|
|
24033
24248
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
24034
24249
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
24035
24250
|
}
|
|
24251
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
24252
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
24253
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
24254
|
+
this.#restampedRecoveredSeqs++;
|
|
24255
|
+
} else if (baseSsId !== void 0) {
|
|
24256
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24257
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24258
|
+
}
|
|
24036
24259
|
}
|
|
24037
24260
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
24038
24261
|
inst.moqBlocks.add(blockStart);
|
|
@@ -24096,24 +24319,35 @@ var MmtpRouter = class {
|
|
|
24096
24319
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
24097
24320
|
const restored = /* @__PURE__ */ new Set();
|
|
24098
24321
|
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24322
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
24099
24323
|
const symbols = result.symbols.flatMap((sym) => {
|
|
24324
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24100
24325
|
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24101
24326
|
if (anchor !== void 0) {
|
|
24102
|
-
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24103
24327
|
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
24104
24328
|
restored.add(packetSequenceNumber);
|
|
24105
24329
|
const out = { ...sym, packetSequenceNumber };
|
|
24106
24330
|
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24107
|
-
if (baseSsId !== void 0)
|
|
24331
|
+
if (baseSsId !== void 0) {
|
|
24332
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24333
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24334
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
24335
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24336
|
+
}
|
|
24337
|
+
}
|
|
24108
24338
|
return [out];
|
|
24109
24339
|
}
|
|
24110
24340
|
if (!restamped) {
|
|
24111
|
-
const
|
|
24112
|
-
|
|
24113
|
-
|
|
24114
|
-
|
|
24115
|
-
|
|
24116
|
-
|
|
24341
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24342
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24343
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24344
|
+
if (baseSsId !== void 0) {
|
|
24345
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24346
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
24347
|
+
} else {
|
|
24348
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
24349
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
24350
|
+
}
|
|
24117
24351
|
return [sym];
|
|
24118
24352
|
}
|
|
24119
24353
|
this.#overlayStats.unsequencedDropped++;
|
|
@@ -24137,12 +24371,13 @@ var MmtpRouter = class {
|
|
|
24137
24371
|
firstSsIds
|
|
24138
24372
|
});
|
|
24139
24373
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24140
|
-
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24374
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
24141
24375
|
}
|
|
24142
24376
|
/**
|
|
24143
24377
|
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24144
24378
|
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24145
|
-
* from a received member of its MPU, else unstampedBaseSsId
|
|
24379
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
24380
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
24146
24381
|
* so it is fed to the base decoder as a rebuilt source
|
|
24147
24382
|
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24148
24383
|
* fewer to solve, and its repair can recover the block's other packets.
|
|
@@ -24897,7 +25132,7 @@ var MmtpRouter = class {
|
|
|
24897
25132
|
catalogTrackName
|
|
24898
25133
|
)) continue;
|
|
24899
25134
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24900
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25135
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24901
25136
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24902
25137
|
}
|
|
24903
25138
|
if (payload.oversized) continue;
|
|
@@ -24985,6 +25220,7 @@ var MmtpRouter = class {
|
|
|
24985
25220
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24986
25221
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
24987
25222
|
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25223
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24988
25224
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24989
25225
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24990
25226
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -25167,7 +25403,9 @@ var MmtpRouter = class {
|
|
|
25167
25403
|
droppedIncomingFrames: 0
|
|
25168
25404
|
};
|
|
25169
25405
|
this.#staged = state;
|
|
25170
|
-
this.#
|
|
25406
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25407
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25408
|
+
}
|
|
25171
25409
|
return {
|
|
25172
25410
|
cancel: () => {
|
|
25173
25411
|
if (this.#staged !== state) return false;
|
|
@@ -25250,7 +25488,17 @@ var MmtpRouter = class {
|
|
|
25250
25488
|
*/
|
|
25251
25489
|
#routeParsedFrame(data, meta) {
|
|
25252
25490
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
25253
|
-
if (this.#staged !== null
|
|
25491
|
+
if (this.#staged !== null) {
|
|
25492
|
+
let consumed = false;
|
|
25493
|
+
this.#fecPokeDepth++;
|
|
25494
|
+
try {
|
|
25495
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
25496
|
+
} finally {
|
|
25497
|
+
this.#fecPokeDepth--;
|
|
25498
|
+
this.#flushPendingFecPokes();
|
|
25499
|
+
}
|
|
25500
|
+
if (consumed) return;
|
|
25501
|
+
}
|
|
25254
25502
|
this.#deliverParsedFrame(data, meta);
|
|
25255
25503
|
}
|
|
25256
25504
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -25402,12 +25650,16 @@ var MmtpRouter = class {
|
|
|
25402
25650
|
this.#parser.deferFecRelease(work);
|
|
25403
25651
|
} finally {
|
|
25404
25652
|
this.#fecPokeDepth--;
|
|
25405
|
-
|
|
25406
|
-
|
|
25407
|
-
|
|
25408
|
-
|
|
25409
|
-
|
|
25410
|
-
|
|
25653
|
+
this.#flushPendingFecPokes();
|
|
25654
|
+
}
|
|
25655
|
+
}
|
|
25656
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
25657
|
+
#flushPendingFecPokes() {
|
|
25658
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
25659
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
25660
|
+
this.#pendingFecPokes.clear();
|
|
25661
|
+
for (const poke of pokes) {
|
|
25662
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
25411
25663
|
}
|
|
25412
25664
|
}
|
|
25413
25665
|
}
|
|
@@ -28211,6 +28463,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
28211
28463
|
function isSymbolCounts(value) {
|
|
28212
28464
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
28213
28465
|
}
|
|
28466
|
+
function isSpanFailure(value) {
|
|
28467
|
+
if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
|
|
28468
|
+
const length = value["length"];
|
|
28469
|
+
const sources = value["sources"];
|
|
28470
|
+
return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
|
|
28471
|
+
}
|
|
28214
28472
|
function isNullableNonNegative(value) {
|
|
28215
28473
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
28216
28474
|
}
|
|
@@ -28230,7 +28488,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
28230
28488
|
case "recovered":
|
|
28231
28489
|
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"]);
|
|
28232
28490
|
case "failed":
|
|
28233
|
-
return hasExactKeys(
|
|
28491
|
+
return hasExactKeys(
|
|
28492
|
+
value,
|
|
28493
|
+
"span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
|
|
28494
|
+
) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
|
|
28234
28495
|
case "repair-preempted":
|
|
28235
28496
|
case "repair-deferred":
|
|
28236
28497
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -28496,7 +28757,7 @@ var MulticastManager = class {
|
|
|
28496
28757
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
28497
28758
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
28498
28759
|
},
|
|
28499
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28760
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
28500
28761
|
const pathCount = this.#fec.blockPathCount(
|
|
28501
28762
|
sbn,
|
|
28502
28763
|
packetId,
|
|
@@ -28514,7 +28775,8 @@ var MulticastManager = class {
|
|
|
28514
28775
|
symbols: {
|
|
28515
28776
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
28516
28777
|
moq: pathCount?.moqSymbols ?? 0
|
|
28517
|
-
}
|
|
28778
|
+
},
|
|
28779
|
+
...span === void 0 ? {} : { span }
|
|
28518
28780
|
});
|
|
28519
28781
|
},
|
|
28520
28782
|
onFrameGap: (decision) => {
|
|
@@ -29894,10 +30156,12 @@ var MulticastManager = class {
|
|
|
29894
30156
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
29895
30157
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
29896
30158
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
29897
|
-
* once its range FETCH ends (or none is sent).
|
|
30159
|
+
* once its range FETCH ends (or none is sent). `onExpired` is told of a
|
|
30160
|
+
* pending hole the stream runs past the locator's horizon: returning true,
|
|
30161
|
+
* the caller closes it (`closeIsoHoles`); otherwise it is discarded.
|
|
29898
30162
|
*/
|
|
29899
|
-
onUndelimitedLoss(cb) {
|
|
29900
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30163
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30164
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
29901
30165
|
}
|
|
29902
30166
|
/**
|
|
29903
30167
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -30133,7 +30397,8 @@ var MulticastManager = class {
|
|
|
30133
30397
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
30134
30398
|
...event,
|
|
30135
30399
|
block: Object.freeze({ ...event.block }),
|
|
30136
|
-
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
|
|
30400
|
+
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
|
|
30401
|
+
..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
|
|
30137
30402
|
}
|
|
30138
30403
|
);
|
|
30139
30404
|
if (this.#logRepairLifecycle) {
|
|
@@ -32805,6 +33070,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32805
33070
|
|
|
32806
33071
|
// src/repair-fetch.ts
|
|
32807
33072
|
var MOQT_PRIORITY_MAX = 255;
|
|
33073
|
+
var WALK_LEAD_MS = 3;
|
|
32808
33074
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
32809
33075
|
function isMoqtPriority(value) {
|
|
32810
33076
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -32855,8 +33121,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
32855
33121
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
32856
33122
|
};
|
|
32857
33123
|
const clearEntry = (entry) => {
|
|
32858
|
-
|
|
32859
|
-
|
|
33124
|
+
const { range } = entry;
|
|
33125
|
+
if (range) {
|
|
33126
|
+
atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
|
|
33127
|
+
} else {
|
|
33128
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
33129
|
+
}
|
|
33130
|
+
};
|
|
33131
|
+
const atReport = (range, work) => {
|
|
33132
|
+
const fec = mc.fecManager;
|
|
33133
|
+
if (range.groups !== null || fec.withReceiptTime === void 0) work();
|
|
33134
|
+
else fec.withReceiptTime(range.reportedAt, work);
|
|
32860
33135
|
};
|
|
32861
33136
|
const releaseHold = (entry) => {
|
|
32862
33137
|
if (entry.range) return;
|
|
@@ -32923,21 +33198,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
32923
33198
|
const admit = (entry, now) => {
|
|
32924
33199
|
const budgetMs = entry.deadlineAt - now;
|
|
32925
33200
|
const rttMs = readRtt();
|
|
32926
|
-
const reason =
|
|
33201
|
+
const reason = refusal(entry, now, rttMs);
|
|
32927
33202
|
if (reason === null) return rttMs;
|
|
32928
33203
|
skip(entry, reason, rttMs, budgetMs);
|
|
32929
33204
|
return null;
|
|
32930
33205
|
};
|
|
33206
|
+
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";
|
|
32931
33207
|
const retire = (entry) => {
|
|
32932
33208
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
32933
33209
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
32934
33210
|
};
|
|
32935
|
-
const
|
|
33211
|
+
const holdsBack = (entry, priority, videoWalks = true) => {
|
|
33212
|
+
if (entry.priority >= priority) return false;
|
|
33213
|
+
const { walk } = entry;
|
|
33214
|
+
if (walk === void 0) return true;
|
|
33215
|
+
if (entry.priority !== 0 || !videoWalks) return false;
|
|
33216
|
+
return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
|
|
33217
|
+
};
|
|
33218
|
+
const outstandingBelow = (priority, videoWalks = true) => {
|
|
32936
33219
|
for (const entry of entries.values()) {
|
|
32937
|
-
if (entry
|
|
33220
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
32938
33221
|
}
|
|
32939
33222
|
return false;
|
|
32940
33223
|
};
|
|
33224
|
+
const yieldVideoWalks = (queued) => {
|
|
33225
|
+
const now = performance.now();
|
|
33226
|
+
const rttMs = readRtt();
|
|
33227
|
+
const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
|
|
33228
|
+
if (!due) return;
|
|
33229
|
+
for (const entry of entries.values()) {
|
|
33230
|
+
const walk = entry.walk;
|
|
33231
|
+
if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
|
|
33232
|
+
if (walk.group.controller.signal.aborted) continue;
|
|
33233
|
+
walk.yielded = true;
|
|
33234
|
+
walk.group.controller.abort();
|
|
33235
|
+
}
|
|
33236
|
+
};
|
|
32941
33237
|
const preemptBestEffort = () => {
|
|
32942
33238
|
for (const entry of entries.values()) {
|
|
32943
33239
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -32966,7 +33262,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
32966
33262
|
}
|
|
32967
33263
|
const rttMs = admit(entry, now);
|
|
32968
33264
|
if (rttMs === null) return;
|
|
32969
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33265
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33266
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
32970
33267
|
if (counted) entry.attempts++;
|
|
32971
33268
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
32972
33269
|
rttMs,
|
|
@@ -32988,6 +33285,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
32988
33285
|
}
|
|
32989
33286
|
void execute(entry, run);
|
|
32990
33287
|
};
|
|
33288
|
+
const leaveWalkedGroup = (block) => {
|
|
33289
|
+
for (const entry of entries.values()) {
|
|
33290
|
+
const group = entry.walk?.group;
|
|
33291
|
+
if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
|
|
33292
|
+
group.controller.abort();
|
|
33293
|
+
}
|
|
33294
|
+
}
|
|
33295
|
+
};
|
|
32991
33296
|
const dispatch = () => {
|
|
32992
33297
|
if (disposed) return;
|
|
32993
33298
|
const now = performance.now();
|
|
@@ -33012,6 +33317,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
33012
33317
|
for (const entry of queued) {
|
|
33013
33318
|
if (entry.priority === 0) start(entry);
|
|
33014
33319
|
}
|
|
33320
|
+
yieldVideoWalks(queued);
|
|
33015
33321
|
if (outstandingBelow(1)) return;
|
|
33016
33322
|
for (const entry of queued) {
|
|
33017
33323
|
if (entry.priority === 1) start(entry);
|
|
@@ -33038,6 +33344,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
33038
33344
|
}
|
|
33039
33345
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
33040
33346
|
};
|
|
33347
|
+
const groupServed = (sbn, packetId, catalogTrackName) => {
|
|
33348
|
+
if (mc.fecManager.blockSymbolCounts === void 0) return false;
|
|
33349
|
+
const counts = symbolCounts(sbn, packetId, catalogTrackName);
|
|
33350
|
+
if (counts === void 0 || counts.authoredLength === void 0) return true;
|
|
33351
|
+
return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
|
|
33352
|
+
};
|
|
33353
|
+
const statedEnd = (sbn, packetId, catalogTrackName) => {
|
|
33354
|
+
const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
|
|
33355
|
+
return length === void 0 ? void 0 : serial32Add(sbn, length);
|
|
33356
|
+
};
|
|
33041
33357
|
const finish = (entry, run, fed, error) => {
|
|
33042
33358
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
33043
33359
|
return;
|
|
@@ -33082,27 +33398,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
33082
33398
|
signal: run.controller.signal,
|
|
33083
33399
|
subscriberPriority: entry.subscriberPriority
|
|
33084
33400
|
};
|
|
33401
|
+
const { range } = entry;
|
|
33402
|
+
const feed = (packet) => {
|
|
33403
|
+
if (entry.catalogTrackName === void 0) {
|
|
33404
|
+
mc.processRepairFromMoQ(packet, entry.packetId);
|
|
33405
|
+
} else {
|
|
33406
|
+
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
33407
|
+
}
|
|
33408
|
+
};
|
|
33085
33409
|
try {
|
|
33086
|
-
const packets = entry.
|
|
33410
|
+
const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
|
|
33087
33411
|
...common,
|
|
33088
|
-
startGroup:
|
|
33089
|
-
endGroup:
|
|
33090
|
-
lostSsId:
|
|
33091
|
-
})
|
|
33412
|
+
startGroup: range.groups.startGroup,
|
|
33413
|
+
endGroup: range.groups.endGroup,
|
|
33414
|
+
lostSsId: range.lostSsId
|
|
33415
|
+
});
|
|
33092
33416
|
for await (const packet of packets) {
|
|
33093
33417
|
if (run.stopped !== null || entry.running !== run) break;
|
|
33094
33418
|
fed++;
|
|
33095
|
-
if (
|
|
33096
|
-
|
|
33097
|
-
} else {
|
|
33098
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
33099
|
-
}
|
|
33419
|
+
if (range === void 0) feed(packet);
|
|
33420
|
+
else atReport(range, () => feed(packet));
|
|
33100
33421
|
}
|
|
33101
33422
|
finish(entry, run, fed, void 0);
|
|
33102
33423
|
} catch (error) {
|
|
33103
33424
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
33104
33425
|
}
|
|
33105
33426
|
};
|
|
33427
|
+
async function* walkHole(entry, walk, hole, common) {
|
|
33428
|
+
const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
|
|
33429
|
+
for (let first = true; !common.signal.aborted; first = false) {
|
|
33430
|
+
if (!first && entry.priority !== 0 && outstandingBelow(1)) {
|
|
33431
|
+
stop(entry, "preempted");
|
|
33432
|
+
note("repair-preempted", entry);
|
|
33433
|
+
return;
|
|
33434
|
+
}
|
|
33435
|
+
if (!first && entry.priority === 0) {
|
|
33436
|
+
dispatch();
|
|
33437
|
+
if (common.signal.aborted) return;
|
|
33438
|
+
}
|
|
33439
|
+
const sbn = walk.next;
|
|
33440
|
+
let next;
|
|
33441
|
+
if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
|
|
33442
|
+
const rsIds = /* @__PURE__ */ new Set();
|
|
33443
|
+
const group = new AbortController();
|
|
33444
|
+
const cancel = () => group.abort();
|
|
33445
|
+
common.signal.addEventListener("abort", cancel, { once: true });
|
|
33446
|
+
walk.group = { sbn, controller: group };
|
|
33447
|
+
try {
|
|
33448
|
+
for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
|
|
33449
|
+
yield packet;
|
|
33450
|
+
const id = extractRepairFecPayloadId(packet);
|
|
33451
|
+
if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
|
|
33452
|
+
next ??= serial32Add(sbn, id.ssbLength);
|
|
33453
|
+
rsIds.add(id.rsId);
|
|
33454
|
+
if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
|
|
33455
|
+
group.abort();
|
|
33456
|
+
break;
|
|
33457
|
+
}
|
|
33458
|
+
}
|
|
33459
|
+
} catch (error) {
|
|
33460
|
+
if (!group.signal.aborted || common.signal.aborted) throw error;
|
|
33461
|
+
} finally {
|
|
33462
|
+
common.signal.removeEventListener("abort", cancel);
|
|
33463
|
+
walk.group = null;
|
|
33464
|
+
}
|
|
33465
|
+
if (walk.yielded) {
|
|
33466
|
+
walk.yielded = false;
|
|
33467
|
+
continue;
|
|
33468
|
+
}
|
|
33469
|
+
}
|
|
33470
|
+
next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
|
|
33471
|
+
if (next === void 0) return;
|
|
33472
|
+
if (serial32Distance(entry.sbn, next) > lostAt) return;
|
|
33473
|
+
walk.next = next;
|
|
33474
|
+
}
|
|
33475
|
+
}
|
|
33106
33476
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
33107
33477
|
for (const entry of [...entries.values()]) {
|
|
33108
33478
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -33298,7 +33668,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
33298
33668
|
throw error;
|
|
33299
33669
|
}
|
|
33300
33670
|
};
|
|
33301
|
-
|
|
33671
|
+
const trackKind = (packetId) => {
|
|
33672
|
+
try {
|
|
33673
|
+
return mc.fecManager.trackKindFor?.(packetId);
|
|
33674
|
+
} catch {
|
|
33675
|
+
return void 0;
|
|
33676
|
+
}
|
|
33677
|
+
};
|
|
33678
|
+
const walkDeadlineAt = (packetId, now, deadlineAt) => {
|
|
33679
|
+
let repairDeadlineMs;
|
|
33680
|
+
try {
|
|
33681
|
+
repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
|
|
33682
|
+
} catch {
|
|
33683
|
+
repairDeadlineMs = void 0;
|
|
33684
|
+
}
|
|
33685
|
+
if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
|
|
33686
|
+
return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
|
|
33687
|
+
};
|
|
33688
|
+
const onHoleExpired = (hole) => {
|
|
33689
|
+
if (disposed) return false;
|
|
33690
|
+
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);
|
|
33691
|
+
if (entry === void 0) return false;
|
|
33692
|
+
queueMicrotask(() => {
|
|
33693
|
+
if (entries.get(entry.key) !== entry) return;
|
|
33694
|
+
retire(entry);
|
|
33695
|
+
stop(entry, "expired");
|
|
33696
|
+
clearEntry(entry);
|
|
33697
|
+
dispatch();
|
|
33698
|
+
});
|
|
33699
|
+
return true;
|
|
33700
|
+
};
|
|
33701
|
+
if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
|
|
33302
33702
|
mc.onUndelimitedLoss((loss) => {
|
|
33303
33703
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
33304
33704
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -33306,9 +33706,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
33306
33706
|
const blockProfile = blockProfileOf(packetId);
|
|
33307
33707
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
33308
33708
|
if (!target) return close();
|
|
33309
|
-
const
|
|
33310
|
-
const
|
|
33311
|
-
|
|
33709
|
+
const walked = opts.rangeTransport === void 0;
|
|
33710
|
+
const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
|
|
33711
|
+
const priority = repairPriorityForClass(holeClass);
|
|
33712
|
+
const groups = walked ? null : rangeFor(loss, target.repairTrack);
|
|
33713
|
+
if (!walked && groups === null) {
|
|
33312
33714
|
close();
|
|
33313
33715
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
33314
33716
|
return;
|
|
@@ -33319,8 +33721,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
33319
33721
|
packetId,
|
|
33320
33722
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
33321
33723
|
};
|
|
33322
|
-
const wire = wirePriority(identity,
|
|
33323
|
-
|
|
33724
|
+
const wire = wirePriority(identity, holeClass);
|
|
33725
|
+
if (wire === null) return close();
|
|
33726
|
+
const now = performance.now();
|
|
33727
|
+
const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
|
|
33324
33728
|
if (deadline === null) {
|
|
33325
33729
|
close();
|
|
33326
33730
|
mc.noteRepairSkipped(
|
|
@@ -33332,6 +33736,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
33332
33736
|
);
|
|
33333
33737
|
return;
|
|
33334
33738
|
}
|
|
33739
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
33335
33740
|
const entry = {
|
|
33336
33741
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
33337
33742
|
...identity,
|
|
@@ -33339,15 +33744,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
33339
33744
|
blockProfile,
|
|
33340
33745
|
priority,
|
|
33341
33746
|
subscriberPriority: wire.subscriberPriority,
|
|
33342
|
-
deadlineAt
|
|
33343
|
-
playoutDeadlineAt:
|
|
33747
|
+
deadlineAt,
|
|
33748
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
33344
33749
|
anchorAt: deadline.anchorAt,
|
|
33345
33750
|
delayMs: deadline.delayMs,
|
|
33346
33751
|
bound: deadline.bound,
|
|
33347
33752
|
order: enqueueOrder++,
|
|
33348
33753
|
attempts: 0,
|
|
33349
33754
|
running: null,
|
|
33350
|
-
range: {
|
|
33755
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33756
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
33351
33757
|
};
|
|
33352
33758
|
const previous = entries.get(entry.key);
|
|
33353
33759
|
if (previous) {
|
|
@@ -33357,7 +33763,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
33357
33763
|
entries.set(entry.key, entry);
|
|
33358
33764
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
33359
33765
|
dispatch();
|
|
33360
|
-
});
|
|
33766
|
+
}, onHoleExpired);
|
|
33361
33767
|
}
|
|
33362
33768
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
33363
33769
|
queueMicrotask(() => {
|