@blockcast/mmt-container 2.2.0-main.a16efba1d85a → 2.2.0-main.a5b276c121d4
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 +1055 -151
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +67 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +377 -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 +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +44 -8
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +122 -17
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +120 -18
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +121 -17
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +28 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +417 -54
- 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 +876 -125
- 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 +407 -28
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +133 -31
- package/src/mmtp-container-parser.ts +214 -22
- package/src/mmtp-router.ts +502 -38
- 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
|
|
@@ -12949,7 +12970,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12949
12970
|
#retentionBlocks;
|
|
12950
12971
|
#heldEvicted;
|
|
12951
12972
|
#newest;
|
|
12973
|
+
#newestSource;
|
|
12952
12974
|
#holes;
|
|
12975
|
+
#expired;
|
|
12976
|
+
#newlyExpired;
|
|
12977
|
+
#closedBehind;
|
|
12953
12978
|
static #checkedRetention(retentionBlocks) {
|
|
12954
12979
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
12955
12980
|
throw new Error(
|
|
@@ -12986,6 +13011,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12986
13011
|
observe(position) {
|
|
12987
13012
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12988
13013
|
}
|
|
13014
|
+
/**
|
|
13015
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
13016
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
13017
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
13018
|
+
* reordering.
|
|
13019
|
+
*/
|
|
13020
|
+
get newestSource() {
|
|
13021
|
+
return this.#newestSource;
|
|
13022
|
+
}
|
|
13023
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
13024
|
+
noteSource(ssId) {
|
|
13025
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
13026
|
+
this.#newestSource = ssId;
|
|
13027
|
+
return true;
|
|
13028
|
+
}
|
|
12989
13029
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12990
13030
|
get heldEvicted() {
|
|
12991
13031
|
return this.#heldEvicted;
|
|
@@ -13004,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13004
13044
|
}
|
|
13005
13045
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
13006
13046
|
locate(ssId) {
|
|
13007
|
-
for (
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
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
|
+
}
|
|
13011
13053
|
}
|
|
13012
13054
|
}
|
|
13013
13055
|
return null;
|
|
13014
13056
|
}
|
|
13015
|
-
/** Number of pending
|
|
13057
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
13016
13058
|
get holeCount() {
|
|
13017
|
-
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);
|
|
13018
13070
|
}
|
|
13019
13071
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
13020
13072
|
blockEndingAt(end) {
|
|
@@ -13070,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13070
13122
|
/**
|
|
13071
13123
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
13072
13124
|
* block states its range and places every held source in it; later repair
|
|
13073
|
-
* symbols of the same block change nothing. A range
|
|
13074
|
-
*
|
|
13075
|
-
*
|
|
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.
|
|
13076
13130
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
13077
13131
|
* A block so far past `newest` that stating it would retire the position
|
|
13078
13132
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -13091,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13091
13145
|
const known = this.blockAt(start);
|
|
13092
13146
|
if (known !== void 0) {
|
|
13093
13147
|
if (known.span) {
|
|
13094
|
-
|
|
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}` };
|
|
13095
13158
|
}
|
|
13096
13159
|
if (known.length !== length) {
|
|
13097
13160
|
return {
|
|
@@ -13159,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13159
13222
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
13160
13223
|
}
|
|
13161
13224
|
/**
|
|
13162
|
-
* Turn every
|
|
13163
|
-
*
|
|
13164
|
-
*
|
|
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.
|
|
13165
13231
|
*/
|
|
13166
13232
|
closeHolesWithin(start, length) {
|
|
13167
13233
|
const closed = [];
|
|
13168
|
-
for (const
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
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
|
+
}
|
|
13174
13247
|
}
|
|
13175
13248
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
13176
13249
|
}
|
|
@@ -13184,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13184
13257
|
return void 0;
|
|
13185
13258
|
}
|
|
13186
13259
|
/**
|
|
13187
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
13188
|
-
* lies more than `retentionBlocks` blocks behind
|
|
13189
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13190
|
-
* 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.
|
|
13191
13265
|
*/
|
|
13192
13266
|
retireBehind(position) {
|
|
13193
13267
|
this.observe(position);
|
|
13194
13268
|
const horizon = this.#retentionBlocks;
|
|
13195
13269
|
const retired = [];
|
|
13270
|
+
this.#closedBehind.length = 0;
|
|
13196
13271
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
13197
13272
|
const block = this.#blocks[i];
|
|
13198
13273
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -13204,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13204
13279
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
13205
13280
|
const hole = this.#holes[i];
|
|
13206
13281
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
13207
|
-
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);
|
|
13208
13286
|
}
|
|
13209
13287
|
for (const ssId of [...this.#held.keys()]) {
|
|
13210
|
-
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
|
+
}
|
|
13211
13291
|
}
|
|
13212
13292
|
return retired;
|
|
13213
13293
|
}
|
|
@@ -13215,9 +13295,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13215
13295
|
reset() {
|
|
13216
13296
|
this.#blocks.length = 0;
|
|
13217
13297
|
this.#holes.length = 0;
|
|
13298
|
+
this.#expired.length = 0;
|
|
13299
|
+
this.#newlyExpired.length = 0;
|
|
13300
|
+
this.#closedBehind.length = 0;
|
|
13218
13301
|
this.#held.clear();
|
|
13219
13302
|
this.#coverageEnd = void 0;
|
|
13220
13303
|
this.#newest = void 0;
|
|
13304
|
+
this.#newestSource = void 0;
|
|
13221
13305
|
}
|
|
13222
13306
|
#claim(block) {
|
|
13223
13307
|
const placed = [];
|
|
@@ -13837,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13837
13921
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13838
13922
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13839
13923
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13924
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13840
13925
|
function isMulticastSymbolDelivery(value) {
|
|
13841
13926
|
return value === "datagram" || value === "ordered-relay";
|
|
13842
13927
|
}
|
|
@@ -13930,10 +14015,18 @@ var FECManager = class _FECManager {
|
|
|
13930
14015
|
// 'pending' forever, which would park every later frame of its stream in
|
|
13931
14016
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
13932
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
|
+
*/
|
|
13933
14022
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
13934
14023
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13935
14024
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13936
14025
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
14026
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
14027
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
14028
|
+
// not network arrivals and never make a block clean.
|
|
14029
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13937
14030
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13938
14031
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13939
14032
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -13942,6 +14035,9 @@ var FECManager = class _FECManager {
|
|
|
13942
14035
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
13943
14036
|
// symbols; waiting for K fails every one of them.
|
|
13944
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();
|
|
13945
14041
|
this.#ssbLengthConflictCount = 0;
|
|
13946
14042
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
13947
14043
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -14139,7 +14235,10 @@ var FECManager = class _FECManager {
|
|
|
14139
14235
|
// arrives via onBlockRecovered callback.
|
|
14140
14236
|
this.onBlockStarted = null;
|
|
14141
14237
|
this.onBlockComplete = null;
|
|
14142
|
-
/**
|
|
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
|
+
*/
|
|
14143
14242
|
this.onBlockFailed = null;
|
|
14144
14243
|
/**
|
|
14145
14244
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -14181,6 +14280,8 @@ var FECManager = class _FECManager {
|
|
|
14181
14280
|
* for it, which it must be.
|
|
14182
14281
|
*/
|
|
14183
14282
|
this.#onUndelimitedLoss = null;
|
|
14283
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
14284
|
+
this.#onIsoHoleExpired = null;
|
|
14184
14285
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
14185
14286
|
this.onBlockVanished = null;
|
|
14186
14287
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -14223,7 +14324,9 @@ var FECManager = class _FECManager {
|
|
|
14223
14324
|
#pendingWorkerCleanupBounds;
|
|
14224
14325
|
#blockSourceCounts;
|
|
14225
14326
|
#blockSourceEsis;
|
|
14327
|
+
#blockRebuiltEsis;
|
|
14226
14328
|
#blockAuthoredLengths;
|
|
14329
|
+
#isoSpanKeys;
|
|
14227
14330
|
#ssbLengthConflictCount;
|
|
14228
14331
|
#blockRepairRsIds;
|
|
14229
14332
|
#blockRsbLengths;
|
|
@@ -14290,6 +14393,7 @@ var FECManager = class _FECManager {
|
|
|
14290
14393
|
#decoderUnavailableEvents;
|
|
14291
14394
|
#repairSymbolSizeMismatchCount;
|
|
14292
14395
|
#onUndelimitedLoss;
|
|
14396
|
+
#onIsoHoleExpired;
|
|
14293
14397
|
#encodeTrackName(catalogTrackName) {
|
|
14294
14398
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
14295
14399
|
if (encoded === void 0) {
|
|
@@ -14761,7 +14865,10 @@ var FECManager = class _FECManager {
|
|
|
14761
14865
|
timer.handle = null;
|
|
14762
14866
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
14763
14867
|
};
|
|
14764
|
-
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
|
+
);
|
|
14765
14872
|
this.#repairDeadlines.set(key, timer);
|
|
14766
14873
|
}
|
|
14767
14874
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -14905,8 +15012,14 @@ var FECManager = class _FECManager {
|
|
|
14905
15012
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
14906
15013
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
14907
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).
|
|
14908
15021
|
*/
|
|
14909
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
15022
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
14910
15023
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
14911
15024
|
const now = performance.now();
|
|
14912
15025
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -14945,7 +15058,7 @@ var FECManager = class _FECManager {
|
|
|
14945
15058
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
14946
15059
|
};
|
|
14947
15060
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
14948
|
-
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))) {
|
|
14949
15062
|
discontinuity();
|
|
14950
15063
|
return;
|
|
14951
15064
|
}
|
|
@@ -15207,17 +15320,21 @@ var FECManager = class _FECManager {
|
|
|
15207
15320
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
15208
15321
|
this.#repairHoldUntilMs.delete(key);
|
|
15209
15322
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
15210
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
15323
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
15211
15324
|
};
|
|
15212
|
-
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
|
+
);
|
|
15213
15329
|
this.#activeBlocks.set(key, timer);
|
|
15214
15330
|
this.#indexBlockKey(key, sbn, packetId);
|
|
15215
15331
|
}
|
|
15216
15332
|
/**
|
|
15217
15333
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
15218
|
-
* 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.
|
|
15219
15336
|
*/
|
|
15220
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
15337
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
15221
15338
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
15222
15339
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
15223
15340
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -15233,7 +15350,7 @@ var FECManager = class _FECManager {
|
|
|
15233
15350
|
if (workerClient) {
|
|
15234
15351
|
this.#pendingWorkerCleanups.add(key);
|
|
15235
15352
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
15236
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
15353
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
15237
15354
|
workerClient.cleanup(sbn);
|
|
15238
15355
|
} else {
|
|
15239
15356
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -15545,7 +15662,9 @@ var FECManager = class _FECManager {
|
|
|
15545
15662
|
}
|
|
15546
15663
|
resetMap(this.#blockSourceCounts);
|
|
15547
15664
|
resetMap(this.#blockSourceEsis);
|
|
15665
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15548
15666
|
resetMap(this.#blockAuthoredLengths);
|
|
15667
|
+
resetSet(this.#isoSpanKeys);
|
|
15549
15668
|
resetMap(this.#blockRepairRsIds);
|
|
15550
15669
|
resetMap(this.#blockRsbLengths);
|
|
15551
15670
|
resetMap(this.#heldLayerStates);
|
|
@@ -15669,7 +15788,9 @@ var FECManager = class _FECManager {
|
|
|
15669
15788
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15670
15789
|
dropMap(this.#blockSourceCounts);
|
|
15671
15790
|
dropMap(this.#blockSourceEsis);
|
|
15791
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15672
15792
|
dropMap(this.#blockAuthoredLengths);
|
|
15793
|
+
dropSet(this.#isoSpanKeys);
|
|
15673
15794
|
dropMap(this.#blockRepairRsIds);
|
|
15674
15795
|
dropMap(this.#blockRsbLengths);
|
|
15675
15796
|
dropMap(this.#blockPathCounts);
|
|
@@ -15831,6 +15952,9 @@ var FECManager = class _FECManager {
|
|
|
15831
15952
|
this.#unindexBlockKey(key);
|
|
15832
15953
|
}
|
|
15833
15954
|
}
|
|
15955
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15956
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15957
|
+
}
|
|
15834
15958
|
return true;
|
|
15835
15959
|
}
|
|
15836
15960
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -16009,8 +16133,9 @@ var FECManager = class _FECManager {
|
|
|
16009
16133
|
return true;
|
|
16010
16134
|
}
|
|
16011
16135
|
/**
|
|
16012
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
16013
|
-
*
|
|
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.
|
|
16014
16139
|
*/
|
|
16015
16140
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
16016
16141
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -16148,6 +16273,8 @@ var FECManager = class _FECManager {
|
|
|
16148
16273
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16149
16274
|
const evictedBefore = locator.heldEvicted;
|
|
16150
16275
|
const held = locator.hold(ssId, source);
|
|
16276
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16277
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16151
16278
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16152
16279
|
if (evicted > 0) {
|
|
16153
16280
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16168,17 +16295,20 @@ var FECManager = class _FECManager {
|
|
|
16168
16295
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
16169
16296
|
*/
|
|
16170
16297
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
16171
|
-
const
|
|
16298
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16299
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
16300
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16172
16301
|
if (placement.status !== "placed") {
|
|
16173
16302
|
this.#isoRepairRefusedCount++;
|
|
16174
16303
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
16175
16304
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
16176
16305
|
}
|
|
16306
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
16177
16307
|
return placement;
|
|
16178
16308
|
}
|
|
16309
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
16179
16310
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
16180
16311
|
if (placement.hole !== void 0) {
|
|
16181
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
16182
16312
|
const hole = placement.hole.block;
|
|
16183
16313
|
const previous = locator.blockEndingAt(hole.start);
|
|
16184
16314
|
const loss = {
|
|
@@ -16199,17 +16329,29 @@ var FECManager = class _FECManager {
|
|
|
16199
16329
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
16200
16330
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
16201
16331
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
16202
|
-
* 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.
|
|
16203
16336
|
*/
|
|
16204
|
-
setUndelimitedLossHandler(handler) {
|
|
16337
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
16205
16338
|
this.#onUndelimitedLoss = handler;
|
|
16339
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
16206
16340
|
this.#deferIsoGaps = handler !== null;
|
|
16207
16341
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
16208
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
|
+
}
|
|
16209
16350
|
/**
|
|
16210
|
-
* The range repair for a hole ended: every hole
|
|
16211
|
-
*
|
|
16212
|
-
* 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).
|
|
16213
16355
|
*/
|
|
16214
16356
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
16215
16357
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -16228,7 +16370,10 @@ var FECManager = class _FECManager {
|
|
|
16228
16370
|
*/
|
|
16229
16371
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
16230
16372
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
16231
|
-
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
|
+
}
|
|
16232
16377
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
16233
16378
|
}
|
|
16234
16379
|
/**
|
|
@@ -16569,11 +16714,14 @@ var FECManager = class _FECManager {
|
|
|
16569
16714
|
});
|
|
16570
16715
|
client.onCleanupComplete((sbn) => {
|
|
16571
16716
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
16572
|
-
const
|
|
16717
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
16718
|
+
const cleanupKeys = owed.filter((key) => {
|
|
16573
16719
|
const block = this.#parseBlockKey(key);
|
|
16574
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
16720
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
16575
16721
|
});
|
|
16576
|
-
|
|
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");
|
|
16577
16725
|
});
|
|
16578
16726
|
client.onError((msg, code) => {
|
|
16579
16727
|
const requestedKind = code ?? msg;
|
|
@@ -16643,9 +16791,11 @@ var FECManager = class _FECManager {
|
|
|
16643
16791
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
16644
16792
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
16645
16793
|
*/
|
|
16646
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
16794
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
16647
16795
|
this.#clearOwedCleanupBound(key);
|
|
16648
16796
|
const bound = {
|
|
16797
|
+
dueAtMs,
|
|
16798
|
+
askedAtMs: performance.now(),
|
|
16649
16799
|
handle: setTimeout(() => {
|
|
16650
16800
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
16651
16801
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -16743,7 +16893,15 @@ var FECManager = class _FECManager {
|
|
|
16743
16893
|
this.#isoSourceOversizeCount++;
|
|
16744
16894
|
return null;
|
|
16745
16895
|
}
|
|
16746
|
-
|
|
16896
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16897
|
+
}
|
|
16898
|
+
/**
|
|
16899
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16900
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16901
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16902
|
+
* stated for `ssId`.
|
|
16903
|
+
*/
|
|
16904
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16747
16905
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16748
16906
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16749
16907
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16772,6 +16930,74 @@ var FECManager = class _FECManager {
|
|
|
16772
16930
|
}
|
|
16773
16931
|
return recovered;
|
|
16774
16932
|
}
|
|
16933
|
+
/**
|
|
16934
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16935
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16936
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16937
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16938
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16939
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16940
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16941
|
+
*
|
|
16942
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16943
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16944
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16945
|
+
* feedSourceSymbol takes it.
|
|
16946
|
+
*
|
|
16947
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16948
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16949
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16950
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16951
|
+
* block as clean on its own side), the block is completed here as
|
|
16952
|
+
* recovered, with nothing left to route.
|
|
16953
|
+
*/
|
|
16954
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16955
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16956
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16957
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16958
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16959
|
+
const sbn = located.block.start;
|
|
16960
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16961
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16962
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16963
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16964
|
+
if (rebuilt === void 0) {
|
|
16965
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16966
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16967
|
+
}
|
|
16968
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16969
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16970
|
+
rebuilt.add(located.esi);
|
|
16971
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16972
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
16973
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
16974
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16977
|
+
return { taken: true, recovered };
|
|
16978
|
+
}
|
|
16979
|
+
/**
|
|
16980
|
+
* A block that holds every one of its `length` sources, received or
|
|
16981
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16982
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16983
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16984
|
+
*/
|
|
16985
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16986
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16987
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16988
|
+
let present = rebuilt.size;
|
|
16989
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16990
|
+
if (present < length) return false;
|
|
16991
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16992
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16993
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16994
|
+
if (catalogTrackName === void 0) {
|
|
16995
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16996
|
+
} else {
|
|
16997
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16998
|
+
}
|
|
16999
|
+
return true;
|
|
17000
|
+
}
|
|
16775
17001
|
/**
|
|
16776
17002
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16777
17003
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16974,8 +17200,75 @@ var FECManager = class _FECManager {
|
|
|
16974
17200
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16975
17201
|
return null;
|
|
16976
17202
|
}
|
|
17203
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16977
17204
|
return recovered;
|
|
16978
17205
|
}
|
|
17206
|
+
/**
|
|
17207
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17208
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17209
|
+
* position past a block leaves the sources that block still misses
|
|
17210
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17211
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17212
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17213
|
+
* sent before it.
|
|
17214
|
+
*/
|
|
17215
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17216
|
+
const previous = locator.newestSource;
|
|
17217
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17218
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17219
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17220
|
+
this.#shortenUnrecoverableWait(
|
|
17221
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17222
|
+
block.start,
|
|
17223
|
+
packetId,
|
|
17224
|
+
catalogTrackName
|
|
17225
|
+
);
|
|
17226
|
+
}
|
|
17227
|
+
}
|
|
17228
|
+
/**
|
|
17229
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17230
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17231
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17232
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17233
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17234
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17235
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17236
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17237
|
+
* block.
|
|
17238
|
+
*/
|
|
17239
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17240
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17241
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17242
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
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;
|
|
17247
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17248
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17249
|
+
return void 0;
|
|
17250
|
+
}
|
|
17251
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17252
|
+
}
|
|
17253
|
+
/**
|
|
17254
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17255
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17256
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17257
|
+
*/
|
|
17258
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17259
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17260
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17261
|
+
if (config === void 0) return;
|
|
17262
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17263
|
+
if (waitMs === void 0) return;
|
|
17264
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17265
|
+
this.#armBlockTimeout(
|
|
17266
|
+
sbn,
|
|
17267
|
+
packetId,
|
|
17268
|
+
catalogTrackName,
|
|
17269
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17270
|
+
);
|
|
17271
|
+
}
|
|
16979
17272
|
/**
|
|
16980
17273
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16981
17274
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -17073,12 +17366,13 @@ var FECManager = class _FECManager {
|
|
|
17073
17366
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
17074
17367
|
* classification evidence, and marks the block clean when all of its
|
|
17075
17368
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
17076
|
-
* otherwise K.
|
|
17369
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
17370
|
+
* placed only now (see #noteSbnSeen).
|
|
17077
17371
|
*
|
|
17078
17372
|
* Throws for a packetId that was never configured, so a classified block
|
|
17079
17373
|
* never reads the manager-global K or timeout.
|
|
17080
17374
|
*/
|
|
17081
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
17375
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
17082
17376
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
17083
17377
|
if (trackConfig === void 0) {
|
|
17084
17378
|
throw new Error(
|
|
@@ -17094,7 +17388,8 @@ var FECManager = class _FECManager {
|
|
|
17094
17388
|
catalogTrackName,
|
|
17095
17389
|
"source",
|
|
17096
17390
|
evidence.packetSequenceNumber,
|
|
17097
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
17391
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
17392
|
+
held
|
|
17098
17393
|
);
|
|
17099
17394
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
17100
17395
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -17130,6 +17425,15 @@ var FECManager = class _FECManager {
|
|
|
17130
17425
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17131
17426
|
return;
|
|
17132
17427
|
}
|
|
17428
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17429
|
+
key,
|
|
17430
|
+
sbn,
|
|
17431
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17432
|
+
packetId,
|
|
17433
|
+
catalogTrackName
|
|
17434
|
+
)) {
|
|
17435
|
+
return;
|
|
17436
|
+
}
|
|
17133
17437
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17134
17438
|
}
|
|
17135
17439
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17138,7 +17442,9 @@ var FECManager = class _FECManager {
|
|
|
17138
17442
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17139
17443
|
this.#blockSourceCounts.delete(key);
|
|
17140
17444
|
this.#blockSourceEsis.delete(key);
|
|
17445
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17141
17446
|
this.#blockAuthoredLengths.delete(key);
|
|
17447
|
+
this.#isoSpanKeys.delete(key);
|
|
17142
17448
|
this.#blockRepairRsIds.delete(key);
|
|
17143
17449
|
this.#blockRsbLengths.delete(key);
|
|
17144
17450
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -17169,6 +17475,19 @@ var FECManager = class _FECManager {
|
|
|
17169
17475
|
#receiptNow() {
|
|
17170
17476
|
return this.#receiptAtMs ?? performance.now();
|
|
17171
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
|
+
}
|
|
17172
17491
|
/**
|
|
17173
17492
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
17174
17493
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -17204,10 +17523,20 @@ var FECManager = class _FECManager {
|
|
|
17204
17523
|
this.#repairDeadlineMs(trackConfig)
|
|
17205
17524
|
);
|
|
17206
17525
|
}
|
|
17207
|
-
this.#
|
|
17526
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17527
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17528
|
+
return;
|
|
17529
|
+
}
|
|
17530
|
+
this.#armBlockTimeout(
|
|
17531
|
+
sbn,
|
|
17532
|
+
packetId,
|
|
17533
|
+
catalogTrackName,
|
|
17534
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17535
|
+
);
|
|
17208
17536
|
}
|
|
17209
17537
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17210
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;
|
|
17211
17540
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
17212
17541
|
this.#clearRepairDeadline(key);
|
|
17213
17542
|
this.#clearBlockTimer(key);
|
|
@@ -17217,6 +17546,7 @@ var FECManager = class _FECManager {
|
|
|
17217
17546
|
}
|
|
17218
17547
|
this.#blockSourceCounts.delete(key);
|
|
17219
17548
|
this.#blockSourceEsis.delete(key);
|
|
17549
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17220
17550
|
this.#blockAuthoredLengths.delete(key);
|
|
17221
17551
|
this.#blockRepairRsIds.delete(key);
|
|
17222
17552
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17226,7 +17556,8 @@ var FECManager = class _FECManager {
|
|
|
17226
17556
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
17227
17557
|
this.#fecStats.blocksFailed++;
|
|
17228
17558
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
17229
|
-
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);
|
|
17230
17561
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
17231
17562
|
}
|
|
17232
17563
|
/**
|
|
@@ -17255,7 +17586,9 @@ var FECManager = class _FECManager {
|
|
|
17255
17586
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17256
17587
|
this.#blockSourceCounts.delete(key);
|
|
17257
17588
|
this.#blockSourceEsis.delete(key);
|
|
17589
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17258
17590
|
this.#blockAuthoredLengths.delete(key);
|
|
17591
|
+
this.#isoSpanKeys.delete(key);
|
|
17259
17592
|
this.#blockRepairRsIds.delete(key);
|
|
17260
17593
|
this.#blockRsbLengths.delete(key);
|
|
17261
17594
|
this.#clearBlockTimer(key);
|
|
@@ -17346,7 +17679,9 @@ var FECManager = class _FECManager {
|
|
|
17346
17679
|
this.#blockKeysBySbn.clear();
|
|
17347
17680
|
this.#blockSourceCounts.clear();
|
|
17348
17681
|
this.#blockSourceEsis.clear();
|
|
17682
|
+
this.#blockRebuiltEsis.clear();
|
|
17349
17683
|
this.#blockAuthoredLengths.clear();
|
|
17684
|
+
this.#isoSpanKeys.clear();
|
|
17350
17685
|
this.#blockRepairRsIds.clear();
|
|
17351
17686
|
this.#blockRsbLengths.clear();
|
|
17352
17687
|
this.#heldLayerStates.clear();
|
|
@@ -18248,6 +18583,9 @@ function authoredGap(before, after) {
|
|
|
18248
18583
|
function ssIdAfter(a, b) {
|
|
18249
18584
|
return (a - b | 0) > 0;
|
|
18250
18585
|
}
|
|
18586
|
+
function holdsRebuilt(stream) {
|
|
18587
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18588
|
+
}
|
|
18251
18589
|
var FecReleaseSequencer = class {
|
|
18252
18590
|
#streams = /* @__PURE__ */ new Map();
|
|
18253
18591
|
#gate;
|
|
@@ -18310,6 +18648,7 @@ var FecReleaseSequencer = class {
|
|
|
18310
18648
|
stream.lastSbn = void 0;
|
|
18311
18649
|
stream.discontinuity = false;
|
|
18312
18650
|
stream.rapSsId = void 0;
|
|
18651
|
+
stream.sourced = false;
|
|
18313
18652
|
}
|
|
18314
18653
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
18315
18654
|
this.#recordStat("droppedStale");
|
|
@@ -18336,6 +18675,7 @@ var FecReleaseSequencer = class {
|
|
|
18336
18675
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18337
18676
|
else hi = mid;
|
|
18338
18677
|
}
|
|
18678
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18339
18679
|
stream.buf.splice(lo, 0, frame);
|
|
18340
18680
|
this.#drainUnlessDeferred(stream);
|
|
18341
18681
|
}
|
|
@@ -18371,6 +18711,41 @@ var FecReleaseSequencer = class {
|
|
|
18371
18711
|
}
|
|
18372
18712
|
return false;
|
|
18373
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
|
+
}
|
|
18374
18749
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18375
18750
|
poke(streamKey) {
|
|
18376
18751
|
if (streamKey !== void 0) {
|
|
@@ -18462,16 +18837,32 @@ var FecReleaseSequencer = class {
|
|
|
18462
18837
|
last: null,
|
|
18463
18838
|
lastSbn: void 0,
|
|
18464
18839
|
discontinuity: false,
|
|
18465
|
-
rapSsId: void 0
|
|
18840
|
+
rapSsId: void 0,
|
|
18841
|
+
sourced: false,
|
|
18842
|
+
draining: 0
|
|
18466
18843
|
};
|
|
18467
18844
|
this.#streams.set(streamKey, stream);
|
|
18468
18845
|
}
|
|
18469
18846
|
return stream;
|
|
18470
18847
|
}
|
|
18471
|
-
#
|
|
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) {
|
|
18472
18858
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18473
18859
|
const head = stream.buf[0];
|
|
18474
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
|
+
}
|
|
18475
18866
|
if (stream.discontinuity) {
|
|
18476
18867
|
stream.discontinuity = false;
|
|
18477
18868
|
stream.buf.shift();
|
|
@@ -18485,9 +18876,10 @@ var FecReleaseSequencer = class {
|
|
|
18485
18876
|
}
|
|
18486
18877
|
const headSbn = head.meta.fecSbn;
|
|
18487
18878
|
const from = stream.lastSbn ?? headSbn;
|
|
18488
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18879
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18489
18880
|
let lost = [];
|
|
18490
18881
|
const unanchored = from === void 0 && to === void 0;
|
|
18882
|
+
let unresolved = false;
|
|
18491
18883
|
if (from !== void 0 && to !== void 0) {
|
|
18492
18884
|
const resolved = this.#gate.resolvedLoss(
|
|
18493
18885
|
stream.packetId,
|
|
@@ -18496,7 +18888,8 @@ var FecReleaseSequencer = class {
|
|
|
18496
18888
|
Math.max(from, to),
|
|
18497
18889
|
from
|
|
18498
18890
|
);
|
|
18499
|
-
|
|
18891
|
+
unresolved = resolved === null;
|
|
18892
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18500
18893
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18501
18894
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18502
18895
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18504,22 +18897,56 @@ var FecReleaseSequencer = class {
|
|
|
18504
18897
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18505
18898
|
continue;
|
|
18506
18899
|
}
|
|
18507
|
-
lost = resolved;
|
|
18900
|
+
lost = resolved ?? [];
|
|
18508
18901
|
}
|
|
18509
18902
|
stream.buf.shift();
|
|
18510
18903
|
this.#release(
|
|
18511
18904
|
stream,
|
|
18512
18905
|
head,
|
|
18513
|
-
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
|
|
18514
18907
|
);
|
|
18515
18908
|
}
|
|
18516
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
|
+
}
|
|
18517
18939
|
#release(stream, frame, decision) {
|
|
18940
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18941
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18518
18942
|
if (decision) frame.meta.gapBefore = decision;
|
|
18519
18943
|
stream.last = frame;
|
|
18520
|
-
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;
|
|
18521
18948
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18522
|
-
this.#emit(frame.data, frame.meta);
|
|
18949
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18523
18950
|
}
|
|
18524
18951
|
};
|
|
18525
18952
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -18780,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18780
19207
|
/**
|
|
18781
19208
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18782
19209
|
*
|
|
18783
|
-
*
|
|
18784
|
-
*
|
|
18785
|
-
*
|
|
18786
|
-
*
|
|
18787
|
-
*
|
|
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.
|
|
18788
19216
|
*/
|
|
18789
19217
|
get timingBudgetStats() {
|
|
18790
19218
|
return {
|
|
@@ -18844,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18844
19272
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18845
19273
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18846
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
|
+
}
|
|
18847
19283
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18848
19284
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18849
19285
|
if (!this.#fecSequencer) return;
|
|
@@ -19097,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19097
19533
|
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19098
19534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19099
19535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19100
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19101
19537
|
const admitted = resolver2.addMovieFragment(
|
|
19102
19538
|
mpu.mpuSequenceNumber,
|
|
19103
19539
|
data,
|
|
@@ -19170,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19170
19606
|
info,
|
|
19171
19607
|
resolver,
|
|
19172
19608
|
options?.recovered === true,
|
|
19173
|
-
options?.fecSbn
|
|
19609
|
+
options?.fecSbn,
|
|
19610
|
+
options?.fecSbnFirst
|
|
19174
19611
|
);
|
|
19175
19612
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19176
19613
|
}
|
|
@@ -19263,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19263
19700
|
info,
|
|
19264
19701
|
resolver,
|
|
19265
19702
|
options?.recovered === true,
|
|
19266
|
-
options?.fecSbn
|
|
19703
|
+
options?.fecSbn,
|
|
19704
|
+
options?.fecSbnFirst
|
|
19267
19705
|
);
|
|
19268
19706
|
if (outcome.record !== void 0) return outcome.record;
|
|
19269
19707
|
return this.#drop(outcome.reason);
|
|
@@ -19283,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19283
19721
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19284
19722
|
* is what keeps both true.
|
|
19285
19723
|
*/
|
|
19286
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19724
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19287
19725
|
const timing = resolver.sampleTiming(
|
|
19288
19726
|
combined.mpuSequenceNumber,
|
|
19289
19727
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19300,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19300
19738
|
);
|
|
19301
19739
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19302
19740
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19741
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19303
19742
|
if (!resolver.consumeSample(
|
|
19304
19743
|
combined.mpuSequenceNumber,
|
|
19305
19744
|
combined.movieFragmentSequenceNumber,
|
|
@@ -22805,7 +23244,18 @@ function keyframeOverlaySsId(header) {
|
|
|
22805
23244
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22806
23245
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22807
23246
|
}
|
|
23247
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23248
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23249
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23250
|
+
}
|
|
23251
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23252
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23253
|
+
return headerPsn + offset >>> 0;
|
|
23254
|
+
}
|
|
23255
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23256
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22808
23257
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23258
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
22809
23259
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22810
23260
|
function assertStreamIdentity(role, stream) {
|
|
22811
23261
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -22916,6 +23366,7 @@ var MmtpRouter = class {
|
|
|
22916
23366
|
#provenance = /* @__PURE__ */ new Map();
|
|
22917
23367
|
#onFlowProtected = null;
|
|
22918
23368
|
#unadmittedListedDrops = 0;
|
|
23369
|
+
#admittedSourceDatagrams = 0;
|
|
22919
23370
|
#unlistedRepairSetAside = 0;
|
|
22920
23371
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22921
23372
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22972,6 +23423,26 @@ var MmtpRouter = class {
|
|
|
22972
23423
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22973
23424
|
*/
|
|
22974
23425
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23426
|
+
/**
|
|
23427
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23428
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23429
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23430
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23431
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23432
|
+
* an MPU no member of which arrived).
|
|
23433
|
+
*/
|
|
23434
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23435
|
+
/**
|
|
23436
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
23437
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
23438
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
23439
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
23440
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
23441
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
23442
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
23443
|
+
* this offset did.
|
|
23444
|
+
*/
|
|
23445
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22975
23446
|
/**
|
|
22976
23447
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22977
23448
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -23041,7 +23512,9 @@ var MmtpRouter = class {
|
|
|
23041
23512
|
recovered: 0,
|
|
23042
23513
|
restarts: 0,
|
|
23043
23514
|
unsequencedDropped: 0,
|
|
23044
|
-
protectedDropped: 0
|
|
23515
|
+
protectedDropped: 0,
|
|
23516
|
+
baseFed: 0,
|
|
23517
|
+
baseUndetermined: 0
|
|
23045
23518
|
};
|
|
23046
23519
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
23047
23520
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23214,7 +23687,7 @@ var MmtpRouter = class {
|
|
|
23214
23687
|
const cb = this.#getFrameCallback(packetId);
|
|
23215
23688
|
if (isVideo && !assembler) {
|
|
23216
23689
|
if (frameMeta.recovered) {
|
|
23217
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23690
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23218
23691
|
} else {
|
|
23219
23692
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23220
23693
|
}
|
|
@@ -23247,13 +23720,18 @@ var MmtpRouter = class {
|
|
|
23247
23720
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
23248
23721
|
}
|
|
23249
23722
|
};
|
|
23250
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23723
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23251
23724
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
23252
23725
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
23253
23726
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
23254
23727
|
}
|
|
23255
|
-
|
|
23256
|
-
|
|
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
|
+
}
|
|
23257
23735
|
};
|
|
23258
23736
|
}
|
|
23259
23737
|
}
|
|
@@ -23298,8 +23776,9 @@ var MmtpRouter = class {
|
|
|
23298
23776
|
}
|
|
23299
23777
|
/**
|
|
23300
23778
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23301
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23302
|
-
*
|
|
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.
|
|
23303
23782
|
*/
|
|
23304
23783
|
get restampedRecoveredSequenceCount() {
|
|
23305
23784
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23478,6 +23957,7 @@ var MmtpRouter = class {
|
|
|
23478
23957
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23479
23958
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23480
23959
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23960
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23481
23961
|
if (previous) {
|
|
23482
23962
|
previous.flush();
|
|
23483
23963
|
previous.dispose();
|
|
@@ -23628,7 +24108,8 @@ var MmtpRouter = class {
|
|
|
23628
24108
|
fec,
|
|
23629
24109
|
k,
|
|
23630
24110
|
offsets: /* @__PURE__ */ new Map(),
|
|
23631
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24111
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24112
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23632
24113
|
};
|
|
23633
24114
|
this.#overlays.set(repairPacketId, inst);
|
|
23634
24115
|
return () => {
|
|
@@ -23658,12 +24139,18 @@ var MmtpRouter = class {
|
|
|
23658
24139
|
*/
|
|
23659
24140
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23660
24141
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
24142
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
24143
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
24144
|
+
stream.overlayToBase.routed.clear();
|
|
24145
|
+
stream.overlayToBase.pending.clear();
|
|
24146
|
+
}
|
|
23661
24147
|
}
|
|
23662
24148
|
#resetOverlay(inst) {
|
|
23663
24149
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23664
24150
|
inst.last = void 0;
|
|
23665
24151
|
inst.offsets.clear();
|
|
23666
24152
|
inst.moqBlocks.clear();
|
|
24153
|
+
inst.rebuilt.clear();
|
|
23667
24154
|
}
|
|
23668
24155
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23669
24156
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23714,18 +24201,33 @@ var MmtpRouter = class {
|
|
|
23714
24201
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23715
24202
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23716
24203
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23717
|
-
* included (7.1, 7.4.3).
|
|
23718
|
-
|
|
23719
|
-
|
|
24204
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
24205
|
+
* FEC reads it.
|
|
24206
|
+
*
|
|
24207
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
24208
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
24209
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
24210
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
24211
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
24212
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
24213
|
+
*/
|
|
24214
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23720
24215
|
if (this.#overlays.size === 0) return;
|
|
23721
24216
|
const ssId = keyframeOverlaySsId(header);
|
|
23722
24217
|
if (ssId === void 0) return;
|
|
24218
|
+
let rebuiltPsn;
|
|
23723
24219
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23724
24220
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24221
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24222
|
+
inst.rebuilt.delete(ssId);
|
|
23725
24223
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23726
24224
|
if (identity !== null) {
|
|
23727
24225
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23728
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
24226
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
24227
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
24228
|
+
overlaySsId: ssId,
|
|
24229
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
24230
|
+
});
|
|
23729
24231
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23730
24232
|
}
|
|
23731
24233
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23747,6 +24249,14 @@ var MmtpRouter = class {
|
|
|
23747
24249
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23748
24250
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23749
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
|
+
}
|
|
23750
24260
|
}
|
|
23751
24261
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23752
24262
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23780,11 +24290,16 @@ var MmtpRouter = class {
|
|
|
23780
24290
|
}
|
|
23781
24291
|
/**
|
|
23782
24292
|
* Route an overlay-recovered block's packets as source media of the
|
|
23783
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23784
|
-
*
|
|
23785
|
-
*
|
|
23786
|
-
*
|
|
23787
|
-
*
|
|
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.
|
|
23788
24303
|
*/
|
|
23789
24304
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23790
24305
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23804,22 +24319,139 @@ var MmtpRouter = class {
|
|
|
23804
24319
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23805
24320
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23806
24321
|
const restored = /* @__PURE__ */ new Set();
|
|
24322
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24323
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
23807
24324
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23808
|
-
const
|
|
23809
|
-
|
|
23810
|
-
|
|
24325
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24326
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24327
|
+
if (anchor !== void 0) {
|
|
24328
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23811
24329
|
restored.add(packetSequenceNumber);
|
|
23812
|
-
|
|
24330
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24331
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24332
|
+
if (baseSsId !== void 0) {
|
|
24333
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24334
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24335
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
24336
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24337
|
+
}
|
|
24338
|
+
}
|
|
24339
|
+
return [out];
|
|
24340
|
+
}
|
|
24341
|
+
if (!restamped) {
|
|
24342
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24343
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24344
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24345
|
+
if (baseSsId !== void 0) {
|
|
24346
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24347
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
24348
|
+
} else {
|
|
24349
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
24350
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
24351
|
+
}
|
|
24352
|
+
return [sym];
|
|
23813
24353
|
}
|
|
23814
|
-
if (!restamped) return [sym];
|
|
23815
24354
|
this.#overlayStats.unsequencedDropped++;
|
|
23816
24355
|
return [];
|
|
23817
24356
|
});
|
|
23818
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
|
+
}
|
|
23819
24369
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23820
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24370
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24371
|
+
baseSsIds,
|
|
24372
|
+
firstSsIds
|
|
23821
24373
|
});
|
|
23822
24374
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24375
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
24376
|
+
}
|
|
24377
|
+
/**
|
|
24378
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24379
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24380
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
24381
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
24382
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24383
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24384
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24385
|
+
* A base block no repair has stated yet takes it when one does
|
|
24386
|
+
* (#feedPendingRebuilt).
|
|
24387
|
+
*
|
|
24388
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24389
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24390
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24391
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24392
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24393
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24394
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24395
|
+
* block's own arrivals cannot show.
|
|
24396
|
+
*/
|
|
24397
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24398
|
+
const fec = this.#fecManager;
|
|
24399
|
+
const owner = inst.leg.sourcePacketId;
|
|
24400
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24401
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24402
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24403
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24404
|
+
const feeds = [];
|
|
24405
|
+
for (const sym of symbols) {
|
|
24406
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24407
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24408
|
+
if (ssId === void 0) {
|
|
24409
|
+
this.#overlayStats.baseUndetermined++;
|
|
24410
|
+
continue;
|
|
24411
|
+
}
|
|
24412
|
+
routed.delete(ssId);
|
|
24413
|
+
routed.add(ssId);
|
|
24414
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24415
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24416
|
+
}
|
|
24417
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24418
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24419
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24420
|
+
}
|
|
24421
|
+
}
|
|
24422
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24423
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24424
|
+
const fec = this.#fecManager;
|
|
24425
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24426
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24427
|
+
if (coords == null) {
|
|
24428
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24429
|
+
pending.delete(ssId);
|
|
24430
|
+
pending.set(ssId, symbolData.slice());
|
|
24431
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24432
|
+
return;
|
|
24433
|
+
}
|
|
24434
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24435
|
+
if (!fed.taken) return;
|
|
24436
|
+
this.#overlayStats.baseFed++;
|
|
24437
|
+
if (fed.recovered === null) return;
|
|
24438
|
+
const sbn = coords.sbn;
|
|
24439
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24440
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24441
|
+
fec.countRecovery(owner, sbn, name);
|
|
24442
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24443
|
+
});
|
|
24444
|
+
}
|
|
24445
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24446
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24447
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24448
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24449
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24450
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24451
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24452
|
+
pending.delete(ssId);
|
|
24453
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24454
|
+
}
|
|
23823
24455
|
}
|
|
23824
24456
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23825
24457
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23959,6 +24591,10 @@ var MmtpRouter = class {
|
|
|
23959
24591
|
get unadmittedListedDropCount() {
|
|
23960
24592
|
return this.#unadmittedListedDrops;
|
|
23961
24593
|
}
|
|
24594
|
+
/** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
|
|
24595
|
+
get admittedSourceDatagramCount() {
|
|
24596
|
+
return this.#admittedSourceDatagrams;
|
|
24597
|
+
}
|
|
23962
24598
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23963
24599
|
get unlistedRepairSetAsideCount() {
|
|
23964
24600
|
return this.#unlistedRepairSetAside;
|
|
@@ -24445,6 +25081,8 @@ var MmtpRouter = class {
|
|
|
24445
25081
|
)) return;
|
|
24446
25082
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24447
25083
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
25084
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
25085
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24448
25086
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24449
25087
|
const recovered = fec.feedRepairSymbol(
|
|
24450
25088
|
ownerPacketId,
|
|
@@ -24495,7 +25133,7 @@ var MmtpRouter = class {
|
|
|
24495
25133
|
catalogTrackName
|
|
24496
25134
|
)) continue;
|
|
24497
25135
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24498
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25136
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24499
25137
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24500
25138
|
}
|
|
24501
25139
|
if (payload.oversized) continue;
|
|
@@ -24582,6 +25220,8 @@ var MmtpRouter = class {
|
|
|
24582
25220
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24583
25221
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24584
25222
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
25223
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25224
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24585
25225
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24586
25226
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24587
25227
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24708,6 +25348,12 @@ var MmtpRouter = class {
|
|
|
24708
25348
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24709
25349
|
* own RAP cadence, so no timer is involved.
|
|
24710
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
|
+
*
|
|
24711
25357
|
* The commit is one synchronous transition inside the frame path: the
|
|
24712
25358
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24713
25359
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24758,6 +25404,9 @@ var MmtpRouter = class {
|
|
|
24758
25404
|
droppedIncomingFrames: 0
|
|
24759
25405
|
};
|
|
24760
25406
|
this.#staged = state;
|
|
25407
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25408
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25409
|
+
}
|
|
24761
25410
|
return {
|
|
24762
25411
|
cancel: () => {
|
|
24763
25412
|
if (this.#staged !== state) return false;
|
|
@@ -24840,7 +25489,17 @@ var MmtpRouter = class {
|
|
|
24840
25489
|
*/
|
|
24841
25490
|
#routeParsedFrame(data, meta) {
|
|
24842
25491
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
24843
|
-
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
|
+
}
|
|
24844
25503
|
this.#deliverParsedFrame(data, meta);
|
|
24845
25504
|
}
|
|
24846
25505
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -24992,12 +25651,16 @@ var MmtpRouter = class {
|
|
|
24992
25651
|
this.#parser.deferFecRelease(work);
|
|
24993
25652
|
} finally {
|
|
24994
25653
|
this.#fecPokeDepth--;
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
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);
|
|
25001
25664
|
}
|
|
25002
25665
|
}
|
|
25003
25666
|
}
|
|
@@ -25075,6 +25738,14 @@ var MmtpRouter = class {
|
|
|
25075
25738
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
25076
25739
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
25077
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
|
+
},
|
|
25078
25749
|
onReleased: (data, meta) => {
|
|
25079
25750
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
25080
25751
|
const timing = meta.authoredTiming;
|
|
@@ -25216,6 +25887,7 @@ var MmtpRouter = class {
|
|
|
25216
25887
|
const parsed = this.#parser.parsePacket(packet);
|
|
25217
25888
|
if (!parsed) return;
|
|
25218
25889
|
const info = parsed.info;
|
|
25890
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25219
25891
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25220
25892
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25221
25893
|
}
|
|
@@ -25497,7 +26169,8 @@ var MmtpRouter = class {
|
|
|
25497
26169
|
parsed.header,
|
|
25498
26170
|
info.sequenceNumber,
|
|
25499
26171
|
info.rapFlag,
|
|
25500
|
-
source
|
|
26172
|
+
source,
|
|
26173
|
+
fecPayloadId?.ssId
|
|
25501
26174
|
);
|
|
25502
26175
|
}
|
|
25503
26176
|
}
|
|
@@ -25523,7 +26196,8 @@ var MmtpRouter = class {
|
|
|
25523
26196
|
source: parserOptions?.source,
|
|
25524
26197
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25525
26198
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25526
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26199
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26200
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25527
26201
|
};
|
|
25528
26202
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25529
26203
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25647,7 +26321,8 @@ var MmtpRouter = class {
|
|
|
25647
26321
|
if (stream === void 0) {
|
|
25648
26322
|
stream = {
|
|
25649
26323
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25650
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26324
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26325
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25651
26326
|
};
|
|
25652
26327
|
byName.set(catalogTrackName, stream);
|
|
25653
26328
|
}
|
|
@@ -25762,8 +26437,16 @@ var MmtpRouter = class {
|
|
|
25762
26437
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25763
26438
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25764
26439
|
}
|
|
25765
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26440
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25766
26441
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26442
|
+
if (this.#isFecTrack(packetId)) {
|
|
26443
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26444
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26445
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26446
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26447
|
+
return;
|
|
26448
|
+
}
|
|
26449
|
+
}
|
|
25767
26450
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25768
26451
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25769
26452
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25917,6 +26600,13 @@ var MmtpRouter = class {
|
|
|
25917
26600
|
* frames by epoch only if pre-protection input ever matters.
|
|
25918
26601
|
*/
|
|
25919
26602
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26603
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26604
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26605
|
+
result = {
|
|
26606
|
+
...result,
|
|
26607
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26608
|
+
};
|
|
26609
|
+
}
|
|
25920
26610
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25921
26611
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25922
26612
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25973,7 +26663,12 @@ var MmtpRouter = class {
|
|
|
25973
26663
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25974
26664
|
if (ownerPacketId == null) return;
|
|
25975
26665
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25976
|
-
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
|
+
};
|
|
25977
26672
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25978
26673
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25979
26674
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -25981,7 +26676,10 @@ var MmtpRouter = class {
|
|
|
25981
26676
|
...result,
|
|
25982
26677
|
symbols: result.symbols.map((sym) => {
|
|
25983
26678
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25984
|
-
if (wire === sym.packetSequenceNumber)
|
|
26679
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26680
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26681
|
+
return sym;
|
|
26682
|
+
}
|
|
25985
26683
|
this.#restampedRecoveredSeqs++;
|
|
25986
26684
|
this.#restampedStreams.add(streamKey);
|
|
25987
26685
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -27766,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
27766
28464
|
function isSymbolCounts(value) {
|
|
27767
28465
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
27768
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
|
+
}
|
|
27769
28473
|
function isNullableNonNegative(value) {
|
|
27770
28474
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
27771
28475
|
}
|
|
@@ -27785,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
27785
28489
|
case "recovered":
|
|
27786
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"]);
|
|
27787
28491
|
case "failed":
|
|
27788
|
-
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"]));
|
|
27789
28496
|
case "repair-preempted":
|
|
27790
28497
|
case "repair-deferred":
|
|
27791
28498
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -27994,6 +28701,8 @@ var MulticastManager = class {
|
|
|
27994
28701
|
/** Live joinRepairEndpoints() releases → the repair owners they registered,
|
|
27995
28702
|
* re-applied after configureMultiTrack() clears catalog-derived owners. */
|
|
27996
28703
|
#repairReleases = /* @__PURE__ */ new Map();
|
|
28704
|
+
/** Protected repair flows owned by each live repair-endpoint release. */
|
|
28705
|
+
#repairProvenanceFlows = /* @__PURE__ */ new Map();
|
|
27997
28706
|
/** Each source's catalog keyframe overlay, from its `ok` binding. */
|
|
27998
28707
|
#overlayBindings = /* @__PURE__ */ new Map();
|
|
27999
28708
|
/** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
|
|
@@ -28051,7 +28760,7 @@ var MulticastManager = class {
|
|
|
28051
28760
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
28052
28761
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
28053
28762
|
},
|
|
28054
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28763
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
28055
28764
|
const pathCount = this.#fec.blockPathCount(
|
|
28056
28765
|
sbn,
|
|
28057
28766
|
packetId,
|
|
@@ -28069,7 +28778,8 @@ var MulticastManager = class {
|
|
|
28069
28778
|
symbols: {
|
|
28070
28779
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
28071
28780
|
moq: pathCount?.moqSymbols ?? 0
|
|
28072
|
-
}
|
|
28781
|
+
},
|
|
28782
|
+
...span === void 0 ? {} : { span }
|
|
28073
28783
|
});
|
|
28074
28784
|
},
|
|
28075
28785
|
onFrameGap: (decision) => {
|
|
@@ -28659,6 +29369,9 @@ var MulticastManager = class {
|
|
|
28659
29369
|
this.#transport.onFailback((reason) => {
|
|
28660
29370
|
const joined = this.#provenanceJoin;
|
|
28661
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
|
+
}
|
|
28662
29375
|
this.#onFailback?.(reason);
|
|
28663
29376
|
});
|
|
28664
29377
|
}
|
|
@@ -28693,6 +29406,12 @@ var MulticastManager = class {
|
|
|
28693
29406
|
"[MulticastManager] join: provenance flow (S, G, port) must match sourceAddress ?? senderAddress, groupAddress and port"
|
|
28694
29407
|
);
|
|
28695
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
|
+
}
|
|
28696
29415
|
const staged = this.#renditionSwitch;
|
|
28697
29416
|
if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
|
|
28698
29417
|
this.configureFec(config);
|
|
@@ -28700,8 +29419,8 @@ var MulticastManager = class {
|
|
|
28700
29419
|
const selectedPacketIds = config.packetIds?.length ? new Set(config.packetIds) : null;
|
|
28701
29420
|
const catalogTracks = config.tracks?.filter(({ packetId }) => selectedPacketIds === null || selectedPacketIds.has(packetId));
|
|
28702
29421
|
const ingressTracks = catalogTracks ? catalogTracks : selectedPacketIds === null ? void 0 : [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
|
|
28703
|
-
this.#releaseProvenanceJoin();
|
|
28704
29422
|
const provenance = config.provenance;
|
|
29423
|
+
this.#releaseProvenanceJoin();
|
|
28705
29424
|
const protectedTracks = ingressTracks ?? provenance?.flow.tracks;
|
|
28706
29425
|
const ingress = protectedTracks === void 0 ? void 0 : {
|
|
28707
29426
|
tracks: Object.freeze(
|
|
@@ -28754,8 +29473,13 @@ var MulticastManager = class {
|
|
|
28754
29473
|
* feeds the flow's receiver; media datagrams are held until one admits them.
|
|
28755
29474
|
*/
|
|
28756
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) {
|
|
28757
29481
|
const { flow, auth } = provenance;
|
|
28758
|
-
|
|
29482
|
+
return new BcProvenanceV2Receiver({
|
|
28759
29483
|
...provenance.receiver,
|
|
28760
29484
|
// The caller's ctx when it passes one, else the catalog's: an explicit
|
|
28761
29485
|
// `catalogCtx: undefined` must not switch off the 7.2.4 ctx check.
|
|
@@ -28772,9 +29496,6 @@ var MulticastManager = class {
|
|
|
28772
29496
|
maxAuthDelayMs: flow.maxAuthDelayMs,
|
|
28773
29497
|
manifestTimerMs: flow.manifestTimerMs
|
|
28774
29498
|
});
|
|
28775
|
-
this.#router.registerProvenanceFlow(ingress, receiver);
|
|
28776
|
-
this.#hookFailback();
|
|
28777
|
-
this.#provenanceJoin = { flow: ingress.flow };
|
|
28778
29499
|
}
|
|
28779
29500
|
/** Un-protect: drop the flow's hold queues (no expiry counted). */
|
|
28780
29501
|
#releaseProvenanceJoin() {
|
|
@@ -28805,8 +29526,25 @@ var MulticastManager = class {
|
|
|
28805
29526
|
* callback delivers must come from a subscription opened on
|
|
28806
29527
|
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
28807
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.
|
|
28808
29535
|
*/
|
|
28809
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
|
+
}
|
|
28810
29548
|
const releases = [];
|
|
28811
29549
|
let released = false;
|
|
28812
29550
|
const safely = (each) => {
|
|
@@ -28816,17 +29554,36 @@ var MulticastManager = class {
|
|
|
28816
29554
|
console.warn("[MulticastManager] repair group release failed", error);
|
|
28817
29555
|
}
|
|
28818
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 = [];
|
|
28819
29568
|
const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
|
|
28820
29569
|
...owner,
|
|
28821
29570
|
ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
|
|
28822
29571
|
repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
|
|
28823
29572
|
}));
|
|
28824
29573
|
this.#applyRepairOwners(repairOwners);
|
|
28825
|
-
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);
|
|
28826
29576
|
const ingress = {
|
|
28827
29577
|
tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
|
|
28828
|
-
...
|
|
29578
|
+
...flow === void 0 ? {} : { flow }
|
|
28829
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
|
+
}
|
|
28830
29587
|
let sender;
|
|
28831
29588
|
const feed = (packet, sourceAddress, ingressToFeed) => {
|
|
28832
29589
|
if (released) return;
|
|
@@ -28856,7 +29613,7 @@ var MulticastManager = class {
|
|
|
28856
29613
|
packetIds: endpoint.packetIds,
|
|
28857
29614
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
28858
29615
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
28859
|
-
...
|
|
29616
|
+
...flow === void 0 ? {} : { protected: true }
|
|
28860
29617
|
}, ingress).then((joined) => {
|
|
28861
29618
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
28862
29619
|
});
|
|
@@ -28865,9 +29622,12 @@ var MulticastManager = class {
|
|
|
28865
29622
|
if (released) return;
|
|
28866
29623
|
released = true;
|
|
28867
29624
|
this.#repairReleases.delete(release);
|
|
29625
|
+
this.#repairProvenanceFlows.delete(release);
|
|
28868
29626
|
for (const each of releases.splice(0)) safely(each);
|
|
28869
29627
|
};
|
|
28870
29628
|
this.#repairReleases.set(release, repairOwners);
|
|
29629
|
+
this.#repairProvenanceFlows.set(release, provenanceFlows);
|
|
29630
|
+
if (provenanceFlows.length) this.#hookFailback();
|
|
28871
29631
|
return release;
|
|
28872
29632
|
}
|
|
28873
29633
|
#applyRepairOwners(owners) {
|
|
@@ -29449,10 +30209,12 @@ var MulticastManager = class {
|
|
|
29449
30209
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
29450
30210
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
29451
30211
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
29452
|
-
* 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.
|
|
29453
30215
|
*/
|
|
29454
|
-
onUndelimitedLoss(cb) {
|
|
29455
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30216
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30217
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
29456
30218
|
}
|
|
29457
30219
|
/**
|
|
29458
30220
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -29688,7 +30450,8 @@ var MulticastManager = class {
|
|
|
29688
30450
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
29689
30451
|
...event,
|
|
29690
30452
|
block: Object.freeze({ ...event.block }),
|
|
29691
|
-
..."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 }) } : {}
|
|
29692
30455
|
}
|
|
29693
30456
|
);
|
|
29694
30457
|
if (this.#logRepairLifecycle) {
|
|
@@ -32360,6 +33123,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32360
33123
|
|
|
32361
33124
|
// src/repair-fetch.ts
|
|
32362
33125
|
var MOQT_PRIORITY_MAX = 255;
|
|
33126
|
+
var WALK_LEAD_MS = 3;
|
|
32363
33127
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
32364
33128
|
function isMoqtPriority(value) {
|
|
32365
33129
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -32410,8 +33174,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
32410
33174
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
32411
33175
|
};
|
|
32412
33176
|
const clearEntry = (entry) => {
|
|
32413
|
-
|
|
32414
|
-
|
|
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);
|
|
32415
33188
|
};
|
|
32416
33189
|
const releaseHold = (entry) => {
|
|
32417
33190
|
if (entry.range) return;
|
|
@@ -32478,21 +33251,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
32478
33251
|
const admit = (entry, now) => {
|
|
32479
33252
|
const budgetMs = entry.deadlineAt - now;
|
|
32480
33253
|
const rttMs = readRtt();
|
|
32481
|
-
const reason =
|
|
33254
|
+
const reason = refusal(entry, now, rttMs);
|
|
32482
33255
|
if (reason === null) return rttMs;
|
|
32483
33256
|
skip(entry, reason, rttMs, budgetMs);
|
|
32484
33257
|
return null;
|
|
32485
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";
|
|
32486
33260
|
const retire = (entry) => {
|
|
32487
33261
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
32488
33262
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
32489
33263
|
};
|
|
32490
|
-
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) => {
|
|
32491
33272
|
for (const entry of entries.values()) {
|
|
32492
|
-
if (entry
|
|
33273
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
32493
33274
|
}
|
|
32494
33275
|
return false;
|
|
32495
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
|
+
};
|
|
32496
33290
|
const preemptBestEffort = () => {
|
|
32497
33291
|
for (const entry of entries.values()) {
|
|
32498
33292
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -32521,7 +33315,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
32521
33315
|
}
|
|
32522
33316
|
const rttMs = admit(entry, now);
|
|
32523
33317
|
if (rttMs === null) return;
|
|
32524
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33318
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33319
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
32525
33320
|
if (counted) entry.attempts++;
|
|
32526
33321
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
32527
33322
|
rttMs,
|
|
@@ -32543,6 +33338,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
32543
33338
|
}
|
|
32544
33339
|
void execute(entry, run);
|
|
32545
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
|
+
};
|
|
32546
33349
|
const dispatch = () => {
|
|
32547
33350
|
if (disposed) return;
|
|
32548
33351
|
const now = performance.now();
|
|
@@ -32567,6 +33370,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32567
33370
|
for (const entry of queued) {
|
|
32568
33371
|
if (entry.priority === 0) start(entry);
|
|
32569
33372
|
}
|
|
33373
|
+
yieldVideoWalks(queued);
|
|
32570
33374
|
if (outstandingBelow(1)) return;
|
|
32571
33375
|
for (const entry of queued) {
|
|
32572
33376
|
if (entry.priority === 1) start(entry);
|
|
@@ -32593,6 +33397,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32593
33397
|
}
|
|
32594
33398
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
32595
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
|
+
};
|
|
32596
33410
|
const finish = (entry, run, fed, error) => {
|
|
32597
33411
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
32598
33412
|
return;
|
|
@@ -32637,27 +33451,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
32637
33451
|
signal: run.controller.signal,
|
|
32638
33452
|
subscriberPriority: entry.subscriberPriority
|
|
32639
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
|
+
};
|
|
32640
33462
|
try {
|
|
32641
|
-
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({
|
|
32642
33464
|
...common,
|
|
32643
|
-
startGroup:
|
|
32644
|
-
endGroup:
|
|
32645
|
-
lostSsId:
|
|
32646
|
-
})
|
|
33465
|
+
startGroup: range.groups.startGroup,
|
|
33466
|
+
endGroup: range.groups.endGroup,
|
|
33467
|
+
lostSsId: range.lostSsId
|
|
33468
|
+
});
|
|
32647
33469
|
for await (const packet of packets) {
|
|
32648
33470
|
if (run.stopped !== null || entry.running !== run) break;
|
|
32649
33471
|
fed++;
|
|
32650
|
-
if (
|
|
32651
|
-
|
|
32652
|
-
} else {
|
|
32653
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
32654
|
-
}
|
|
33472
|
+
if (range === void 0) feed(packet);
|
|
33473
|
+
else atReport(range, () => feed(packet));
|
|
32655
33474
|
}
|
|
32656
33475
|
finish(entry, run, fed, void 0);
|
|
32657
33476
|
} catch (error) {
|
|
32658
33477
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
32659
33478
|
}
|
|
32660
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
|
+
}
|
|
32661
33529
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
32662
33530
|
for (const entry of [...entries.values()]) {
|
|
32663
33531
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -32853,7 +33721,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
32853
33721
|
throw error;
|
|
32854
33722
|
}
|
|
32855
33723
|
};
|
|
32856
|
-
|
|
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) {
|
|
32857
33755
|
mc.onUndelimitedLoss((loss) => {
|
|
32858
33756
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
32859
33757
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -32861,9 +33759,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
32861
33759
|
const blockProfile = blockProfileOf(packetId);
|
|
32862
33760
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
32863
33761
|
if (!target) return close();
|
|
32864
|
-
const
|
|
32865
|
-
const
|
|
32866
|
-
|
|
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) {
|
|
32867
33767
|
close();
|
|
32868
33768
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
32869
33769
|
return;
|
|
@@ -32874,8 +33774,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
32874
33774
|
packetId,
|
|
32875
33775
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
32876
33776
|
};
|
|
32877
|
-
const wire = wirePriority(identity,
|
|
32878
|
-
|
|
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);
|
|
32879
33781
|
if (deadline === null) {
|
|
32880
33782
|
close();
|
|
32881
33783
|
mc.noteRepairSkipped(
|
|
@@ -32887,6 +33789,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32887
33789
|
);
|
|
32888
33790
|
return;
|
|
32889
33791
|
}
|
|
33792
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
32890
33793
|
const entry = {
|
|
32891
33794
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
32892
33795
|
...identity,
|
|
@@ -32894,15 +33797,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32894
33797
|
blockProfile,
|
|
32895
33798
|
priority,
|
|
32896
33799
|
subscriberPriority: wire.subscriberPriority,
|
|
32897
|
-
deadlineAt
|
|
32898
|
-
playoutDeadlineAt:
|
|
33800
|
+
deadlineAt,
|
|
33801
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
32899
33802
|
anchorAt: deadline.anchorAt,
|
|
32900
33803
|
delayMs: deadline.delayMs,
|
|
32901
33804
|
bound: deadline.bound,
|
|
32902
33805
|
order: enqueueOrder++,
|
|
32903
33806
|
attempts: 0,
|
|
32904
33807
|
running: null,
|
|
32905
|
-
range: {
|
|
33808
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33809
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
32906
33810
|
};
|
|
32907
33811
|
const previous = entries.get(entry.key);
|
|
32908
33812
|
if (previous) {
|
|
@@ -32912,7 +33816,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32912
33816
|
entries.set(entry.key, entry);
|
|
32913
33817
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
32914
33818
|
dispatch();
|
|
32915
|
-
});
|
|
33819
|
+
}, onHoleExpired);
|
|
32916
33820
|
}
|
|
32917
33821
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
32918
33822
|
queueMicrotask(() => {
|