@blockcast/mmt-container 2.1.0 → 2.2.0-main.02321582f1ca
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 +1302 -249
- 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 +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +213 -6
- 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 +5 -4
- 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 +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- 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 +37 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +98 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- 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 +25 -2
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1749 -56
- 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 +15 -2
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15683 -14941
- 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 +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- 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.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- 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 +256 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +96 -8
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +414 -29
- package/src/multicast-manager.ts +26 -5
- package/src/multicast-transport.ts +44 -20
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -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
|
};
|
|
@@ -6153,6 +6226,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6153
6226
|
this.#reclaimedTimingSamples = 0;
|
|
6154
6227
|
this.#evictedTimingSamples = 0;
|
|
6155
6228
|
this.#rejectedMovieFragments = 0;
|
|
6229
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
6230
|
+
held: 0,
|
|
6231
|
+
expired: 0,
|
|
6232
|
+
discarded: 0,
|
|
6233
|
+
superseded: 0
|
|
6234
|
+
};
|
|
6156
6235
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
6157
6236
|
this.#fecGate = null;
|
|
6158
6237
|
this.#fecSequencer = null;
|
|
@@ -6261,6 +6340,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6261
6340
|
#reclaimedTimingSamples;
|
|
6262
6341
|
#evictedTimingSamples;
|
|
6263
6342
|
#rejectedMovieFragments;
|
|
6343
|
+
#recoveredHeadsAwaitingTiming;
|
|
6264
6344
|
#sequencers;
|
|
6265
6345
|
#fecGate;
|
|
6266
6346
|
#fecSequencer;
|
|
@@ -6381,6 +6461,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6381
6461
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
6382
6462
|
};
|
|
6383
6463
|
}
|
|
6464
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
6465
|
+
get recoveredHeadsAwaitingTiming() {
|
|
6466
|
+
let pending = 0;
|
|
6467
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
6468
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
6469
|
+
}
|
|
6470
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
6471
|
+
}
|
|
6384
6472
|
get reorderStats() {
|
|
6385
6473
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
6386
6474
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -6554,7 +6642,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6554
6642
|
const now = Date.now();
|
|
6555
6643
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
6556
6644
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
6557
|
-
awaitingTiming.shift();
|
|
6645
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
6558
6646
|
}
|
|
6559
6647
|
}
|
|
6560
6648
|
}
|
|
@@ -6672,11 +6760,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6672
6760
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
6673
6761
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
6674
6762
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
6675
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
6763
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
6676
6764
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
6677
6765
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
6678
6766
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
|
|
6679
|
-
const admitted = resolver2.addMovieFragment(
|
|
6767
|
+
const admitted = resolver2.addMovieFragment(
|
|
6768
|
+
mpu.mpuSequenceNumber,
|
|
6769
|
+
data,
|
|
6770
|
+
remaining,
|
|
6771
|
+
options?.recovered === true
|
|
6772
|
+
);
|
|
6680
6773
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
6681
6774
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
6682
6775
|
if (!admitted) {
|
|
@@ -6696,7 +6789,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6696
6789
|
};
|
|
6697
6790
|
}
|
|
6698
6791
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
6699
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
6792
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
6700
6793
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
6701
6794
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
6702
6795
|
return this.#drop("mfu-empty-payload");
|
|
@@ -6717,6 +6810,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6717
6810
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
6718
6811
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
6719
6812
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
6813
|
+
sequenceNumber: info.sequenceNumber,
|
|
6720
6814
|
arrivedAt: Date.now()
|
|
6721
6815
|
});
|
|
6722
6816
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -7040,6 +7134,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7040
7134
|
}
|
|
7041
7135
|
this.#emitFrame(data, meta);
|
|
7042
7136
|
}
|
|
7137
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
7138
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
7139
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
7140
|
+
}
|
|
7043
7141
|
/**
|
|
7044
7142
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
7045
7143
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -7101,6 +7199,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7101
7199
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
7102
7200
|
this.#sequencers.clear();
|
|
7103
7201
|
this.#fecSequencer?.discard();
|
|
7202
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
7203
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
7204
|
+
}
|
|
7104
7205
|
this.#timingStreams.clear();
|
|
7105
7206
|
this.#pendingTimingSamples = 0;
|
|
7106
7207
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -7119,10 +7220,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7119
7220
|
return resolver;
|
|
7120
7221
|
}
|
|
7121
7222
|
#awaitTiming(streamKey, head) {
|
|
7122
|
-
const
|
|
7123
|
-
if (!
|
|
7223
|
+
const state = this.#timingStreams.get(streamKey);
|
|
7224
|
+
if (!state) return;
|
|
7225
|
+
if (!head.options?.recovered) {
|
|
7226
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
7227
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
7228
|
+
state.awaitingTiming = kept;
|
|
7229
|
+
}
|
|
7230
|
+
const awaitingTiming = state.awaitingTiming;
|
|
7124
7231
|
awaitingTiming.push(head);
|
|
7125
|
-
if (
|
|
7232
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
7233
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
7234
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
7235
|
+
}
|
|
7236
|
+
}
|
|
7237
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
7238
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
7239
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
7126
7240
|
}
|
|
7127
7241
|
/**
|
|
7128
7242
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -7150,6 +7264,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7150
7264
|
const state = this.#timingStreams.get(streamKey);
|
|
7151
7265
|
if (!state) return;
|
|
7152
7266
|
this.#timingStreams.delete(streamKey);
|
|
7267
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
7153
7268
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
7154
7269
|
if (this.#pendingTimingSamples < 0) {
|
|
7155
7270
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -8071,7 +8186,14 @@ var HeldLayerAdmission = class {
|
|
|
8071
8186
|
var PATH_BITS = { moq: 1, multicast: 2 };
|
|
8072
8187
|
var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
|
|
8073
8188
|
var MmtpFecSession = class {
|
|
8189
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
8190
|
+
get repairSymbols() {
|
|
8191
|
+
return this.#repairSymbols;
|
|
8192
|
+
}
|
|
8074
8193
|
#decoder;
|
|
8194
|
+
#repairSymbols;
|
|
8195
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
8196
|
+
#message;
|
|
8075
8197
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
8076
8198
|
#blocks = /* @__PURE__ */ new Map();
|
|
8077
8199
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -8109,6 +8231,24 @@ var MmtpFecSession = class {
|
|
|
8109
8231
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
8110
8232
|
*/
|
|
8111
8233
|
constructor(config) {
|
|
8234
|
+
if (config && "alFecFlow" in config) {
|
|
8235
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow);
|
|
8236
|
+
if (!message) {
|
|
8237
|
+
throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
|
|
8238
|
+
}
|
|
8239
|
+
this.#message = message;
|
|
8240
|
+
this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
|
|
8241
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
|
|
8242
|
+
this.sourceSymbols = message.maximumK;
|
|
8243
|
+
this.#repairSymbols = message.maximumP;
|
|
8244
|
+
this.symbolSize = message.repairSymbolSize;
|
|
8245
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
|
|
8246
|
+
this.#repairPacketId = message.repairFlowId;
|
|
8247
|
+
this.#heldLayers = layerZero(message.maximumP);
|
|
8248
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
8249
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
8250
|
+
return;
|
|
8251
|
+
}
|
|
8112
8252
|
const fec = config?.fec;
|
|
8113
8253
|
if (!fec || typeof fec !== "object") {
|
|
8114
8254
|
throw new Error("MmtpFecSession: the catalog fec instance is required");
|
|
@@ -8119,7 +8259,7 @@ var MmtpFecSession = class {
|
|
|
8119
8259
|
}
|
|
8120
8260
|
this.algorithm = fec.scheme;
|
|
8121
8261
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
|
|
8122
|
-
this
|
|
8262
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
|
|
8123
8263
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
|
|
8124
8264
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
|
|
8125
8265
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
|
|
@@ -8139,17 +8279,30 @@ var MmtpFecSession = class {
|
|
|
8139
8279
|
config.heldLayers
|
|
8140
8280
|
);
|
|
8141
8281
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8282
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
8283
|
+
}
|
|
8284
|
+
/**
|
|
8285
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
8286
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
8287
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
8288
|
+
* previous one stays valid before the first block carrying the new total
|
|
8289
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
8290
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
8291
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
8292
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
8293
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
8294
|
+
*/
|
|
8295
|
+
updateAlFecFlow(flow) {
|
|
8296
|
+
const held = this.#message;
|
|
8297
|
+
if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
|
|
8298
|
+
const next = alFecRaptorQFlowConfig(flow);
|
|
8299
|
+
if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
|
|
8300
|
+
return "refused";
|
|
8301
|
+
}
|
|
8302
|
+
if (next.maximumP <= this.#repairSymbols) return "unchanged";
|
|
8303
|
+
this.#repairSymbols = next.maximumP;
|
|
8304
|
+
this.#heldLayers = layerZero(next.maximumP);
|
|
8305
|
+
return "raised";
|
|
8153
8306
|
}
|
|
8154
8307
|
/** True once release() ran. */
|
|
8155
8308
|
get released() {
|
|
@@ -8244,7 +8397,7 @@ var MmtpFecSession = class {
|
|
|
8244
8397
|
}
|
|
8245
8398
|
if (admission.reject) {
|
|
8246
8399
|
const verdict2 = this.#heldLayers.failsClosed(
|
|
8247
|
-
repairTrackName,
|
|
8400
|
+
this.#trackFor(repairTrackName),
|
|
8248
8401
|
frame.ssStart,
|
|
8249
8402
|
frame.rsbLength,
|
|
8250
8403
|
this.#anchorHorizon,
|
|
@@ -8259,7 +8412,7 @@ var MmtpFecSession = class {
|
|
|
8259
8412
|
this.#repairPacketId ??= frame.packetId;
|
|
8260
8413
|
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
8261
8414
|
const verdict = this.#heldLayers.admit(
|
|
8262
|
-
repairTrackName,
|
|
8415
|
+
this.#trackFor(repairTrackName),
|
|
8263
8416
|
frame.ssStart,
|
|
8264
8417
|
frame.rsbLength,
|
|
8265
8418
|
this.#anchorHorizon,
|
|
@@ -8318,6 +8471,10 @@ var MmtpFecSession = class {
|
|
|
8318
8471
|
decoder.free();
|
|
8319
8472
|
return true;
|
|
8320
8473
|
}
|
|
8474
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
8475
|
+
#trackFor(repairTrackName) {
|
|
8476
|
+
return this.#message ? void 0 : repairTrackName;
|
|
8477
|
+
}
|
|
8321
8478
|
#drop(reason) {
|
|
8322
8479
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
|
|
8323
8480
|
return [];
|
|
@@ -8439,6 +8596,22 @@ var MmtpFecSession = class {
|
|
|
8439
8596
|
}
|
|
8440
8597
|
}
|
|
8441
8598
|
};
|
|
8599
|
+
function layerZero(total) {
|
|
8600
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
|
|
8601
|
+
}
|
|
8602
|
+
function buildDecoder(Decoder, session) {
|
|
8603
|
+
if (typeof Decoder !== "function") {
|
|
8604
|
+
throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
|
|
8605
|
+
}
|
|
8606
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
|
|
8607
|
+
try {
|
|
8608
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize);
|
|
8609
|
+
} catch (error) {
|
|
8610
|
+
decoder.free();
|
|
8611
|
+
throw error;
|
|
8612
|
+
}
|
|
8613
|
+
return decoder;
|
|
8614
|
+
}
|
|
8442
8615
|
function requirePositiveInteger(value, path) {
|
|
8443
8616
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
8444
8617
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
|