@blockcast/mmt-container 2.2.0-main.4a79a288e69e → 2.2.0-main.4ccba0f2f32a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-runtime.js +1119 -165
- 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 +2 -2
- 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 +64 -8
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +159 -21
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +158 -23
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +158 -21
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +31 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +481 -68
- 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 +940 -139
- 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 +158 -21
- 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 +158 -21
- 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 +3 -0
- package/src/iso-block-locator.ts +133 -31
- package/src/mmtp-container-parser.ts +273 -26
- package/src/mmtp-router.ts +541 -55
- 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"]);
|
|
@@ -18560,6 +18987,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18560
18987
|
this.#reclaimedTimingSamples = 0;
|
|
18561
18988
|
this.#evictedTimingSamples = 0;
|
|
18562
18989
|
this.#rejectedMovieFragments = 0;
|
|
18990
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18991
|
+
held: 0,
|
|
18992
|
+
expired: 0,
|
|
18993
|
+
discarded: 0,
|
|
18994
|
+
superseded: 0
|
|
18995
|
+
};
|
|
18563
18996
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18564
18997
|
this.#fecGate = null;
|
|
18565
18998
|
this.#fecSequencer = null;
|
|
@@ -18668,6 +19101,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18668
19101
|
#reclaimedTimingSamples;
|
|
18669
19102
|
#evictedTimingSamples;
|
|
18670
19103
|
#rejectedMovieFragments;
|
|
19104
|
+
#recoveredHeadsAwaitingTiming;
|
|
18671
19105
|
#sequencers;
|
|
18672
19106
|
#fecGate;
|
|
18673
19107
|
#fecSequencer;
|
|
@@ -18773,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18773
19207
|
/**
|
|
18774
19208
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18775
19209
|
*
|
|
18776
|
-
*
|
|
18777
|
-
*
|
|
18778
|
-
*
|
|
18779
|
-
*
|
|
18780
|
-
*
|
|
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.
|
|
18781
19216
|
*/
|
|
18782
19217
|
get timingBudgetStats() {
|
|
18783
19218
|
return {
|
|
@@ -18788,6 +19223,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18788
19223
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18789
19224
|
};
|
|
18790
19225
|
}
|
|
19226
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
19227
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19228
|
+
let pending = 0;
|
|
19229
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19230
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
19231
|
+
}
|
|
19232
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
19233
|
+
}
|
|
18791
19234
|
get reorderStats() {
|
|
18792
19235
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18793
19236
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18829,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18829
19272
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18830
19273
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18831
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
|
+
}
|
|
18832
19283
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18833
19284
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18834
19285
|
if (!this.#fecSequencer) return;
|
|
@@ -18961,7 +19412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18961
19412
|
const now = Date.now();
|
|
18962
19413
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18963
19414
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18964
|
-
awaitingTiming.shift();
|
|
19415
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18965
19416
|
}
|
|
18966
19417
|
}
|
|
18967
19418
|
}
|
|
@@ -19082,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19082
19533
|
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19083
19534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19084
19535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19085
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19086
19537
|
const admitted = resolver2.addMovieFragment(
|
|
19087
19538
|
mpu.mpuSequenceNumber,
|
|
19088
19539
|
data,
|
|
@@ -19129,6 +19580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19129
19580
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19130
19581
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19131
19582
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19583
|
+
sequenceNumber: info.sequenceNumber,
|
|
19132
19584
|
arrivedAt: Date.now()
|
|
19133
19585
|
});
|
|
19134
19586
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19154,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19154
19606
|
info,
|
|
19155
19607
|
resolver,
|
|
19156
19608
|
options?.recovered === true,
|
|
19157
|
-
options?.fecSbn
|
|
19609
|
+
options?.fecSbn,
|
|
19610
|
+
options?.fecSbnFirst
|
|
19158
19611
|
);
|
|
19159
19612
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
19160
19613
|
}
|
|
@@ -19247,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19247
19700
|
info,
|
|
19248
19701
|
resolver,
|
|
19249
19702
|
options?.recovered === true,
|
|
19250
|
-
options?.fecSbn
|
|
19703
|
+
options?.fecSbn,
|
|
19704
|
+
options?.fecSbnFirst
|
|
19251
19705
|
);
|
|
19252
19706
|
if (outcome.record !== void 0) return outcome.record;
|
|
19253
19707
|
return this.#drop(outcome.reason);
|
|
@@ -19267,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19267
19721
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
19268
19722
|
* is what keeps both true.
|
|
19269
19723
|
*/
|
|
19270
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19724
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
19271
19725
|
const timing = resolver.sampleTiming(
|
|
19272
19726
|
combined.mpuSequenceNumber,
|
|
19273
19727
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19284,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19284
19738
|
);
|
|
19285
19739
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
19286
19740
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19741
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
19287
19742
|
if (!resolver.consumeSample(
|
|
19288
19743
|
combined.mpuSequenceNumber,
|
|
19289
19744
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19517,6 +19972,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19517
19972
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19518
19973
|
this.#sequencers.clear();
|
|
19519
19974
|
this.#fecSequencer?.discard();
|
|
19975
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19976
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19977
|
+
}
|
|
19520
19978
|
this.#timingStreams.clear();
|
|
19521
19979
|
this.#pendingTimingSamples = 0;
|
|
19522
19980
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19535,10 +19993,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19535
19993
|
return resolver;
|
|
19536
19994
|
}
|
|
19537
19995
|
#awaitTiming(streamKey, head) {
|
|
19538
|
-
const
|
|
19539
|
-
if (!
|
|
19996
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19997
|
+
if (!state) return;
|
|
19998
|
+
if (!head.options?.recovered) {
|
|
19999
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
20000
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
20001
|
+
state.awaitingTiming = kept;
|
|
20002
|
+
}
|
|
20003
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19540
20004
|
awaitingTiming.push(head);
|
|
19541
|
-
if (
|
|
20005
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
20006
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
20007
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
20008
|
+
}
|
|
20009
|
+
}
|
|
20010
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
20011
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
20012
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19542
20013
|
}
|
|
19543
20014
|
/**
|
|
19544
20015
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19566,6 +20037,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19566
20037
|
const state = this.#timingStreams.get(streamKey);
|
|
19567
20038
|
if (!state) return;
|
|
19568
20039
|
this.#timingStreams.delete(streamKey);
|
|
20040
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19569
20041
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19570
20042
|
if (this.#pendingTimingSamples < 0) {
|
|
19571
20043
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22772,7 +23244,18 @@ function keyframeOverlaySsId(header) {
|
|
|
22772
23244
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22773
23245
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22774
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;
|
|
22775
23257
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23258
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
22776
23259
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22777
23260
|
function assertStreamIdentity(role, stream) {
|
|
22778
23261
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -22883,6 +23366,7 @@ var MmtpRouter = class {
|
|
|
22883
23366
|
#provenance = /* @__PURE__ */ new Map();
|
|
22884
23367
|
#onFlowProtected = null;
|
|
22885
23368
|
#unadmittedListedDrops = 0;
|
|
23369
|
+
#admittedSourceDatagrams = 0;
|
|
22886
23370
|
#unlistedRepairSetAside = 0;
|
|
22887
23371
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22888
23372
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22939,6 +23423,26 @@ var MmtpRouter = class {
|
|
|
22939
23423
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22940
23424
|
*/
|
|
22941
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();
|
|
22942
23446
|
/**
|
|
22943
23447
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22944
23448
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -23008,7 +23512,9 @@ var MmtpRouter = class {
|
|
|
23008
23512
|
recovered: 0,
|
|
23009
23513
|
restarts: 0,
|
|
23010
23514
|
unsequencedDropped: 0,
|
|
23011
|
-
protectedDropped: 0
|
|
23515
|
+
protectedDropped: 0,
|
|
23516
|
+
baseFed: 0,
|
|
23517
|
+
baseUndetermined: 0
|
|
23012
23518
|
};
|
|
23013
23519
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
23014
23520
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23181,7 +23687,7 @@ var MmtpRouter = class {
|
|
|
23181
23687
|
const cb = this.#getFrameCallback(packetId);
|
|
23182
23688
|
if (isVideo && !assembler) {
|
|
23183
23689
|
if (frameMeta.recovered) {
|
|
23184
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23690
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23185
23691
|
} else {
|
|
23186
23692
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23187
23693
|
}
|
|
@@ -23214,13 +23720,18 @@ var MmtpRouter = class {
|
|
|
23214
23720
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
23215
23721
|
}
|
|
23216
23722
|
};
|
|
23217
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23723
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
23218
23724
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
23219
23725
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
23220
23726
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
23221
23727
|
}
|
|
23222
|
-
|
|
23223
|
-
|
|
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
|
+
}
|
|
23224
23735
|
};
|
|
23225
23736
|
}
|
|
23226
23737
|
}
|
|
@@ -23265,8 +23776,9 @@ var MmtpRouter = class {
|
|
|
23265
23776
|
}
|
|
23266
23777
|
/**
|
|
23267
23778
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
23268
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
23269
|
-
*
|
|
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.
|
|
23270
23782
|
*/
|
|
23271
23783
|
get restampedRecoveredSequenceCount() {
|
|
23272
23784
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23445,6 +23957,7 @@ var MmtpRouter = class {
|
|
|
23445
23957
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23446
23958
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23447
23959
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23960
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23448
23961
|
if (previous) {
|
|
23449
23962
|
previous.flush();
|
|
23450
23963
|
previous.dispose();
|
|
@@ -23571,6 +24084,10 @@ var MmtpRouter = class {
|
|
|
23571
24084
|
get keyframeOverlayStats() {
|
|
23572
24085
|
return { ...this.#overlayStats };
|
|
23573
24086
|
}
|
|
24087
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
24088
|
+
get recoveredHeadsAwaitingTiming() {
|
|
24089
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
24090
|
+
}
|
|
23574
24091
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23575
24092
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23576
24093
|
}
|
|
@@ -23591,7 +24108,8 @@ var MmtpRouter = class {
|
|
|
23591
24108
|
fec,
|
|
23592
24109
|
k,
|
|
23593
24110
|
offsets: /* @__PURE__ */ new Map(),
|
|
23594
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24111
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24112
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23595
24113
|
};
|
|
23596
24114
|
this.#overlays.set(repairPacketId, inst);
|
|
23597
24115
|
return () => {
|
|
@@ -23621,12 +24139,18 @@ var MmtpRouter = class {
|
|
|
23621
24139
|
*/
|
|
23622
24140
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23623
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
|
+
}
|
|
23624
24147
|
}
|
|
23625
24148
|
#resetOverlay(inst) {
|
|
23626
24149
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23627
24150
|
inst.last = void 0;
|
|
23628
24151
|
inst.offsets.clear();
|
|
23629
24152
|
inst.moqBlocks.clear();
|
|
24153
|
+
inst.rebuilt.clear();
|
|
23630
24154
|
}
|
|
23631
24155
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23632
24156
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23677,18 +24201,33 @@ var MmtpRouter = class {
|
|
|
23677
24201
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23678
24202
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23679
24203
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23680
|
-
* included (7.1, 7.4.3).
|
|
23681
|
-
|
|
23682
|
-
|
|
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) {
|
|
23683
24215
|
if (this.#overlays.size === 0) return;
|
|
23684
24216
|
const ssId = keyframeOverlaySsId(header);
|
|
23685
24217
|
if (ssId === void 0) return;
|
|
24218
|
+
let rebuiltPsn;
|
|
23686
24219
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23687
24220
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24221
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24222
|
+
inst.rebuilt.delete(ssId);
|
|
23688
24223
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23689
24224
|
if (identity !== null) {
|
|
23690
24225
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23691
|
-
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
|
+
});
|
|
23692
24231
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23693
24232
|
}
|
|
23694
24233
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23710,6 +24249,14 @@ var MmtpRouter = class {
|
|
|
23710
24249
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23711
24250
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23712
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
|
+
}
|
|
23713
24260
|
}
|
|
23714
24261
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23715
24262
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23743,11 +24290,16 @@ var MmtpRouter = class {
|
|
|
23743
24290
|
}
|
|
23744
24291
|
/**
|
|
23745
24292
|
* Route an overlay-recovered block's packets as source media of the
|
|
23746
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23747
|
-
*
|
|
23748
|
-
*
|
|
23749
|
-
*
|
|
23750
|
-
*
|
|
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.
|
|
23751
24303
|
*/
|
|
23752
24304
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23753
24305
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23767,22 +24319,139 @@ var MmtpRouter = class {
|
|
|
23767
24319
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23768
24320
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23769
24321
|
const restored = /* @__PURE__ */ new Set();
|
|
24322
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24323
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
23770
24324
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23771
|
-
const
|
|
23772
|
-
|
|
23773
|
-
|
|
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;
|
|
23774
24329
|
restored.add(packetSequenceNumber);
|
|
23775
|
-
|
|
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];
|
|
23776
24353
|
}
|
|
23777
|
-
if (!restamped) return [sym];
|
|
23778
24354
|
this.#overlayStats.unsequencedDropped++;
|
|
23779
24355
|
return [];
|
|
23780
24356
|
});
|
|
23781
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
|
+
}
|
|
23782
24369
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23783
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24370
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24371
|
+
baseSsIds,
|
|
24372
|
+
firstSsIds
|
|
23784
24373
|
});
|
|
23785
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
|
+
}
|
|
23786
24455
|
}
|
|
23787
24456
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23788
24457
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23922,6 +24591,10 @@ var MmtpRouter = class {
|
|
|
23922
24591
|
get unadmittedListedDropCount() {
|
|
23923
24592
|
return this.#unadmittedListedDrops;
|
|
23924
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
|
+
}
|
|
23925
24598
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23926
24599
|
get unlistedRepairSetAsideCount() {
|
|
23927
24600
|
return this.#unlistedRepairSetAside;
|
|
@@ -24408,6 +25081,8 @@ var MmtpRouter = class {
|
|
|
24408
25081
|
)) return;
|
|
24409
25082
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24410
25083
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
25084
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
25085
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24411
25086
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24412
25087
|
const recovered = fec.feedRepairSymbol(
|
|
24413
25088
|
ownerPacketId,
|
|
@@ -24458,7 +25133,7 @@ var MmtpRouter = class {
|
|
|
24458
25133
|
catalogTrackName
|
|
24459
25134
|
)) continue;
|
|
24460
25135
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24461
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25136
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24462
25137
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24463
25138
|
}
|
|
24464
25139
|
if (payload.oversized) continue;
|
|
@@ -24545,6 +25220,8 @@ var MmtpRouter = class {
|
|
|
24545
25220
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24546
25221
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24547
25222
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
25223
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25224
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24548
25225
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24549
25226
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24550
25227
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24671,6 +25348,12 @@ var MmtpRouter = class {
|
|
|
24671
25348
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24672
25349
|
* own RAP cadence, so no timer is involved.
|
|
24673
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
|
+
*
|
|
24674
25357
|
* The commit is one synchronous transition inside the frame path: the
|
|
24675
25358
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24676
25359
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24721,6 +25404,9 @@ var MmtpRouter = class {
|
|
|
24721
25404
|
droppedIncomingFrames: 0
|
|
24722
25405
|
};
|
|
24723
25406
|
this.#staged = state;
|
|
25407
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25408
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25409
|
+
}
|
|
24724
25410
|
return {
|
|
24725
25411
|
cancel: () => {
|
|
24726
25412
|
if (this.#staged !== state) return false;
|
|
@@ -24803,7 +25489,17 @@ var MmtpRouter = class {
|
|
|
24803
25489
|
*/
|
|
24804
25490
|
#routeParsedFrame(data, meta) {
|
|
24805
25491
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
24806
|
-
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
|
+
}
|
|
24807
25503
|
this.#deliverParsedFrame(data, meta);
|
|
24808
25504
|
}
|
|
24809
25505
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -24955,12 +25651,16 @@ var MmtpRouter = class {
|
|
|
24955
25651
|
this.#parser.deferFecRelease(work);
|
|
24956
25652
|
} finally {
|
|
24957
25653
|
this.#fecPokeDepth--;
|
|
24958
|
-
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
|
|
24962
|
-
|
|
24963
|
-
|
|
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);
|
|
24964
25664
|
}
|
|
24965
25665
|
}
|
|
24966
25666
|
}
|
|
@@ -25038,6 +25738,14 @@ var MmtpRouter = class {
|
|
|
25038
25738
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
25039
25739
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
25040
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
|
+
},
|
|
25041
25749
|
onReleased: (data, meta) => {
|
|
25042
25750
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
25043
25751
|
const timing = meta.authoredTiming;
|
|
@@ -25179,6 +25887,7 @@ var MmtpRouter = class {
|
|
|
25179
25887
|
const parsed = this.#parser.parsePacket(packet);
|
|
25180
25888
|
if (!parsed) return;
|
|
25181
25889
|
const info = parsed.info;
|
|
25890
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25182
25891
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25183
25892
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25184
25893
|
}
|
|
@@ -25460,7 +26169,8 @@ var MmtpRouter = class {
|
|
|
25460
26169
|
parsed.header,
|
|
25461
26170
|
info.sequenceNumber,
|
|
25462
26171
|
info.rapFlag,
|
|
25463
|
-
source
|
|
26172
|
+
source,
|
|
26173
|
+
fecPayloadId?.ssId
|
|
25464
26174
|
);
|
|
25465
26175
|
}
|
|
25466
26176
|
}
|
|
@@ -25486,7 +26196,8 @@ var MmtpRouter = class {
|
|
|
25486
26196
|
source: parserOptions?.source,
|
|
25487
26197
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25488
26198
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25489
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26199
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26200
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25490
26201
|
};
|
|
25491
26202
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25492
26203
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25610,7 +26321,8 @@ var MmtpRouter = class {
|
|
|
25610
26321
|
if (stream === void 0) {
|
|
25611
26322
|
stream = {
|
|
25612
26323
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25613
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26324
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26325
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25614
26326
|
};
|
|
25615
26327
|
byName.set(catalogTrackName, stream);
|
|
25616
26328
|
}
|
|
@@ -25725,8 +26437,16 @@ var MmtpRouter = class {
|
|
|
25725
26437
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25726
26438
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25727
26439
|
}
|
|
25728
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26440
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25729
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
|
+
}
|
|
25730
26450
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25731
26451
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25732
26452
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25880,6 +26600,13 @@ var MmtpRouter = class {
|
|
|
25880
26600
|
* frames by epoch only if pre-protection input ever matters.
|
|
25881
26601
|
*/
|
|
25882
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
|
+
}
|
|
25883
26610
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25884
26611
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25885
26612
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25936,7 +26663,12 @@ var MmtpRouter = class {
|
|
|
25936
26663
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25937
26664
|
if (ownerPacketId == null) return;
|
|
25938
26665
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25939
|
-
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
|
+
};
|
|
25940
26672
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25941
26673
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25942
26674
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -25944,7 +26676,10 @@ var MmtpRouter = class {
|
|
|
25944
26676
|
...result,
|
|
25945
26677
|
symbols: result.symbols.map((sym) => {
|
|
25946
26678
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25947
|
-
if (wire === sym.packetSequenceNumber)
|
|
26679
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26680
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26681
|
+
return sym;
|
|
26682
|
+
}
|
|
25948
26683
|
this.#restampedRecoveredSeqs++;
|
|
25949
26684
|
this.#restampedStreams.add(streamKey);
|
|
25950
26685
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25980,16 +26715,29 @@ var MmtpRouter = class {
|
|
|
25980
26715
|
}
|
|
25981
26716
|
);
|
|
25982
26717
|
}
|
|
25983
|
-
|
|
25984
|
-
|
|
25985
|
-
|
|
25986
|
-
|
|
25987
|
-
|
|
25988
|
-
|
|
25989
|
-
|
|
25990
|
-
|
|
25991
|
-
))
|
|
25992
|
-
|
|
26718
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26719
|
+
ownerPacketId,
|
|
26720
|
+
sym.fragment.mpuSequenceNumber,
|
|
26721
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26722
|
+
sym.fragment.sampleNumber,
|
|
26723
|
+
catalogTrackName
|
|
26724
|
+
);
|
|
26725
|
+
for (const sym of result.symbols) {
|
|
26726
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26727
|
+
this.#feedViaParser(
|
|
26728
|
+
sym.rawSymbol,
|
|
26729
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26730
|
+
void 0,
|
|
26731
|
+
{
|
|
26732
|
+
skipFecPayloadId: true,
|
|
26733
|
+
recovered: true,
|
|
26734
|
+
deliveryProvenance,
|
|
26735
|
+
...fecSbnOf(sym.index),
|
|
26736
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26737
|
+
}
|
|
26738
|
+
);
|
|
26739
|
+
}
|
|
26740
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25993
26741
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25994
26742
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25995
26743
|
if (assembler) {
|
|
@@ -27716,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
27716
28464
|
function isSymbolCounts(value) {
|
|
27717
28465
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
27718
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
|
+
}
|
|
27719
28473
|
function isNullableNonNegative(value) {
|
|
27720
28474
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
27721
28475
|
}
|
|
@@ -27735,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
27735
28489
|
case "recovered":
|
|
27736
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"]);
|
|
27737
28491
|
case "failed":
|
|
27738
|
-
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"]));
|
|
27739
28496
|
case "repair-preempted":
|
|
27740
28497
|
case "repair-deferred":
|
|
27741
28498
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -27944,6 +28701,8 @@ var MulticastManager = class {
|
|
|
27944
28701
|
/** Live joinRepairEndpoints() releases → the repair owners they registered,
|
|
27945
28702
|
* re-applied after configureMultiTrack() clears catalog-derived owners. */
|
|
27946
28703
|
#repairReleases = /* @__PURE__ */ new Map();
|
|
28704
|
+
/** Protected repair flows owned by each live repair-endpoint release. */
|
|
28705
|
+
#repairProvenanceFlows = /* @__PURE__ */ new Map();
|
|
27947
28706
|
/** Each source's catalog keyframe overlay, from its `ok` binding. */
|
|
27948
28707
|
#overlayBindings = /* @__PURE__ */ new Map();
|
|
27949
28708
|
/** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
|
|
@@ -28001,7 +28760,7 @@ var MulticastManager = class {
|
|
|
28001
28760
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
28002
28761
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
28003
28762
|
},
|
|
28004
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28763
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
28005
28764
|
const pathCount = this.#fec.blockPathCount(
|
|
28006
28765
|
sbn,
|
|
28007
28766
|
packetId,
|
|
@@ -28019,7 +28778,8 @@ var MulticastManager = class {
|
|
|
28019
28778
|
symbols: {
|
|
28020
28779
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
28021
28780
|
moq: pathCount?.moqSymbols ?? 0
|
|
28022
|
-
}
|
|
28781
|
+
},
|
|
28782
|
+
...span === void 0 ? {} : { span }
|
|
28023
28783
|
});
|
|
28024
28784
|
},
|
|
28025
28785
|
onFrameGap: (decision) => {
|
|
@@ -28609,6 +29369,9 @@ var MulticastManager = class {
|
|
|
28609
29369
|
this.#transport.onFailback((reason) => {
|
|
28610
29370
|
const joined = this.#provenanceJoin;
|
|
28611
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
|
+
}
|
|
28612
29375
|
this.#onFailback?.(reason);
|
|
28613
29376
|
});
|
|
28614
29377
|
}
|
|
@@ -28643,6 +29406,12 @@ var MulticastManager = class {
|
|
|
28643
29406
|
"[MulticastManager] join: provenance flow (S, G, port) must match sourceAddress ?? senderAddress, groupAddress and port"
|
|
28644
29407
|
);
|
|
28645
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
|
+
}
|
|
28646
29415
|
const staged = this.#renditionSwitch;
|
|
28647
29416
|
if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
|
|
28648
29417
|
this.configureFec(config);
|
|
@@ -28650,8 +29419,8 @@ var MulticastManager = class {
|
|
|
28650
29419
|
const selectedPacketIds = config.packetIds?.length ? new Set(config.packetIds) : null;
|
|
28651
29420
|
const catalogTracks = config.tracks?.filter(({ packetId }) => selectedPacketIds === null || selectedPacketIds.has(packetId));
|
|
28652
29421
|
const ingressTracks = catalogTracks ? catalogTracks : selectedPacketIds === null ? void 0 : [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
|
|
28653
|
-
this.#releaseProvenanceJoin();
|
|
28654
29422
|
const provenance = config.provenance;
|
|
29423
|
+
this.#releaseProvenanceJoin();
|
|
28655
29424
|
const protectedTracks = ingressTracks ?? provenance?.flow.tracks;
|
|
28656
29425
|
const ingress = protectedTracks === void 0 ? void 0 : {
|
|
28657
29426
|
tracks: Object.freeze(
|
|
@@ -28704,8 +29473,13 @@ var MulticastManager = class {
|
|
|
28704
29473
|
* feeds the flow's receiver; media datagrams are held until one admits them.
|
|
28705
29474
|
*/
|
|
28706
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) {
|
|
28707
29481
|
const { flow, auth } = provenance;
|
|
28708
|
-
|
|
29482
|
+
return new BcProvenanceV2Receiver({
|
|
28709
29483
|
...provenance.receiver,
|
|
28710
29484
|
// The caller's ctx when it passes one, else the catalog's: an explicit
|
|
28711
29485
|
// `catalogCtx: undefined` must not switch off the 7.2.4 ctx check.
|
|
@@ -28722,9 +29496,6 @@ var MulticastManager = class {
|
|
|
28722
29496
|
maxAuthDelayMs: flow.maxAuthDelayMs,
|
|
28723
29497
|
manifestTimerMs: flow.manifestTimerMs
|
|
28724
29498
|
});
|
|
28725
|
-
this.#router.registerProvenanceFlow(ingress, receiver);
|
|
28726
|
-
this.#hookFailback();
|
|
28727
|
-
this.#provenanceJoin = { flow: ingress.flow };
|
|
28728
29499
|
}
|
|
28729
29500
|
/** Un-protect: drop the flow's hold queues (no expiry counted). */
|
|
28730
29501
|
#releaseProvenanceJoin() {
|
|
@@ -28755,8 +29526,25 @@ var MulticastManager = class {
|
|
|
28755
29526
|
* callback delivers must come from a subscription opened on
|
|
28756
29527
|
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
28757
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.
|
|
28758
29535
|
*/
|
|
28759
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
|
+
}
|
|
28760
29548
|
const releases = [];
|
|
28761
29549
|
let released = false;
|
|
28762
29550
|
const safely = (each) => {
|
|
@@ -28766,17 +29554,36 @@ var MulticastManager = class {
|
|
|
28766
29554
|
console.warn("[MulticastManager] repair group release failed", error);
|
|
28767
29555
|
}
|
|
28768
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 = [];
|
|
28769
29568
|
const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
|
|
28770
29569
|
...owner,
|
|
28771
29570
|
ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
|
|
28772
29571
|
repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
|
|
28773
29572
|
}));
|
|
28774
29573
|
this.#applyRepairOwners(repairOwners);
|
|
28775
|
-
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);
|
|
28776
29576
|
const ingress = {
|
|
28777
29577
|
tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
|
|
28778
|
-
...
|
|
29578
|
+
...flow === void 0 ? {} : { flow }
|
|
28779
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
|
+
}
|
|
28780
29587
|
let sender;
|
|
28781
29588
|
const feed = (packet, sourceAddress, ingressToFeed) => {
|
|
28782
29589
|
if (released) return;
|
|
@@ -28806,7 +29613,7 @@ var MulticastManager = class {
|
|
|
28806
29613
|
packetIds: endpoint.packetIds,
|
|
28807
29614
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
28808
29615
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
28809
|
-
...
|
|
29616
|
+
...flow === void 0 ? {} : { protected: true }
|
|
28810
29617
|
}, ingress).then((joined) => {
|
|
28811
29618
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
28812
29619
|
});
|
|
@@ -28815,9 +29622,12 @@ var MulticastManager = class {
|
|
|
28815
29622
|
if (released) return;
|
|
28816
29623
|
released = true;
|
|
28817
29624
|
this.#repairReleases.delete(release);
|
|
29625
|
+
this.#repairProvenanceFlows.delete(release);
|
|
28818
29626
|
for (const each of releases.splice(0)) safely(each);
|
|
28819
29627
|
};
|
|
28820
29628
|
this.#repairReleases.set(release, repairOwners);
|
|
29629
|
+
this.#repairProvenanceFlows.set(release, provenanceFlows);
|
|
29630
|
+
if (provenanceFlows.length) this.#hookFailback();
|
|
28821
29631
|
return release;
|
|
28822
29632
|
}
|
|
28823
29633
|
#applyRepairOwners(owners) {
|
|
@@ -29399,10 +30209,12 @@ var MulticastManager = class {
|
|
|
29399
30209
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
29400
30210
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
29401
30211
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
29402
|
-
* 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.
|
|
29403
30215
|
*/
|
|
29404
|
-
onUndelimitedLoss(cb) {
|
|
29405
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30216
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30217
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
29406
30218
|
}
|
|
29407
30219
|
/**
|
|
29408
30220
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -29638,7 +30450,8 @@ var MulticastManager = class {
|
|
|
29638
30450
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
29639
30451
|
...event,
|
|
29640
30452
|
block: Object.freeze({ ...event.block }),
|
|
29641
|
-
..."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 }) } : {}
|
|
29642
30455
|
}
|
|
29643
30456
|
);
|
|
29644
30457
|
if (this.#logRepairLifecycle) {
|
|
@@ -32310,6 +33123,7 @@ var RepairLayerSubscriptions = class {
|
|
|
32310
33123
|
|
|
32311
33124
|
// src/repair-fetch.ts
|
|
32312
33125
|
var MOQT_PRIORITY_MAX = 255;
|
|
33126
|
+
var WALK_LEAD_MS = 3;
|
|
32313
33127
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
32314
33128
|
function isMoqtPriority(value) {
|
|
32315
33129
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -32360,8 +33174,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
32360
33174
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
32361
33175
|
};
|
|
32362
33176
|
const clearEntry = (entry) => {
|
|
32363
|
-
|
|
32364
|
-
|
|
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);
|
|
32365
33188
|
};
|
|
32366
33189
|
const releaseHold = (entry) => {
|
|
32367
33190
|
if (entry.range) return;
|
|
@@ -32428,21 +33251,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
32428
33251
|
const admit = (entry, now) => {
|
|
32429
33252
|
const budgetMs = entry.deadlineAt - now;
|
|
32430
33253
|
const rttMs = readRtt();
|
|
32431
|
-
const reason =
|
|
33254
|
+
const reason = refusal(entry, now, rttMs);
|
|
32432
33255
|
if (reason === null) return rttMs;
|
|
32433
33256
|
skip(entry, reason, rttMs, budgetMs);
|
|
32434
33257
|
return null;
|
|
32435
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";
|
|
32436
33260
|
const retire = (entry) => {
|
|
32437
33261
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
32438
33262
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
32439
33263
|
};
|
|
32440
|
-
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) => {
|
|
32441
33272
|
for (const entry of entries.values()) {
|
|
32442
|
-
if (entry
|
|
33273
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
32443
33274
|
}
|
|
32444
33275
|
return false;
|
|
32445
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
|
+
};
|
|
32446
33290
|
const preemptBestEffort = () => {
|
|
32447
33291
|
for (const entry of entries.values()) {
|
|
32448
33292
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -32471,7 +33315,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
32471
33315
|
}
|
|
32472
33316
|
const rttMs = admit(entry, now);
|
|
32473
33317
|
if (rttMs === null) return;
|
|
32474
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33318
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33319
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
32475
33320
|
if (counted) entry.attempts++;
|
|
32476
33321
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
32477
33322
|
rttMs,
|
|
@@ -32493,6 +33338,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
32493
33338
|
}
|
|
32494
33339
|
void execute(entry, run);
|
|
32495
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
|
+
};
|
|
32496
33349
|
const dispatch = () => {
|
|
32497
33350
|
if (disposed) return;
|
|
32498
33351
|
const now = performance.now();
|
|
@@ -32517,6 +33370,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32517
33370
|
for (const entry of queued) {
|
|
32518
33371
|
if (entry.priority === 0) start(entry);
|
|
32519
33372
|
}
|
|
33373
|
+
yieldVideoWalks(queued);
|
|
32520
33374
|
if (outstandingBelow(1)) return;
|
|
32521
33375
|
for (const entry of queued) {
|
|
32522
33376
|
if (entry.priority === 1) start(entry);
|
|
@@ -32543,6 +33397,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32543
33397
|
}
|
|
32544
33398
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
32545
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
|
+
};
|
|
32546
33410
|
const finish = (entry, run, fed, error) => {
|
|
32547
33411
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
32548
33412
|
return;
|
|
@@ -32587,27 +33451,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
32587
33451
|
signal: run.controller.signal,
|
|
32588
33452
|
subscriberPriority: entry.subscriberPriority
|
|
32589
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
|
+
};
|
|
32590
33462
|
try {
|
|
32591
|
-
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({
|
|
32592
33464
|
...common,
|
|
32593
|
-
startGroup:
|
|
32594
|
-
endGroup:
|
|
32595
|
-
lostSsId:
|
|
32596
|
-
})
|
|
33465
|
+
startGroup: range.groups.startGroup,
|
|
33466
|
+
endGroup: range.groups.endGroup,
|
|
33467
|
+
lostSsId: range.lostSsId
|
|
33468
|
+
});
|
|
32597
33469
|
for await (const packet of packets) {
|
|
32598
33470
|
if (run.stopped !== null || entry.running !== run) break;
|
|
32599
33471
|
fed++;
|
|
32600
|
-
if (
|
|
32601
|
-
|
|
32602
|
-
} else {
|
|
32603
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
32604
|
-
}
|
|
33472
|
+
if (range === void 0) feed(packet);
|
|
33473
|
+
else atReport(range, () => feed(packet));
|
|
32605
33474
|
}
|
|
32606
33475
|
finish(entry, run, fed, void 0);
|
|
32607
33476
|
} catch (error) {
|
|
32608
33477
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
32609
33478
|
}
|
|
32610
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
|
+
}
|
|
32611
33529
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
32612
33530
|
for (const entry of [...entries.values()]) {
|
|
32613
33531
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -32803,7 +33721,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
32803
33721
|
throw error;
|
|
32804
33722
|
}
|
|
32805
33723
|
};
|
|
32806
|
-
|
|
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) {
|
|
32807
33755
|
mc.onUndelimitedLoss((loss) => {
|
|
32808
33756
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
32809
33757
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -32811,9 +33759,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
32811
33759
|
const blockProfile = blockProfileOf(packetId);
|
|
32812
33760
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
32813
33761
|
if (!target) return close();
|
|
32814
|
-
const
|
|
32815
|
-
const
|
|
32816
|
-
|
|
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) {
|
|
32817
33767
|
close();
|
|
32818
33768
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
32819
33769
|
return;
|
|
@@ -32824,8 +33774,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
32824
33774
|
packetId,
|
|
32825
33775
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
32826
33776
|
};
|
|
32827
|
-
const wire = wirePriority(identity,
|
|
32828
|
-
|
|
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);
|
|
32829
33781
|
if (deadline === null) {
|
|
32830
33782
|
close();
|
|
32831
33783
|
mc.noteRepairSkipped(
|
|
@@ -32837,6 +33789,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32837
33789
|
);
|
|
32838
33790
|
return;
|
|
32839
33791
|
}
|
|
33792
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
32840
33793
|
const entry = {
|
|
32841
33794
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
32842
33795
|
...identity,
|
|
@@ -32844,15 +33797,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32844
33797
|
blockProfile,
|
|
32845
33798
|
priority,
|
|
32846
33799
|
subscriberPriority: wire.subscriberPriority,
|
|
32847
|
-
deadlineAt
|
|
32848
|
-
playoutDeadlineAt:
|
|
33800
|
+
deadlineAt,
|
|
33801
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
32849
33802
|
anchorAt: deadline.anchorAt,
|
|
32850
33803
|
delayMs: deadline.delayMs,
|
|
32851
33804
|
bound: deadline.bound,
|
|
32852
33805
|
order: enqueueOrder++,
|
|
32853
33806
|
attempts: 0,
|
|
32854
33807
|
running: null,
|
|
32855
|
-
range: {
|
|
33808
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33809
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
32856
33810
|
};
|
|
32857
33811
|
const previous = entries.get(entry.key);
|
|
32858
33812
|
if (previous) {
|
|
@@ -32862,7 +33816,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32862
33816
|
entries.set(entry.key, entry);
|
|
32863
33817
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
32864
33818
|
dispatch();
|
|
32865
|
-
});
|
|
33819
|
+
}, onHoleExpired);
|
|
32866
33820
|
}
|
|
32867
33821
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
32868
33822
|
queueMicrotask(() => {
|