@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0
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 +2 -2
- package/dist/browser-runtime.js +3769 -364
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -1
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +18 -2
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +21 -16
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +197 -80
- package/dist/fec-manager.js.map +3 -3
- package/dist/held-layer-admission.d.ts +58 -0
- package/dist/held-layer-admission.d.ts.map +1 -0
- package/dist/held-layer-admission.js +105 -0
- package/dist/held-layer-admission.js.map +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +10 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +31 -1
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2713 -1024
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +31 -1
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +36 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +114 -5
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +96 -29
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +522 -205
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.d.ts +10 -2
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +422 -22
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-manager.d.ts +78 -11
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3663 -333
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +4 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +20 -3
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.d.ts +1 -2
- package/dist/no-modules.d.ts.map +1 -1
- package/dist/no-modules.js +2699 -1008
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +50 -10
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +62 -20
- package/dist/repair-fetch.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +31 -1
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +7 -15
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +147 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +6 -9
- package/src/mmtp-container-parser.ts +48 -0
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +637 -274
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +274 -49
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +154 -34
- package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
- package/test-vectors/repair-policy-v1.json +113 -0
- package/dist/manifest-auth-plan.d.ts +0 -31
- package/dist/manifest-auth-plan.d.ts.map +0 -1
- package/dist/manifest-auth-plan.js +0 -426
- package/dist/manifest-auth-plan.js.map +0 -7
- package/dist/source-auth.d.ts +0 -2
- package/dist/source-auth.d.ts.map +0 -1
- package/dist/source-auth.js +0 -3912
- package/dist/source-auth.js.map +0 -7
- package/src/manifest-auth-plan.ts +0 -55
- package/src/source-auth.ts +0 -1
- package/test-vectors/generate-source-auth-fixture.mjs +0 -114
- package/test-vectors/source-auth-v1.json +0 -61
package/dist/mmtp-router.js
CHANGED
|
@@ -6573,6 +6573,9 @@ function authoredGap(before, after) {
|
|
|
6573
6573
|
if (!a || !b || a.mediaTimescale !== b.mediaTimescale) return false;
|
|
6574
6574
|
return b.decodeTime > a.decodeTime + BigInt(a.durationTicks);
|
|
6575
6575
|
}
|
|
6576
|
+
function ssIdAfter(a, b) {
|
|
6577
|
+
return (a - b | 0) > 0;
|
|
6578
|
+
}
|
|
6576
6579
|
var FecReleaseSequencer = class {
|
|
6577
6580
|
#streams = /* @__PURE__ */ new Map();
|
|
6578
6581
|
#gate;
|
|
@@ -6634,11 +6637,17 @@ var FecReleaseSequencer = class {
|
|
|
6634
6637
|
stream.last = null;
|
|
6635
6638
|
stream.lastSbn = void 0;
|
|
6636
6639
|
stream.discontinuity = false;
|
|
6640
|
+
stream.rapSsId = void 0;
|
|
6637
6641
|
}
|
|
6638
6642
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
6639
6643
|
this.#recordStat("droppedStale");
|
|
6640
6644
|
return;
|
|
6641
6645
|
}
|
|
6646
|
+
const ssId = meta.fecSbn;
|
|
6647
|
+
if (meta.trackType === "video" && ssId !== void 0 && this.#gate.isRap?.(data, meta) === true) {
|
|
6648
|
+
if (stream.rapSsId === void 0 || ssIdAfter(ssId, stream.rapSsId)) stream.rapSsId = ssId;
|
|
6649
|
+
this.#gate.onRap?.(stream.packetId, stream.catalogTrackName, ssId);
|
|
6650
|
+
}
|
|
6642
6651
|
if (stream.last === null) {
|
|
6643
6652
|
if (meta.trackType === "video" && !meta.keyframe) {
|
|
6644
6653
|
this.#recordStat("droppedBeforeStartupKeyframe");
|
|
@@ -6674,6 +6683,22 @@ var FecReleaseSequencer = class {
|
|
|
6674
6683
|
}
|
|
6675
6684
|
return earliest;
|
|
6676
6685
|
}
|
|
6686
|
+
/**
|
|
6687
|
+
* Whether a complete RAP above SS_ID `ssId` entered the gate of a stream of
|
|
6688
|
+
* `packetId` (draft-ramadan-moq-fec 11.4.2: repair of a block below it is
|
|
6689
|
+
* moot). Its SS_ID is the one its frame completed in; a frame's packets
|
|
6690
|
+
* hold consecutive SS_IDs in authored order, so a complete RAP with a
|
|
6691
|
+
* packet above `ssId` has every packet of it at or above the first SS_ID
|
|
6692
|
+
* any loss at or below `ssId` could have taken.
|
|
6693
|
+
*/
|
|
6694
|
+
rapAbove(packetId, catalogTrackName, ssId) {
|
|
6695
|
+
for (const stream of this.#streams.values()) {
|
|
6696
|
+
if (stream.packetId !== packetId) continue;
|
|
6697
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
6698
|
+
if (stream.rapSsId !== void 0 && ssIdAfter(stream.rapSsId, ssId)) return true;
|
|
6699
|
+
}
|
|
6700
|
+
return false;
|
|
6701
|
+
}
|
|
6677
6702
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
6678
6703
|
poke(streamKey) {
|
|
6679
6704
|
if (streamKey !== void 0) {
|
|
@@ -6764,7 +6789,8 @@ var FecReleaseSequencer = class {
|
|
|
6764
6789
|
buf: [],
|
|
6765
6790
|
last: null,
|
|
6766
6791
|
lastSbn: void 0,
|
|
6767
|
-
discontinuity: false
|
|
6792
|
+
discontinuity: false,
|
|
6793
|
+
rapSsId: void 0
|
|
6768
6794
|
};
|
|
6769
6795
|
this.#streams.set(streamKey, stream);
|
|
6770
6796
|
}
|
|
@@ -7127,6 +7153,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7127
7153
|
fecReleaseEarliestHeldAtMs(packetId, catalogTrackName) {
|
|
7128
7154
|
return this.#fecSequencer?.earliestHeldArrivalMs(packetId, catalogTrackName);
|
|
7129
7155
|
}
|
|
7156
|
+
/** See FecReleaseSequencer.rapAbove; false when no stream is FEC-gated. */
|
|
7157
|
+
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
7158
|
+
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
7159
|
+
}
|
|
7130
7160
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
7131
7161
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
7132
7162
|
if (!this.#fecSequencer) return;
|
|
@@ -8459,6 +8489,10 @@ var NO_BLOCK_SPAN_HORIZON = Object.freeze({
|
|
|
8459
8489
|
reason: "no-block-span"
|
|
8460
8490
|
});
|
|
8461
8491
|
var DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT = 512;
|
|
8492
|
+
var ASSIST_BLOCK_LIMIT = 64;
|
|
8493
|
+
var ASSIST_FAILURE_LEDGER_LIMIT = 4 * ASSIST_BLOCK_LIMIT;
|
|
8494
|
+
var ASSIST_FAILED_DECODES_MAX = 2;
|
|
8495
|
+
var assistKey = (ownerPacketId, catalogTrackName, blockStart) => `${ownerPacketId}/${catalogTrackName ?? ""}/${blockStart}`;
|
|
8462
8496
|
var DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT = 512;
|
|
8463
8497
|
function sourceSymbolEvidence(packet, header, rapFlag, packetSequenceNumber) {
|
|
8464
8498
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
@@ -8479,11 +8513,27 @@ var MmtpRouter = class {
|
|
|
8479
8513
|
#fecManager;
|
|
8480
8514
|
#defaultFecMapper;
|
|
8481
8515
|
#fecMappersByPacketId = /* @__PURE__ */ new Map();
|
|
8482
|
-
|
|
8483
|
-
#
|
|
8484
|
-
#
|
|
8485
|
-
#
|
|
8486
|
-
#
|
|
8516
|
+
/** Protected flows: one 7.2.7 receiver each, and the ingress its datagrams arrive with. */
|
|
8517
|
+
#provenance = /* @__PURE__ */ new Map();
|
|
8518
|
+
#onFlowProtected = null;
|
|
8519
|
+
#unadmittedListedDrops = 0;
|
|
8520
|
+
#unlistedRepairSetAside = 0;
|
|
8521
|
+
#assistBlocks = /* @__PURE__ */ new Map();
|
|
8522
|
+
#assistFailures = /* @__PURE__ */ new Map();
|
|
8523
|
+
#scratchDecoderFactory = null;
|
|
8524
|
+
#unlistedAssistedRecoveries = 0;
|
|
8525
|
+
#unlistedAssistedRefusals = 0;
|
|
8526
|
+
/** The ingress a held datagram arrived with; the receiver returns the same bytes on release. */
|
|
8527
|
+
#heldIngress = /* @__PURE__ */ new WeakMap();
|
|
8528
|
+
#unicastRepair = false;
|
|
8529
|
+
/** How to route a held unicast (MoQ) datagram once a record admits it. */
|
|
8530
|
+
#heldReplay = /* @__PURE__ */ new WeakMap();
|
|
8531
|
+
/** When a held datagram was received: admitted later, it keeps that receipt time (fec-01 9). */
|
|
8532
|
+
#heldAt = /* @__PURE__ */ new WeakMap();
|
|
8533
|
+
/** The flow whose receiver admitted a stream's source datagrams, by packetId then catalog track name: recovered packets answer to it. */
|
|
8534
|
+
#provenanceStreamFlow = /* @__PURE__ */ new Map();
|
|
8535
|
+
#unregisteredFlowDrops = 0;
|
|
8536
|
+
#ambiguousRecoveredDrops = 0;
|
|
8487
8537
|
// Temp state set by #processMMTP before calling parser.feed(),
|
|
8488
8538
|
// read by the parser's onFrame callback for CMAF assembly + dedup.
|
|
8489
8539
|
#currentFecInfo;
|
|
@@ -8565,6 +8615,7 @@ var MmtpRouter = class {
|
|
|
8565
8615
|
#onBlockFailed = null;
|
|
8566
8616
|
/** Typed observer of every gap decision the release gate makes. */
|
|
8567
8617
|
#onFrameGap;
|
|
8618
|
+
#fecRapObservers = /* @__PURE__ */ new Set();
|
|
8568
8619
|
// Pokes requested while a packet is being processed are deferred to its
|
|
8569
8620
|
// end: a synchronous recovery completes its block BEFORE routing the
|
|
8570
8621
|
// recovered frames, and releasing a held gap in between would strand them.
|
|
@@ -8620,10 +8671,6 @@ var MmtpRouter = class {
|
|
|
8620
8671
|
this.#fecManager = config?.fecManager ?? null;
|
|
8621
8672
|
this.#onFrameGap = config?.onFrameGap ?? null;
|
|
8622
8673
|
this.#defaultFecMapper = config?.fecMapper ?? null;
|
|
8623
|
-
this.#manifestVerifier = config?.manifestVerifier ?? null;
|
|
8624
|
-
this.#ownsManifestVerifier = config?.manifestVerifierOwnership !== "caller";
|
|
8625
|
-
this.#resolveManifestIdentity = config?.resolveManifestIdentity ?? null;
|
|
8626
|
-
this.#resolveManifestIdentityForRoute = config?.resolveManifestIdentityForRoute ?? null;
|
|
8627
8674
|
this.#parser = new MmtpContainerParser({
|
|
8628
8675
|
mfuTimeoutMs: config?.mfuTimeoutMs,
|
|
8629
8676
|
reassemblyPolicy: config?.reassemblyPolicy,
|
|
@@ -9155,28 +9202,309 @@ var MmtpRouter = class {
|
|
|
9155
9202
|
}
|
|
9156
9203
|
});
|
|
9157
9204
|
}
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
this.#
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9205
|
+
/**
|
|
9206
|
+
* Protect a flow (draft-ramadan-moq-multicast-01 7.2.7): every multicast
|
|
9207
|
+
* datagram whose ingress names `ingress.flow`, source and repair alike, is
|
|
9208
|
+
* admitted by `receiver` before anything reads it, and an unadmitted one is
|
|
9209
|
+
* held or dropped. The caller owns the receiver's configuration; the router
|
|
9210
|
+
* releases it on `releaseProvenanceFlow` and on dispose.
|
|
9211
|
+
*/
|
|
9212
|
+
registerProvenanceFlow(ingress, receiver) {
|
|
9213
|
+
this.#provenance.get(ingress.flow)?.receiver.release();
|
|
9214
|
+
this.#provenance.set(ingress.flow, { receiver, ingress });
|
|
9215
|
+
receiver.keepPendingAgeMs = () => this.#oldestOpenBlockAgeMs(ingress.flow);
|
|
9216
|
+
}
|
|
9217
|
+
/**
|
|
9218
|
+
* Declare that this receiver can request unicast repair (a repair FETCH
|
|
9219
|
+
* scheduler is wired): a protected flow then keeps the pending entries of
|
|
9220
|
+
* its open blocks for the repair to consume (7.2.7 step 2).
|
|
9221
|
+
*/
|
|
9222
|
+
setUnicastRepair(enabled) {
|
|
9223
|
+
this.#unicastRepair = enabled;
|
|
9224
|
+
}
|
|
9225
|
+
#oldestOpenBlockAgeMs(flow) {
|
|
9226
|
+
const entry = this.#provenance.get(flow);
|
|
9227
|
+
if (!this.#unicastRepair || !entry) return void 0;
|
|
9228
|
+
let age;
|
|
9229
|
+
for (const { name, packetId } of entry.ingress.tracks) {
|
|
9230
|
+
const a = name ? this.#fecManager?.oldestOpenBlockAgeMs?.(packetId, name) : void 0;
|
|
9231
|
+
if (a !== void 0 && (age === void 0 || a > age)) age = a;
|
|
9232
|
+
}
|
|
9233
|
+
return age;
|
|
9234
|
+
}
|
|
9235
|
+
/**
|
|
9236
|
+
* Replace a registered flow's ingress (a rendition switch staged, committed
|
|
9237
|
+
* or cancelled) while keeping its receiver; a no-op for an unregistered flow.
|
|
9238
|
+
*/
|
|
9239
|
+
updateProvenanceFlowIngress(ingress) {
|
|
9240
|
+
const entry = this.#provenance.get(ingress.flow);
|
|
9241
|
+
if (!entry) return;
|
|
9242
|
+
const added = entry.protected ? ingress.tracks.filter(({ name, packetId }) => name && !entry.ingress.tracks.some((t) => t.name === name && t.packetId === packetId)) : [];
|
|
9243
|
+
const governed = added.map(({ packetId }) => [...this.#repairOwnersByPacketId.get(packetId) ?? [packetId]].some((owner) => this.#governs(owner)));
|
|
9244
|
+
this.#provenance.set(ingress.flow, { ...entry, ingress });
|
|
9245
|
+
added.forEach(({ name, packetId }, i) => {
|
|
9246
|
+
if (governed[i]) for (const block of this.#assistBlocks.values()) block.pooled.clear();
|
|
9247
|
+
else this.#fecManager?.resetTrack?.(packetId, name);
|
|
9248
|
+
});
|
|
9249
|
+
}
|
|
9250
|
+
/**
|
|
9251
|
+
* Called once per protection of a flow: at the admission that makes this
|
|
9252
|
+
* router's consumers protected under it (7.2.7), after their unadmitted
|
|
9253
|
+
* holdings of its listed tracks are discarded. Again after an unprotect.
|
|
9254
|
+
*/
|
|
9255
|
+
onProvenanceFlowProtected(cb) {
|
|
9256
|
+
this.#onFlowProtected = cb;
|
|
9257
|
+
}
|
|
9258
|
+
/**
|
|
9259
|
+
* The receiver ceased to hold the flow's endpoint on failover (7.2.7): drop
|
|
9260
|
+
* its state and hold queues (no expiry counted) and un-protect its tracks,
|
|
9261
|
+
* which then take MoQ again. The registration stays, so multicast resuming
|
|
9262
|
+
* protects the flow anew at its next admission.
|
|
9263
|
+
*/
|
|
9264
|
+
unprotectProvenanceFlow(flow) {
|
|
9265
|
+
const entry = this.#provenance.get(flow);
|
|
9266
|
+
if (!entry) return;
|
|
9267
|
+
entry.receiver.release();
|
|
9268
|
+
entry.protected = false;
|
|
9269
|
+
this.#assistBlocks.clear();
|
|
9270
|
+
this.#assistFailures.clear();
|
|
9271
|
+
}
|
|
9272
|
+
/** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
|
|
9273
|
+
get unadmittedListedDropCount() {
|
|
9274
|
+
return this.#unadmittedListedDrops;
|
|
9275
|
+
}
|
|
9276
|
+
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
9277
|
+
get unlistedRepairSetAsideCount() {
|
|
9278
|
+
return this.#unlistedRepairSetAside;
|
|
9279
|
+
}
|
|
9280
|
+
/** Governed blocks recovered with the help of unlisted repair (7.2.7 step 2). */
|
|
9281
|
+
get unlistedAssistedRecoveryCount() {
|
|
9282
|
+
return this.#unlistedAssistedRecoveries;
|
|
9283
|
+
}
|
|
9284
|
+
/** Unlisted-assisted decodes refused because two already failed for the block (7.2.7 step 2). */
|
|
9285
|
+
get unlistedAssistedRefusalCount() {
|
|
9286
|
+
return this.#unlistedAssistedRefusals;
|
|
9287
|
+
}
|
|
9288
|
+
/** Unlisted repair symbols pooled now, across every governed block (7.2.7 step 2). */
|
|
9289
|
+
get unlistedPoolSymbolCount() {
|
|
9290
|
+
let n = 0;
|
|
9291
|
+
for (const block of this.#assistBlocks.values()) n += block.pooled.size;
|
|
9292
|
+
return n;
|
|
9293
|
+
}
|
|
9294
|
+
/**
|
|
9295
|
+
* Wire the decoder an unlisted-assisted decode runs in: a fresh, configured
|
|
9296
|
+
* decoder of the owner's track, used once and discarded, never the live one.
|
|
9297
|
+
* Without it unlisted repair on a governed instance is set aside and dropped.
|
|
9298
|
+
*/
|
|
9299
|
+
setScratchFecDecoderFactory(factory) {
|
|
9300
|
+
this.#scratchDecoderFactory = factory;
|
|
9301
|
+
if (!factory) {
|
|
9302
|
+
this.#assistBlocks.clear();
|
|
9303
|
+
this.#assistFailures.clear();
|
|
9304
|
+
}
|
|
9305
|
+
}
|
|
9306
|
+
/** Whether a protected flow lists `ownerPacketId`: its FEC instance is governed (7.2.7). */
|
|
9307
|
+
#governs(ownerPacketId) {
|
|
9308
|
+
for (const entry of this.#provenance.values()) {
|
|
9309
|
+
if (entry.protected && ingressHasPacketId(entry.ingress, ownerPacketId)) return true;
|
|
9310
|
+
}
|
|
9311
|
+
return false;
|
|
9312
|
+
}
|
|
9313
|
+
/**
|
|
9314
|
+
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
9315
|
+
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
9316
|
+
* symbols, so the symbol is set aside: pooled for an unlisted-assisted
|
|
9317
|
+
* decode of a block admitted repair already delimits, else dropped. It
|
|
9318
|
+
* never states a block, so the live locator and decoder are unchanged.
|
|
9319
|
+
* Un-listing needs no hook: an unprotected or released flow lists nothing.
|
|
9320
|
+
*/
|
|
9321
|
+
#setAsideUnlistedRepair(ownerPacketId, catalogTrackName, repair) {
|
|
9322
|
+
if (!this.#governs(ownerPacketId)) return false;
|
|
9323
|
+
this.#unlistedRepairSetAside++;
|
|
9324
|
+
const key = assistKey(ownerPacketId, catalogTrackName, repair.ssStart);
|
|
9325
|
+
const block = this.#assistBlocks.get(key);
|
|
9326
|
+
if (block !== void 0 && block.ssbLength === repair.ssbLength && !block.repairs.has(repair.rsId) && block.pooled.size < block.ssbLength && repair.symbolData.byteLength === this.#symbolSizeForPacket(ownerPacketId) && this.#fecManager?.admitRepairBlockLength?.(ownerPacketId, repair.ssbLength, repair.rsbLength, repair.rsId) !== false) {
|
|
9327
|
+
block.pooled.set(repair.rsId, repair.symbolData);
|
|
9328
|
+
this.#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, repair.ssStart);
|
|
9329
|
+
}
|
|
9330
|
+
return true;
|
|
9331
|
+
}
|
|
9332
|
+
/**
|
|
9333
|
+
* An admitted repair placed a governed block (its SSB_length delimits it):
|
|
9334
|
+
* open the block's record, which the symbols its live decoder takes join.
|
|
9335
|
+
*/
|
|
9336
|
+
#openAssistBlock(ownerPacketId, catalogTrackName, blockStart, ssbLength) {
|
|
9337
|
+
const key = assistKey(ownerPacketId, catalogTrackName, blockStart);
|
|
9338
|
+
if (!this.#scratchDecoderFactory || this.#assistBlocks.has(key) || !this.#governs(ownerPacketId)) return;
|
|
9339
|
+
this.#assistBlocks.set(key, { ssbLength, sources: /* @__PURE__ */ new Map(), repairs: /* @__PURE__ */ new Map(), pooled: /* @__PURE__ */ new Map() });
|
|
9340
|
+
for (const oldest of this.#assistBlocks.keys()) {
|
|
9341
|
+
if (this.#assistBlocks.size <= ASSIST_BLOCK_LIMIT) break;
|
|
9342
|
+
this.#assistBlocks.delete(oldest);
|
|
9343
|
+
}
|
|
9344
|
+
}
|
|
9345
|
+
/** A symbol fed to an open governed block's live decoder that did not recover it. */
|
|
9346
|
+
#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, symbol, data) {
|
|
9347
|
+
const key = assistKey(ownerPacketId, catalogTrackName, blockStart);
|
|
9348
|
+
const block = this.#assistBlocks.get(key);
|
|
9349
|
+
if (block === void 0) return;
|
|
9350
|
+
if ("rsId" in symbol) {
|
|
9351
|
+
block.repairs.set(symbol.rsId, data);
|
|
9352
|
+
block.pooled.delete(symbol.rsId);
|
|
9353
|
+
} else {
|
|
9354
|
+
block.sources.set(symbol.esi, data);
|
|
9355
|
+
}
|
|
9356
|
+
this.#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, blockStart);
|
|
9357
|
+
}
|
|
9358
|
+
/**
|
|
9359
|
+
* 7.2.7 step 2, phase two: a governed block its admitted symbols cannot
|
|
9360
|
+
* decode (the live decoder already had them all) is decoded in a scratch
|
|
9361
|
+
* decoder from its admitted symbols plus pooled unlisted repair, only as
|
|
9362
|
+
* many as close the shortfall. What it recovers is released only through
|
|
9363
|
+
* admission (#routeRecoveredBlock), so the live decoder is never touched and
|
|
9364
|
+
* a refused recovery leaves the block to fail at its FEC deadline. At most
|
|
9365
|
+
* two such decodes may fail per block; a third is refused.
|
|
9366
|
+
*/
|
|
9367
|
+
#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, blockStart) {
|
|
9368
|
+
const fec = this.#fecManager;
|
|
9369
|
+
const admitted = block.sources.size + block.repairs.size;
|
|
9370
|
+
if (!fec || block.pooled.size === 0) return;
|
|
9371
|
+
if (admitted >= block.ssbLength || admitted + block.pooled.size < block.ssbLength) return;
|
|
9372
|
+
if (fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName)) {
|
|
9373
|
+
this.#assistBlocks.delete(key);
|
|
9374
|
+
return;
|
|
9375
|
+
}
|
|
9376
|
+
if ((this.#assistFailures.get(key) ?? 0) >= ASSIST_FAILED_DECODES_MAX) {
|
|
9377
|
+
this.#unlistedAssistedRefusals++;
|
|
9378
|
+
return;
|
|
9379
|
+
}
|
|
9380
|
+
const decoder = this.#scratchDecoderFactory?.(ownerPacketId, catalogTrackName);
|
|
9381
|
+
if (!decoder) return;
|
|
9382
|
+
let recovered = null;
|
|
9383
|
+
const fed = [];
|
|
9384
|
+
try {
|
|
9385
|
+
for (const [esi, data] of block.sources) decoder.add_source(serial32Add(blockStart, esi), data, 0);
|
|
9386
|
+
for (const [rsId, data] of block.repairs) {
|
|
9387
|
+
recovered ??= decoder.add_repair(blockStart, block.ssbLength, rsId, data, 0);
|
|
9177
9388
|
}
|
|
9178
|
-
|
|
9389
|
+
for (const [rsId, data] of block.pooled) {
|
|
9390
|
+
if (recovered || fed.length >= block.ssbLength - admitted) break;
|
|
9391
|
+
fed.push(rsId);
|
|
9392
|
+
recovered = decoder.add_repair(blockStart, block.ssbLength, rsId, data, 0);
|
|
9393
|
+
}
|
|
9394
|
+
} catch {
|
|
9395
|
+
recovered = null;
|
|
9396
|
+
} finally {
|
|
9397
|
+
decoder.free?.();
|
|
9398
|
+
}
|
|
9399
|
+
if (recovered && this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
|
|
9400
|
+
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
9401
|
+
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
9402
|
+
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
9403
|
+
})) {
|
|
9404
|
+
this.#unlistedAssistedRecoveries++;
|
|
9405
|
+
this.#assistBlocks.delete(key);
|
|
9406
|
+
return;
|
|
9407
|
+
}
|
|
9408
|
+
for (const rsId of fed) block.pooled.delete(rsId);
|
|
9409
|
+
const failed = (this.#assistFailures.get(key) ?? 0) + 1;
|
|
9410
|
+
this.#assistFailures.delete(key);
|
|
9411
|
+
this.#assistFailures.set(key, failed);
|
|
9412
|
+
for (const oldest of this.#assistFailures.keys()) {
|
|
9413
|
+
if (this.#assistFailures.size <= ASSIST_FAILURE_LEDGER_LIMIT) break;
|
|
9414
|
+
this.#assistFailures.delete(oldest);
|
|
9415
|
+
}
|
|
9416
|
+
}
|
|
9417
|
+
/**
|
|
9418
|
+
* The admission that makes this router protected under `flow` (7.2.7):
|
|
9419
|
+
* its FEC instances discard what they hold of the flow's listed tracks so
|
|
9420
|
+
* no unadmitted symbol stands for an admitted one, then notify. Before it
|
|
9421
|
+
* nothing of the flow was admitted, so every such symbol is unadmitted.
|
|
9422
|
+
* The playback parse ledger is kept: it records MoQ copies already played,
|
|
9423
|
+
* not holdings, and clearing it re-emits that media (audio twice).
|
|
9424
|
+
* The FEC reset is the whole 7.2.7 discard here: no keyframe-overlay
|
|
9425
|
+
* instance exists to hold anything (repair-layers.ts is the catalog
|
|
9426
|
+
* contract only, no decoder), and packet_id 0 is never a listed track
|
|
9427
|
+
* (`ingressHasPacketId`), so nothing of it is held for the flow.
|
|
9428
|
+
*/
|
|
9429
|
+
#noteFlowAdmitted(flow) {
|
|
9430
|
+
const entry = this.#provenance.get(flow);
|
|
9431
|
+
if (!entry || entry.protected) return;
|
|
9432
|
+
entry.protected = true;
|
|
9433
|
+
for (const { name, packetId } of entry.ingress.tracks) {
|
|
9434
|
+
if (name) this.#fecManager?.resetTrack?.(packetId, name);
|
|
9179
9435
|
}
|
|
9436
|
+
this.#onFlowProtected?.(flow);
|
|
9437
|
+
}
|
|
9438
|
+
/**
|
|
9439
|
+
* Admit a MoQ datagram of `packetId` on the unicast path of the protected
|
|
9440
|
+
* flow that lists it (7.2.7). `unlisted`: no protected flow lists it, so it
|
|
9441
|
+
* is read as before. A held datagram is routed by `replay` when a record
|
|
9442
|
+
* admits it; one more than one protected flow lists is dropped. So is an
|
|
9443
|
+
* overlap copy: it never enters FEC accounting, so admitting it would leave
|
|
9444
|
+
* its multicast twin a duplicate and the block one symbol short.
|
|
9445
|
+
*/
|
|
9446
|
+
#admitUnicast(packetId, packet, replay, overlapCopy = false) {
|
|
9447
|
+
let receiver;
|
|
9448
|
+
for (const entry of this.#provenance.values()) {
|
|
9449
|
+
if (!entry.protected || !ingressHasPacketId(entry.ingress, packetId)) continue;
|
|
9450
|
+
if (receiver || overlapCopy) {
|
|
9451
|
+
this.#unadmittedListedDrops++;
|
|
9452
|
+
return "dropped";
|
|
9453
|
+
}
|
|
9454
|
+
receiver = entry.receiver;
|
|
9455
|
+
}
|
|
9456
|
+
if (!receiver) return "unlisted";
|
|
9457
|
+
const { verdict } = receiver.onDatagram(packet, "unicast");
|
|
9458
|
+
if (verdict === "admitted") return "admitted";
|
|
9459
|
+
if (verdict === "held") {
|
|
9460
|
+
this.#heldReplay.set(packet, replay);
|
|
9461
|
+
this.#heldAt.set(packet, performance.now());
|
|
9462
|
+
return "held";
|
|
9463
|
+
}
|
|
9464
|
+
this.#unadmittedListedDrops++;
|
|
9465
|
+
return "dropped";
|
|
9466
|
+
}
|
|
9467
|
+
/**
|
|
9468
|
+
* Feed one bc-provenance record of `flow`: a datagram from `manifestPort`
|
|
9469
|
+
* (`multicast`) or an object of the manifest track (`mirror`). Held
|
|
9470
|
+
* datagrams the record admits are routed before this returns.
|
|
9471
|
+
*/
|
|
9472
|
+
feedProvenanceRecord(flow, record, path) {
|
|
9473
|
+
const entry = this.#provenance.get(flow);
|
|
9474
|
+
if (!entry || this.#disposed) return "unregistered";
|
|
9475
|
+
const result = entry.receiver.onRecord(record, path);
|
|
9476
|
+
if (result.admitted.length > 0) {
|
|
9477
|
+
this.#noteFlowAdmitted(flow);
|
|
9478
|
+
this.#withDeferredFecPokes(() => {
|
|
9479
|
+
for (const datagram of result.admitted) {
|
|
9480
|
+
const route = () => {
|
|
9481
|
+
const replay = this.#heldReplay.get(datagram);
|
|
9482
|
+
if (replay) replay();
|
|
9483
|
+
else this.#processMMTP(datagram, "multicast", true, this.#heldIngress.get(datagram) ?? entry.ingress, false, true);
|
|
9484
|
+
};
|
|
9485
|
+
const heldAt = this.#heldAt.get(datagram);
|
|
9486
|
+
if (heldAt !== void 0 && this.#fecManager?.withReceiptTime) this.#fecManager.withReceiptTime(heldAt, route);
|
|
9487
|
+
else route();
|
|
9488
|
+
}
|
|
9489
|
+
});
|
|
9490
|
+
}
|
|
9491
|
+
return result.verdict;
|
|
9492
|
+
}
|
|
9493
|
+
/** The receiver ceased to hold the flow: its hold queues are dropped, not expired. */
|
|
9494
|
+
releaseProvenanceFlow(flow) {
|
|
9495
|
+
this.#provenance.get(flow)?.receiver.release();
|
|
9496
|
+
this.#provenance.delete(flow);
|
|
9497
|
+
for (const byName of this.#provenanceStreamFlow.values()) {
|
|
9498
|
+
for (const [name, bound] of byName) if (bound === flow) byName.delete(name);
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
/** Multicast datagrams dropped because their ingress names a flow with no registered receiver. */
|
|
9502
|
+
get unregisteredFlowDropCount() {
|
|
9503
|
+
return this.#unregisteredFlowDrops;
|
|
9504
|
+
}
|
|
9505
|
+
/** Recovered packets dropped because more than one protected flow could own their stream. */
|
|
9506
|
+
get ambiguousRecoveredDropCount() {
|
|
9507
|
+
return this.#ambiguousRecoveredDrops;
|
|
9180
9508
|
}
|
|
9181
9509
|
/**
|
|
9182
9510
|
* Main entry point: feed a raw MMTP packet for processing.
|
|
@@ -9192,10 +9520,10 @@ var MmtpRouter = class {
|
|
|
9192
9520
|
* they may never have crossed — and that attribution is what the physical-
|
|
9193
9521
|
* delivery evidence path is built on. Absence is not multicast; say which.
|
|
9194
9522
|
*/
|
|
9195
|
-
feedPacket(packet, source,
|
|
9523
|
+
feedPacket(packet, source, ingress) {
|
|
9196
9524
|
if (this.#disposed) return;
|
|
9197
9525
|
this.#withDeferredFecPokes(() => {
|
|
9198
|
-
this.#processMMTP(packet, source,
|
|
9526
|
+
this.#processMMTP(packet, source, false, ingress);
|
|
9199
9527
|
});
|
|
9200
9528
|
}
|
|
9201
9529
|
/**
|
|
@@ -9230,7 +9558,7 @@ var MmtpRouter = class {
|
|
|
9230
9558
|
feedOverlapCopy(packet, ingress) {
|
|
9231
9559
|
if (this.#disposed) return;
|
|
9232
9560
|
this.#withDeferredFecPokes(() => {
|
|
9233
|
-
this.#processMMTP(packet, "moq",
|
|
9561
|
+
this.#processMMTP(packet, "moq", false, ingress, true);
|
|
9234
9562
|
});
|
|
9235
9563
|
}
|
|
9236
9564
|
/**
|
|
@@ -9243,7 +9571,7 @@ var MmtpRouter = class {
|
|
|
9243
9571
|
this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName);
|
|
9244
9572
|
});
|
|
9245
9573
|
}
|
|
9246
|
-
#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName) {
|
|
9574
|
+
#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, admitted = false) {
|
|
9247
9575
|
let header;
|
|
9248
9576
|
try {
|
|
9249
9577
|
header = parseMMTPHeader(packet);
|
|
@@ -9260,6 +9588,11 @@ var MmtpRouter = class {
|
|
|
9260
9588
|
`[MmtpRouter] Dropping MoQ repair packet pid=${repairPacketId}: owner hint is required`
|
|
9261
9589
|
);
|
|
9262
9590
|
}
|
|
9591
|
+
if (ownerPacketId != null && !admitted) {
|
|
9592
|
+
const admit = this.#admitUnicast(repairPacketId, packet, () => this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, true));
|
|
9593
|
+
if (admit === "held" || admit === "dropped") return;
|
|
9594
|
+
if (admit === "unlisted" && this.#fecManager && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackName, parsed)) return;
|
|
9595
|
+
}
|
|
9263
9596
|
if (ownerPacketId != null && this.#fecManager) {
|
|
9264
9597
|
this.#moqRepairPidCounts[repairPacketId] = (this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
|
|
9265
9598
|
this.#moqRepairOwnerCounts[ownerPacketId] = (this.#moqRepairOwnerCounts[ownerPacketId] || 0) + 1;
|
|
@@ -9378,6 +9711,7 @@ var MmtpRouter = class {
|
|
|
9378
9711
|
const placement = fec.placeIsoRepair(ownerPacketId, catalogTrackName, repair.ssStart, repair.ssbLength);
|
|
9379
9712
|
if (placement.status !== "placed") return;
|
|
9380
9713
|
const blockStart = placement.block.start;
|
|
9714
|
+
this.#openAssistBlock(ownerPacketId, catalogTrackName, blockStart, repair.ssbLength);
|
|
9381
9715
|
if (placement.span !== void 0) {
|
|
9382
9716
|
this.#replayIsoSources(ownerPacketId, catalogTrackName, placement.span.block.start, placement.span.placed);
|
|
9383
9717
|
}
|
|
@@ -9403,10 +9737,13 @@ var MmtpRouter = class {
|
|
|
9403
9737
|
catalogTrackName
|
|
9404
9738
|
);
|
|
9405
9739
|
if (recovered && !alreadyCompleted) {
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9740
|
+
this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
|
|
9741
|
+
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
9742
|
+
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
9743
|
+
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
9744
|
+
});
|
|
9745
|
+
} else if (!recovered) {
|
|
9746
|
+
this.#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, { rsId: repair.rsId }, repair.symbolData);
|
|
9410
9747
|
}
|
|
9411
9748
|
}
|
|
9412
9749
|
/**
|
|
@@ -9446,11 +9783,13 @@ var MmtpRouter = class {
|
|
|
9446
9783
|
if (payload.oversized) continue;
|
|
9447
9784
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
9448
9785
|
const recovered = fec.feedSourceSymbol(ownerPacketId, ssId, payload.symbolData, catalogTrackName);
|
|
9786
|
+
if (!recovered) this.#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, { esi }, payload.symbolData);
|
|
9449
9787
|
if (!recovered || alreadyCompleted) continue;
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9788
|
+
this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
|
|
9789
|
+
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
9790
|
+
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
9791
|
+
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
9792
|
+
});
|
|
9454
9793
|
}
|
|
9455
9794
|
}
|
|
9456
9795
|
#resolveRepairOwnerPacketId(packetId, ingress) {
|
|
@@ -9473,14 +9812,18 @@ var MmtpRouter = class {
|
|
|
9473
9812
|
* Sync WASM decoders return the block's K' = SSB_length fixed-width
|
|
9474
9813
|
* ssbg_mode1 source symbols concatenated. */
|
|
9475
9814
|
routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
|
|
9476
|
-
|
|
9815
|
+
let recovered = false;
|
|
9816
|
+
this.#withDeferredFecPokes(() => {
|
|
9817
|
+
recovered = this.#routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
|
|
9818
|
+
});
|
|
9819
|
+
return recovered;
|
|
9477
9820
|
}
|
|
9478
9821
|
/** Route a single recovered source symbol through the parser + dedup pipeline.
|
|
9479
9822
|
* Worker-backed decoders emit one padded source symbol per frame event. */
|
|
9480
9823
|
routeRecoveredSymbol(blockId, data, packetId, esi = 0, catalogTrackName) {
|
|
9481
9824
|
const blockProfile = this.#blockProfileFor(packetId);
|
|
9482
9825
|
const result = processFecSymbol(data, { ownerPacketId: packetId, index: esi, blockProfile });
|
|
9483
|
-
this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
|
|
9826
|
+
return this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
|
|
9484
9827
|
}
|
|
9485
9828
|
// ── Keyframe timer cancellation ─────────────────────────────────
|
|
9486
9829
|
/** Cancel the active keyframe loss timer.
|
|
@@ -9511,6 +9854,9 @@ var MmtpRouter = class {
|
|
|
9511
9854
|
}
|
|
9512
9855
|
}
|
|
9513
9856
|
this.#parser.resetTrack(packetId, catalogTrackName);
|
|
9857
|
+
const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
|
|
9858
|
+
for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
|
|
9859
|
+
for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
|
|
9514
9860
|
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
9515
9861
|
this.#trackSynced.delete(streamKey);
|
|
9516
9862
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
@@ -9523,7 +9869,6 @@ var MmtpRouter = class {
|
|
|
9523
9869
|
this.#lastDirectSourceAudioCursorByStream.delete(streamKey);
|
|
9524
9870
|
this.#lastDirectEmittedAudioCursorByStream.delete(streamKey);
|
|
9525
9871
|
this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
|
|
9526
|
-
this.#manifestRecoveryQueues.delete(streamKey);
|
|
9527
9872
|
this.#deliveredVideoHighWaterByStream.delete(streamKey);
|
|
9528
9873
|
this.#stream(packetId, catalogTrackName).duplicates.clear();
|
|
9529
9874
|
const assemblersToReset = /* @__PURE__ */ new Set();
|
|
@@ -9573,7 +9918,6 @@ var MmtpRouter = class {
|
|
|
9573
9918
|
this.#lastDirectSourceAudioCursorByStream.delete(key);
|
|
9574
9919
|
this.#lastDirectEmittedAudioCursorByStream.delete(key);
|
|
9575
9920
|
this.#pendingDirectRecoveredAudioByStream.delete(key);
|
|
9576
|
-
this.#manifestRecoveryQueues.delete(key);
|
|
9577
9921
|
this.#trackInitDelivered.delete(key);
|
|
9578
9922
|
this.#parserDiagnostics.delete(key);
|
|
9579
9923
|
this.#pendingFecPokes.delete(key);
|
|
@@ -9834,8 +10178,9 @@ var MmtpRouter = class {
|
|
|
9834
10178
|
this.#parserDiagnostics.clear();
|
|
9835
10179
|
this.#catalogTrackNamesByPacketId.clear();
|
|
9836
10180
|
this.#repairOwnersByPacketId.clear();
|
|
9837
|
-
|
|
9838
|
-
this.#
|
|
10181
|
+
for (const { receiver } of this.#provenance.values()) receiver.release();
|
|
10182
|
+
this.#provenance.clear();
|
|
10183
|
+
this.#provenanceStreamFlow.clear();
|
|
9839
10184
|
for (const assembler of this.#mseAssemblers.values()) assembler.dispose();
|
|
9840
10185
|
this.#mseAssemblers.clear();
|
|
9841
10186
|
}
|
|
@@ -9929,6 +10274,24 @@ var MmtpRouter = class {
|
|
|
9929
10274
|
fecReleaseEarliestHeldAtMs(packetId, catalogTrackName) {
|
|
9930
10275
|
return this.#parser.fecReleaseEarliestHeldAtMs(packetId, catalogTrackName);
|
|
9931
10276
|
}
|
|
10277
|
+
/**
|
|
10278
|
+
* Whether a complete RAP above SS_ID `ssId` entered this stream's FEC
|
|
10279
|
+
* release gate (see FecReleaseSequencer.rapAbove).
|
|
10280
|
+
*/
|
|
10281
|
+
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
10282
|
+
return this.#parser.fecReleaseRapAbove(packetId, catalogTrackName, ssId);
|
|
10283
|
+
}
|
|
10284
|
+
/**
|
|
10285
|
+
* Observe each complete RAP entering an FEC release gate, with the SS_ID
|
|
10286
|
+
* its frame completed in. Called synchronously from the frame push: an
|
|
10287
|
+
* observer that drives FEC state must defer that work.
|
|
10288
|
+
*/
|
|
10289
|
+
addFecRapObserver(observer) {
|
|
10290
|
+
this.#fecRapObservers.add(observer);
|
|
10291
|
+
return () => {
|
|
10292
|
+
this.#fecRapObservers.delete(observer);
|
|
10293
|
+
};
|
|
10294
|
+
}
|
|
9932
10295
|
#requestFecPoke(packetId, catalogTrackName) {
|
|
9933
10296
|
if (this.#disposed) return;
|
|
9934
10297
|
if (this.#fecPokeDepth > 0) {
|
|
@@ -9974,6 +10337,10 @@ var MmtpRouter = class {
|
|
|
9974
10337
|
this.#onFrameGap?.(decision);
|
|
9975
10338
|
return decision;
|
|
9976
10339
|
},
|
|
10340
|
+
isRap: (data, meta) => this.#isRapForGap(data, meta),
|
|
10341
|
+
onRap: (packetId, catalogTrackName, ssId) => {
|
|
10342
|
+
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
10343
|
+
},
|
|
9977
10344
|
onReleased: (data, meta) => {
|
|
9978
10345
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
9979
10346
|
const timing = meta.authoredTiming;
|
|
@@ -10112,7 +10479,7 @@ var MmtpRouter = class {
|
|
|
10112
10479
|
* 1. parseHeader() → extract packetId + fecPayloadId for FEC feeding
|
|
10113
10480
|
* 2. feed() → MFU extraction, reassembly, keyframe detection, emit via callbacks
|
|
10114
10481
|
*/
|
|
10115
|
-
#processMMTP(packet, source,
|
|
10482
|
+
#processMMTP(packet, source, manifestAdmitted = false, ingress, overlapCopy = false, releasedFromQuarantine = false) {
|
|
10116
10483
|
const parsed = this.#parser.parsePacket(packet);
|
|
10117
10484
|
if (!parsed) return;
|
|
10118
10485
|
const info = parsed.info;
|
|
@@ -10137,43 +10504,32 @@ var MmtpRouter = class {
|
|
|
10137
10504
|
}
|
|
10138
10505
|
const fecUnreadable = !info.isRepair && info.fecPayloadId != null && this.#fecManager !== null && this.#classifyPacketId(info.packetId) !== null && this.#configuredBlockProfile(info.packetId) === void 0;
|
|
10139
10506
|
if (fecUnreadable && !manifestAdmitted) this.#noteUnreadableFecPayloadId(info.packetId, "source");
|
|
10140
|
-
if (!info.isRepair &&
|
|
10141
|
-
let
|
|
10142
|
-
if (!
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
recovered: false,
|
|
10151
|
-
sourceSymbolFormat: "full-mmtp-packet"
|
|
10152
|
-
}) : this.#resolveManifestIdentity(
|
|
10153
|
-
info.packetId,
|
|
10154
|
-
coordinates.sbn,
|
|
10155
|
-
coordinates.esi,
|
|
10156
|
-
catalogTrackName
|
|
10157
|
-
)) ?? void 0;
|
|
10158
|
-
}
|
|
10159
|
-
if (identity) {
|
|
10160
|
-
identity = {
|
|
10161
|
-
...identity,
|
|
10162
|
-
packetId: info.packetId,
|
|
10163
|
-
...info.fecPayloadId ? { ssId: info.fecPayloadId.ssId } : {}
|
|
10164
|
-
};
|
|
10165
|
-
const authenticated = info.fecPayloadId ? packet.subarray(0, packet.length - 4) : packet;
|
|
10166
|
-
let quarantined = false;
|
|
10167
|
-
const admission = this.#manifestVerifier.admitObject(identity, authenticated, () => {
|
|
10168
|
-
if (!this.#disposed) {
|
|
10169
|
-
this.#processMMTP(packet, source, identity, true, ingress, overlapCopy, quarantined);
|
|
10170
|
-
}
|
|
10171
|
-
});
|
|
10172
|
-
quarantined = admission.action === "stall";
|
|
10507
|
+
if (source === "multicast" && manifestAdmitted && !info.isRepair && ingress?.flow !== void 0) {
|
|
10508
|
+
let byName = this.#provenanceStreamFlow.get(info.packetId);
|
|
10509
|
+
if (!byName) this.#provenanceStreamFlow.set(info.packetId, byName = /* @__PURE__ */ new Map());
|
|
10510
|
+
byName.set(catalogTrackNameForPacket(ingress, info.packetId), ingress.flow);
|
|
10511
|
+
}
|
|
10512
|
+
if (source === "multicast" && !manifestAdmitted && ingress?.flow !== void 0) {
|
|
10513
|
+
const protectedFlow = this.#provenance.get(ingress.flow);
|
|
10514
|
+
if (!protectedFlow) {
|
|
10515
|
+
this.#unregisteredFlowDrops++;
|
|
10516
|
+
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
10173
10517
|
return;
|
|
10174
10518
|
}
|
|
10175
|
-
|
|
10176
|
-
|
|
10519
|
+
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
10520
|
+
if (verdict === "admitted") {
|
|
10521
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
10522
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
10523
|
+
} else if (verdict === "held") {
|
|
10524
|
+
this.#heldIngress.set(packet, ingress);
|
|
10525
|
+
this.#heldAt.set(packet, performance.now());
|
|
10526
|
+
}
|
|
10527
|
+
return;
|
|
10528
|
+
}
|
|
10529
|
+
if (source === "moq" && !manifestAdmitted) {
|
|
10530
|
+
const admit = this.#admitUnicast(info.packetId, packet, () => this.#processMMTP(packet, source, true, ingress, overlapCopy, true), overlapCopy);
|
|
10531
|
+
if (admit !== "unlisted") {
|
|
10532
|
+
if (admit === "admitted") this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
10177
10533
|
return;
|
|
10178
10534
|
}
|
|
10179
10535
|
}
|
|
@@ -10183,6 +10539,7 @@ var MmtpRouter = class {
|
|
|
10183
10539
|
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
10184
10540
|
if (ownerPacketId != null && this.#fecManager) {
|
|
10185
10541
|
if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
10542
|
+
if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
10186
10543
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
10187
10544
|
const count = this.#repairPidCounts[info.packetId];
|
|
10188
10545
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -10350,36 +10707,31 @@ var MmtpRouter = class {
|
|
|
10350
10707
|
sourceFec.symbolData,
|
|
10351
10708
|
catalogTrackName
|
|
10352
10709
|
);
|
|
10353
|
-
if (recovered) {
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10710
|
+
if (!recovered) {
|
|
10711
|
+
this.#noteAssistAdmitted(
|
|
10712
|
+
info.packetId,
|
|
10713
|
+
catalogTrackName,
|
|
10714
|
+
sourceFec.sbn,
|
|
10715
|
+
{ esi: serial32Distance(sourceFec.sbn, sourceFec.ssId) },
|
|
10716
|
+
sourceFec.symbolData
|
|
10717
|
+
);
|
|
10718
|
+
}
|
|
10719
|
+
if (recovered && !alreadyCompleted) {
|
|
10720
|
+
const fec = this.#fecManager;
|
|
10721
|
+
const sbn = sourceFec.sbn;
|
|
10722
|
+
const packetId = info.packetId;
|
|
10723
|
+
const tracked = this.#classifyPacketId(packetId) !== null;
|
|
10724
|
+
this.#routeRecoveredBlock(sbn, recovered, packetId, catalogTrackName, () => {
|
|
10367
10725
|
if (catalogTrackName === void 0) {
|
|
10368
|
-
|
|
10726
|
+
if (tracked) fec.cancelBlockTimeout(sbn, packetId);
|
|
10727
|
+
fec.countRecovery(packetId, sbn, catalogTrackName);
|
|
10728
|
+
fec.markBlockCompleted(sbn, packetId);
|
|
10369
10729
|
} else {
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
catalogTrackName
|
|
10374
|
-
);
|
|
10730
|
+
if (tracked) fec.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
10731
|
+
fec.countRecovery(packetId, sbn, catalogTrackName);
|
|
10732
|
+
fec.markBlockCompleted(sbn, packetId, catalogTrackName);
|
|
10375
10733
|
}
|
|
10376
|
-
|
|
10377
|
-
sourceFec.sbn,
|
|
10378
|
-
recovered,
|
|
10379
|
-
info.packetId,
|
|
10380
|
-
catalogTrackName
|
|
10381
|
-
);
|
|
10382
|
-
}
|
|
10734
|
+
});
|
|
10383
10735
|
}
|
|
10384
10736
|
}
|
|
10385
10737
|
}
|
|
@@ -10755,7 +11107,7 @@ var MmtpRouter = class {
|
|
|
10755
11107
|
}
|
|
10756
11108
|
// ── Private: FEC recovery routing ───────────────────────────────
|
|
10757
11109
|
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
10758
|
-
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName) {
|
|
11110
|
+
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
|
|
10759
11111
|
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
10760
11112
|
if (ownerPacketId === void 0) {
|
|
10761
11113
|
throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
|
|
@@ -10772,103 +11124,68 @@ var MmtpRouter = class {
|
|
|
10772
11124
|
ownerPacketId,
|
|
10773
11125
|
blockProfile
|
|
10774
11126
|
});
|
|
10775
|
-
this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
11127
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
10776
11128
|
}
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
11129
|
+
/**
|
|
11130
|
+
* A recovered source packet of a protected flow is admitted on its own
|
|
11131
|
+
* digest: its bytes with the SS_ID it was sent with (SS_Start + ESI)
|
|
11132
|
+
* appended are the datagram the record listed (7.2.7). One that no record
|
|
11133
|
+
* lists is dropped; holding it would let it out later as a multicast arrival.
|
|
11134
|
+
* Returns whether the decode recovered the block: every packet it yields
|
|
11135
|
+
* that this router has not parsed already is admitted. A block one of whose
|
|
11136
|
+
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
11137
|
+
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
11138
|
+
* outside a protected flow.
|
|
11139
|
+
*/
|
|
11140
|
+
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
|
|
11141
|
+
const receiver = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
11142
|
+
let recovered = true;
|
|
11143
|
+
if (receiver) {
|
|
11144
|
+
const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
|
|
11145
|
+
result = {
|
|
11146
|
+
...result,
|
|
11147
|
+
symbols: result.symbols.filter((sym) => {
|
|
11148
|
+
const ssId = this.#fecAnchorForRecovered(blockId, sym.index);
|
|
11149
|
+
if (parsed.has(ssId)) return false;
|
|
11150
|
+
let admitted = false;
|
|
11151
|
+
if (receiver === "ambiguous") {
|
|
11152
|
+
this.#ambiguousRecoveredDrops++;
|
|
11153
|
+
} else {
|
|
11154
|
+
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
11155
|
+
wire.set(sym.rawSymbol);
|
|
11156
|
+
new DataView(wire.buffer).setUint32(sym.rawSymbol.byteLength, ssId);
|
|
11157
|
+
admitted = receiver.admitRecovered(wire);
|
|
11158
|
+
}
|
|
11159
|
+
if (!admitted) recovered = false;
|
|
11160
|
+
return admitted;
|
|
11161
|
+
})
|
|
11162
|
+
};
|
|
10786
11163
|
}
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
11164
|
+
if (recovered) complete?.();
|
|
11165
|
+
this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
11166
|
+
return recovered;
|
|
11167
|
+
}
|
|
11168
|
+
/**
|
|
11169
|
+
* The receiver a recovered packet of `packetId` answers to: the flow that
|
|
11170
|
+
* admitted the stream's source datagrams. Before any was admitted, the one
|
|
11171
|
+
* flow whose registration lists the stream; more than one (a repair flow
|
|
11172
|
+
* also lists its owners' source tracks) cannot be told apart, so none.
|
|
11173
|
+
*/
|
|
11174
|
+
#provenanceFor(packetId, catalogTrackName) {
|
|
11175
|
+
const byName = this.#provenanceStreamFlow.get(packetId);
|
|
11176
|
+
const flows = new Set(
|
|
11177
|
+
catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
|
|
11178
|
+
);
|
|
11179
|
+
if (flows.size > 1) return "ambiguous";
|
|
11180
|
+
for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
|
|
11181
|
+
let match;
|
|
11182
|
+
for (const { receiver, ingress } of this.#provenance.values()) {
|
|
11183
|
+
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
11184
|
+
if (match) return "ambiguous";
|
|
11185
|
+
match = receiver;
|
|
10797
11186
|
}
|
|
10798
|
-
this.#admitRecoveredResult(
|
|
10799
|
-
next.blockId,
|
|
10800
|
-
next.result,
|
|
10801
|
-
next.ownerPacketId,
|
|
10802
|
-
next.catalogTrackName,
|
|
10803
|
-
() => {
|
|
10804
|
-
queue.shift();
|
|
10805
|
-
drain();
|
|
10806
|
-
}
|
|
10807
|
-
);
|
|
10808
|
-
};
|
|
10809
|
-
drain();
|
|
10810
|
-
}
|
|
10811
|
-
#admitRecoveredResult(blockId, result, ownerPacketId, catalogTrackName, done) {
|
|
10812
|
-
const verifier = this.#manifestVerifier;
|
|
10813
|
-
const resolveIdentity = this.#resolveManifestIdentity;
|
|
10814
|
-
const resolveIdentityForRoute = this.#resolveManifestIdentityForRoute;
|
|
10815
|
-
if (!verifier) {
|
|
10816
|
-
this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
10817
|
-
done();
|
|
10818
|
-
return;
|
|
10819
11187
|
}
|
|
10820
|
-
|
|
10821
|
-
const symbols = result.symbols;
|
|
10822
|
-
const admitNext = (index) => {
|
|
10823
|
-
const symbol = symbols[index];
|
|
10824
|
-
if (!symbol) {
|
|
10825
|
-
done();
|
|
10826
|
-
return;
|
|
10827
|
-
}
|
|
10828
|
-
const identity = resolveIdentityForRoute ? resolveIdentityForRoute({
|
|
10829
|
-
packetId: ownerPacketId,
|
|
10830
|
-
sbn: blockId,
|
|
10831
|
-
esi: symbol.index,
|
|
10832
|
-
...catalogTrackName === void 0 ? {} : { catalogTrackName },
|
|
10833
|
-
recovered: true,
|
|
10834
|
-
sourceSymbolFormat: "fec-padded-recovery"
|
|
10835
|
-
}) : resolveIdentity(ownerPacketId, blockId, symbol.index, catalogTrackName);
|
|
10836
|
-
if (!identity) {
|
|
10837
|
-
console.warn(`[MmtpRouter] manifest identity unresolved; recovered symbol is fail-open for packetId=${ownerPacketId} block=${blockId} esi=${symbol.index}`);
|
|
10838
|
-
this.#routeProcessedFecSymbols(
|
|
10839
|
-
blockId,
|
|
10840
|
-
{ ...result, symbols: [symbol] },
|
|
10841
|
-
ownerPacketId,
|
|
10842
|
-
catalogTrackName
|
|
10843
|
-
);
|
|
10844
|
-
admitNext(index + 1);
|
|
10845
|
-
return;
|
|
10846
|
-
}
|
|
10847
|
-
const admission = verifier.admitObject(
|
|
10848
|
-
{
|
|
10849
|
-
...identity,
|
|
10850
|
-
packetId: ownerPacketId
|
|
10851
|
-
},
|
|
10852
|
-
symbol.rawSymbol,
|
|
10853
|
-
() => {
|
|
10854
|
-
if (!this.#disposed) {
|
|
10855
|
-
this.#routeProcessedFecSymbols(
|
|
10856
|
-
blockId,
|
|
10857
|
-
{ ...result, symbols: [symbol] },
|
|
10858
|
-
ownerPacketId,
|
|
10859
|
-
catalogTrackName
|
|
10860
|
-
);
|
|
10861
|
-
}
|
|
10862
|
-
admitNext(index + 1);
|
|
10863
|
-
},
|
|
10864
|
-
// A stalled symbol that is later discarded instead of played must still
|
|
10865
|
-
// advance this loop; enforce mode never calls the release callback for
|
|
10866
|
-
// it (BLO-30095).
|
|
10867
|
-
() => admitNext(index + 1)
|
|
10868
|
-
);
|
|
10869
|
-
if (admission.action === "drop") admitNext(index + 1);
|
|
10870
|
-
};
|
|
10871
|
-
admitNext(0);
|
|
11188
|
+
return match;
|
|
10872
11189
|
}
|
|
10873
11190
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName) {
|
|
10874
11191
|
if (ownerPacketId == null) return;
|