@blockcast/mmt-container 2.2.0-main.3cc3ad1f4323 → 2.2.0-main.4ccba0f2f32a
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 +714 -147
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +37 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +192 -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 +18 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +261 -50
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +26 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +535 -121
- 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 +179 -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 +231 -58
- package/src/multicast-manager.ts +118 -14
- 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;
|
|
@@ -13948,6 +14015,10 @@ var FECManager = class _FECManager {
|
|
|
13948
14015
|
// 'pending' forever, which would park every later frame of its stream in
|
|
13949
14016
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
13950
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
|
+
*/
|
|
13951
14022
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
13952
14023
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13953
14024
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
@@ -13964,6 +14035,9 @@ var FECManager = class _FECManager {
|
|
|
13964
14035
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
13965
14036
|
// symbols; waiting for K fails every one of them.
|
|
13966
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();
|
|
13967
14041
|
this.#ssbLengthConflictCount = 0;
|
|
13968
14042
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
13969
14043
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -14161,7 +14235,10 @@ var FECManager = class _FECManager {
|
|
|
14161
14235
|
// arrives via onBlockRecovered callback.
|
|
14162
14236
|
this.onBlockStarted = null;
|
|
14163
14237
|
this.onBlockComplete = null;
|
|
14164
|
-
/**
|
|
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
|
+
*/
|
|
14165
14242
|
this.onBlockFailed = null;
|
|
14166
14243
|
/**
|
|
14167
14244
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -14203,6 +14280,8 @@ var FECManager = class _FECManager {
|
|
|
14203
14280
|
* for it, which it must be.
|
|
14204
14281
|
*/
|
|
14205
14282
|
this.#onUndelimitedLoss = null;
|
|
14283
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
14284
|
+
this.#onIsoHoleExpired = null;
|
|
14206
14285
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
14207
14286
|
this.onBlockVanished = null;
|
|
14208
14287
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -14247,6 +14326,7 @@ var FECManager = class _FECManager {
|
|
|
14247
14326
|
#blockSourceEsis;
|
|
14248
14327
|
#blockRebuiltEsis;
|
|
14249
14328
|
#blockAuthoredLengths;
|
|
14329
|
+
#isoSpanKeys;
|
|
14250
14330
|
#ssbLengthConflictCount;
|
|
14251
14331
|
#blockRepairRsIds;
|
|
14252
14332
|
#blockRsbLengths;
|
|
@@ -14313,6 +14393,7 @@ var FECManager = class _FECManager {
|
|
|
14313
14393
|
#decoderUnavailableEvents;
|
|
14314
14394
|
#repairSymbolSizeMismatchCount;
|
|
14315
14395
|
#onUndelimitedLoss;
|
|
14396
|
+
#onIsoHoleExpired;
|
|
14316
14397
|
#encodeTrackName(catalogTrackName) {
|
|
14317
14398
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
14318
14399
|
if (encoded === void 0) {
|
|
@@ -14784,7 +14865,10 @@ var FECManager = class _FECManager {
|
|
|
14784
14865
|
timer.handle = null;
|
|
14785
14866
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
14786
14867
|
};
|
|
14787
|
-
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
|
+
);
|
|
14788
14872
|
this.#repairDeadlines.set(key, timer);
|
|
14789
14873
|
}
|
|
14790
14874
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -14928,8 +15012,14 @@ var FECManager = class _FECManager {
|
|
|
14928
15012
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
14929
15013
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
14930
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).
|
|
14931
15021
|
*/
|
|
14932
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
15022
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
14933
15023
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
14934
15024
|
const now = performance.now();
|
|
14935
15025
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -14968,7 +15058,7 @@ var FECManager = class _FECManager {
|
|
|
14968
15058
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
14969
15059
|
};
|
|
14970
15060
|
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))) {
|
|
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))) {
|
|
14972
15062
|
discontinuity();
|
|
14973
15063
|
return;
|
|
14974
15064
|
}
|
|
@@ -15230,17 +15320,21 @@ var FECManager = class _FECManager {
|
|
|
15230
15320
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
15231
15321
|
this.#repairHoldUntilMs.delete(key);
|
|
15232
15322
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
15233
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
15323
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
15234
15324
|
};
|
|
15235
|
-
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
|
+
);
|
|
15236
15329
|
this.#activeBlocks.set(key, timer);
|
|
15237
15330
|
this.#indexBlockKey(key, sbn, packetId);
|
|
15238
15331
|
}
|
|
15239
15332
|
/**
|
|
15240
15333
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
15241
|
-
* 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.
|
|
15242
15336
|
*/
|
|
15243
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
15337
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
15244
15338
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
15245
15339
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
15246
15340
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -15256,7 +15350,7 @@ var FECManager = class _FECManager {
|
|
|
15256
15350
|
if (workerClient) {
|
|
15257
15351
|
this.#pendingWorkerCleanups.add(key);
|
|
15258
15352
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
15259
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
15353
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
15260
15354
|
workerClient.cleanup(sbn);
|
|
15261
15355
|
} else {
|
|
15262
15356
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -15570,6 +15664,7 @@ var FECManager = class _FECManager {
|
|
|
15570
15664
|
resetMap(this.#blockSourceEsis);
|
|
15571
15665
|
resetMap(this.#blockRebuiltEsis);
|
|
15572
15666
|
resetMap(this.#blockAuthoredLengths);
|
|
15667
|
+
resetSet(this.#isoSpanKeys);
|
|
15573
15668
|
resetMap(this.#blockRepairRsIds);
|
|
15574
15669
|
resetMap(this.#blockRsbLengths);
|
|
15575
15670
|
resetMap(this.#heldLayerStates);
|
|
@@ -15695,6 +15790,7 @@ var FECManager = class _FECManager {
|
|
|
15695
15790
|
dropMap(this.#blockSourceEsis);
|
|
15696
15791
|
dropMap(this.#blockRebuiltEsis);
|
|
15697
15792
|
dropMap(this.#blockAuthoredLengths);
|
|
15793
|
+
dropSet(this.#isoSpanKeys);
|
|
15698
15794
|
dropMap(this.#blockRepairRsIds);
|
|
15699
15795
|
dropMap(this.#blockRsbLengths);
|
|
15700
15796
|
dropMap(this.#blockPathCounts);
|
|
@@ -16037,8 +16133,9 @@ var FECManager = class _FECManager {
|
|
|
16037
16133
|
return true;
|
|
16038
16134
|
}
|
|
16039
16135
|
/**
|
|
16040
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
16041
|
-
*
|
|
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.
|
|
16042
16139
|
*/
|
|
16043
16140
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
16044
16141
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -16176,6 +16273,7 @@ var FECManager = class _FECManager {
|
|
|
16176
16273
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16177
16274
|
const evictedBefore = locator.heldEvicted;
|
|
16178
16275
|
const held = locator.hold(ssId, source);
|
|
16276
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16179
16277
|
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16180
16278
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16181
16279
|
if (evicted > 0) {
|
|
@@ -16197,17 +16295,20 @@ var FECManager = class _FECManager {
|
|
|
16197
16295
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
16198
16296
|
*/
|
|
16199
16297
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
16200
|
-
const
|
|
16298
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16299
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
16300
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16201
16301
|
if (placement.status !== "placed") {
|
|
16202
16302
|
this.#isoRepairRefusedCount++;
|
|
16203
16303
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
16204
16304
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
16205
16305
|
}
|
|
16306
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
16206
16307
|
return placement;
|
|
16207
16308
|
}
|
|
16309
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
16208
16310
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
16209
16311
|
if (placement.hole !== void 0) {
|
|
16210
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
16211
16312
|
const hole = placement.hole.block;
|
|
16212
16313
|
const previous = locator.blockEndingAt(hole.start);
|
|
16213
16314
|
const loss = {
|
|
@@ -16228,17 +16329,29 @@ var FECManager = class _FECManager {
|
|
|
16228
16329
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
16229
16330
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
16230
16331
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
16231
|
-
* 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.
|
|
16232
16336
|
*/
|
|
16233
|
-
setUndelimitedLossHandler(handler) {
|
|
16337
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
16234
16338
|
this.#onUndelimitedLoss = handler;
|
|
16339
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
16235
16340
|
this.#deferIsoGaps = handler !== null;
|
|
16236
16341
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
16237
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
|
+
}
|
|
16238
16350
|
/**
|
|
16239
|
-
* The range repair for a hole ended: every hole
|
|
16240
|
-
*
|
|
16241
|
-
* 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).
|
|
16242
16355
|
*/
|
|
16243
16356
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
16244
16357
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -16257,7 +16370,10 @@ var FECManager = class _FECManager {
|
|
|
16257
16370
|
*/
|
|
16258
16371
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
16259
16372
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
16260
|
-
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
|
+
}
|
|
16261
16377
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
16262
16378
|
}
|
|
16263
16379
|
/**
|
|
@@ -16598,11 +16714,14 @@ var FECManager = class _FECManager {
|
|
|
16598
16714
|
});
|
|
16599
16715
|
client.onCleanupComplete((sbn) => {
|
|
16600
16716
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
16601
|
-
const
|
|
16717
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
16718
|
+
const cleanupKeys = owed.filter((key) => {
|
|
16602
16719
|
const block = this.#parseBlockKey(key);
|
|
16603
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
16720
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
16604
16721
|
});
|
|
16605
|
-
|
|
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");
|
|
16606
16725
|
});
|
|
16607
16726
|
client.onError((msg, code) => {
|
|
16608
16727
|
const requestedKind = code ?? msg;
|
|
@@ -16672,9 +16791,11 @@ var FECManager = class _FECManager {
|
|
|
16672
16791
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
16673
16792
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
16674
16793
|
*/
|
|
16675
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
16794
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
16676
16795
|
this.#clearOwedCleanupBound(key);
|
|
16677
16796
|
const bound = {
|
|
16797
|
+
dueAtMs,
|
|
16798
|
+
askedAtMs: performance.now(),
|
|
16678
16799
|
handle: setTimeout(() => {
|
|
16679
16800
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
16680
16801
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -16844,9 +16965,15 @@ var FECManager = class _FECManager {
|
|
|
16844
16965
|
rebuilt = /* @__PURE__ */ new Set();
|
|
16845
16966
|
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16846
16967
|
}
|
|
16968
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16969
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16847
16970
|
rebuilt.add(located.esi);
|
|
16848
16971
|
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16849
|
-
if (recovered === null
|
|
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
|
+
}
|
|
16850
16977
|
return { taken: true, recovered };
|
|
16851
16978
|
}
|
|
16852
16979
|
/**
|
|
@@ -17113,7 +17240,10 @@ var FECManager = class _FECManager {
|
|
|
17113
17240
|
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17114
17241
|
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17115
17242
|
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17116
|
-
|
|
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;
|
|
17117
17247
|
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17118
17248
|
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17119
17249
|
return void 0;
|
|
@@ -17236,12 +17366,13 @@ var FECManager = class _FECManager {
|
|
|
17236
17366
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
17237
17367
|
* classification evidence, and marks the block clean when all of its
|
|
17238
17368
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
17239
|
-
* otherwise K.
|
|
17369
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
17370
|
+
* placed only now (see #noteSbnSeen).
|
|
17240
17371
|
*
|
|
17241
17372
|
* Throws for a packetId that was never configured, so a classified block
|
|
17242
17373
|
* never reads the manager-global K or timeout.
|
|
17243
17374
|
*/
|
|
17244
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
17375
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
17245
17376
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
17246
17377
|
if (trackConfig === void 0) {
|
|
17247
17378
|
throw new Error(
|
|
@@ -17257,7 +17388,8 @@ var FECManager = class _FECManager {
|
|
|
17257
17388
|
catalogTrackName,
|
|
17258
17389
|
"source",
|
|
17259
17390
|
evidence.packetSequenceNumber,
|
|
17260
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
17391
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
17392
|
+
held
|
|
17261
17393
|
);
|
|
17262
17394
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
17263
17395
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -17312,6 +17444,7 @@ var FECManager = class _FECManager {
|
|
|
17312
17444
|
this.#blockSourceEsis.delete(key);
|
|
17313
17445
|
this.#blockRebuiltEsis.delete(key);
|
|
17314
17446
|
this.#blockAuthoredLengths.delete(key);
|
|
17447
|
+
this.#isoSpanKeys.delete(key);
|
|
17315
17448
|
this.#blockRepairRsIds.delete(key);
|
|
17316
17449
|
this.#blockRsbLengths.delete(key);
|
|
17317
17450
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -17342,6 +17475,19 @@ var FECManager = class _FECManager {
|
|
|
17342
17475
|
#receiptNow() {
|
|
17343
17476
|
return this.#receiptAtMs ?? performance.now();
|
|
17344
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
|
+
}
|
|
17345
17491
|
/**
|
|
17346
17492
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
17347
17493
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -17390,6 +17536,7 @@ var FECManager = class _FECManager {
|
|
|
17390
17536
|
}
|
|
17391
17537
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17392
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;
|
|
17393
17540
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
17394
17541
|
this.#clearRepairDeadline(key);
|
|
17395
17542
|
this.#clearBlockTimer(key);
|
|
@@ -17409,7 +17556,8 @@ var FECManager = class _FECManager {
|
|
|
17409
17556
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
17410
17557
|
this.#fecStats.blocksFailed++;
|
|
17411
17558
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
17412
|
-
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);
|
|
17413
17561
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
17414
17562
|
}
|
|
17415
17563
|
/**
|
|
@@ -17440,6 +17588,7 @@ var FECManager = class _FECManager {
|
|
|
17440
17588
|
this.#blockSourceEsis.delete(key);
|
|
17441
17589
|
this.#blockRebuiltEsis.delete(key);
|
|
17442
17590
|
this.#blockAuthoredLengths.delete(key);
|
|
17591
|
+
this.#isoSpanKeys.delete(key);
|
|
17443
17592
|
this.#blockRepairRsIds.delete(key);
|
|
17444
17593
|
this.#blockRsbLengths.delete(key);
|
|
17445
17594
|
this.#clearBlockTimer(key);
|
|
@@ -17532,6 +17681,7 @@ var FECManager = class _FECManager {
|
|
|
17532
17681
|
this.#blockSourceEsis.clear();
|
|
17533
17682
|
this.#blockRebuiltEsis.clear();
|
|
17534
17683
|
this.#blockAuthoredLengths.clear();
|
|
17684
|
+
this.#isoSpanKeys.clear();
|
|
17535
17685
|
this.#blockRepairRsIds.clear();
|
|
17536
17686
|
this.#blockRsbLengths.clear();
|
|
17537
17687
|
this.#heldLayerStates.clear();
|
|
@@ -18433,6 +18583,9 @@ function authoredGap(before, after) {
|
|
|
18433
18583
|
function ssIdAfter(a, b) {
|
|
18434
18584
|
return (a - b | 0) > 0;
|
|
18435
18585
|
}
|
|
18586
|
+
function holdsRebuilt(stream) {
|
|
18587
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18588
|
+
}
|
|
18436
18589
|
var FecReleaseSequencer = class {
|
|
18437
18590
|
#streams = /* @__PURE__ */ new Map();
|
|
18438
18591
|
#gate;
|
|
@@ -18495,6 +18648,7 @@ var FecReleaseSequencer = class {
|
|
|
18495
18648
|
stream.lastSbn = void 0;
|
|
18496
18649
|
stream.discontinuity = false;
|
|
18497
18650
|
stream.rapSsId = void 0;
|
|
18651
|
+
stream.sourced = false;
|
|
18498
18652
|
}
|
|
18499
18653
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
18500
18654
|
this.#recordStat("droppedStale");
|
|
@@ -18521,6 +18675,7 @@ var FecReleaseSequencer = class {
|
|
|
18521
18675
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18522
18676
|
else hi = mid;
|
|
18523
18677
|
}
|
|
18678
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18524
18679
|
stream.buf.splice(lo, 0, frame);
|
|
18525
18680
|
this.#drainUnlessDeferred(stream);
|
|
18526
18681
|
}
|
|
@@ -18556,6 +18711,41 @@ var FecReleaseSequencer = class {
|
|
|
18556
18711
|
}
|
|
18557
18712
|
return false;
|
|
18558
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
|
+
}
|
|
18559
18749
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18560
18750
|
poke(streamKey) {
|
|
18561
18751
|
if (streamKey !== void 0) {
|
|
@@ -18647,16 +18837,32 @@ var FecReleaseSequencer = class {
|
|
|
18647
18837
|
last: null,
|
|
18648
18838
|
lastSbn: void 0,
|
|
18649
18839
|
discontinuity: false,
|
|
18650
|
-
rapSsId: void 0
|
|
18840
|
+
rapSsId: void 0,
|
|
18841
|
+
sourced: false,
|
|
18842
|
+
draining: 0
|
|
18651
18843
|
};
|
|
18652
18844
|
this.#streams.set(streamKey, stream);
|
|
18653
18845
|
}
|
|
18654
18846
|
return stream;
|
|
18655
18847
|
}
|
|
18656
|
-
#
|
|
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) {
|
|
18657
18858
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18658
18859
|
const head = stream.buf[0];
|
|
18659
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
|
+
}
|
|
18660
18866
|
if (stream.discontinuity) {
|
|
18661
18867
|
stream.discontinuity = false;
|
|
18662
18868
|
stream.buf.shift();
|
|
@@ -18670,9 +18876,10 @@ var FecReleaseSequencer = class {
|
|
|
18670
18876
|
}
|
|
18671
18877
|
const headSbn = head.meta.fecSbn;
|
|
18672
18878
|
const from = stream.lastSbn ?? headSbn;
|
|
18673
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18879
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18674
18880
|
let lost = [];
|
|
18675
18881
|
const unanchored = from === void 0 && to === void 0;
|
|
18882
|
+
let unresolved = false;
|
|
18676
18883
|
if (from !== void 0 && to !== void 0) {
|
|
18677
18884
|
const resolved = this.#gate.resolvedLoss(
|
|
18678
18885
|
stream.packetId,
|
|
@@ -18681,7 +18888,8 @@ var FecReleaseSequencer = class {
|
|
|
18681
18888
|
Math.max(from, to),
|
|
18682
18889
|
from
|
|
18683
18890
|
);
|
|
18684
|
-
|
|
18891
|
+
unresolved = resolved === null;
|
|
18892
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18685
18893
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18686
18894
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18687
18895
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18689,22 +18897,56 @@ var FecReleaseSequencer = class {
|
|
|
18689
18897
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18690
18898
|
continue;
|
|
18691
18899
|
}
|
|
18692
|
-
lost = resolved;
|
|
18900
|
+
lost = resolved ?? [];
|
|
18693
18901
|
}
|
|
18694
18902
|
stream.buf.shift();
|
|
18695
18903
|
this.#release(
|
|
18696
18904
|
stream,
|
|
18697
18905
|
head,
|
|
18698
|
-
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
|
|
18699
18907
|
);
|
|
18700
18908
|
}
|
|
18701
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
|
+
}
|
|
18702
18939
|
#release(stream, frame, decision) {
|
|
18940
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18941
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18703
18942
|
if (decision) frame.meta.gapBefore = decision;
|
|
18704
18943
|
stream.last = frame;
|
|
18705
|
-
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;
|
|
18706
18948
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18707
|
-
this.#emit(frame.data, frame.meta);
|
|
18949
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18708
18950
|
}
|
|
18709
18951
|
};
|
|
18710
18952
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -18965,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18965
19207
|
/**
|
|
18966
19208
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18967
19209
|
*
|
|
18968
|
-
*
|
|
18969
|
-
*
|
|
18970
|
-
*
|
|
18971
|
-
*
|
|
18972
|
-
*
|
|
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.
|
|
18973
19216
|
*/
|
|
18974
19217
|
get timingBudgetStats() {
|
|
18975
19218
|
return {
|
|
@@ -19029,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19029
19272
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
19030
19273
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
19031
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
|
+
}
|
|
19032
19283
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
19033
19284
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
19034
19285
|
if (!this.#fecSequencer) return;
|
|
@@ -19282,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19282
19533
|
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19283
19534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19284
19535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19285
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19286
19537
|
const admitted = resolver2.addMovieFragment(
|
|
19287
19538
|
mpu.mpuSequenceNumber,
|
|
19288
19539
|
data,
|
|
@@ -19355,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19355
19606
|
info,
|
|
19356
19607
|
resolver,
|
|
19357
19608
|
options?.recovered === true,
|
|
19358
|
-
options?.fecSbn
|
|
19609
|
+
options?.fecSbn,
|
|
19610
|
+
options?.fecSbnFirst
|
|
19359
19611
|
);
|
|
19360
19612
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19361
19613
|
}
|
|
@@ -19448,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19448
19700
|
info,
|
|
19449
19701
|
resolver,
|
|
19450
19702
|
options?.recovered === true,
|
|
19451
|
-
options?.fecSbn
|
|
19703
|
+
options?.fecSbn,
|
|
19704
|
+
options?.fecSbnFirst
|
|
19452
19705
|
);
|
|
19453
19706
|
if (outcome.record !== void 0) return outcome.record;
|
|
19454
19707
|
return this.#drop(outcome.reason);
|
|
@@ -19468,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19468
19721
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19469
19722
|
* is what keeps both true.
|
|
19470
19723
|
*/
|
|
19471
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19724
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19472
19725
|
const timing = resolver.sampleTiming(
|
|
19473
19726
|
combined.mpuSequenceNumber,
|
|
19474
19727
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19485,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19485
19738
|
);
|
|
19486
19739
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19487
19740
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19741
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19488
19742
|
if (!resolver.consumeSample(
|
|
19489
19743
|
combined.mpuSequenceNumber,
|
|
19490
19744
|
combined.movieFragmentSequenceNumber,
|
|
@@ -23001,6 +23255,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
|
23001
23255
|
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23002
23256
|
var OVERLAY_PENDING_LIMIT = 256;
|
|
23003
23257
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23258
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
23004
23259
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
23005
23260
|
function assertStreamIdentity(role, stream) {
|
|
23006
23261
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -23177,6 +23432,17 @@ var MmtpRouter = class {
|
|
|
23177
23432
|
* an MPU no member of which arrived).
|
|
23178
23433
|
*/
|
|
23179
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();
|
|
23180
23446
|
/**
|
|
23181
23447
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
23182
23448
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -23454,13 +23720,18 @@ var MmtpRouter = class {
|
|
|
23454
23720
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
23455
23721
|
}
|
|
23456
23722
|
};
|
|
23457
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23723
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23458
23724
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
23459
23725
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
23460
23726
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
23461
23727
|
}
|
|
23462
|
-
|
|
23463
|
-
|
|
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
|
+
}
|
|
23464
23735
|
};
|
|
23465
23736
|
}
|
|
23466
23737
|
}
|
|
@@ -23505,8 +23776,9 @@ var MmtpRouter = class {
|
|
|
23505
23776
|
}
|
|
23506
23777
|
/**
|
|
23507
23778
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23508
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23509
|
-
*
|
|
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.
|
|
23510
23782
|
*/
|
|
23511
23783
|
get restampedRecoveredSequenceCount() {
|
|
23512
23784
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23685,6 +23957,7 @@ var MmtpRouter = class {
|
|
|
23685
23957
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23686
23958
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23687
23959
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23960
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23688
23961
|
if (previous) {
|
|
23689
23962
|
previous.flush();
|
|
23690
23963
|
previous.dispose();
|
|
@@ -23835,7 +24108,8 @@ var MmtpRouter = class {
|
|
|
23835
24108
|
fec,
|
|
23836
24109
|
k,
|
|
23837
24110
|
offsets: /* @__PURE__ */ new Map(),
|
|
23838
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24111
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24112
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23839
24113
|
};
|
|
23840
24114
|
this.#overlays.set(repairPacketId, inst);
|
|
23841
24115
|
return () => {
|
|
@@ -23876,6 +24150,7 @@ var MmtpRouter = class {
|
|
|
23876
24150
|
inst.last = void 0;
|
|
23877
24151
|
inst.offsets.clear();
|
|
23878
24152
|
inst.moqBlocks.clear();
|
|
24153
|
+
inst.rebuilt.clear();
|
|
23879
24154
|
}
|
|
23880
24155
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23881
24156
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23928,13 +24203,23 @@ var MmtpRouter = class {
|
|
|
23928
24203
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23929
24204
|
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
23930
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).
|
|
23931
24213
|
*/
|
|
23932
24214
|
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23933
24215
|
if (this.#overlays.size === 0) return;
|
|
23934
24216
|
const ssId = keyframeOverlaySsId(header);
|
|
23935
24217
|
if (ssId === void 0) return;
|
|
24218
|
+
let rebuiltPsn;
|
|
23936
24219
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23937
24220
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24221
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24222
|
+
inst.rebuilt.delete(ssId);
|
|
23938
24223
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23939
24224
|
if (identity !== null) {
|
|
23940
24225
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
@@ -23964,6 +24249,14 @@ var MmtpRouter = class {
|
|
|
23964
24249
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23965
24250
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23966
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
|
+
}
|
|
23967
24260
|
}
|
|
23968
24261
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23969
24262
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23997,11 +24290,16 @@ var MmtpRouter = class {
|
|
|
23997
24290
|
}
|
|
23998
24291
|
/**
|
|
23999
24292
|
* Route an overlay-recovered block's packets as source media of the
|
|
24000
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
24001
|
-
*
|
|
24002
|
-
*
|
|
24003
|
-
*
|
|
24004
|
-
*
|
|
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.
|
|
24005
24303
|
*/
|
|
24006
24304
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
24007
24305
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -24022,40 +24320,65 @@ var MmtpRouter = class {
|
|
|
24022
24320
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
24023
24321
|
const restored = /* @__PURE__ */ new Set();
|
|
24024
24322
|
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24323
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
24025
24324
|
const symbols = result.symbols.flatMap((sym) => {
|
|
24325
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24026
24326
|
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24027
24327
|
if (anchor !== void 0) {
|
|
24028
|
-
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24029
24328
|
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
24030
24329
|
restored.add(packetSequenceNumber);
|
|
24031
24330
|
const out = { ...sym, packetSequenceNumber };
|
|
24032
24331
|
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24033
|
-
if (baseSsId !== void 0)
|
|
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
|
+
}
|
|
24034
24339
|
return [out];
|
|
24035
24340
|
}
|
|
24036
24341
|
if (!restamped) {
|
|
24037
|
-
const
|
|
24038
|
-
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
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
|
+
}
|
|
24043
24352
|
return [sym];
|
|
24044
24353
|
}
|
|
24045
24354
|
this.#overlayStats.unsequencedDropped++;
|
|
24046
24355
|
return [];
|
|
24047
24356
|
});
|
|
24048
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
|
+
}
|
|
24049
24369
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
24050
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24370
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24371
|
+
baseSsIds,
|
|
24372
|
+
firstSsIds
|
|
24051
24373
|
});
|
|
24052
24374
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24053
|
-
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24375
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
24054
24376
|
}
|
|
24055
24377
|
/**
|
|
24056
24378
|
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24057
24379
|
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24058
|
-
* from a received member of its MPU, else unstampedBaseSsId
|
|
24380
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
24381
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
24059
24382
|
* so it is fed to the base decoder as a rebuilt source
|
|
24060
24383
|
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24061
24384
|
* fewer to solve, and its repair can recover the block's other packets.
|
|
@@ -24081,7 +24404,7 @@ var MmtpRouter = class {
|
|
|
24081
24404
|
const feeds = [];
|
|
24082
24405
|
for (const sym of symbols) {
|
|
24083
24406
|
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24084
|
-
const ssId = baseSsIds.get(sym);
|
|
24407
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24085
24408
|
if (ssId === void 0) {
|
|
24086
24409
|
this.#overlayStats.baseUndetermined++;
|
|
24087
24410
|
continue;
|
|
@@ -24810,7 +25133,7 @@ var MmtpRouter = class {
|
|
|
24810
25133
|
catalogTrackName
|
|
24811
25134
|
)) continue;
|
|
24812
25135
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24813
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25136
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24814
25137
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24815
25138
|
}
|
|
24816
25139
|
if (payload.oversized) continue;
|
|
@@ -24898,6 +25221,7 @@ var MmtpRouter = class {
|
|
|
24898
25221
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24899
25222
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
24900
25223
|
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25224
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24901
25225
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24902
25226
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24903
25227
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -25024,6 +25348,12 @@ var MmtpRouter = class {
|
|
|
25024
25348
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
25025
25349
|
* own RAP cadence, so no timer is involved.
|
|
25026
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
|
+
*
|
|
25027
25357
|
* The commit is one synchronous transition inside the frame path: the
|
|
25028
25358
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
25029
25359
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -25074,6 +25404,9 @@ var MmtpRouter = class {
|
|
|
25074
25404
|
droppedIncomingFrames: 0
|
|
25075
25405
|
};
|
|
25076
25406
|
this.#staged = state;
|
|
25407
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25408
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25409
|
+
}
|
|
25077
25410
|
return {
|
|
25078
25411
|
cancel: () => {
|
|
25079
25412
|
if (this.#staged !== state) return false;
|
|
@@ -25156,7 +25489,17 @@ var MmtpRouter = class {
|
|
|
25156
25489
|
*/
|
|
25157
25490
|
#routeParsedFrame(data, meta) {
|
|
25158
25491
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
25159
|
-
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
|
+
}
|
|
25160
25503
|
this.#deliverParsedFrame(data, meta);
|
|
25161
25504
|
}
|
|
25162
25505
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -25308,12 +25651,16 @@ var MmtpRouter = class {
|
|
|
25308
25651
|
this.#parser.deferFecRelease(work);
|
|
25309
25652
|
} finally {
|
|
25310
25653
|
this.#fecPokeDepth--;
|
|
25311
|
-
|
|
25312
|
-
|
|
25313
|
-
|
|
25314
|
-
|
|
25315
|
-
|
|
25316
|
-
|
|
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);
|
|
25317
25664
|
}
|
|
25318
25665
|
}
|
|
25319
25666
|
}
|
|
@@ -25391,6 +25738,14 @@ var MmtpRouter = class {
|
|
|
25391
25738
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
25392
25739
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
25393
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
|
+
},
|
|
25394
25749
|
onReleased: (data, meta) => {
|
|
25395
25750
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
25396
25751
|
const timing = meta.authoredTiming;
|
|
@@ -25841,7 +26196,8 @@ var MmtpRouter = class {
|
|
|
25841
26196
|
source: parserOptions?.source,
|
|
25842
26197
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25843
26198
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25844
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26199
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26200
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25845
26201
|
};
|
|
25846
26202
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25847
26203
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -26307,7 +26663,12 @@ var MmtpRouter = class {
|
|
|
26307
26663
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
26308
26664
|
if (ownerPacketId == null) return;
|
|
26309
26665
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
26310
|
-
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
|
+
};
|
|
26311
26672
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
26312
26673
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
26313
26674
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -28103,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
28103
28464
|
function isSymbolCounts(value) {
|
|
28104
28465
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
28105
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
|
+
}
|
|
28106
28473
|
function isNullableNonNegative(value) {
|
|
28107
28474
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
28108
28475
|
}
|
|
@@ -28122,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
28122
28489
|
case "recovered":
|
|
28123
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"]);
|
|
28124
28491
|
case "failed":
|
|
28125
|
-
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"]));
|
|
28126
28496
|
case "repair-preempted":
|
|
28127
28497
|
case "repair-deferred":
|
|
28128
28498
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -28331,6 +28701,8 @@ var MulticastManager = class {
|
|
|
28331
28701
|
/** Live joinRepairEndpoints() releases → the repair owners they registered,
|
|
28332
28702
|
* re-applied after configureMultiTrack() clears catalog-derived owners. */
|
|
28333
28703
|
#repairReleases = /* @__PURE__ */ new Map();
|
|
28704
|
+
/** Protected repair flows owned by each live repair-endpoint release. */
|
|
28705
|
+
#repairProvenanceFlows = /* @__PURE__ */ new Map();
|
|
28334
28706
|
/** Each source's catalog keyframe overlay, from its `ok` binding. */
|
|
28335
28707
|
#overlayBindings = /* @__PURE__ */ new Map();
|
|
28336
28708
|
/** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
|
|
@@ -28388,7 +28760,7 @@ var MulticastManager = class {
|
|
|
28388
28760
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
28389
28761
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
28390
28762
|
},
|
|
28391
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28763
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
28392
28764
|
const pathCount = this.#fec.blockPathCount(
|
|
28393
28765
|
sbn,
|
|
28394
28766
|
packetId,
|
|
@@ -28406,7 +28778,8 @@ var MulticastManager = class {
|
|
|
28406
28778
|
symbols: {
|
|
28407
28779
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
28408
28780
|
moq: pathCount?.moqSymbols ?? 0
|
|
28409
|
-
}
|
|
28781
|
+
},
|
|
28782
|
+
...span === void 0 ? {} : { span }
|
|
28410
28783
|
});
|
|
28411
28784
|
},
|
|
28412
28785
|
onFrameGap: (decision) => {
|
|
@@ -28996,6 +29369,9 @@ var MulticastManager = class {
|
|
|
28996
29369
|
this.#transport.onFailback((reason) => {
|
|
28997
29370
|
const joined = this.#provenanceJoin;
|
|
28998
29371
|
if (joined !== null) this.#router.unprotectProvenanceFlow(joined.flow);
|
|
29372
|
+
for (const flows of this.#repairProvenanceFlows.values()) {
|
|
29373
|
+
for (const flow of flows) this.#router.unprotectProvenanceFlow(flow);
|
|
29374
|
+
}
|
|
28999
29375
|
this.#onFailback?.(reason);
|
|
29000
29376
|
});
|
|
29001
29377
|
}
|
|
@@ -29030,6 +29406,12 @@ var MulticastManager = class {
|
|
|
29030
29406
|
"[MulticastManager] join: provenance flow (S, G, port) must match sourceAddress ?? senderAddress, groupAddress and port"
|
|
29031
29407
|
);
|
|
29032
29408
|
}
|
|
29409
|
+
if (provenanceFlow !== void 0) {
|
|
29410
|
+
const flow = multicastProvenanceFlowKey(provenanceFlow);
|
|
29411
|
+
if ([...this.#repairProvenanceFlows.values()].some((flows) => flows.includes(flow))) {
|
|
29412
|
+
throw new TypeError(`[MulticastManager] join: provenance flow already registered by a repair endpoint: ${flow}`);
|
|
29413
|
+
}
|
|
29414
|
+
}
|
|
29033
29415
|
const staged = this.#renditionSwitch;
|
|
29034
29416
|
if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
|
|
29035
29417
|
this.configureFec(config);
|
|
@@ -29037,8 +29419,8 @@ var MulticastManager = class {
|
|
|
29037
29419
|
const selectedPacketIds = config.packetIds?.length ? new Set(config.packetIds) : null;
|
|
29038
29420
|
const catalogTracks = config.tracks?.filter(({ packetId }) => selectedPacketIds === null || selectedPacketIds.has(packetId));
|
|
29039
29421
|
const ingressTracks = catalogTracks ? catalogTracks : selectedPacketIds === null ? void 0 : [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
|
|
29040
|
-
this.#releaseProvenanceJoin();
|
|
29041
29422
|
const provenance = config.provenance;
|
|
29423
|
+
this.#releaseProvenanceJoin();
|
|
29042
29424
|
const protectedTracks = ingressTracks ?? provenance?.flow.tracks;
|
|
29043
29425
|
const ingress = protectedTracks === void 0 ? void 0 : {
|
|
29044
29426
|
tracks: Object.freeze(
|
|
@@ -29091,8 +29473,13 @@ var MulticastManager = class {
|
|
|
29091
29473
|
* feeds the flow's receiver; media datagrams are held until one admits them.
|
|
29092
29474
|
*/
|
|
29093
29475
|
#joinProvenance(provenance, ingress) {
|
|
29476
|
+
this.#router.registerProvenanceFlow(ingress, this.#provenanceReceiver(provenance));
|
|
29477
|
+
this.#hookFailback();
|
|
29478
|
+
this.#provenanceJoin = { flow: ingress.flow };
|
|
29479
|
+
}
|
|
29480
|
+
#provenanceReceiver(provenance) {
|
|
29094
29481
|
const { flow, auth } = provenance;
|
|
29095
|
-
|
|
29482
|
+
return new BcProvenanceV2Receiver({
|
|
29096
29483
|
...provenance.receiver,
|
|
29097
29484
|
// The caller's ctx when it passes one, else the catalog's: an explicit
|
|
29098
29485
|
// `catalogCtx: undefined` must not switch off the 7.2.4 ctx check.
|
|
@@ -29109,9 +29496,6 @@ var MulticastManager = class {
|
|
|
29109
29496
|
maxAuthDelayMs: flow.maxAuthDelayMs,
|
|
29110
29497
|
manifestTimerMs: flow.manifestTimerMs
|
|
29111
29498
|
});
|
|
29112
|
-
this.#router.registerProvenanceFlow(ingress, receiver);
|
|
29113
|
-
this.#hookFailback();
|
|
29114
|
-
this.#provenanceJoin = { flow: ingress.flow };
|
|
29115
29499
|
}
|
|
29116
29500
|
/** Un-protect: drop the flow's hold queues (no expiry counted). */
|
|
29117
29501
|
#releaseProvenanceJoin() {
|
|
@@ -29142,8 +29526,25 @@ var MulticastManager = class {
|
|
|
29142
29526
|
* callback delivers must come from a subscription opened on
|
|
29143
29527
|
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
29144
29528
|
* WebSocket repair feed) must not name a native provider here.
|
|
29529
|
+
*
|
|
29530
|
+
* An endpoint's `provenance` makes its group a protected flow of its own
|
|
29531
|
+
* (multicast-02 7.2.7), registered here and released with the group; its
|
|
29532
|
+
* records ride in band on the group's port. On the user-agent path a
|
|
29533
|
+
* flow-less repair group of a protected track is set aside, never read, so
|
|
29534
|
+
* a player joining a protected catalog's repair groups passes it.
|
|
29145
29535
|
*/
|
|
29146
29536
|
joinRepairEndpoints(endpoints, provider, options) {
|
|
29537
|
+
for (const { provenance, flow, source, group, port: port2 } of endpoints) {
|
|
29538
|
+
if (provenance === void 0) continue;
|
|
29539
|
+
if (flow !== void 0) {
|
|
29540
|
+
throw new TypeError("[MulticastManager] joinRepairEndpoints: an endpoint names provenance or flow, never both");
|
|
29541
|
+
}
|
|
29542
|
+
if (!sameIpAddress(source, provenance.flow.source) || !sameIpAddress(group, provenance.flow.groupAddress) || port2 !== provenance.flow.port) {
|
|
29543
|
+
throw new RangeError(
|
|
29544
|
+
"[MulticastManager] joinRepairEndpoints: provenance flow (S, G, port) must match the endpoint's source, group and port"
|
|
29545
|
+
);
|
|
29546
|
+
}
|
|
29547
|
+
}
|
|
29147
29548
|
const releases = [];
|
|
29148
29549
|
let released = false;
|
|
29149
29550
|
const safely = (each) => {
|
|
@@ -29153,17 +29554,36 @@ var MulticastManager = class {
|
|
|
29153
29554
|
console.warn("[MulticastManager] repair group release failed", error);
|
|
29154
29555
|
}
|
|
29155
29556
|
};
|
|
29557
|
+
const usedFlows = /* @__PURE__ */ new Set();
|
|
29558
|
+
const preparedProvenance = endpoints.map((endpoint) => {
|
|
29559
|
+
if (endpoint.provenance === void 0) return void 0;
|
|
29560
|
+
const flow = multicastProvenanceFlowKey(endpoint.provenance.flow);
|
|
29561
|
+
if (usedFlows.has(flow) || this.#provenanceJoin?.flow === flow || [...this.#repairProvenanceFlows.values()].some((flows) => flows.includes(flow))) {
|
|
29562
|
+
throw new TypeError(`[MulticastManager] joinRepairEndpoints: provenance flow already registered: ${flow}`);
|
|
29563
|
+
}
|
|
29564
|
+
usedFlows.add(flow);
|
|
29565
|
+
return { flow, receiver: this.#provenanceReceiver(endpoint.provenance) };
|
|
29566
|
+
});
|
|
29567
|
+
const provenanceFlows = [];
|
|
29156
29568
|
const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
|
|
29157
29569
|
...owner,
|
|
29158
29570
|
ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
|
|
29159
29571
|
repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
|
|
29160
29572
|
}));
|
|
29161
29573
|
this.#applyRepairOwners(repairOwners);
|
|
29162
|
-
for (const endpoint of endpoints) {
|
|
29574
|
+
for (const [index, endpoint] of endpoints.entries()) {
|
|
29575
|
+
const flow = endpoint.provenance === void 0 ? endpoint.flow : multicastProvenanceFlowKey(endpoint.provenance.flow);
|
|
29163
29576
|
const ingress = {
|
|
29164
29577
|
tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
|
|
29165
|
-
...
|
|
29578
|
+
...flow === void 0 ? {} : { flow }
|
|
29166
29579
|
};
|
|
29580
|
+
if (endpoint.provenance !== void 0) {
|
|
29581
|
+
const prepared = preparedProvenance[index];
|
|
29582
|
+
if (prepared === void 0) throw new Error("[MulticastManager] repair provenance preparation missing");
|
|
29583
|
+
this.#router.registerProvenanceFlow(ingress, prepared.receiver);
|
|
29584
|
+
provenanceFlows.push(prepared.flow);
|
|
29585
|
+
releases.push(() => this.#router.releaseProvenanceFlow(flow));
|
|
29586
|
+
}
|
|
29167
29587
|
let sender;
|
|
29168
29588
|
const feed = (packet, sourceAddress, ingressToFeed) => {
|
|
29169
29589
|
if (released) return;
|
|
@@ -29193,7 +29613,7 @@ var MulticastManager = class {
|
|
|
29193
29613
|
packetIds: endpoint.packetIds,
|
|
29194
29614
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
29195
29615
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
29196
|
-
...
|
|
29616
|
+
...flow === void 0 ? {} : { protected: true }
|
|
29197
29617
|
}, ingress).then((joined) => {
|
|
29198
29618
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
29199
29619
|
});
|
|
@@ -29202,9 +29622,12 @@ var MulticastManager = class {
|
|
|
29202
29622
|
if (released) return;
|
|
29203
29623
|
released = true;
|
|
29204
29624
|
this.#repairReleases.delete(release);
|
|
29625
|
+
this.#repairProvenanceFlows.delete(release);
|
|
29205
29626
|
for (const each of releases.splice(0)) safely(each);
|
|
29206
29627
|
};
|
|
29207
29628
|
this.#repairReleases.set(release, repairOwners);
|
|
29629
|
+
this.#repairProvenanceFlows.set(release, provenanceFlows);
|
|
29630
|
+
if (provenanceFlows.length) this.#hookFailback();
|
|
29208
29631
|
return release;
|
|
29209
29632
|
}
|
|
29210
29633
|
#applyRepairOwners(owners) {
|
|
@@ -29786,10 +30209,12 @@ var MulticastManager = class {
|
|
|
29786
30209
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
29787
30210
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
29788
30211
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
29789
|
-
* once its range FETCH ends (or none is sent).
|
|
30212
|
+
* once its range FETCH ends (or none is sent). `onExpired` is told of a
|
|
30213
|
+
* pending hole the stream runs past the locator's horizon: returning true,
|
|
30214
|
+
* the caller closes it (`closeIsoHoles`); otherwise it is discarded.
|
|
29790
30215
|
*/
|
|
29791
|
-
onUndelimitedLoss(cb) {
|
|
29792
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30216
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30217
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
29793
30218
|
}
|
|
29794
30219
|
/**
|
|
29795
30220
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -30025,7 +30450,8 @@ var MulticastManager = class {
|
|
|
30025
30450
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
30026
30451
|
...event,
|
|
30027
30452
|
block: Object.freeze({ ...event.block }),
|
|
30028
|
-
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
|
|
30453
|
+
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
|
|
30454
|
+
..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
|
|
30029
30455
|
}
|
|
30030
30456
|
);
|
|
30031
30457
|
if (this.#logRepairLifecycle) {
|
|
@@ -32697,6 +33123,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32697
33123
|
|
|
32698
33124
|
// src/repair-fetch.ts
|
|
32699
33125
|
var MOQT_PRIORITY_MAX = 255;
|
|
33126
|
+
var WALK_LEAD_MS = 3;
|
|
32700
33127
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
32701
33128
|
function isMoqtPriority(value) {
|
|
32702
33129
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -32747,8 +33174,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
32747
33174
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
32748
33175
|
};
|
|
32749
33176
|
const clearEntry = (entry) => {
|
|
32750
|
-
|
|
32751
|
-
|
|
33177
|
+
const { range } = entry;
|
|
33178
|
+
if (range) {
|
|
33179
|
+
atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
|
|
33180
|
+
} else {
|
|
33181
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
33182
|
+
}
|
|
33183
|
+
};
|
|
33184
|
+
const atReport = (range, work) => {
|
|
33185
|
+
const fec = mc.fecManager;
|
|
33186
|
+
if (range.groups !== null || fec.withReceiptTime === void 0) work();
|
|
33187
|
+
else fec.withReceiptTime(range.reportedAt, work);
|
|
32752
33188
|
};
|
|
32753
33189
|
const releaseHold = (entry) => {
|
|
32754
33190
|
if (entry.range) return;
|
|
@@ -32815,21 +33251,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
32815
33251
|
const admit = (entry, now) => {
|
|
32816
33252
|
const budgetMs = entry.deadlineAt - now;
|
|
32817
33253
|
const rttMs = readRtt();
|
|
32818
|
-
const reason =
|
|
33254
|
+
const reason = refusal(entry, now, rttMs);
|
|
32819
33255
|
if (reason === null) return rttMs;
|
|
32820
33256
|
skip(entry, reason, rttMs, budgetMs);
|
|
32821
33257
|
return null;
|
|
32822
33258
|
};
|
|
33259
|
+
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";
|
|
32823
33260
|
const retire = (entry) => {
|
|
32824
33261
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
32825
33262
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
32826
33263
|
};
|
|
32827
|
-
const
|
|
33264
|
+
const holdsBack = (entry, priority, videoWalks = true) => {
|
|
33265
|
+
if (entry.priority >= priority) return false;
|
|
33266
|
+
const { walk } = entry;
|
|
33267
|
+
if (walk === void 0) return true;
|
|
33268
|
+
if (entry.priority !== 0 || !videoWalks) return false;
|
|
33269
|
+
return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
|
|
33270
|
+
};
|
|
33271
|
+
const outstandingBelow = (priority, videoWalks = true) => {
|
|
32828
33272
|
for (const entry of entries.values()) {
|
|
32829
|
-
if (entry
|
|
33273
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
32830
33274
|
}
|
|
32831
33275
|
return false;
|
|
32832
33276
|
};
|
|
33277
|
+
const yieldVideoWalks = (queued) => {
|
|
33278
|
+
const now = performance.now();
|
|
33279
|
+
const rttMs = readRtt();
|
|
33280
|
+
const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
|
|
33281
|
+
if (!due) return;
|
|
33282
|
+
for (const entry of entries.values()) {
|
|
33283
|
+
const walk = entry.walk;
|
|
33284
|
+
if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
|
|
33285
|
+
if (walk.group.controller.signal.aborted) continue;
|
|
33286
|
+
walk.yielded = true;
|
|
33287
|
+
walk.group.controller.abort();
|
|
33288
|
+
}
|
|
33289
|
+
};
|
|
32833
33290
|
const preemptBestEffort = () => {
|
|
32834
33291
|
for (const entry of entries.values()) {
|
|
32835
33292
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -32858,7 +33315,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
32858
33315
|
}
|
|
32859
33316
|
const rttMs = admit(entry, now);
|
|
32860
33317
|
if (rttMs === null) return;
|
|
32861
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33318
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33319
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
32862
33320
|
if (counted) entry.attempts++;
|
|
32863
33321
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
32864
33322
|
rttMs,
|
|
@@ -32880,6 +33338,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
32880
33338
|
}
|
|
32881
33339
|
void execute(entry, run);
|
|
32882
33340
|
};
|
|
33341
|
+
const leaveWalkedGroup = (block) => {
|
|
33342
|
+
for (const entry of entries.values()) {
|
|
33343
|
+
const group = entry.walk?.group;
|
|
33344
|
+
if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
|
|
33345
|
+
group.controller.abort();
|
|
33346
|
+
}
|
|
33347
|
+
}
|
|
33348
|
+
};
|
|
32883
33349
|
const dispatch = () => {
|
|
32884
33350
|
if (disposed) return;
|
|
32885
33351
|
const now = performance.now();
|
|
@@ -32904,6 +33370,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32904
33370
|
for (const entry of queued) {
|
|
32905
33371
|
if (entry.priority === 0) start(entry);
|
|
32906
33372
|
}
|
|
33373
|
+
yieldVideoWalks(queued);
|
|
32907
33374
|
if (outstandingBelow(1)) return;
|
|
32908
33375
|
for (const entry of queued) {
|
|
32909
33376
|
if (entry.priority === 1) start(entry);
|
|
@@ -32930,6 +33397,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32930
33397
|
}
|
|
32931
33398
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
32932
33399
|
};
|
|
33400
|
+
const groupServed = (sbn, packetId, catalogTrackName) => {
|
|
33401
|
+
if (mc.fecManager.blockSymbolCounts === void 0) return false;
|
|
33402
|
+
const counts = symbolCounts(sbn, packetId, catalogTrackName);
|
|
33403
|
+
if (counts === void 0 || counts.authoredLength === void 0) return true;
|
|
33404
|
+
return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
|
|
33405
|
+
};
|
|
33406
|
+
const statedEnd = (sbn, packetId, catalogTrackName) => {
|
|
33407
|
+
const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
|
|
33408
|
+
return length === void 0 ? void 0 : serial32Add(sbn, length);
|
|
33409
|
+
};
|
|
32933
33410
|
const finish = (entry, run, fed, error) => {
|
|
32934
33411
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
32935
33412
|
return;
|
|
@@ -32974,27 +33451,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
32974
33451
|
signal: run.controller.signal,
|
|
32975
33452
|
subscriberPriority: entry.subscriberPriority
|
|
32976
33453
|
};
|
|
33454
|
+
const { range } = entry;
|
|
33455
|
+
const feed = (packet) => {
|
|
33456
|
+
if (entry.catalogTrackName === void 0) {
|
|
33457
|
+
mc.processRepairFromMoQ(packet, entry.packetId);
|
|
33458
|
+
} else {
|
|
33459
|
+
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
33460
|
+
}
|
|
33461
|
+
};
|
|
32977
33462
|
try {
|
|
32978
|
-
const packets = entry.
|
|
33463
|
+
const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
|
|
32979
33464
|
...common,
|
|
32980
|
-
startGroup:
|
|
32981
|
-
endGroup:
|
|
32982
|
-
lostSsId:
|
|
32983
|
-
})
|
|
33465
|
+
startGroup: range.groups.startGroup,
|
|
33466
|
+
endGroup: range.groups.endGroup,
|
|
33467
|
+
lostSsId: range.lostSsId
|
|
33468
|
+
});
|
|
32984
33469
|
for await (const packet of packets) {
|
|
32985
33470
|
if (run.stopped !== null || entry.running !== run) break;
|
|
32986
33471
|
fed++;
|
|
32987
|
-
if (
|
|
32988
|
-
|
|
32989
|
-
} else {
|
|
32990
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
32991
|
-
}
|
|
33472
|
+
if (range === void 0) feed(packet);
|
|
33473
|
+
else atReport(range, () => feed(packet));
|
|
32992
33474
|
}
|
|
32993
33475
|
finish(entry, run, fed, void 0);
|
|
32994
33476
|
} catch (error) {
|
|
32995
33477
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
32996
33478
|
}
|
|
32997
33479
|
};
|
|
33480
|
+
async function* walkHole(entry, walk, hole, common) {
|
|
33481
|
+
const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
|
|
33482
|
+
for (let first = true; !common.signal.aborted; first = false) {
|
|
33483
|
+
if (!first && entry.priority !== 0 && outstandingBelow(1)) {
|
|
33484
|
+
stop(entry, "preempted");
|
|
33485
|
+
note("repair-preempted", entry);
|
|
33486
|
+
return;
|
|
33487
|
+
}
|
|
33488
|
+
if (!first && entry.priority === 0) {
|
|
33489
|
+
dispatch();
|
|
33490
|
+
if (common.signal.aborted) return;
|
|
33491
|
+
}
|
|
33492
|
+
const sbn = walk.next;
|
|
33493
|
+
let next;
|
|
33494
|
+
if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
|
|
33495
|
+
const rsIds = /* @__PURE__ */ new Set();
|
|
33496
|
+
const group = new AbortController();
|
|
33497
|
+
const cancel = () => group.abort();
|
|
33498
|
+
common.signal.addEventListener("abort", cancel, { once: true });
|
|
33499
|
+
walk.group = { sbn, controller: group };
|
|
33500
|
+
try {
|
|
33501
|
+
for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
|
|
33502
|
+
yield packet;
|
|
33503
|
+
const id = extractRepairFecPayloadId(packet);
|
|
33504
|
+
if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
|
|
33505
|
+
next ??= serial32Add(sbn, id.ssbLength);
|
|
33506
|
+
rsIds.add(id.rsId);
|
|
33507
|
+
if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
|
|
33508
|
+
group.abort();
|
|
33509
|
+
break;
|
|
33510
|
+
}
|
|
33511
|
+
}
|
|
33512
|
+
} catch (error) {
|
|
33513
|
+
if (!group.signal.aborted || common.signal.aborted) throw error;
|
|
33514
|
+
} finally {
|
|
33515
|
+
common.signal.removeEventListener("abort", cancel);
|
|
33516
|
+
walk.group = null;
|
|
33517
|
+
}
|
|
33518
|
+
if (walk.yielded) {
|
|
33519
|
+
walk.yielded = false;
|
|
33520
|
+
continue;
|
|
33521
|
+
}
|
|
33522
|
+
}
|
|
33523
|
+
next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
|
|
33524
|
+
if (next === void 0) return;
|
|
33525
|
+
if (serial32Distance(entry.sbn, next) > lostAt) return;
|
|
33526
|
+
walk.next = next;
|
|
33527
|
+
}
|
|
33528
|
+
}
|
|
32998
33529
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
32999
33530
|
for (const entry of [...entries.values()]) {
|
|
33000
33531
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -33190,7 +33721,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
33190
33721
|
throw error;
|
|
33191
33722
|
}
|
|
33192
33723
|
};
|
|
33193
|
-
|
|
33724
|
+
const trackKind = (packetId) => {
|
|
33725
|
+
try {
|
|
33726
|
+
return mc.fecManager.trackKindFor?.(packetId);
|
|
33727
|
+
} catch {
|
|
33728
|
+
return void 0;
|
|
33729
|
+
}
|
|
33730
|
+
};
|
|
33731
|
+
const walkDeadlineAt = (packetId, now, deadlineAt) => {
|
|
33732
|
+
let repairDeadlineMs;
|
|
33733
|
+
try {
|
|
33734
|
+
repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
|
|
33735
|
+
} catch {
|
|
33736
|
+
repairDeadlineMs = void 0;
|
|
33737
|
+
}
|
|
33738
|
+
if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
|
|
33739
|
+
return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
|
|
33740
|
+
};
|
|
33741
|
+
const onHoleExpired = (hole) => {
|
|
33742
|
+
if (disposed) return false;
|
|
33743
|
+
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);
|
|
33744
|
+
if (entry === void 0) return false;
|
|
33745
|
+
queueMicrotask(() => {
|
|
33746
|
+
if (entries.get(entry.key) !== entry) return;
|
|
33747
|
+
retire(entry);
|
|
33748
|
+
stop(entry, "expired");
|
|
33749
|
+
clearEntry(entry);
|
|
33750
|
+
dispatch();
|
|
33751
|
+
});
|
|
33752
|
+
return true;
|
|
33753
|
+
};
|
|
33754
|
+
if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
|
|
33194
33755
|
mc.onUndelimitedLoss((loss) => {
|
|
33195
33756
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
33196
33757
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -33198,9 +33759,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
33198
33759
|
const blockProfile = blockProfileOf(packetId);
|
|
33199
33760
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
33200
33761
|
if (!target) return close();
|
|
33201
|
-
const
|
|
33202
|
-
const
|
|
33203
|
-
|
|
33762
|
+
const walked = opts.rangeTransport === void 0;
|
|
33763
|
+
const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
|
|
33764
|
+
const priority = repairPriorityForClass(holeClass);
|
|
33765
|
+
const groups = walked ? null : rangeFor(loss, target.repairTrack);
|
|
33766
|
+
if (!walked && groups === null) {
|
|
33204
33767
|
close();
|
|
33205
33768
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
33206
33769
|
return;
|
|
@@ -33211,8 +33774,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
33211
33774
|
packetId,
|
|
33212
33775
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
33213
33776
|
};
|
|
33214
|
-
const wire = wirePriority(identity,
|
|
33215
|
-
|
|
33777
|
+
const wire = wirePriority(identity, holeClass);
|
|
33778
|
+
if (wire === null) return close();
|
|
33779
|
+
const now = performance.now();
|
|
33780
|
+
const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
|
|
33216
33781
|
if (deadline === null) {
|
|
33217
33782
|
close();
|
|
33218
33783
|
mc.noteRepairSkipped(
|
|
@@ -33224,6 +33789,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
33224
33789
|
);
|
|
33225
33790
|
return;
|
|
33226
33791
|
}
|
|
33792
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
33227
33793
|
const entry = {
|
|
33228
33794
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
33229
33795
|
...identity,
|
|
@@ -33231,15 +33797,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
33231
33797
|
blockProfile,
|
|
33232
33798
|
priority,
|
|
33233
33799
|
subscriberPriority: wire.subscriberPriority,
|
|
33234
|
-
deadlineAt
|
|
33235
|
-
playoutDeadlineAt:
|
|
33800
|
+
deadlineAt,
|
|
33801
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
33236
33802
|
anchorAt: deadline.anchorAt,
|
|
33237
33803
|
delayMs: deadline.delayMs,
|
|
33238
33804
|
bound: deadline.bound,
|
|
33239
33805
|
order: enqueueOrder++,
|
|
33240
33806
|
attempts: 0,
|
|
33241
33807
|
running: null,
|
|
33242
|
-
range: {
|
|
33808
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33809
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
33243
33810
|
};
|
|
33244
33811
|
const previous = entries.get(entry.key);
|
|
33245
33812
|
if (previous) {
|
|
@@ -33249,7 +33816,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
33249
33816
|
entries.set(entry.key, entry);
|
|
33250
33817
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
33251
33818
|
dispatch();
|
|
33252
|
-
});
|
|
33819
|
+
}, onHoleExpired);
|
|
33253
33820
|
}
|
|
33254
33821
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
33255
33822
|
queueMicrotask(() => {
|