@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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/README.md +29 -17
- package/dist/browser-runtime.js +1949 -374
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +336 -59
- package/dist/no-modules.js.map +3 -3
- 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/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- 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-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -1
package/dist/no-modules.js
CHANGED
|
@@ -10,6 +10,8 @@ var PACKET_TYPE_MPU = 0;
|
|
|
10
10
|
var PACKET_TYPE_REPAIR = 3;
|
|
11
11
|
var MMTP_SIGNALING_PACKET_ID = 0;
|
|
12
12
|
var MMTP_PACKET_ID_MAX = 65535;
|
|
13
|
+
var FEC_CODE_REED_SOLOMON = 1;
|
|
14
|
+
var FEC_CODE_RAPTORQ = 3;
|
|
13
15
|
function parseMMTPHeader(buffer) {
|
|
14
16
|
if (buffer.length < MMTP_HEADER_SIZE) {
|
|
15
17
|
throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
|
|
@@ -150,30 +152,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
|
|
|
150
152
|
const authLength = view2.getUint16(2, false);
|
|
151
153
|
return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
|
|
152
154
|
}
|
|
153
|
-
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options
|
|
155
|
+
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
|
|
154
156
|
if (declaredEnd > payload.length)
|
|
155
157
|
return null;
|
|
156
158
|
if (options?.skipFecId) {
|
|
157
|
-
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false
|
|
159
|
+
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
|
|
158
160
|
}
|
|
159
|
-
const overlay = options?.overlayDeclared === true && rapFlag;
|
|
160
161
|
if (fecType === 1) {
|
|
161
|
-
const fecStart = payload.length -
|
|
162
|
+
const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
162
163
|
if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
|
|
163
164
|
return null;
|
|
164
165
|
}
|
|
165
|
-
return { hasFecId: true
|
|
166
|
+
return { hasFecId: true };
|
|
166
167
|
}
|
|
167
|
-
if (fecType !== 0
|
|
168
|
+
if (fecType !== 0)
|
|
168
169
|
return null;
|
|
169
170
|
if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
|
|
170
|
-
return { hasFecId: false
|
|
171
|
+
return { hasFecId: false };
|
|
171
172
|
}
|
|
172
173
|
return null;
|
|
173
174
|
}
|
|
174
|
-
function sourceFecTrailerSize(overlay) {
|
|
175
|
-
return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
176
|
-
}
|
|
177
175
|
function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
178
176
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
|
|
179
177
|
return {
|
|
@@ -226,11 +224,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
|
226
224
|
if ("rejection" in layoutResult)
|
|
227
225
|
return layoutResult;
|
|
228
226
|
const { layout } = layoutResult;
|
|
229
|
-
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options
|
|
227
|
+
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
|
|
230
228
|
if (!trailerLayout) {
|
|
231
229
|
if (!options?.skipFecId && header.fecType === 1) {
|
|
232
230
|
const observed = payload.length - layout.dataEnd;
|
|
233
|
-
const expected =
|
|
231
|
+
const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
234
232
|
if (observed !== expected) {
|
|
235
233
|
return {
|
|
236
234
|
rejection: {
|
|
@@ -262,13 +260,10 @@ function extractPlannedMfu(mmtpPacket, result) {
|
|
|
262
260
|
const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
|
|
263
261
|
const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
|
|
264
262
|
const data = payload.subarray(mfuOffset, dataEnd);
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
|
|
270
|
-
const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
|
|
271
|
-
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
|
|
263
|
+
const fecPayloadId = trailerLayout.hasFecId ? {
|
|
264
|
+
ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
|
|
265
|
+
} : null;
|
|
266
|
+
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
|
|
272
267
|
}
|
|
273
268
|
function sourceFecPayloadIdOf(mmtpPacket, header) {
|
|
274
269
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
|
|
@@ -360,6 +355,50 @@ var NTP_SHORT_BITS = 32;
|
|
|
360
355
|
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
361
356
|
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
362
357
|
|
|
358
|
+
// ../mmt-base/dist/signalling.js
|
|
359
|
+
var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
|
|
360
|
+
var SSBG_MODE_1 = 1;
|
|
361
|
+
var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
|
|
362
|
+
var REPAIR_SYMBOL_ID_MAX = 16777215;
|
|
363
|
+
var MIN_REPAIR_SYMBOL_SIZE = 14;
|
|
364
|
+
var REED_SOLOMON_MAX_BLOCK = 255;
|
|
365
|
+
function alFecRaptorQFlowConfig(flow) {
|
|
366
|
+
if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
|
|
367
|
+
return null;
|
|
368
|
+
if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
|
|
369
|
+
return null;
|
|
370
|
+
if (flow.packetIds.length !== 1)
|
|
371
|
+
return null;
|
|
372
|
+
const repair = flow.repairFlows[0];
|
|
373
|
+
const { fecCodeId, maximumK: k, maximumP: p } = repair;
|
|
374
|
+
const t = flow.repairSymbolSize;
|
|
375
|
+
if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
|
|
376
|
+
return null;
|
|
377
|
+
if (fecCodeId === FEC_CODE_RAPTORQ) {
|
|
378
|
+
if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
|
|
379
|
+
return null;
|
|
380
|
+
} else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
|
|
381
|
+
if (k + p > REED_SOLOMON_MAX_BLOCK)
|
|
382
|
+
return null;
|
|
383
|
+
} else {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
|
|
387
|
+
return null;
|
|
388
|
+
return {
|
|
389
|
+
sourcePacketIds: [...flow.packetIds],
|
|
390
|
+
repairFlowId: repair.repairFlowId,
|
|
391
|
+
fecCodeId,
|
|
392
|
+
ssbgMode: flow.ssbgMode,
|
|
393
|
+
fecPayloadIdMode: flow.fecPayloadIdMode,
|
|
394
|
+
repairSymbolSize: t,
|
|
395
|
+
maximumK: k,
|
|
396
|
+
maximumP: p,
|
|
397
|
+
protectionWindowTime: repair.protectionWindowTime,
|
|
398
|
+
protectionWindowSize: repair.protectionWindowSize
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
363
402
|
// ../mmt-base/dist/serial-number.js
|
|
364
403
|
var SERIAL32_SPAN = 4294967296;
|
|
365
404
|
var SERIAL32_HALF = 2147483648;
|
|
@@ -5529,9 +5568,21 @@ var IsoBmffTimingResolver = class {
|
|
|
5529
5568
|
}
|
|
5530
5569
|
#track = null;
|
|
5531
5570
|
#fragments = /* @__PURE__ */ new Map();
|
|
5571
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
5572
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
5573
|
+
/**
|
|
5574
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
5575
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
5576
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
5577
|
+
*/
|
|
5578
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
5532
5579
|
#pendingSamples = 0;
|
|
5533
5580
|
#evictedSamples = 0;
|
|
5534
5581
|
#lastMovieFragmentRejection = null;
|
|
5582
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
5583
|
+
get initialized() {
|
|
5584
|
+
return this.#track !== null;
|
|
5585
|
+
}
|
|
5535
5586
|
get pendingSampleCount() {
|
|
5536
5587
|
return this.#pendingSamples;
|
|
5537
5588
|
}
|
|
@@ -5555,7 +5606,14 @@ var IsoBmffTimingResolver = class {
|
|
|
5555
5606
|
this.#track = timing;
|
|
5556
5607
|
return true;
|
|
5557
5608
|
}
|
|
5558
|
-
|
|
5609
|
+
/**
|
|
5610
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
5611
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
5612
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
5613
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
5614
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
5615
|
+
*/
|
|
5616
|
+
addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
5559
5617
|
this.#lastMovieFragmentRejection = null;
|
|
5560
5618
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
5561
5619
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -5581,6 +5639,8 @@ var IsoBmffTimingResolver = class {
|
|
|
5581
5639
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
5582
5640
|
);
|
|
5583
5641
|
}
|
|
5642
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
5643
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
5584
5644
|
let freed = 0;
|
|
5585
5645
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
5586
5646
|
freed += this.#evictOldestFragment();
|
|
@@ -5594,6 +5654,7 @@ var IsoBmffTimingResolver = class {
|
|
|
5594
5654
|
return false;
|
|
5595
5655
|
}
|
|
5596
5656
|
this.#fragments.set(key, [...parsed.samples]);
|
|
5657
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
5597
5658
|
this.#pendingSamples += parsed.samples.length;
|
|
5598
5659
|
return true;
|
|
5599
5660
|
}
|
|
@@ -5651,6 +5712,7 @@ var IsoBmffTimingResolver = class {
|
|
|
5651
5712
|
let live = 0;
|
|
5652
5713
|
for (const sample of samples) if (sample !== null) live++;
|
|
5653
5714
|
this.#fragments.delete(oldest);
|
|
5715
|
+
this.#firstSamples.delete(oldest);
|
|
5654
5716
|
this.#pendingSamples -= live;
|
|
5655
5717
|
this.#evictedSamples += live;
|
|
5656
5718
|
return live;
|
|
@@ -5666,12 +5728,23 @@ var IsoBmffTimingResolver = class {
|
|
|
5666
5728
|
if (!samples || !samples[index]) return false;
|
|
5667
5729
|
samples[index] = null;
|
|
5668
5730
|
this.#pendingSamples--;
|
|
5669
|
-
if (samples.every((value) => value === null))
|
|
5731
|
+
if (samples.every((value) => value === null)) {
|
|
5732
|
+
this.#fragments.delete(key);
|
|
5733
|
+
const first = this.#firstSamples.get(key);
|
|
5734
|
+
this.#firstSamples.delete(key);
|
|
5735
|
+
this.#delivered.delete(key);
|
|
5736
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
5737
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
5738
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5670
5741
|
return true;
|
|
5671
5742
|
}
|
|
5672
5743
|
reset() {
|
|
5673
5744
|
this.#track = null;
|
|
5674
5745
|
this.#fragments.clear();
|
|
5746
|
+
this.#firstSamples.clear();
|
|
5747
|
+
this.#delivered.clear();
|
|
5675
5748
|
this.#pendingSamples = 0;
|
|
5676
5749
|
}
|
|
5677
5750
|
};
|
|
@@ -5841,6 +5914,9 @@ function authoredGap(before, after) {
|
|
|
5841
5914
|
function ssIdAfter(a, b) {
|
|
5842
5915
|
return (a - b | 0) > 0;
|
|
5843
5916
|
}
|
|
5917
|
+
function holdsRebuilt(stream) {
|
|
5918
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
5919
|
+
}
|
|
5844
5920
|
var FecReleaseSequencer = class {
|
|
5845
5921
|
#streams = /* @__PURE__ */ new Map();
|
|
5846
5922
|
#gate;
|
|
@@ -5903,6 +5979,7 @@ var FecReleaseSequencer = class {
|
|
|
5903
5979
|
stream.lastSbn = void 0;
|
|
5904
5980
|
stream.discontinuity = false;
|
|
5905
5981
|
stream.rapSsId = void 0;
|
|
5982
|
+
stream.sourced = false;
|
|
5906
5983
|
}
|
|
5907
5984
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
5908
5985
|
this.#recordStat("droppedStale");
|
|
@@ -5929,6 +6006,7 @@ var FecReleaseSequencer = class {
|
|
|
5929
6006
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
5930
6007
|
else hi = mid;
|
|
5931
6008
|
}
|
|
6009
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
5932
6010
|
stream.buf.splice(lo, 0, frame);
|
|
5933
6011
|
this.#drainUnlessDeferred(stream);
|
|
5934
6012
|
}
|
|
@@ -5964,6 +6042,41 @@ var FecReleaseSequencer = class {
|
|
|
5964
6042
|
}
|
|
5965
6043
|
return false;
|
|
5966
6044
|
}
|
|
6045
|
+
/**
|
|
6046
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
6047
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
6048
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
6049
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
6050
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
6051
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
6052
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
6053
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
6054
|
+
* released.
|
|
6055
|
+
*/
|
|
6056
|
+
unsource(packetId, catalogTrackName) {
|
|
6057
|
+
for (const stream of this.#streams.values()) {
|
|
6058
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
6059
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
6060
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
6061
|
+
else this.#drain(stream, true);
|
|
6062
|
+
}
|
|
6063
|
+
stream.sourced = false;
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
/**
|
|
6068
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
6069
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
6070
|
+
* packetId.
|
|
6071
|
+
*/
|
|
6072
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
6073
|
+
for (const stream of this.#streams.values()) {
|
|
6074
|
+
if (stream.packetId !== packetId) continue;
|
|
6075
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
6076
|
+
if (holdsRebuilt(stream)) return true;
|
|
6077
|
+
}
|
|
6078
|
+
return false;
|
|
6079
|
+
}
|
|
5967
6080
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
5968
6081
|
poke(streamKey) {
|
|
5969
6082
|
if (streamKey !== void 0) {
|
|
@@ -6055,16 +6168,32 @@ var FecReleaseSequencer = class {
|
|
|
6055
6168
|
last: null,
|
|
6056
6169
|
lastSbn: void 0,
|
|
6057
6170
|
discontinuity: false,
|
|
6058
|
-
rapSsId: void 0
|
|
6171
|
+
rapSsId: void 0,
|
|
6172
|
+
sourced: false,
|
|
6173
|
+
draining: 0
|
|
6059
6174
|
};
|
|
6060
6175
|
this.#streams.set(streamKey, stream);
|
|
6061
6176
|
}
|
|
6062
6177
|
return stream;
|
|
6063
6178
|
}
|
|
6064
|
-
#
|
|
6179
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
6180
|
+
#drain(stream, asUnanchored = false) {
|
|
6181
|
+
stream.draining++;
|
|
6182
|
+
try {
|
|
6183
|
+
this.#drainLoop(stream, asUnanchored);
|
|
6184
|
+
} finally {
|
|
6185
|
+
stream.draining--;
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6188
|
+
#drainLoop(stream, asUnanchored) {
|
|
6065
6189
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
6066
6190
|
const head = stream.buf[0];
|
|
6067
6191
|
const last = stream.last;
|
|
6192
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
6193
|
+
stream.buf.shift();
|
|
6194
|
+
this.#release(stream, head, void 0);
|
|
6195
|
+
continue;
|
|
6196
|
+
}
|
|
6068
6197
|
if (stream.discontinuity) {
|
|
6069
6198
|
stream.discontinuity = false;
|
|
6070
6199
|
stream.buf.shift();
|
|
@@ -6078,9 +6207,10 @@ var FecReleaseSequencer = class {
|
|
|
6078
6207
|
}
|
|
6079
6208
|
const headSbn = head.meta.fecSbn;
|
|
6080
6209
|
const from = stream.lastSbn ?? headSbn;
|
|
6081
|
-
const to = headSbn ?? stream.lastSbn;
|
|
6210
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
6082
6211
|
let lost = [];
|
|
6083
6212
|
const unanchored = from === void 0 && to === void 0;
|
|
6213
|
+
let unresolved = false;
|
|
6084
6214
|
if (from !== void 0 && to !== void 0) {
|
|
6085
6215
|
const resolved = this.#gate.resolvedLoss(
|
|
6086
6216
|
stream.packetId,
|
|
@@ -6089,7 +6219,8 @@ var FecReleaseSequencer = class {
|
|
|
6089
6219
|
Math.max(from, to),
|
|
6090
6220
|
from
|
|
6091
6221
|
);
|
|
6092
|
-
|
|
6222
|
+
unresolved = resolved === null;
|
|
6223
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
6093
6224
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
6094
6225
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
6095
6226
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -6097,22 +6228,56 @@ var FecReleaseSequencer = class {
|
|
|
6097
6228
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
6098
6229
|
continue;
|
|
6099
6230
|
}
|
|
6100
|
-
lost = resolved;
|
|
6231
|
+
lost = resolved ?? [];
|
|
6101
6232
|
}
|
|
6102
6233
|
stream.buf.shift();
|
|
6103
6234
|
this.#release(
|
|
6104
6235
|
stream,
|
|
6105
6236
|
head,
|
|
6106
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
6237
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
6107
6238
|
);
|
|
6108
6239
|
}
|
|
6109
6240
|
}
|
|
6241
|
+
/**
|
|
6242
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
6243
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
6244
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
6245
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
6246
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
6247
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
6248
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
6249
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
6250
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
6251
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
6252
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
6253
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
6254
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
6255
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
6256
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
6257
|
+
*/
|
|
6258
|
+
#cursorPassed(stream, asUnanchored) {
|
|
6259
|
+
let at = -1;
|
|
6260
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
6261
|
+
const frame = stream.buf[i];
|
|
6262
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
6263
|
+
frame.passed = true;
|
|
6264
|
+
if (at < 0) at = i;
|
|
6265
|
+
} else if (frame.passed !== true) break;
|
|
6266
|
+
}
|
|
6267
|
+
if (at < 0) return false;
|
|
6268
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
6269
|
+
}
|
|
6110
6270
|
#release(stream, frame, decision) {
|
|
6271
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
6272
|
+
if (stale) this.#recordStat("droppedStale");
|
|
6111
6273
|
if (decision) frame.meta.gapBefore = decision;
|
|
6112
6274
|
stream.last = frame;
|
|
6113
|
-
if (frame.meta.fecSbn !== void 0
|
|
6275
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
6276
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
6277
|
+
}
|
|
6278
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
6114
6279
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
6115
|
-
this.#emit(frame.data, frame.meta);
|
|
6280
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
6116
6281
|
}
|
|
6117
6282
|
};
|
|
6118
6283
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -6153,6 +6318,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6153
6318
|
this.#reclaimedTimingSamples = 0;
|
|
6154
6319
|
this.#evictedTimingSamples = 0;
|
|
6155
6320
|
this.#rejectedMovieFragments = 0;
|
|
6321
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
6322
|
+
held: 0,
|
|
6323
|
+
expired: 0,
|
|
6324
|
+
discarded: 0,
|
|
6325
|
+
superseded: 0
|
|
6326
|
+
};
|
|
6156
6327
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
6157
6328
|
this.#fecGate = null;
|
|
6158
6329
|
this.#fecSequencer = null;
|
|
@@ -6261,6 +6432,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6261
6432
|
#reclaimedTimingSamples;
|
|
6262
6433
|
#evictedTimingSamples;
|
|
6263
6434
|
#rejectedMovieFragments;
|
|
6435
|
+
#recoveredHeadsAwaitingTiming;
|
|
6264
6436
|
#sequencers;
|
|
6265
6437
|
#fecGate;
|
|
6266
6438
|
#fecSequencer;
|
|
@@ -6366,11 +6538,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6366
6538
|
/**
|
|
6367
6539
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
6368
6540
|
*
|
|
6369
|
-
*
|
|
6370
|
-
*
|
|
6371
|
-
*
|
|
6372
|
-
*
|
|
6373
|
-
*
|
|
6541
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
6542
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
6543
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
6544
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
6545
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
6546
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
6374
6547
|
*/
|
|
6375
6548
|
get timingBudgetStats() {
|
|
6376
6549
|
return {
|
|
@@ -6381,6 +6554,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6381
6554
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
6382
6555
|
};
|
|
6383
6556
|
}
|
|
6557
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
6558
|
+
get recoveredHeadsAwaitingTiming() {
|
|
6559
|
+
let pending = 0;
|
|
6560
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
6561
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
6562
|
+
}
|
|
6563
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
6564
|
+
}
|
|
6384
6565
|
get reorderStats() {
|
|
6385
6566
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
6386
6567
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -6422,6 +6603,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6422
6603
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
6423
6604
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
6424
6605
|
}
|
|
6606
|
+
/** See FecReleaseSequencer.unsource. */
|
|
6607
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
6608
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
6609
|
+
}
|
|
6610
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
6611
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
6612
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
6613
|
+
}
|
|
6425
6614
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
6426
6615
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
6427
6616
|
if (!this.#fecSequencer) return;
|
|
@@ -6554,7 +6743,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6554
6743
|
const now = Date.now();
|
|
6555
6744
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
6556
6745
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
6557
|
-
awaitingTiming.shift();
|
|
6746
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
6558
6747
|
}
|
|
6559
6748
|
}
|
|
6560
6749
|
}
|
|
@@ -6672,11 +6861,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6672
6861
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
6673
6862
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
6674
6863
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
6675
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
6864
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
6676
6865
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
6677
6866
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
6678
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
6679
|
-
const admitted = resolver2.addMovieFragment(
|
|
6867
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
6868
|
+
const admitted = resolver2.addMovieFragment(
|
|
6869
|
+
mpu.mpuSequenceNumber,
|
|
6870
|
+
data,
|
|
6871
|
+
remaining,
|
|
6872
|
+
options?.recovered === true
|
|
6873
|
+
);
|
|
6680
6874
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
6681
6875
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
6682
6876
|
if (!admitted) {
|
|
@@ -6696,7 +6890,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6696
6890
|
};
|
|
6697
6891
|
}
|
|
6698
6892
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
6699
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
6893
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
6700
6894
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
6701
6895
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
6702
6896
|
return this.#drop("mfu-empty-payload");
|
|
@@ -6717,6 +6911,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6717
6911
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
6718
6912
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
6719
6913
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
6914
|
+
sequenceNumber: info.sequenceNumber,
|
|
6720
6915
|
arrivedAt: Date.now()
|
|
6721
6916
|
});
|
|
6722
6917
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -6742,7 +6937,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6742
6937
|
info,
|
|
6743
6938
|
resolver,
|
|
6744
6939
|
options?.recovered === true,
|
|
6745
|
-
options?.fecSbn
|
|
6940
|
+
options?.fecSbn,
|
|
6941
|
+
options?.fecSbnFirst
|
|
6746
6942
|
);
|
|
6747
6943
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
6748
6944
|
}
|
|
@@ -6835,7 +7031,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6835
7031
|
info,
|
|
6836
7032
|
resolver,
|
|
6837
7033
|
options?.recovered === true,
|
|
6838
|
-
options?.fecSbn
|
|
7034
|
+
options?.fecSbn,
|
|
7035
|
+
options?.fecSbnFirst
|
|
6839
7036
|
);
|
|
6840
7037
|
if (outcome.record !== void 0) return outcome.record;
|
|
6841
7038
|
return this.#drop(outcome.reason);
|
|
@@ -6855,7 +7052,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6855
7052
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
6856
7053
|
* is what keeps both true.
|
|
6857
7054
|
*/
|
|
6858
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
7055
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
6859
7056
|
const timing = resolver.sampleTiming(
|
|
6860
7057
|
combined.mpuSequenceNumber,
|
|
6861
7058
|
combined.movieFragmentSequenceNumber,
|
|
@@ -6872,6 +7069,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6872
7069
|
);
|
|
6873
7070
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
6874
7071
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
7072
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
6875
7073
|
if (!resolver.consumeSample(
|
|
6876
7074
|
combined.mpuSequenceNumber,
|
|
6877
7075
|
combined.movieFragmentSequenceNumber,
|
|
@@ -7040,6 +7238,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7040
7238
|
}
|
|
7041
7239
|
this.#emitFrame(data, meta);
|
|
7042
7240
|
}
|
|
7241
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
7242
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
7243
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
7244
|
+
}
|
|
7043
7245
|
/**
|
|
7044
7246
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
7045
7247
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -7101,6 +7303,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7101
7303
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
7102
7304
|
this.#sequencers.clear();
|
|
7103
7305
|
this.#fecSequencer?.discard();
|
|
7306
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
7307
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
7308
|
+
}
|
|
7104
7309
|
this.#timingStreams.clear();
|
|
7105
7310
|
this.#pendingTimingSamples = 0;
|
|
7106
7311
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -7119,10 +7324,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7119
7324
|
return resolver;
|
|
7120
7325
|
}
|
|
7121
7326
|
#awaitTiming(streamKey, head) {
|
|
7122
|
-
const
|
|
7123
|
-
if (!
|
|
7327
|
+
const state = this.#timingStreams.get(streamKey);
|
|
7328
|
+
if (!state) return;
|
|
7329
|
+
if (!head.options?.recovered) {
|
|
7330
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
7331
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
7332
|
+
state.awaitingTiming = kept;
|
|
7333
|
+
}
|
|
7334
|
+
const awaitingTiming = state.awaitingTiming;
|
|
7124
7335
|
awaitingTiming.push(head);
|
|
7125
|
-
if (
|
|
7336
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
7337
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
7338
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
7342
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
7343
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
7126
7344
|
}
|
|
7127
7345
|
/**
|
|
7128
7346
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -7150,6 +7368,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7150
7368
|
const state = this.#timingStreams.get(streamKey);
|
|
7151
7369
|
if (!state) return;
|
|
7152
7370
|
this.#timingStreams.delete(streamKey);
|
|
7371
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
7153
7372
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
7154
7373
|
if (this.#pendingTimingSamples < 0) {
|
|
7155
7374
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -8071,7 +8290,14 @@ var HeldLayerAdmission = class {
|
|
|
8071
8290
|
var PATH_BITS = { moq: 1, multicast: 2 };
|
|
8072
8291
|
var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
|
|
8073
8292
|
var MmtpFecSession = class {
|
|
8293
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
8294
|
+
get repairSymbols() {
|
|
8295
|
+
return this.#repairSymbols;
|
|
8296
|
+
}
|
|
8074
8297
|
#decoder;
|
|
8298
|
+
#repairSymbols;
|
|
8299
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
8300
|
+
#message;
|
|
8075
8301
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
8076
8302
|
#blocks = /* @__PURE__ */ new Map();
|
|
8077
8303
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -8109,6 +8335,24 @@ var MmtpFecSession = class {
|
|
|
8109
8335
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
8110
8336
|
*/
|
|
8111
8337
|
constructor(config) {
|
|
8338
|
+
if (config && "alFecFlow" in config) {
|
|
8339
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow);
|
|
8340
|
+
if (!message) {
|
|
8341
|
+
throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
|
|
8342
|
+
}
|
|
8343
|
+
this.#message = message;
|
|
8344
|
+
this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
|
|
8345
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
|
|
8346
|
+
this.sourceSymbols = message.maximumK;
|
|
8347
|
+
this.#repairSymbols = message.maximumP;
|
|
8348
|
+
this.symbolSize = message.repairSymbolSize;
|
|
8349
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
|
|
8350
|
+
this.#repairPacketId = message.repairFlowId;
|
|
8351
|
+
this.#heldLayers = layerZero(message.maximumP);
|
|
8352
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
8353
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
8354
|
+
return;
|
|
8355
|
+
}
|
|
8112
8356
|
const fec = config?.fec;
|
|
8113
8357
|
if (!fec || typeof fec !== "object") {
|
|
8114
8358
|
throw new Error("MmtpFecSession: the catalog fec instance is required");
|
|
@@ -8119,7 +8363,7 @@ var MmtpFecSession = class {
|
|
|
8119
8363
|
}
|
|
8120
8364
|
this.algorithm = fec.scheme;
|
|
8121
8365
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
|
|
8122
|
-
this
|
|
8366
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
|
|
8123
8367
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
|
|
8124
8368
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
|
|
8125
8369
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
|
|
@@ -8139,17 +8383,30 @@ var MmtpFecSession = class {
|
|
|
8139
8383
|
config.heldLayers
|
|
8140
8384
|
);
|
|
8141
8385
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8386
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
8387
|
+
}
|
|
8388
|
+
/**
|
|
8389
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
8390
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
8391
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
8392
|
+
* previous one stays valid before the first block carrying the new total
|
|
8393
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
8394
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
8395
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
8396
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
8397
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
8398
|
+
*/
|
|
8399
|
+
updateAlFecFlow(flow) {
|
|
8400
|
+
const held = this.#message;
|
|
8401
|
+
if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
|
|
8402
|
+
const next = alFecRaptorQFlowConfig(flow);
|
|
8403
|
+
if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
|
|
8404
|
+
return "refused";
|
|
8405
|
+
}
|
|
8406
|
+
if (next.maximumP <= this.#repairSymbols) return "unchanged";
|
|
8407
|
+
this.#repairSymbols = next.maximumP;
|
|
8408
|
+
this.#heldLayers = layerZero(next.maximumP);
|
|
8409
|
+
return "raised";
|
|
8153
8410
|
}
|
|
8154
8411
|
/** True once release() ran. */
|
|
8155
8412
|
get released() {
|
|
@@ -8244,7 +8501,7 @@ var MmtpFecSession = class {
|
|
|
8244
8501
|
}
|
|
8245
8502
|
if (admission.reject) {
|
|
8246
8503
|
const verdict2 = this.#heldLayers.failsClosed(
|
|
8247
|
-
repairTrackName,
|
|
8504
|
+
this.#trackFor(repairTrackName),
|
|
8248
8505
|
frame.ssStart,
|
|
8249
8506
|
frame.rsbLength,
|
|
8250
8507
|
this.#anchorHorizon,
|
|
@@ -8259,7 +8516,7 @@ var MmtpFecSession = class {
|
|
|
8259
8516
|
this.#repairPacketId ??= frame.packetId;
|
|
8260
8517
|
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
8261
8518
|
const verdict = this.#heldLayers.admit(
|
|
8262
|
-
repairTrackName,
|
|
8519
|
+
this.#trackFor(repairTrackName),
|
|
8263
8520
|
frame.ssStart,
|
|
8264
8521
|
frame.rsbLength,
|
|
8265
8522
|
this.#anchorHorizon,
|
|
@@ -8318,6 +8575,10 @@ var MmtpFecSession = class {
|
|
|
8318
8575
|
decoder.free();
|
|
8319
8576
|
return true;
|
|
8320
8577
|
}
|
|
8578
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
8579
|
+
#trackFor(repairTrackName) {
|
|
8580
|
+
return this.#message ? void 0 : repairTrackName;
|
|
8581
|
+
}
|
|
8321
8582
|
#drop(reason) {
|
|
8322
8583
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
|
|
8323
8584
|
return [];
|
|
@@ -8439,6 +8700,22 @@ var MmtpFecSession = class {
|
|
|
8439
8700
|
}
|
|
8440
8701
|
}
|
|
8441
8702
|
};
|
|
8703
|
+
function layerZero(total) {
|
|
8704
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
|
|
8705
|
+
}
|
|
8706
|
+
function buildDecoder(Decoder, session) {
|
|
8707
|
+
if (typeof Decoder !== "function") {
|
|
8708
|
+
throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
|
|
8709
|
+
}
|
|
8710
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
|
|
8711
|
+
try {
|
|
8712
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize);
|
|
8713
|
+
} catch (error) {
|
|
8714
|
+
decoder.free();
|
|
8715
|
+
throw error;
|
|
8716
|
+
}
|
|
8717
|
+
return decoder;
|
|
8718
|
+
}
|
|
8442
8719
|
function requirePositiveInteger(value, path) {
|
|
8443
8720
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
8444
8721
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
|