@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.75b39a2cdddf
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 +1001 -215
- 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 +60 -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 +222 -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 +58 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +105 -34
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +229 -51
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +104 -34
- 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 +18 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +323 -64
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.js +547 -72
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +28 -18
- package/dist/multicast-transport.js.map +2 -2
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +222 -48
- 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 +1 -1
- 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 +104 -34
- 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 +189 -179
- package/src/fec-manager.ts +277 -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 +125 -19
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +350 -13
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
|
@@ -171,30 +171,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
|
|
|
171
171
|
const authLength = view2.getUint16(2, false);
|
|
172
172
|
return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
|
|
173
173
|
}
|
|
174
|
-
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options
|
|
174
|
+
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
|
|
175
175
|
if (declaredEnd > payload.length)
|
|
176
176
|
return null;
|
|
177
177
|
if (options?.skipFecId) {
|
|
178
|
-
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false
|
|
178
|
+
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
|
|
179
179
|
}
|
|
180
|
-
const overlay = options?.overlayDeclared === true && rapFlag;
|
|
181
180
|
if (fecType === 1) {
|
|
182
|
-
const fecStart = payload.length -
|
|
181
|
+
const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
183
182
|
if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
|
|
184
183
|
return null;
|
|
185
184
|
}
|
|
186
|
-
return { hasFecId: true
|
|
185
|
+
return { hasFecId: true };
|
|
187
186
|
}
|
|
188
|
-
if (fecType !== 0
|
|
187
|
+
if (fecType !== 0)
|
|
189
188
|
return null;
|
|
190
189
|
if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
|
|
191
|
-
return { hasFecId: false
|
|
190
|
+
return { hasFecId: false };
|
|
192
191
|
}
|
|
193
192
|
return null;
|
|
194
193
|
}
|
|
195
|
-
function sourceFecTrailerSize(overlay) {
|
|
196
|
-
return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
197
|
-
}
|
|
198
194
|
function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
199
195
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
|
|
200
196
|
return {
|
|
@@ -247,11 +243,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
|
247
243
|
if ("rejection" in layoutResult)
|
|
248
244
|
return layoutResult;
|
|
249
245
|
const { layout } = layoutResult;
|
|
250
|
-
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options
|
|
246
|
+
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
|
|
251
247
|
if (!trailerLayout) {
|
|
252
248
|
if (!options?.skipFecId && header.fecType === 1) {
|
|
253
249
|
const observed = payload.length - layout.dataEnd;
|
|
254
|
-
const expected =
|
|
250
|
+
const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
255
251
|
if (observed !== expected) {
|
|
256
252
|
return {
|
|
257
253
|
rejection: {
|
|
@@ -283,13 +279,10 @@ function extractPlannedMfu(mmtpPacket, result) {
|
|
|
283
279
|
const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
|
|
284
280
|
const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
|
|
285
281
|
const data = payload.subarray(mfuOffset, dataEnd);
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
|
|
291
|
-
const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
|
|
292
|
-
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
|
|
282
|
+
const fecPayloadId = trailerLayout.hasFecId ? {
|
|
283
|
+
ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
|
|
284
|
+
} : null;
|
|
285
|
+
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
|
|
293
286
|
}
|
|
294
287
|
function sourceFecPayloadIdOf(mmtpPacket, header) {
|
|
295
288
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
|
|
@@ -1217,6 +1210,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1217
1210
|
#retentionBlocks;
|
|
1218
1211
|
#heldEvicted;
|
|
1219
1212
|
#newest;
|
|
1213
|
+
#newestSource;
|
|
1220
1214
|
#holes;
|
|
1221
1215
|
static #checkedRetention(retentionBlocks) {
|
|
1222
1216
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
@@ -1254,6 +1248,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1254
1248
|
observe(position) {
|
|
1255
1249
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
1256
1250
|
}
|
|
1251
|
+
/**
|
|
1252
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
1253
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
1254
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
1255
|
+
* reordering.
|
|
1256
|
+
*/
|
|
1257
|
+
get newestSource() {
|
|
1258
|
+
return this.#newestSource;
|
|
1259
|
+
}
|
|
1260
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
1261
|
+
noteSource(ssId) {
|
|
1262
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
1263
|
+
this.#newestSource = ssId;
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1257
1266
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
1258
1267
|
get heldEvicted() {
|
|
1259
1268
|
return this.#heldEvicted;
|
|
@@ -1486,6 +1495,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
1486
1495
|
this.#held.clear();
|
|
1487
1496
|
this.#coverageEnd = void 0;
|
|
1488
1497
|
this.#newest = void 0;
|
|
1498
|
+
this.#newestSource = void 0;
|
|
1489
1499
|
}
|
|
1490
1500
|
#claim(block) {
|
|
1491
1501
|
const placed = [];
|
|
@@ -2074,6 +2084,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
2074
2084
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
2075
2085
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
2076
2086
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
2087
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
2077
2088
|
function isMulticastSymbolDelivery(value) {
|
|
2078
2089
|
return value === "datagram" || value === "ordered-relay";
|
|
2079
2090
|
}
|
|
@@ -2171,6 +2182,10 @@ var FECManager = class _FECManager {
|
|
|
2171
2182
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
2172
2183
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
2173
2184
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
2185
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
2186
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
2187
|
+
// not network arrivals and never make a block clean.
|
|
2188
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
2174
2189
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
2175
2190
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
2176
2191
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -2299,6 +2314,10 @@ var FECManager = class _FECManager {
|
|
|
2299
2314
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
2300
2315
|
// One classifier per physical stream, built from that stream's geometry.
|
|
2301
2316
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
2317
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
2318
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
2319
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
2320
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
2302
2321
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
2303
2322
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
2304
2323
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -2456,6 +2475,7 @@ var FECManager = class _FECManager {
|
|
|
2456
2475
|
#pendingWorkerCleanupBounds;
|
|
2457
2476
|
#blockSourceCounts;
|
|
2458
2477
|
#blockSourceEsis;
|
|
2478
|
+
#blockRebuiltEsis;
|
|
2459
2479
|
#blockAuthoredLengths;
|
|
2460
2480
|
#ssbLengthConflictCount;
|
|
2461
2481
|
#blockRepairRsIds;
|
|
@@ -2498,6 +2518,7 @@ var FECManager = class _FECManager {
|
|
|
2498
2518
|
#orderedFrontiers;
|
|
2499
2519
|
#orderedPendingBlocks;
|
|
2500
2520
|
#classifiers;
|
|
2521
|
+
#noSpanRaps;
|
|
2501
2522
|
#streamGeometries;
|
|
2502
2523
|
#failedBlocks;
|
|
2503
2524
|
#resolvedBlockClasses;
|
|
@@ -3159,6 +3180,7 @@ var FECManager = class _FECManager {
|
|
|
3159
3180
|
};
|
|
3160
3181
|
const discontinuity = () => {
|
|
3161
3182
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
3183
|
+
this.#noSpanRaps.delete(streamKey);
|
|
3162
3184
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
3163
3185
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
3164
3186
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -3571,6 +3593,9 @@ var FECManager = class _FECManager {
|
|
|
3571
3593
|
}
|
|
3572
3594
|
if (policyChanged) {
|
|
3573
3595
|
this.#bumpEpoch(packetId);
|
|
3596
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
3597
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
3598
|
+
}
|
|
3574
3599
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
3575
3600
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
3576
3601
|
const stream = this.#parseStreamKey(key);
|
|
@@ -3773,6 +3798,7 @@ var FECManager = class _FECManager {
|
|
|
3773
3798
|
}
|
|
3774
3799
|
resetMap(this.#blockSourceCounts);
|
|
3775
3800
|
resetMap(this.#blockSourceEsis);
|
|
3801
|
+
resetMap(this.#blockRebuiltEsis);
|
|
3776
3802
|
resetMap(this.#blockAuthoredLengths);
|
|
3777
3803
|
resetMap(this.#blockRepairRsIds);
|
|
3778
3804
|
resetMap(this.#blockRsbLengths);
|
|
@@ -3806,6 +3832,7 @@ var FECManager = class _FECManager {
|
|
|
3806
3832
|
resetMap(this.#isoLocators);
|
|
3807
3833
|
resetMap(this.#sourceAnchors);
|
|
3808
3834
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
3835
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
3809
3836
|
this.#bumpEpoch(packetId);
|
|
3810
3837
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
3811
3838
|
for (const key of keys.keys()) {
|
|
@@ -3896,6 +3923,7 @@ var FECManager = class _FECManager {
|
|
|
3896
3923
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
3897
3924
|
dropMap(this.#blockSourceCounts);
|
|
3898
3925
|
dropMap(this.#blockSourceEsis);
|
|
3926
|
+
dropMap(this.#blockRebuiltEsis);
|
|
3899
3927
|
dropMap(this.#blockAuthoredLengths);
|
|
3900
3928
|
dropMap(this.#blockRepairRsIds);
|
|
3901
3929
|
dropMap(this.#blockRsbLengths);
|
|
@@ -3912,6 +3940,7 @@ var FECManager = class _FECManager {
|
|
|
3912
3940
|
dropMap(this.#isoLocators);
|
|
3913
3941
|
dropMap(this.#sourceAnchors);
|
|
3914
3942
|
dropMap(this.#classifiers);
|
|
3943
|
+
dropMap(this.#noSpanRaps);
|
|
3915
3944
|
dropMap(this.#streamGeometries);
|
|
3916
3945
|
dropSet(this.#pendingWorkerCleanups);
|
|
3917
3946
|
dropSet(this.#completedBlocks);
|
|
@@ -4057,6 +4086,9 @@ var FECManager = class _FECManager {
|
|
|
4057
4086
|
this.#unindexBlockKey(key);
|
|
4058
4087
|
}
|
|
4059
4088
|
}
|
|
4089
|
+
if (isSourceSymbol && source === "multicast") {
|
|
4090
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
4091
|
+
}
|
|
4060
4092
|
return true;
|
|
4061
4093
|
}
|
|
4062
4094
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -4374,6 +4406,7 @@ var FECManager = class _FECManager {
|
|
|
4374
4406
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
4375
4407
|
const evictedBefore = locator.heldEvicted;
|
|
4376
4408
|
const held = locator.hold(ssId, source);
|
|
4409
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
4377
4410
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
4378
4411
|
if (evicted > 0) {
|
|
4379
4412
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -4542,11 +4575,14 @@ var FECManager = class _FECManager {
|
|
|
4542
4575
|
/**
|
|
4543
4576
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
4544
4577
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
4545
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
4578
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
4579
|
+
* and neither has a stream without catalog timing until it releases two
|
|
4580
|
+
* RAPs inside one MPU.
|
|
4546
4581
|
*/
|
|
4547
4582
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
4548
|
-
|
|
4549
|
-
return this.#
|
|
4583
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
4584
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
4585
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
4550
4586
|
}
|
|
4551
4587
|
/** A block's class now, or frozen at its resolution. */
|
|
4552
4588
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -4572,7 +4608,14 @@ var FECManager = class _FECManager {
|
|
|
4572
4608
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
4573
4609
|
*/
|
|
4574
4610
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
4575
|
-
if (!this.#trackConfigs.has(packetId)
|
|
4611
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
4612
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
4613
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
4614
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
4615
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
4616
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4576
4619
|
this.#applyClassifierChanges(
|
|
4577
4620
|
packetId,
|
|
4578
4621
|
catalogTrackName,
|
|
@@ -4959,7 +5002,15 @@ var FECManager = class _FECManager {
|
|
|
4959
5002
|
this.#isoSourceOversizeCount++;
|
|
4960
5003
|
return null;
|
|
4961
5004
|
}
|
|
4962
|
-
|
|
5005
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
5006
|
+
}
|
|
5007
|
+
/**
|
|
5008
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
5009
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
5010
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
5011
|
+
* stated for `ssId`.
|
|
5012
|
+
*/
|
|
5013
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
4963
5014
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
4964
5015
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
4965
5016
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -4988,6 +5039,74 @@ var FECManager = class _FECManager {
|
|
|
4988
5039
|
}
|
|
4989
5040
|
return recovered;
|
|
4990
5041
|
}
|
|
5042
|
+
/**
|
|
5043
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
5044
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
5045
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
5046
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
5047
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
5048
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
5049
|
+
* no deadline refresh, and it never makes a block clean.
|
|
5050
|
+
*
|
|
5051
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
5052
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
5053
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
5054
|
+
* feedSourceSymbol takes it.
|
|
5055
|
+
*
|
|
5056
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
5057
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
5058
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
5059
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
5060
|
+
* block as clean on its own side), the block is completed here as
|
|
5061
|
+
* recovered, with nothing left to route.
|
|
5062
|
+
*/
|
|
5063
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
5064
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
5065
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
5066
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
5067
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
5068
|
+
const sbn = located.block.start;
|
|
5069
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
5070
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
5071
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
5072
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
5073
|
+
if (rebuilt === void 0) {
|
|
5074
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
5075
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
5076
|
+
}
|
|
5077
|
+
const config = this.#trackConfigs.get(packetId);
|
|
5078
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
5079
|
+
rebuilt.add(located.esi);
|
|
5080
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
5081
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
5082
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
5083
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
return { taken: true, recovered };
|
|
5087
|
+
}
|
|
5088
|
+
/**
|
|
5089
|
+
* A block that holds every one of its `length` sources, received or
|
|
5090
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
5091
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
5092
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
5093
|
+
*/
|
|
5094
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
5095
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
5096
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
5097
|
+
let present = rebuilt.size;
|
|
5098
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
5099
|
+
if (present < length) return false;
|
|
5100
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
5101
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
5102
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
5103
|
+
if (catalogTrackName === void 0) {
|
|
5104
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
5105
|
+
} else {
|
|
5106
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
5107
|
+
}
|
|
5108
|
+
return true;
|
|
5109
|
+
}
|
|
4991
5110
|
/**
|
|
4992
5111
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
4993
5112
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -5190,8 +5309,75 @@ var FECManager = class _FECManager {
|
|
|
5190
5309
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
5191
5310
|
return null;
|
|
5192
5311
|
}
|
|
5312
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
5193
5313
|
return recovered;
|
|
5194
5314
|
}
|
|
5315
|
+
/**
|
|
5316
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
5317
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
5318
|
+
* position past a block leaves the sources that block still misses
|
|
5319
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
5320
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
5321
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
5322
|
+
* sent before it.
|
|
5323
|
+
*/
|
|
5324
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
5325
|
+
const previous = locator.newestSource;
|
|
5326
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
5327
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
5328
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
5329
|
+
this.#shortenUnrecoverableWait(
|
|
5330
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
5331
|
+
block.start,
|
|
5332
|
+
packetId,
|
|
5333
|
+
catalogTrackName
|
|
5334
|
+
);
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5338
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
5339
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
5340
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
5341
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
5342
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
5343
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
5344
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
5345
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
5346
|
+
* block.
|
|
5347
|
+
*/
|
|
5348
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
5349
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
5350
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
5351
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
5352
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
5353
|
+
const received = this.#blockSourceEsis.get(key);
|
|
5354
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
5355
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
5356
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
5357
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
5358
|
+
return void 0;
|
|
5359
|
+
}
|
|
5360
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
5361
|
+
}
|
|
5362
|
+
/**
|
|
5363
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
5364
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
5365
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
5366
|
+
*/
|
|
5367
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
5368
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
5369
|
+
const config = this.#trackConfigs.get(packetId);
|
|
5370
|
+
if (config === void 0) return;
|
|
5371
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
5372
|
+
if (waitMs === void 0) return;
|
|
5373
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
5374
|
+
this.#armBlockTimeout(
|
|
5375
|
+
sbn,
|
|
5376
|
+
packetId,
|
|
5377
|
+
catalogTrackName,
|
|
5378
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
5195
5381
|
/**
|
|
5196
5382
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
5197
5383
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -5346,6 +5532,15 @@ var FECManager = class _FECManager {
|
|
|
5346
5532
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
5347
5533
|
return;
|
|
5348
5534
|
}
|
|
5535
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
5536
|
+
key,
|
|
5537
|
+
sbn,
|
|
5538
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
5539
|
+
packetId,
|
|
5540
|
+
catalogTrackName
|
|
5541
|
+
)) {
|
|
5542
|
+
return;
|
|
5543
|
+
}
|
|
5349
5544
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
5350
5545
|
}
|
|
5351
5546
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -5354,6 +5549,7 @@ var FECManager = class _FECManager {
|
|
|
5354
5549
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
5355
5550
|
this.#blockSourceCounts.delete(key);
|
|
5356
5551
|
this.#blockSourceEsis.delete(key);
|
|
5552
|
+
this.#blockRebuiltEsis.delete(key);
|
|
5357
5553
|
this.#blockAuthoredLengths.delete(key);
|
|
5358
5554
|
this.#blockRepairRsIds.delete(key);
|
|
5359
5555
|
this.#blockRsbLengths.delete(key);
|
|
@@ -5420,7 +5616,16 @@ var FECManager = class _FECManager {
|
|
|
5420
5616
|
this.#repairDeadlineMs(trackConfig)
|
|
5421
5617
|
);
|
|
5422
5618
|
}
|
|
5423
|
-
this.#
|
|
5619
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
5620
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
5621
|
+
return;
|
|
5622
|
+
}
|
|
5623
|
+
this.#armBlockTimeout(
|
|
5624
|
+
sbn,
|
|
5625
|
+
packetId,
|
|
5626
|
+
catalogTrackName,
|
|
5627
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
5628
|
+
);
|
|
5424
5629
|
}
|
|
5425
5630
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
5426
5631
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -5433,6 +5638,7 @@ var FECManager = class _FECManager {
|
|
|
5433
5638
|
}
|
|
5434
5639
|
this.#blockSourceCounts.delete(key);
|
|
5435
5640
|
this.#blockSourceEsis.delete(key);
|
|
5641
|
+
this.#blockRebuiltEsis.delete(key);
|
|
5436
5642
|
this.#blockAuthoredLengths.delete(key);
|
|
5437
5643
|
this.#blockRepairRsIds.delete(key);
|
|
5438
5644
|
this.#blockRsbLengths.delete(key);
|
|
@@ -5471,6 +5677,7 @@ var FECManager = class _FECManager {
|
|
|
5471
5677
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
5472
5678
|
this.#blockSourceCounts.delete(key);
|
|
5473
5679
|
this.#blockSourceEsis.delete(key);
|
|
5680
|
+
this.#blockRebuiltEsis.delete(key);
|
|
5474
5681
|
this.#blockAuthoredLengths.delete(key);
|
|
5475
5682
|
this.#blockRepairRsIds.delete(key);
|
|
5476
5683
|
this.#blockRsbLengths.delete(key);
|
|
@@ -5562,6 +5769,7 @@ var FECManager = class _FECManager {
|
|
|
5562
5769
|
this.#blockKeysBySbn.clear();
|
|
5563
5770
|
this.#blockSourceCounts.clear();
|
|
5564
5771
|
this.#blockSourceEsis.clear();
|
|
5772
|
+
this.#blockRebuiltEsis.clear();
|
|
5565
5773
|
this.#blockAuthoredLengths.clear();
|
|
5566
5774
|
this.#blockRepairRsIds.clear();
|
|
5567
5775
|
this.#blockRsbLengths.clear();
|
|
@@ -5587,6 +5795,7 @@ var FECManager = class _FECManager {
|
|
|
5587
5795
|
this.#blockAnchorsMs.clear();
|
|
5588
5796
|
this.#blockCatalogTrackNames.clear();
|
|
5589
5797
|
this.#classifiers.clear();
|
|
5798
|
+
this.#noSpanRaps.clear();
|
|
5590
5799
|
this.#streamGeometries.clear();
|
|
5591
5800
|
this.#failedBlocks.clear();
|
|
5592
5801
|
this.#resolvedBlockClasses.clear();
|
|
@@ -6940,9 +7149,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
|
6940
7149
|
} catch (error) {
|
|
6941
7150
|
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
6942
7151
|
}
|
|
6943
|
-
if (
|
|
7152
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
6944
7153
|
await rx.close();
|
|
6945
|
-
throw unsupported(`integrity scheme ${String(
|
|
7154
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
6946
7155
|
}
|
|
6947
7156
|
const events = new EventTarget();
|
|
6948
7157
|
let state = "connected";
|
|
@@ -7061,14 +7270,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
7061
7270
|
});
|
|
7062
7271
|
|
|
7063
7272
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
7273
|
+
var MAX_PENDING_PACKETS = 256;
|
|
7064
7274
|
var AMTGatewayManager = class {
|
|
7065
7275
|
subscriptions = /* @__PURE__ */ new Map();
|
|
7066
7276
|
messageHandler;
|
|
7277
|
+
messageListenerRegistered = false;
|
|
7067
7278
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
7068
7279
|
constructor() {
|
|
7069
7280
|
this.messageHandler = (event) => {
|
|
7070
7281
|
const data = event.data;
|
|
7071
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
7072
7282
|
if (data.type === "UDP_PACKET") {
|
|
7073
7283
|
this.handlePacket(data);
|
|
7074
7284
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -7077,12 +7287,22 @@ var AMTGatewayManager = class {
|
|
|
7077
7287
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
7078
7288
|
}
|
|
7079
7289
|
};
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
console.warn("[AMT] Service worker not available");
|
|
7290
|
+
}
|
|
7291
|
+
ensureMessageListener() {
|
|
7292
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
7293
|
+
return;
|
|
7085
7294
|
}
|
|
7295
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
7296
|
+
this.messageListenerRegistered = true;
|
|
7297
|
+
console.log("[AMT] Registered service worker message handler");
|
|
7298
|
+
}
|
|
7299
|
+
removeMessageListener() {
|
|
7300
|
+
if (!this.messageListenerRegistered)
|
|
7301
|
+
return;
|
|
7302
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
7303
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
7304
|
+
}
|
|
7305
|
+
this.messageListenerRegistered = false;
|
|
7086
7306
|
}
|
|
7087
7307
|
/**
|
|
7088
7308
|
* Subscribe via AMT tunnel
|
|
@@ -7092,6 +7312,7 @@ var AMTGatewayManager = class {
|
|
|
7092
7312
|
if (config.protected) {
|
|
7093
7313
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
7094
7314
|
}
|
|
7315
|
+
this.ensureMessageListener();
|
|
7095
7316
|
const keyStr = this.getKeyString(key);
|
|
7096
7317
|
if (this.subscriptions.has(keyStr)) {
|
|
7097
7318
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -7127,6 +7348,8 @@ var AMTGatewayManager = class {
|
|
|
7127
7348
|
this.subscriptions.delete(keyStr);
|
|
7128
7349
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
7129
7350
|
}
|
|
7351
|
+
if (this.subscriptions.size === 0)
|
|
7352
|
+
this.removeMessageListener();
|
|
7130
7353
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
7131
7354
|
}
|
|
7132
7355
|
}
|
|
@@ -7148,6 +7371,8 @@ var AMTGatewayManager = class {
|
|
|
7148
7371
|
} finally {
|
|
7149
7372
|
this.subscriptions.delete(handle);
|
|
7150
7373
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
7374
|
+
if (this.subscriptions.size === 0)
|
|
7375
|
+
this.removeMessageListener();
|
|
7151
7376
|
}
|
|
7152
7377
|
}
|
|
7153
7378
|
/**
|
|
@@ -7164,14 +7389,12 @@ var AMTGatewayManager = class {
|
|
|
7164
7389
|
if (!subscriptionId)
|
|
7165
7390
|
return;
|
|
7166
7391
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
7167
|
-
if (!subscription)
|
|
7168
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
7392
|
+
if (!subscription)
|
|
7169
7393
|
return;
|
|
7170
|
-
}
|
|
7171
7394
|
if (!subscription.handshakeComplete) {
|
|
7172
7395
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
7173
7396
|
const packetData2 = data;
|
|
7174
|
-
if (queue) {
|
|
7397
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
7175
7398
|
queue.push({
|
|
7176
7399
|
data: packetData2.data,
|
|
7177
7400
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -7197,19 +7420,15 @@ var AMTGatewayManager = class {
|
|
|
7197
7420
|
if (!subscriptionId)
|
|
7198
7421
|
return;
|
|
7199
7422
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
7200
|
-
if (!subscription)
|
|
7201
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
7423
|
+
if (!subscription)
|
|
7202
7424
|
return;
|
|
7203
|
-
}
|
|
7204
7425
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
7205
7426
|
}
|
|
7206
7427
|
/**
|
|
7207
7428
|
* Cleanup on destroy
|
|
7208
7429
|
*/
|
|
7209
7430
|
destroy() {
|
|
7210
|
-
|
|
7211
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
7212
|
-
}
|
|
7431
|
+
this.removeMessageListener();
|
|
7213
7432
|
const handles = Array.from(this.subscriptions.keys());
|
|
7214
7433
|
for (const handle of handles) {
|
|
7215
7434
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -13118,9 +13337,21 @@ var IsoBmffTimingResolver = class {
|
|
|
13118
13337
|
}
|
|
13119
13338
|
#track = null;
|
|
13120
13339
|
#fragments = /* @__PURE__ */ new Map();
|
|
13340
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
13341
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
13342
|
+
/**
|
|
13343
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
13344
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
13345
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
13346
|
+
*/
|
|
13347
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
13121
13348
|
#pendingSamples = 0;
|
|
13122
13349
|
#evictedSamples = 0;
|
|
13123
13350
|
#lastMovieFragmentRejection = null;
|
|
13351
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
13352
|
+
get initialized() {
|
|
13353
|
+
return this.#track !== null;
|
|
13354
|
+
}
|
|
13124
13355
|
get pendingSampleCount() {
|
|
13125
13356
|
return this.#pendingSamples;
|
|
13126
13357
|
}
|
|
@@ -13144,7 +13375,14 @@ var IsoBmffTimingResolver = class {
|
|
|
13144
13375
|
this.#track = timing;
|
|
13145
13376
|
return true;
|
|
13146
13377
|
}
|
|
13147
|
-
|
|
13378
|
+
/**
|
|
13379
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
13380
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
13381
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
13382
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
13383
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
13384
|
+
*/
|
|
13385
|
+
addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
13148
13386
|
this.#lastMovieFragmentRejection = null;
|
|
13149
13387
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
13150
13388
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -13170,6 +13408,8 @@ var IsoBmffTimingResolver = class {
|
|
|
13170
13408
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
13171
13409
|
);
|
|
13172
13410
|
}
|
|
13411
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
13412
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
13173
13413
|
let freed = 0;
|
|
13174
13414
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
13175
13415
|
freed += this.#evictOldestFragment();
|
|
@@ -13183,6 +13423,7 @@ var IsoBmffTimingResolver = class {
|
|
|
13183
13423
|
return false;
|
|
13184
13424
|
}
|
|
13185
13425
|
this.#fragments.set(key, [...parsed.samples]);
|
|
13426
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
13186
13427
|
this.#pendingSamples += parsed.samples.length;
|
|
13187
13428
|
return true;
|
|
13188
13429
|
}
|
|
@@ -13240,6 +13481,7 @@ var IsoBmffTimingResolver = class {
|
|
|
13240
13481
|
let live = 0;
|
|
13241
13482
|
for (const sample of samples) if (sample !== null) live++;
|
|
13242
13483
|
this.#fragments.delete(oldest);
|
|
13484
|
+
this.#firstSamples.delete(oldest);
|
|
13243
13485
|
this.#pendingSamples -= live;
|
|
13244
13486
|
this.#evictedSamples += live;
|
|
13245
13487
|
return live;
|
|
@@ -13255,12 +13497,23 @@ var IsoBmffTimingResolver = class {
|
|
|
13255
13497
|
if (!samples || !samples[index]) return false;
|
|
13256
13498
|
samples[index] = null;
|
|
13257
13499
|
this.#pendingSamples--;
|
|
13258
|
-
if (samples.every((value) => value === null))
|
|
13500
|
+
if (samples.every((value) => value === null)) {
|
|
13501
|
+
this.#fragments.delete(key);
|
|
13502
|
+
const first = this.#firstSamples.get(key);
|
|
13503
|
+
this.#firstSamples.delete(key);
|
|
13504
|
+
this.#delivered.delete(key);
|
|
13505
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
13506
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
13507
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13259
13510
|
return true;
|
|
13260
13511
|
}
|
|
13261
13512
|
reset() {
|
|
13262
13513
|
this.#track = null;
|
|
13263
13514
|
this.#fragments.clear();
|
|
13515
|
+
this.#firstSamples.clear();
|
|
13516
|
+
this.#delivered.clear();
|
|
13264
13517
|
this.#pendingSamples = 0;
|
|
13265
13518
|
}
|
|
13266
13519
|
};
|
|
@@ -13742,6 +13995,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13742
13995
|
this.#reclaimedTimingSamples = 0;
|
|
13743
13996
|
this.#evictedTimingSamples = 0;
|
|
13744
13997
|
this.#rejectedMovieFragments = 0;
|
|
13998
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
13999
|
+
held: 0,
|
|
14000
|
+
expired: 0,
|
|
14001
|
+
discarded: 0,
|
|
14002
|
+
superseded: 0
|
|
14003
|
+
};
|
|
13745
14004
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
13746
14005
|
this.#fecGate = null;
|
|
13747
14006
|
this.#fecSequencer = null;
|
|
@@ -13850,6 +14109,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13850
14109
|
#reclaimedTimingSamples;
|
|
13851
14110
|
#evictedTimingSamples;
|
|
13852
14111
|
#rejectedMovieFragments;
|
|
14112
|
+
#recoveredHeadsAwaitingTiming;
|
|
13853
14113
|
#sequencers;
|
|
13854
14114
|
#fecGate;
|
|
13855
14115
|
#fecSequencer;
|
|
@@ -13955,11 +14215,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13955
14215
|
/**
|
|
13956
14216
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
13957
14217
|
*
|
|
13958
|
-
*
|
|
13959
|
-
*
|
|
13960
|
-
*
|
|
13961
|
-
*
|
|
13962
|
-
*
|
|
14218
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
14219
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
14220
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
14221
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
14222
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
14223
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
13963
14224
|
*/
|
|
13964
14225
|
get timingBudgetStats() {
|
|
13965
14226
|
return {
|
|
@@ -13970,6 +14231,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13970
14231
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
13971
14232
|
};
|
|
13972
14233
|
}
|
|
14234
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
14235
|
+
get recoveredHeadsAwaitingTiming() {
|
|
14236
|
+
let pending = 0;
|
|
14237
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14238
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
14239
|
+
}
|
|
14240
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
14241
|
+
}
|
|
13973
14242
|
get reorderStats() {
|
|
13974
14243
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
13975
14244
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -14143,7 +14412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14143
14412
|
const now = Date.now();
|
|
14144
14413
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14145
14414
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
14146
|
-
awaitingTiming.shift();
|
|
14415
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
14147
14416
|
}
|
|
14148
14417
|
}
|
|
14149
14418
|
}
|
|
@@ -14261,11 +14530,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14261
14530
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
14262
14531
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
14263
14532
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
14264
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
14533
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
14265
14534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
14266
14535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
14267
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
14268
|
-
const admitted = resolver2.addMovieFragment(
|
|
14536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
14537
|
+
const admitted = resolver2.addMovieFragment(
|
|
14538
|
+
mpu.mpuSequenceNumber,
|
|
14539
|
+
data,
|
|
14540
|
+
remaining,
|
|
14541
|
+
options?.recovered === true
|
|
14542
|
+
);
|
|
14269
14543
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
14270
14544
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
14271
14545
|
if (!admitted) {
|
|
@@ -14285,7 +14559,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14285
14559
|
};
|
|
14286
14560
|
}
|
|
14287
14561
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
14288
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
14562
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
14289
14563
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
14290
14564
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
14291
14565
|
return this.#drop("mfu-empty-payload");
|
|
@@ -14306,6 +14580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14306
14580
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
14307
14581
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
14308
14582
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
14583
|
+
sequenceNumber: info.sequenceNumber,
|
|
14309
14584
|
arrivedAt: Date.now()
|
|
14310
14585
|
});
|
|
14311
14586
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -14629,6 +14904,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14629
14904
|
}
|
|
14630
14905
|
this.#emitFrame(data, meta);
|
|
14631
14906
|
}
|
|
14907
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
14908
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
14909
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
14910
|
+
}
|
|
14632
14911
|
/**
|
|
14633
14912
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
14634
14913
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -14690,6 +14969,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14690
14969
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
14691
14970
|
this.#sequencers.clear();
|
|
14692
14971
|
this.#fecSequencer?.discard();
|
|
14972
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14973
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
14974
|
+
}
|
|
14693
14975
|
this.#timingStreams.clear();
|
|
14694
14976
|
this.#pendingTimingSamples = 0;
|
|
14695
14977
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -14708,10 +14990,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14708
14990
|
return resolver;
|
|
14709
14991
|
}
|
|
14710
14992
|
#awaitTiming(streamKey, head) {
|
|
14711
|
-
const
|
|
14712
|
-
if (!
|
|
14993
|
+
const state = this.#timingStreams.get(streamKey);
|
|
14994
|
+
if (!state) return;
|
|
14995
|
+
if (!head.options?.recovered) {
|
|
14996
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
14997
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
14998
|
+
state.awaitingTiming = kept;
|
|
14999
|
+
}
|
|
15000
|
+
const awaitingTiming = state.awaitingTiming;
|
|
14713
15001
|
awaitingTiming.push(head);
|
|
14714
|
-
if (
|
|
15002
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
15003
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
15004
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
15005
|
+
}
|
|
15006
|
+
}
|
|
15007
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
15008
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
15009
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
14715
15010
|
}
|
|
14716
15011
|
/**
|
|
14717
15012
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -14739,6 +15034,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14739
15034
|
const state = this.#timingStreams.get(streamKey);
|
|
14740
15035
|
if (!state) return;
|
|
14741
15036
|
this.#timingStreams.delete(streamKey);
|
|
15037
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
14742
15038
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
14743
15039
|
if (this.#pendingTimingSamples < 0) {
|
|
14744
15040
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -17944,6 +18240,16 @@ function keyframeOverlaySsId(header) {
|
|
|
17944
18240
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
17945
18241
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
17946
18242
|
}
|
|
18243
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
18244
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
18245
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
18246
|
+
}
|
|
18247
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
18248
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
18249
|
+
return headerPsn + offset >>> 0;
|
|
18250
|
+
}
|
|
18251
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
18252
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
17947
18253
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
17948
18254
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
17949
18255
|
function assertStreamIdentity(role, stream) {
|
|
@@ -18055,6 +18361,7 @@ var MmtpRouter = class {
|
|
|
18055
18361
|
#provenance = /* @__PURE__ */ new Map();
|
|
18056
18362
|
#onFlowProtected = null;
|
|
18057
18363
|
#unadmittedListedDrops = 0;
|
|
18364
|
+
#admittedSourceDatagrams = 0;
|
|
18058
18365
|
#unlistedRepairSetAside = 0;
|
|
18059
18366
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
18060
18367
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -18111,6 +18418,15 @@ var MmtpRouter = class {
|
|
|
18111
18418
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
18112
18419
|
*/
|
|
18113
18420
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
18421
|
+
/**
|
|
18422
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
18423
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
18424
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
18425
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
18426
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
18427
|
+
* an MPU no member of which arrived).
|
|
18428
|
+
*/
|
|
18429
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
18114
18430
|
/**
|
|
18115
18431
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
18116
18432
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -18180,7 +18496,9 @@ var MmtpRouter = class {
|
|
|
18180
18496
|
recovered: 0,
|
|
18181
18497
|
restarts: 0,
|
|
18182
18498
|
unsequencedDropped: 0,
|
|
18183
|
-
protectedDropped: 0
|
|
18499
|
+
protectedDropped: 0,
|
|
18500
|
+
baseFed: 0,
|
|
18501
|
+
baseUndetermined: 0
|
|
18184
18502
|
};
|
|
18185
18503
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
18186
18504
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -18353,7 +18671,7 @@ var MmtpRouter = class {
|
|
|
18353
18671
|
const cb = this.#getFrameCallback(packetId);
|
|
18354
18672
|
if (isVideo && !assembler) {
|
|
18355
18673
|
if (frameMeta.recovered) {
|
|
18356
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
18674
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
18357
18675
|
} else {
|
|
18358
18676
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
18359
18677
|
}
|
|
@@ -18743,6 +19061,10 @@ var MmtpRouter = class {
|
|
|
18743
19061
|
get keyframeOverlayStats() {
|
|
18744
19062
|
return { ...this.#overlayStats };
|
|
18745
19063
|
}
|
|
19064
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
19065
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19066
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
19067
|
+
}
|
|
18746
19068
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
18747
19069
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
18748
19070
|
}
|
|
@@ -18793,6 +19115,11 @@ var MmtpRouter = class {
|
|
|
18793
19115
|
*/
|
|
18794
19116
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
18795
19117
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
19118
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
19119
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
19120
|
+
stream.overlayToBase.routed.clear();
|
|
19121
|
+
stream.overlayToBase.pending.clear();
|
|
19122
|
+
}
|
|
18796
19123
|
}
|
|
18797
19124
|
#resetOverlay(inst) {
|
|
18798
19125
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
@@ -18849,9 +19176,10 @@ var MmtpRouter = class {
|
|
|
18849
19176
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
18850
19177
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
18851
19178
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
18852
|
-
* included (7.1, 7.4.3).
|
|
19179
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
19180
|
+
* FEC reads it.
|
|
18853
19181
|
*/
|
|
18854
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
19182
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
18855
19183
|
if (this.#overlays.size === 0) return;
|
|
18856
19184
|
const ssId = keyframeOverlaySsId(header);
|
|
18857
19185
|
if (ssId === void 0) return;
|
|
@@ -18860,7 +19188,11 @@ var MmtpRouter = class {
|
|
|
18860
19188
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
18861
19189
|
if (identity !== null) {
|
|
18862
19190
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
18863
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
19191
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
19192
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
19193
|
+
overlaySsId: ssId,
|
|
19194
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
19195
|
+
});
|
|
18864
19196
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
18865
19197
|
}
|
|
18866
19198
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -18939,14 +19271,27 @@ var MmtpRouter = class {
|
|
|
18939
19271
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
18940
19272
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
18941
19273
|
const restored = /* @__PURE__ */ new Set();
|
|
19274
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
18942
19275
|
const symbols = result.symbols.flatMap((sym) => {
|
|
18943
|
-
const
|
|
18944
|
-
if (
|
|
18945
|
-
const
|
|
19276
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
19277
|
+
if (anchor !== void 0) {
|
|
19278
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
19279
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
18946
19280
|
restored.add(packetSequenceNumber);
|
|
18947
|
-
|
|
19281
|
+
const out = { ...sym, packetSequenceNumber };
|
|
19282
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
19283
|
+
if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
|
|
19284
|
+
return [out];
|
|
19285
|
+
}
|
|
19286
|
+
if (!restamped) {
|
|
19287
|
+
const baseSsId = unstampedBaseSsId(
|
|
19288
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
19289
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
19290
|
+
sym.packetSequenceNumber
|
|
19291
|
+
);
|
|
19292
|
+
if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
|
|
19293
|
+
return [sym];
|
|
18948
19294
|
}
|
|
18949
|
-
if (!restamped) return [sym];
|
|
18950
19295
|
this.#overlayStats.unsequencedDropped++;
|
|
18951
19296
|
return [];
|
|
18952
19297
|
});
|
|
@@ -18955,6 +19300,85 @@ var MmtpRouter = class {
|
|
|
18955
19300
|
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
18956
19301
|
});
|
|
18957
19302
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
19303
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
19304
|
+
}
|
|
19305
|
+
/**
|
|
19306
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
19307
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
19308
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
19309
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
19310
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
19311
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
19312
|
+
* A base block no repair has stated yet takes it when one does
|
|
19313
|
+
* (#feedPendingRebuilt).
|
|
19314
|
+
*
|
|
19315
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
19316
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
19317
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
19318
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
19319
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
19320
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
19321
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
19322
|
+
* block's own arrivals cannot show.
|
|
19323
|
+
*/
|
|
19324
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
19325
|
+
const fec = this.#fecManager;
|
|
19326
|
+
const owner = inst.leg.sourcePacketId;
|
|
19327
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
19328
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
19329
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
19330
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
19331
|
+
const feeds = [];
|
|
19332
|
+
for (const sym of symbols) {
|
|
19333
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
19334
|
+
const ssId = baseSsIds.get(sym);
|
|
19335
|
+
if (ssId === void 0) {
|
|
19336
|
+
this.#overlayStats.baseUndetermined++;
|
|
19337
|
+
continue;
|
|
19338
|
+
}
|
|
19339
|
+
routed.delete(ssId);
|
|
19340
|
+
routed.add(ssId);
|
|
19341
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
19342
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
19343
|
+
}
|
|
19344
|
+
for (const [ssId, symbolData] of feeds) {
|
|
19345
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
19346
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
19347
|
+
}
|
|
19348
|
+
}
|
|
19349
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
19350
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
19351
|
+
const fec = this.#fecManager;
|
|
19352
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
19353
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
19354
|
+
if (coords == null) {
|
|
19355
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
19356
|
+
pending.delete(ssId);
|
|
19357
|
+
pending.set(ssId, symbolData.slice());
|
|
19358
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
19359
|
+
return;
|
|
19360
|
+
}
|
|
19361
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
19362
|
+
if (!fed.taken) return;
|
|
19363
|
+
this.#overlayStats.baseFed++;
|
|
19364
|
+
if (fed.recovered === null) return;
|
|
19365
|
+
const sbn = coords.sbn;
|
|
19366
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
19367
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
19368
|
+
fec.countRecovery(owner, sbn, name);
|
|
19369
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
19370
|
+
});
|
|
19371
|
+
}
|
|
19372
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
19373
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
19374
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
19375
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
19376
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
19377
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
19378
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
19379
|
+
pending.delete(ssId);
|
|
19380
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
19381
|
+
}
|
|
18958
19382
|
}
|
|
18959
19383
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
18960
19384
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -19094,6 +19518,10 @@ var MmtpRouter = class {
|
|
|
19094
19518
|
get unadmittedListedDropCount() {
|
|
19095
19519
|
return this.#unadmittedListedDrops;
|
|
19096
19520
|
}
|
|
19521
|
+
/** 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. */
|
|
19522
|
+
get admittedSourceDatagramCount() {
|
|
19523
|
+
return this.#admittedSourceDatagrams;
|
|
19524
|
+
}
|
|
19097
19525
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
19098
19526
|
get unlistedRepairSetAsideCount() {
|
|
19099
19527
|
return this.#unlistedRepairSetAside;
|
|
@@ -19580,6 +20008,8 @@ var MmtpRouter = class {
|
|
|
19580
20008
|
)) return;
|
|
19581
20009
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
19582
20010
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
20011
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
20012
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
19583
20013
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
19584
20014
|
const recovered = fec.feedRepairSymbol(
|
|
19585
20015
|
ownerPacketId,
|
|
@@ -19717,6 +20147,7 @@ var MmtpRouter = class {
|
|
|
19717
20147
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
19718
20148
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
19719
20149
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
20150
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
19720
20151
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
19721
20152
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
19722
20153
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -20351,6 +20782,7 @@ var MmtpRouter = class {
|
|
|
20351
20782
|
const parsed = this.#parser.parsePacket(packet);
|
|
20352
20783
|
if (!parsed) return;
|
|
20353
20784
|
const info = parsed.info;
|
|
20785
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
20354
20786
|
if (source === "multicast" && !manifestAdmitted) {
|
|
20355
20787
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
20356
20788
|
}
|
|
@@ -20632,7 +21064,8 @@ var MmtpRouter = class {
|
|
|
20632
21064
|
parsed.header,
|
|
20633
21065
|
info.sequenceNumber,
|
|
20634
21066
|
info.rapFlag,
|
|
20635
|
-
source
|
|
21067
|
+
source,
|
|
21068
|
+
fecPayloadId?.ssId
|
|
20636
21069
|
);
|
|
20637
21070
|
}
|
|
20638
21071
|
}
|
|
@@ -20782,7 +21215,8 @@ var MmtpRouter = class {
|
|
|
20782
21215
|
if (stream === void 0) {
|
|
20783
21216
|
stream = {
|
|
20784
21217
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
20785
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
21218
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
21219
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
20786
21220
|
};
|
|
20787
21221
|
byName.set(catalogTrackName, stream);
|
|
20788
21222
|
}
|
|
@@ -20897,8 +21331,16 @@ var MmtpRouter = class {
|
|
|
20897
21331
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
20898
21332
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
20899
21333
|
}
|
|
20900
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
21334
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
20901
21335
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
21336
|
+
if (this.#isFecTrack(packetId)) {
|
|
21337
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
21338
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
21339
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
21340
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
21341
|
+
return;
|
|
21342
|
+
}
|
|
21343
|
+
}
|
|
20902
21344
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
20903
21345
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
20904
21346
|
this.#drainDirectRecoveredVideo(
|
|
@@ -21052,6 +21494,13 @@ var MmtpRouter = class {
|
|
|
21052
21494
|
* frames by epoch only if pre-protection input ever matters.
|
|
21053
21495
|
*/
|
|
21054
21496
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
21497
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
21498
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
21499
|
+
result = {
|
|
21500
|
+
...result,
|
|
21501
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
21502
|
+
};
|
|
21503
|
+
}
|
|
21055
21504
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
21056
21505
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
21057
21506
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -21116,7 +21565,10 @@ var MmtpRouter = class {
|
|
|
21116
21565
|
...result,
|
|
21117
21566
|
symbols: result.symbols.map((sym) => {
|
|
21118
21567
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
21119
|
-
if (wire === sym.packetSequenceNumber)
|
|
21568
|
+
if (wire === sym.packetSequenceNumber) {
|
|
21569
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
21570
|
+
return sym;
|
|
21571
|
+
}
|
|
21120
21572
|
this.#restampedRecoveredSeqs++;
|
|
21121
21573
|
this.#restampedStreams.add(streamKey);
|
|
21122
21574
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -21152,6 +21604,29 @@ var MmtpRouter = class {
|
|
|
21152
21604
|
}
|
|
21153
21605
|
);
|
|
21154
21606
|
}
|
|
21607
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
21608
|
+
ownerPacketId,
|
|
21609
|
+
sym.fragment.mpuSequenceNumber,
|
|
21610
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
21611
|
+
sym.fragment.sampleNumber,
|
|
21612
|
+
catalogTrackName
|
|
21613
|
+
);
|
|
21614
|
+
for (const sym of result.symbols) {
|
|
21615
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
21616
|
+
this.#feedViaParser(
|
|
21617
|
+
sym.rawSymbol,
|
|
21618
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
21619
|
+
void 0,
|
|
21620
|
+
{
|
|
21621
|
+
skipFecPayloadId: true,
|
|
21622
|
+
recovered: true,
|
|
21623
|
+
deliveryProvenance,
|
|
21624
|
+
...fecSbnOf(sym.index),
|
|
21625
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
21626
|
+
}
|
|
21627
|
+
);
|
|
21628
|
+
}
|
|
21629
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
21155
21630
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
21156
21631
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
21157
21632
|
if (assembler) {
|