@blockcast/mmt-container 2.0.1 → 2.1.0-main.36792a9d6614
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-runtime.js +817 -64
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -0
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-router.d.ts +78 -3
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1812 -27
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-manager.d.ts +33 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15362 -14626
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +255 -19
- package/dist/multicast-transport.js.map +4 -4
- package/dist/repair-fetch.d.ts +10 -3
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +15 -0
- package/dist/repair-fetch.js.map +2 -2
- package/package.json +5 -5
- package/src/cmcd-producer.ts +5 -0
- package/src/index.ts +5 -0
- package/src/mmtp-router.ts +549 -28
- package/src/multicast-manager.ts +186 -22
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +32 -2
package/dist/browser-runtime.js
CHANGED
|
@@ -7457,11 +7457,33 @@ var AdaptiveBitrateController = class {
|
|
|
7457
7457
|
};
|
|
7458
7458
|
|
|
7459
7459
|
// node_modules/@blockcast/mmt-base/dist/mmtp.js
|
|
7460
|
+
var MMTP_HEADER_SIZE2 = 12;
|
|
7460
7461
|
var MPU_HEADER_SIZE2 = 8;
|
|
7461
7462
|
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
7462
7463
|
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
7463
7464
|
var MMTP_SIGNALING_PACKET_ID2 = 0;
|
|
7464
7465
|
var MMTP_PACKET_ID_MAX2 = 65535;
|
|
7466
|
+
function readMMTPPacketId2(buffer) {
|
|
7467
|
+
if (buffer.length < MMTP_HEADER_SIZE2)
|
|
7468
|
+
return null;
|
|
7469
|
+
const flags = buffer[0];
|
|
7470
|
+
if ((flags >> 6 & 3) !== 0)
|
|
7471
|
+
return null;
|
|
7472
|
+
let offset = MMTP_HEADER_SIZE2;
|
|
7473
|
+
if ((flags & 32) !== 0) {
|
|
7474
|
+
if (offset + 4 > buffer.length)
|
|
7475
|
+
return null;
|
|
7476
|
+
offset += 4;
|
|
7477
|
+
}
|
|
7478
|
+
if ((flags & 2) !== 0) {
|
|
7479
|
+
if (offset + 4 > buffer.length)
|
|
7480
|
+
return null;
|
|
7481
|
+
const length = buffer[offset + 2] << 8 | buffer[offset + 3];
|
|
7482
|
+
if (offset + 4 + length > buffer.length)
|
|
7483
|
+
return null;
|
|
7484
|
+
}
|
|
7485
|
+
return buffer[2] << 8 | buffer[3];
|
|
7486
|
+
}
|
|
7465
7487
|
|
|
7466
7488
|
// node_modules/@blockcast/mmt-base/dist/net-constants.js
|
|
7467
7489
|
var DEFAULT_AMT_RELAY_PORT2 = 2268;
|
|
@@ -8447,6 +8469,205 @@ var TransportManager = class {
|
|
|
8447
8469
|
};
|
|
8448
8470
|
var transportManager = new TransportManager();
|
|
8449
8471
|
|
|
8472
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
|
|
8473
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
|
|
8474
|
+
var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
|
|
8475
|
+
var INTEGRITY_SCHEME = "manifest-ed25519-2";
|
|
8476
|
+
var MulticastError = class extends Error {
|
|
8477
|
+
code;
|
|
8478
|
+
constructor(code, message, cause) {
|
|
8479
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
8480
|
+
this.name = "MulticastError";
|
|
8481
|
+
this.code = code;
|
|
8482
|
+
}
|
|
8483
|
+
};
|
|
8484
|
+
function datagramsOf(chunk) {
|
|
8485
|
+
const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
|
|
8486
|
+
const out = [];
|
|
8487
|
+
for (const d of list) {
|
|
8488
|
+
const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
|
|
8489
|
+
if (data instanceof Uint8Array)
|
|
8490
|
+
out.push(data);
|
|
8491
|
+
else if (data instanceof ArrayBuffer)
|
|
8492
|
+
out.push(new Uint8Array(data));
|
|
8493
|
+
}
|
|
8494
|
+
return out;
|
|
8495
|
+
}
|
|
8496
|
+
function reasonOf(value) {
|
|
8497
|
+
return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
|
|
8498
|
+
}
|
|
8499
|
+
function registry() {
|
|
8500
|
+
const g = globalThis;
|
|
8501
|
+
if (g[REGISTRY] === void 0) {
|
|
8502
|
+
const providers = /* @__PURE__ */ new WeakSet();
|
|
8503
|
+
const byScope = /* @__PURE__ */ new WeakMap();
|
|
8504
|
+
Object.defineProperty(g, REGISTRY, {
|
|
8505
|
+
value: Object.freeze({
|
|
8506
|
+
isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
|
|
8507
|
+
providerFor: (scope) => {
|
|
8508
|
+
let provider = byScope.get(scope);
|
|
8509
|
+
if (provider === void 0) {
|
|
8510
|
+
provider = createProvider(scope);
|
|
8511
|
+
providers.add(provider);
|
|
8512
|
+
byScope.set(scope, provider);
|
|
8513
|
+
}
|
|
8514
|
+
return provider;
|
|
8515
|
+
}
|
|
8516
|
+
}),
|
|
8517
|
+
writable: false,
|
|
8518
|
+
configurable: false,
|
|
8519
|
+
enumerable: false
|
|
8520
|
+
});
|
|
8521
|
+
}
|
|
8522
|
+
return g[REGISTRY];
|
|
8523
|
+
}
|
|
8524
|
+
function resolveMulticastGateway(scope = globalThis) {
|
|
8525
|
+
const s = scope;
|
|
8526
|
+
if (typeof s.MulticastReceiver === "function")
|
|
8527
|
+
return registry().providerFor(scope);
|
|
8528
|
+
if (typeof s.multicast?.subscribe === "function")
|
|
8529
|
+
return s.multicast;
|
|
8530
|
+
return void 0;
|
|
8531
|
+
}
|
|
8532
|
+
function isUserAgentVerifiedGateway(gateway) {
|
|
8533
|
+
return registry().isProvider(gateway);
|
|
8534
|
+
}
|
|
8535
|
+
function userAgentVerifiedIngress(ingress, gateway) {
|
|
8536
|
+
if (isUserAgentVerifiedGateway(gateway))
|
|
8537
|
+
return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
|
|
8538
|
+
if (!("admittedBy" in ingress))
|
|
8539
|
+
return ingress;
|
|
8540
|
+
const { admittedBy: _, ...rest } = ingress;
|
|
8541
|
+
return rest;
|
|
8542
|
+
}
|
|
8543
|
+
function unsupported(message, cause) {
|
|
8544
|
+
return new MulticastError("capability-unsupported", message, cause);
|
|
8545
|
+
}
|
|
8546
|
+
function createProvider(scope) {
|
|
8547
|
+
const Receiver = scope.MulticastReceiver;
|
|
8548
|
+
let gate = null;
|
|
8549
|
+
return {
|
|
8550
|
+
capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
|
|
8551
|
+
subscribe: async (config) => {
|
|
8552
|
+
const c = config;
|
|
8553
|
+
if (c.consume !== "bytes")
|
|
8554
|
+
throw unsupported(`consume mode ${String(c.consume)} is not supported`);
|
|
8555
|
+
const origin = c.origin ?? "ip-multicast";
|
|
8556
|
+
if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
|
|
8557
|
+
throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
|
|
8558
|
+
}
|
|
8559
|
+
if (c.relay !== void 0)
|
|
8560
|
+
throw unsupported("MulticastReceiver picks its own AMT relay");
|
|
8561
|
+
if (!c.source)
|
|
8562
|
+
throw unsupported("MulticastReceiver needs an SSM source");
|
|
8563
|
+
if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
|
|
8564
|
+
throw unsupported("MulticastReceiver filters by source only");
|
|
8565
|
+
}
|
|
8566
|
+
const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
|
|
8567
|
+
if (gate !== null)
|
|
8568
|
+
await gate;
|
|
8569
|
+
let rx;
|
|
8570
|
+
try {
|
|
8571
|
+
rx = new Receiver(options);
|
|
8572
|
+
} catch (error) {
|
|
8573
|
+
throw unsupported("MulticastReceiver refused the request", error);
|
|
8574
|
+
}
|
|
8575
|
+
const opened = rx.opened;
|
|
8576
|
+
if (gate === null)
|
|
8577
|
+
gate = opened.then(() => void 0, () => void 0);
|
|
8578
|
+
return openSubscription(rx, opened, options, c.packetIds);
|
|
8579
|
+
}
|
|
8580
|
+
};
|
|
8581
|
+
}
|
|
8582
|
+
async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
8583
|
+
let opened;
|
|
8584
|
+
try {
|
|
8585
|
+
opened = await openedPromise;
|
|
8586
|
+
} catch (error) {
|
|
8587
|
+
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8588
|
+
}
|
|
8589
|
+
if (rx.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8590
|
+
await rx.close();
|
|
8591
|
+
throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8592
|
+
}
|
|
8593
|
+
const events = new EventTarget();
|
|
8594
|
+
let state = "connected";
|
|
8595
|
+
let filter = packetIds?.length ? new Set(packetIds) : null;
|
|
8596
|
+
let closing = false;
|
|
8597
|
+
const setState = (next) => {
|
|
8598
|
+
if (state === next)
|
|
8599
|
+
return;
|
|
8600
|
+
state = next;
|
|
8601
|
+
events.dispatchEvent(new CustomEvent("state", { detail: next }));
|
|
8602
|
+
};
|
|
8603
|
+
const sourceAddress = options.sourceAddress;
|
|
8604
|
+
const subscription = {
|
|
8605
|
+
id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
|
|
8606
|
+
get state() {
|
|
8607
|
+
return state;
|
|
8608
|
+
},
|
|
8609
|
+
delivery: "datagram",
|
|
8610
|
+
eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
|
|
8611
|
+
setPacketIds(ids) {
|
|
8612
|
+
filter = ids.length ? new Set(ids) : null;
|
|
8613
|
+
},
|
|
8614
|
+
addEventListener: events.addEventListener.bind(events),
|
|
8615
|
+
removeEventListener: events.removeEventListener.bind(events),
|
|
8616
|
+
async unsubscribe() {
|
|
8617
|
+
closing = true;
|
|
8618
|
+
await rx.close();
|
|
8619
|
+
setState("closed");
|
|
8620
|
+
}
|
|
8621
|
+
};
|
|
8622
|
+
void (async () => {
|
|
8623
|
+
const reader = opened.readable.getReader();
|
|
8624
|
+
try {
|
|
8625
|
+
for (; ; ) {
|
|
8626
|
+
const { value, done } = await reader.read();
|
|
8627
|
+
if (done)
|
|
8628
|
+
break;
|
|
8629
|
+
const packets = [];
|
|
8630
|
+
for (const data of datagramsOf(value)) {
|
|
8631
|
+
if (filter !== null) {
|
|
8632
|
+
const id = readMMTPPacketId2(data);
|
|
8633
|
+
if (id === null || !filter.has(id))
|
|
8634
|
+
continue;
|
|
8635
|
+
}
|
|
8636
|
+
const copy = data.slice().buffer;
|
|
8637
|
+
packets.push(copy);
|
|
8638
|
+
events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
|
|
8639
|
+
}
|
|
8640
|
+
if (packets.length > 0)
|
|
8641
|
+
events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
|
|
8642
|
+
}
|
|
8643
|
+
} catch {
|
|
8644
|
+
}
|
|
8645
|
+
})();
|
|
8646
|
+
void rx.closed.then((value) => {
|
|
8647
|
+
const reason = reasonOf(value);
|
|
8648
|
+
if (!closing) {
|
|
8649
|
+
if (reason === "consent-withdrawn") {
|
|
8650
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8651
|
+
detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
|
|
8652
|
+
}));
|
|
8653
|
+
} else if (reason === "oversubscribed" || reason === "network") {
|
|
8654
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8655
|
+
detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
|
|
8656
|
+
}));
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
setState("closed");
|
|
8660
|
+
}, (error) => {
|
|
8661
|
+
if (!closing) {
|
|
8662
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8663
|
+
detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
|
|
8664
|
+
}));
|
|
8665
|
+
}
|
|
8666
|
+
setState("closed");
|
|
8667
|
+
});
|
|
8668
|
+
return subscription;
|
|
8669
|
+
}
|
|
8670
|
+
|
|
8450
8671
|
// node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
|
|
8451
8672
|
var MAX_MOQT_PRIORITY = 255;
|
|
8452
8673
|
function moqtPriority(value, field) {
|
|
@@ -8733,9 +8954,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
|
|
|
8733
8954
|
}
|
|
8734
8955
|
function validateMmtpDashTemplate(template, path) {
|
|
8735
8956
|
const variables = [...template.matchAll(TEMPLATE_VARIABLE)].map((match) => match[1]);
|
|
8736
|
-
const
|
|
8737
|
-
if (
|
|
8738
|
-
throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${
|
|
8957
|
+
const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
|
|
8958
|
+
if (unsupported2 !== void 0) {
|
|
8959
|
+
throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
|
|
8739
8960
|
}
|
|
8740
8961
|
if (variables.filter((variable) => variable === "Number").length !== 1) {
|
|
8741
8962
|
throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
|
|
@@ -17098,17 +17319,17 @@ var FECManager = class _FECManager {
|
|
|
17098
17319
|
|
|
17099
17320
|
// src/mfu-reassembly-policy.ts
|
|
17100
17321
|
function validateReliableMoqOptionShape(value) {
|
|
17101
|
-
const
|
|
17322
|
+
const unsupported2 = Object.keys(value).filter(
|
|
17102
17323
|
(key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
|
|
17103
17324
|
);
|
|
17104
|
-
if (
|
|
17105
|
-
if (
|
|
17325
|
+
if (unsupported2.length > 0) {
|
|
17326
|
+
if (unsupported2.includes("maxRacingGroups")) {
|
|
17106
17327
|
throw new Error(
|
|
17107
17328
|
"[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
|
|
17108
17329
|
);
|
|
17109
17330
|
}
|
|
17110
17331
|
throw new Error(
|
|
17111
|
-
`[MFU reassembly] unsupported reliable MoQ option(s): ${
|
|
17332
|
+
`[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
|
|
17112
17333
|
);
|
|
17113
17334
|
}
|
|
17114
17335
|
const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
|
|
@@ -17152,12 +17373,12 @@ function assertValidMfuReassemblyPolicy(policy) {
|
|
|
17152
17373
|
return;
|
|
17153
17374
|
}
|
|
17154
17375
|
if (value["transport"] === "datagram") {
|
|
17155
|
-
const
|
|
17376
|
+
const unsupported2 = Object.keys(value).filter(
|
|
17156
17377
|
(key) => key !== "transport" && key !== "deadlineMs"
|
|
17157
17378
|
);
|
|
17158
|
-
if (
|
|
17379
|
+
if (unsupported2.length > 0) {
|
|
17159
17380
|
throw new Error(
|
|
17160
|
-
`[MFU reassembly] unsupported datagram option(s): ${
|
|
17381
|
+
`[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
|
|
17161
17382
|
);
|
|
17162
17383
|
}
|
|
17163
17384
|
assertValidDatagramDeadline(value["deadlineMs"]);
|
|
@@ -22431,6 +22652,14 @@ function isRouterDebugEnabled() {
|
|
|
22431
22652
|
function debugRouter(message) {
|
|
22432
22653
|
if (isRouterDebugEnabled()) console.debug(message);
|
|
22433
22654
|
}
|
|
22655
|
+
var KEYFRAME_OVERLAY_EXTENSION_TYPE = 35585;
|
|
22656
|
+
function keyframeOverlaySsId(header) {
|
|
22657
|
+
const ext = header.extension;
|
|
22658
|
+
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22659
|
+
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22660
|
+
}
|
|
22661
|
+
var OVERLAY_OFFSET_MPUS = 16;
|
|
22662
|
+
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22434
22663
|
function assertStreamIdentity(role, stream) {
|
|
22435
22664
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
22436
22665
|
throw new Error(
|
|
@@ -22647,6 +22876,26 @@ var MmtpRouter = class {
|
|
|
22647
22876
|
#packetIdRoles = /* @__PURE__ */ new Map();
|
|
22648
22877
|
/** Explicit catalog ownership for dedicated repair-track packet IDs. */
|
|
22649
22878
|
#repairOwnersByPacketId = /* @__PURE__ */ new Map();
|
|
22879
|
+
/** Keyframe-overlay repair PID → the source its FEC instance protects
|
|
22880
|
+
* (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
|
|
22881
|
+
* #repairOwnersByPacketId: the overlay is decoded as its own instance. */
|
|
22882
|
+
#overlayLegs = /* @__PURE__ */ new Map();
|
|
22883
|
+
/** Keyframe-overlay repair track names, which name a leg too: a catalog
|
|
22884
|
+
* may state no PID for it (learned at subscribe time). */
|
|
22885
|
+
#overlayRepairNames = /* @__PURE__ */ new Map();
|
|
22886
|
+
/** Installed overlay instances, by overlay repair PID (opt-in only). */
|
|
22887
|
+
#overlays = /* @__PURE__ */ new Map();
|
|
22888
|
+
#overlayRecoveredObservers = /* @__PURE__ */ new Set();
|
|
22889
|
+
/** Streams whose recovered sequence the base path ever re-stamped. */
|
|
22890
|
+
#restampedStreams = /* @__PURE__ */ new Set();
|
|
22891
|
+
#overlayStats = {
|
|
22892
|
+
repairDropped: 0,
|
|
22893
|
+
ownerRefused: 0,
|
|
22894
|
+
recovered: 0,
|
|
22895
|
+
restarts: 0,
|
|
22896
|
+
unsequencedDropped: 0,
|
|
22897
|
+
protectedDropped: 0
|
|
22898
|
+
};
|
|
22650
22899
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22651
22900
|
#onDeltaBlockCompleteCb = null;
|
|
22652
22901
|
// Debug counters
|
|
@@ -22843,6 +23092,7 @@ var MmtpRouter = class {
|
|
|
22843
23092
|
this.#fecManager.onSourceDiscontinuity = (packetId, catalogTrackName) => {
|
|
22844
23093
|
this.#parser.markFecDiscontinuity(packetId, catalogTrackName);
|
|
22845
23094
|
this.#requestFecPoke(packetId, catalogTrackName);
|
|
23095
|
+
this.resetKeyframeOverlays(packetId, catalogTrackName);
|
|
22846
23096
|
};
|
|
22847
23097
|
this.#fecManager.onBlockComplete = (sbn, packetId, catalogTrackName) => {
|
|
22848
23098
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
|
|
@@ -23165,13 +23415,287 @@ var MmtpRouter = class {
|
|
|
23165
23415
|
/** Replace catalog-derived repair ownership before a catalog reconfiguration. */
|
|
23166
23416
|
clearRepairPacketIdOwners() {
|
|
23167
23417
|
this.#repairOwnersByPacketId.clear();
|
|
23418
|
+
this.#overlayLegs.clear();
|
|
23419
|
+
this.#overlayRepairNames.clear();
|
|
23168
23420
|
}
|
|
23169
|
-
/** Bind one dedicated repair-track PID to its authoritative media PID.
|
|
23421
|
+
/** Bind one dedicated repair-track PID to its authoritative media PID. A
|
|
23422
|
+
* known keyframe-overlay PID is refused, and counted: its symbols belong to
|
|
23423
|
+
* the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
|
|
23170
23424
|
setRepairPacketIdOwner(repairPacketId, ownerPacketId) {
|
|
23425
|
+
if (this.#overlayLegs.has(repairPacketId)) {
|
|
23426
|
+
this.#overlayStats.ownerRefused++;
|
|
23427
|
+
return;
|
|
23428
|
+
}
|
|
23171
23429
|
const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? /* @__PURE__ */ new Set();
|
|
23172
23430
|
owners.add(ownerPacketId);
|
|
23173
23431
|
this.#repairOwnersByPacketId.set(repairPacketId, owners);
|
|
23174
23432
|
}
|
|
23433
|
+
/**
|
|
23434
|
+
* Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
|
|
23435
|
+
* PID, the source it protects, and its repair track name. Either one names
|
|
23436
|
+
* the leg, so a catalog that states no PID (learned at subscribe time) is
|
|
23437
|
+
* covered by the name. Overlay repair is then never resolved to a base
|
|
23438
|
+
* owner, on multicast or MoQ. Cleared with the catalog owners
|
|
23439
|
+
* (`clearRepairPacketIdOwners`).
|
|
23440
|
+
*/
|
|
23441
|
+
setKeyframeOverlayLeg(repairPacketId, leg) {
|
|
23442
|
+
if (leg.repairTrackName !== void 0) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
|
|
23443
|
+
if (repairPacketId === void 0) return;
|
|
23444
|
+
this.#repairOwnersByPacketId.delete(repairPacketId);
|
|
23445
|
+
this.#overlayLegs.set(repairPacketId, { ...leg });
|
|
23446
|
+
}
|
|
23447
|
+
/** The keyframe-overlay leg registered on `repairPacketId`, if any. */
|
|
23448
|
+
keyframeOverlayLeg(repairPacketId) {
|
|
23449
|
+
return this.#overlayLegs.get(repairPacketId);
|
|
23450
|
+
}
|
|
23451
|
+
/**
|
|
23452
|
+
* Overlay counters: repair dropped for want of an installed instance, base
|
|
23453
|
+
* owners refused on an overlay PID, overlay blocks recovered, overlay
|
|
23454
|
+
* restarts, and recovered packets dropped for want of a wire sequence or
|
|
23455
|
+
* for a MoQ symbol on a protected flow (multicast-02 7.2.7).
|
|
23456
|
+
*/
|
|
23457
|
+
get keyframeOverlayStats() {
|
|
23458
|
+
return { ...this.#overlayStats };
|
|
23459
|
+
}
|
|
23460
|
+
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23461
|
+
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23462
|
+
}
|
|
23463
|
+
/**
|
|
23464
|
+
* Install the keyframe overlay decoded on `fec` for a known overlay leg
|
|
23465
|
+
* (draft-ramadan-mmt-fec-02 7.4.4: decoded independently of the base). `fec`
|
|
23466
|
+
* holds the overlay stream under `repairPacketId` and `streamName`. Replaces
|
|
23467
|
+
* an earlier install on the same leg; the returned disposer removes only
|
|
23468
|
+
* this one.
|
|
23469
|
+
*/
|
|
23470
|
+
installKeyframeOverlay(repairPacketId, fec, k, streamName) {
|
|
23471
|
+
const leg = this.#overlayLegs.get(repairPacketId);
|
|
23472
|
+
if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
|
|
23473
|
+
const inst = {
|
|
23474
|
+
repairPacketId,
|
|
23475
|
+
streamName,
|
|
23476
|
+
leg: { ...leg },
|
|
23477
|
+
fec,
|
|
23478
|
+
k,
|
|
23479
|
+
offsets: /* @__PURE__ */ new Map(),
|
|
23480
|
+
moqBlocks: /* @__PURE__ */ new Set()
|
|
23481
|
+
};
|
|
23482
|
+
this.#overlays.set(repairPacketId, inst);
|
|
23483
|
+
return () => {
|
|
23484
|
+
if (this.#overlays.get(repairPacketId) === inst) this.#overlays.delete(repairPacketId);
|
|
23485
|
+
};
|
|
23486
|
+
}
|
|
23487
|
+
/**
|
|
23488
|
+
* Observe each base block whose RAP MFU fragments a keyframe-overlay
|
|
23489
|
+
* recovery supplied, with the keyframe's FT=0 and FT=1 held
|
|
23490
|
+
* (fec-multicast-00 §6.6). The block is named by the recovered packets'
|
|
23491
|
+
* restored wire sequence through the base SS_ID offset, never by an overlay
|
|
23492
|
+
* coordinate. Called synchronously from the recovered-block route.
|
|
23493
|
+
*/
|
|
23494
|
+
addKeyframeOverlayRecoveredObserver(observer) {
|
|
23495
|
+
this.#overlayRecoveredObservers.add(observer);
|
|
23496
|
+
return () => {
|
|
23497
|
+
this.#overlayRecoveredObservers.delete(observer);
|
|
23498
|
+
};
|
|
23499
|
+
}
|
|
23500
|
+
/** Whether an overlay is installed on `repairPacketId`. */
|
|
23501
|
+
hasKeyframeOverlay(repairPacketId) {
|
|
23502
|
+
return this.#overlays.has(repairPacketId);
|
|
23503
|
+
}
|
|
23504
|
+
/**
|
|
23505
|
+
* Start a new epoch of every overlay protecting `sourcePacketId` (its
|
|
23506
|
+
* source restarted): empty overlay stream, no coverage, no offsets.
|
|
23507
|
+
*/
|
|
23508
|
+
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23509
|
+
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
23510
|
+
}
|
|
23511
|
+
#resetOverlay(inst) {
|
|
23512
|
+
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23513
|
+
inst.last = void 0;
|
|
23514
|
+
inst.offsets.clear();
|
|
23515
|
+
inst.moqBlocks.clear();
|
|
23516
|
+
}
|
|
23517
|
+
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23518
|
+
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
23519
|
+
}
|
|
23520
|
+
/**
|
|
23521
|
+
* The overlay instance a repair packet belongs to, by the track it arrived
|
|
23522
|
+
* on (7.4.2: its PID or its repair track name); null for overlay repair
|
|
23523
|
+
* with no instance installed; undefined for repair of no overlay.
|
|
23524
|
+
*/
|
|
23525
|
+
#overlayRepairTarget(repairPacketId, repairTrackName) {
|
|
23526
|
+
const inst = this.#overlays.get(repairPacketId) ?? (repairTrackName === void 0 ? void 0 : [...this.#overlays.values()].find(({ leg }) => leg.repairTrackName === repairTrackName));
|
|
23527
|
+
if (inst) return inst;
|
|
23528
|
+
return this.#isOverlayRepair(repairPacketId, repairTrackName) ? null : void 0;
|
|
23529
|
+
}
|
|
23530
|
+
/** Overlay repair goes to its own instance, never to a base owner, locator
|
|
23531
|
+
* or decoder; with none installed it is dropped and counted. */
|
|
23532
|
+
#routeOverlayRepair(inst, repairPacketId, packet, header, source) {
|
|
23533
|
+
const repair = inst ? parseMoqRepairFrameWithHeader(packet, header) : null;
|
|
23534
|
+
if (!inst || !repair) {
|
|
23535
|
+
this.#overlayStats.repairDropped++;
|
|
23536
|
+
debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
|
|
23537
|
+
return;
|
|
23538
|
+
}
|
|
23539
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
23540
|
+
if (!fec.admitRepairBlockLength(pid, repair.ssbLength, repair.rsbLength, repair.rsId)) return;
|
|
23541
|
+
if (!fec.admitRepairSymbolSize(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData.byteLength)) return;
|
|
23542
|
+
const placement = fec.placeIsoRepair(pid, name, repair.ssStart, repair.ssbLength);
|
|
23543
|
+
if (placement.status !== "placed") return;
|
|
23544
|
+
const blockStart = placement.block.start;
|
|
23545
|
+
if (placement.span !== void 0) this.#replayOverlaySources(inst, placement.span.block.start, placement.span.placed);
|
|
23546
|
+
this.#replayOverlaySources(inst, blockStart, placement.placed);
|
|
23547
|
+
if (source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
|
|
23548
|
+
const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
|
|
23549
|
+
const recovered = fec.feedRepairSymbol(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData, name);
|
|
23550
|
+
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
|
|
23551
|
+
}
|
|
23552
|
+
#replayOverlaySources(inst, blockStart, placed) {
|
|
23553
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
23554
|
+
for (const { ssId, payload } of placed) {
|
|
23555
|
+
if (payload.source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
|
|
23556
|
+
if (payload.oversized) continue;
|
|
23557
|
+
const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
|
|
23558
|
+
const recovered = fec.feedSourceSymbol(pid, ssId, payload.symbolData, name);
|
|
23559
|
+
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
|
|
23560
|
+
}
|
|
23561
|
+
}
|
|
23562
|
+
/**
|
|
23563
|
+
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23564
|
+
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23565
|
+
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23566
|
+
* included (7.1, 7.4.3).
|
|
23567
|
+
*/
|
|
23568
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
23569
|
+
if (this.#overlays.size === 0) return;
|
|
23570
|
+
const ssId = keyframeOverlaySsId(header);
|
|
23571
|
+
if (ssId === void 0) return;
|
|
23572
|
+
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23573
|
+
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
23574
|
+
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23575
|
+
if (identity !== null) {
|
|
23576
|
+
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23577
|
+
inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
|
|
23578
|
+
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23579
|
+
}
|
|
23580
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
23581
|
+
const symbolData = packet.subarray(0, packet.length - 4);
|
|
23582
|
+
const oversized = !fec.isoSourceFits(pid, symbolData.length);
|
|
23583
|
+
const coords = fec.locateIsoSource(pid, name, ssId);
|
|
23584
|
+
if (coords === null) {
|
|
23585
|
+
fec.holdIsoSource(pid, name, ssId, {
|
|
23586
|
+
symbolData,
|
|
23587
|
+
source,
|
|
23588
|
+
oversized,
|
|
23589
|
+
evidence: sourceSymbolEvidence(packet, header, rapFlag, sequenceNumber)
|
|
23590
|
+
});
|
|
23591
|
+
continue;
|
|
23592
|
+
}
|
|
23593
|
+
if (source === "moq") this.#noteOverlayMoqBlock(inst, coords.sbn);
|
|
23594
|
+
if (oversized) continue;
|
|
23595
|
+
const alreadyCompleted = fec.isBlockCompleted(coords.sbn, pid, name);
|
|
23596
|
+
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23597
|
+
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23598
|
+
}
|
|
23599
|
+
}
|
|
23600
|
+
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23601
|
+
inst.moqBlocks.add(blockStart);
|
|
23602
|
+
const newest = inst.last?.ssId;
|
|
23603
|
+
if (newest === void 0) return;
|
|
23604
|
+
for (const start of inst.moqBlocks) {
|
|
23605
|
+
if ((newest - start | 0) > OVERLAY_MOQ_HORIZON_BLOCKS * inst.k) inst.moqBlocks.delete(start);
|
|
23606
|
+
}
|
|
23607
|
+
}
|
|
23608
|
+
/**
|
|
23609
|
+
* An overlay SS_ID inconsistent with the source's packet_sequence_number
|
|
23610
|
+
* advance since the last one is an overlay restart (an FFmpeg muxer start,
|
|
23611
|
+
* a bridge swap that keeps psn continuous): it moved the other way, or
|
|
23612
|
+
* further than the psn advance plus K_overlay. Reordering keeps the two
|
|
23613
|
+
* consistent and is no restart.
|
|
23614
|
+
*/
|
|
23615
|
+
#noteOverlaySsId(inst, ssId, psn) {
|
|
23616
|
+
const last = inst.last;
|
|
23617
|
+
if (last !== void 0) {
|
|
23618
|
+
const dSs = ssId - last.ssId | 0;
|
|
23619
|
+
const dPsn = psn - last.psn | 0;
|
|
23620
|
+
const consistent = dSs === 0 ? dPsn === 0 : Math.sign(dSs) === Math.sign(dPsn) && Math.abs(dSs) <= Math.abs(dPsn) + inst.k;
|
|
23621
|
+
if (!consistent) {
|
|
23622
|
+
this.#overlayStats.restarts++;
|
|
23623
|
+
this.#resetOverlay(inst);
|
|
23624
|
+
} else if (dPsn < 0) {
|
|
23625
|
+
return;
|
|
23626
|
+
}
|
|
23627
|
+
}
|
|
23628
|
+
inst.last = { ssId, psn };
|
|
23629
|
+
}
|
|
23630
|
+
/**
|
|
23631
|
+
* Route an overlay-recovered block's packets as source media of the
|
|
23632
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
|
|
23633
|
+
* base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
|
|
23634
|
+
* base re-stamp, no block advance. Each packet's wire sequence is restored
|
|
23635
|
+
* from its keyframe's own offset; without one, a stream the base never
|
|
23636
|
+
* re-stamped keeps the header's sequence and any other drops the packet.
|
|
23637
|
+
*/
|
|
23638
|
+
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23639
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
23640
|
+
fec.markBlockCompleted(blockStart, pid, name);
|
|
23641
|
+
const sourcePacketId = inst.leg.sourcePacketId;
|
|
23642
|
+
const sourceName = inst.leg.sourceCatalogTrackName;
|
|
23643
|
+
if (this.#classifyPacketId(sourcePacketId) === null) return;
|
|
23644
|
+
const receiver = this.#provenanceFor(sourcePacketId, sourceName);
|
|
23645
|
+
if (receiver !== void 0 && inst.moqBlocks.has(blockStart)) {
|
|
23646
|
+
this.#overlayStats.protectedDropped++;
|
|
23647
|
+
return;
|
|
23648
|
+
}
|
|
23649
|
+
const blockProfile = fec.blockProfileForPacket(pid);
|
|
23650
|
+
const symbolSize = fec.symbolSizeForPacket(pid);
|
|
23651
|
+
if (blockProfile === void 0 || symbolSize === void 0 || !(symbolSize > 0)) return;
|
|
23652
|
+
const result = processFecBlock(recovered, { symbolSize, ownerPacketId: sourcePacketId, blockProfile });
|
|
23653
|
+
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23654
|
+
const restamped = this.#restampedStreams.has(streamKey);
|
|
23655
|
+
const restored = /* @__PURE__ */ new Set();
|
|
23656
|
+
const symbols = result.symbols.flatMap((sym) => {
|
|
23657
|
+
const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
23658
|
+
if (offset !== void 0) {
|
|
23659
|
+
const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
|
|
23660
|
+
restored.add(packetSequenceNumber);
|
|
23661
|
+
return [{ ...sym, packetSequenceNumber }];
|
|
23662
|
+
}
|
|
23663
|
+
if (!restamped) return [sym];
|
|
23664
|
+
this.#overlayStats.unsequencedDropped++;
|
|
23665
|
+
return [];
|
|
23666
|
+
});
|
|
23667
|
+
this.#overlayStats.recovered++;
|
|
23668
|
+
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23669
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
23670
|
+
});
|
|
23671
|
+
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
23672
|
+
}
|
|
23673
|
+
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23674
|
+
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
23675
|
+
* packet whose wire sequence was not restored names no base block. */
|
|
23676
|
+
#noteOverlayRecoveredRaps(packetId, catalogTrackName, streamKey, symbols) {
|
|
23677
|
+
const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
23678
|
+
if (ssIdSeqOffset === void 0 || this.#overlayRecoveredObservers.size === 0) return;
|
|
23679
|
+
const withMeta = (type) => new Set(symbols.filter((sym) => sym.fragmentType === type).map((sym) => sym.fragment.mpuSequenceNumber));
|
|
23680
|
+
const ft0 = withMeta(0);
|
|
23681
|
+
const ft1 = withMeta(1);
|
|
23682
|
+
const rapMfu = (fi) => new Set(symbols.filter((sym) => sym.fragmentType === 2 && sym.fragment.rapFlag && sym.fragment.fragmentationIndicator === fi).map((sym) => sym.fragment.mpuSequenceNumber));
|
|
23683
|
+
const single = rapMfu(0);
|
|
23684
|
+
const first = rapMfu(1);
|
|
23685
|
+
const last = rapMfu(3);
|
|
23686
|
+
const blocks = /* @__PURE__ */ new Set();
|
|
23687
|
+
for (const sym of symbols) {
|
|
23688
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
23689
|
+
if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
|
|
23690
|
+
if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
|
|
23691
|
+
const ssId = sym.packetSequenceNumber + ssIdSeqOffset >>> 0;
|
|
23692
|
+
const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
|
|
23693
|
+
if (sbn !== void 0) blocks.add(sbn);
|
|
23694
|
+
}
|
|
23695
|
+
for (const sbn of blocks) {
|
|
23696
|
+
for (const observer of this.#overlayRecoveredObservers) observer(packetId, catalogTrackName, sbn);
|
|
23697
|
+
}
|
|
23698
|
+
}
|
|
23175
23699
|
/**
|
|
23176
23700
|
* Register callback fired when a delta-track FEC block completes or fails.
|
|
23177
23701
|
*
|
|
@@ -23276,10 +23800,11 @@ var MmtpRouter = class {
|
|
|
23276
23800
|
if (!entry) return;
|
|
23277
23801
|
entry.receiver.release();
|
|
23278
23802
|
entry.protected = false;
|
|
23803
|
+
entry.userAgent = false;
|
|
23279
23804
|
this.#assistBlocks.clear();
|
|
23280
23805
|
this.#assistFailures.clear();
|
|
23281
23806
|
}
|
|
23282
|
-
/**
|
|
23807
|
+
/** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
|
|
23283
23808
|
get unadmittedListedDropCount() {
|
|
23284
23809
|
return this.#unadmittedListedDrops;
|
|
23285
23810
|
}
|
|
@@ -23320,6 +23845,13 @@ var MmtpRouter = class {
|
|
|
23320
23845
|
}
|
|
23321
23846
|
return false;
|
|
23322
23847
|
}
|
|
23848
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
23849
|
+
#userAgentGoverns(packetId) {
|
|
23850
|
+
for (const entry of this.#provenance.values()) {
|
|
23851
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
23852
|
+
}
|
|
23853
|
+
return false;
|
|
23854
|
+
}
|
|
23323
23855
|
/**
|
|
23324
23856
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
23325
23857
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -23410,7 +23942,7 @@ var MmtpRouter = class {
|
|
|
23410
23942
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
23411
23943
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
23412
23944
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
23413
|
-
})) {
|
|
23945
|
+
}, true)) {
|
|
23414
23946
|
this.#unlistedAssistedRecoveries++;
|
|
23415
23947
|
this.#assistBlocks.delete(key);
|
|
23416
23948
|
return;
|
|
@@ -23595,6 +24127,11 @@ var MmtpRouter = class {
|
|
|
23595
24127
|
return;
|
|
23596
24128
|
}
|
|
23597
24129
|
const repairPacketId = header.packetId;
|
|
24130
|
+
const overlay = this.#overlayRepairTarget(repairPacketId, repairTrackName);
|
|
24131
|
+
if (overlay !== void 0) {
|
|
24132
|
+
this.#routeOverlayRepair(overlay, repairPacketId, packet, header, "moq");
|
|
24133
|
+
return;
|
|
24134
|
+
}
|
|
23598
24135
|
const hintedOwner = ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null ? ownerPacketIdHint : null;
|
|
23599
24136
|
const ownerPacketId = hintedOwner ?? (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
|
|
23600
24137
|
if (ownerPacketId === null) {
|
|
@@ -23884,6 +24421,8 @@ var MmtpRouter = class {
|
|
|
23884
24421
|
}
|
|
23885
24422
|
}
|
|
23886
24423
|
this.#parser.resetTrack(packetId, catalogTrackName);
|
|
24424
|
+
this.resetKeyframeOverlays(packetId, catalogTrackName);
|
|
24425
|
+
this.#restampedStreams.delete(this.#mediaStreamKey(packetId, catalogTrackName));
|
|
23887
24426
|
const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
|
|
23888
24427
|
for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
|
|
23889
24428
|
for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
|
|
@@ -23970,6 +24509,17 @@ var MmtpRouter = class {
|
|
|
23970
24509
|
this.#packetIdRoles.delete(packetId);
|
|
23971
24510
|
this.#catalogTrackNamesByPacketId.delete(packetId);
|
|
23972
24511
|
this.#repairOwnersByPacketId.delete(packetId);
|
|
24512
|
+
this.#overlayLegs.delete(packetId);
|
|
24513
|
+
for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
|
|
24514
|
+
if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
|
|
24515
|
+
}
|
|
24516
|
+
for (const [name, leg] of [...this.#overlayRepairNames]) {
|
|
24517
|
+
if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
|
|
24518
|
+
}
|
|
24519
|
+
this.#overlays.delete(packetId);
|
|
24520
|
+
for (const [repairPacketId, inst] of [...this.#overlays]) {
|
|
24521
|
+
if (inst.leg.sourcePacketId === packetId) this.#overlays.delete(repairPacketId);
|
|
24522
|
+
}
|
|
23973
24523
|
delete this.#pidCounts[packetId];
|
|
23974
24524
|
delete this.#repairPidCounts[packetId];
|
|
23975
24525
|
delete this.#moqRepairPidCounts[packetId];
|
|
@@ -24208,6 +24758,9 @@ var MmtpRouter = class {
|
|
|
24208
24758
|
this.#parserDiagnostics.clear();
|
|
24209
24759
|
this.#catalogTrackNamesByPacketId.clear();
|
|
24210
24760
|
this.#repairOwnersByPacketId.clear();
|
|
24761
|
+
this.#overlayLegs.clear();
|
|
24762
|
+
this.#overlayRepairNames.clear();
|
|
24763
|
+
this.#overlays.clear();
|
|
24211
24764
|
for (const { receiver } of this.#provenance.values()) receiver.release();
|
|
24212
24765
|
this.#provenance.clear();
|
|
24213
24766
|
this.#provenanceStreamFlow.clear();
|
|
@@ -24545,6 +25098,12 @@ var MmtpRouter = class {
|
|
|
24545
25098
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
24546
25099
|
return;
|
|
24547
25100
|
}
|
|
25101
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
25102
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
25103
|
+
protectedFlow.userAgent = true;
|
|
25104
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
25105
|
+
return;
|
|
25106
|
+
}
|
|
24548
25107
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
24549
25108
|
if (verdict === "admitted") {
|
|
24550
25109
|
this.#noteFlowAdmitted(ingress.flow);
|
|
@@ -24562,7 +25121,17 @@ var MmtpRouter = class {
|
|
|
24562
25121
|
return;
|
|
24563
25122
|
}
|
|
24564
25123
|
}
|
|
25124
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
|
|
25125
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
25126
|
+
this.#unadmittedListedDrops++;
|
|
25127
|
+
return;
|
|
25128
|
+
}
|
|
24565
25129
|
if (info.isRepair) {
|
|
25130
|
+
const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
|
|
25131
|
+
if (overlay !== void 0) {
|
|
25132
|
+
this.#routeOverlayRepair(overlay, info.packetId, packet, parsed.header, source);
|
|
25133
|
+
return;
|
|
25134
|
+
}
|
|
24566
25135
|
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
24567
25136
|
if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
24568
25137
|
const repair = this.#readRepairObject(
|
|
@@ -24574,7 +25143,7 @@ var MmtpRouter = class {
|
|
|
24574
25143
|
);
|
|
24575
25144
|
if (!repair) return;
|
|
24576
25145
|
if (ownerPacketId != null && this.#fecManager) {
|
|
24577
|
-
if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
25146
|
+
if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
24578
25147
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
24579
25148
|
const count = this.#repairPidCounts[info.packetId];
|
|
24580
25149
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -24769,6 +25338,17 @@ var MmtpRouter = class {
|
|
|
24769
25338
|
});
|
|
24770
25339
|
}
|
|
24771
25340
|
}
|
|
25341
|
+
if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
|
|
25342
|
+
this.#feedOverlaySource(
|
|
25343
|
+
info.packetId,
|
|
25344
|
+
catalogTrackName,
|
|
25345
|
+
packet,
|
|
25346
|
+
parsed.header,
|
|
25347
|
+
info.sequenceNumber,
|
|
25348
|
+
info.rapFlag,
|
|
25349
|
+
source
|
|
25350
|
+
);
|
|
25351
|
+
}
|
|
24772
25352
|
}
|
|
24773
25353
|
}
|
|
24774
25354
|
/**
|
|
@@ -25142,7 +25722,7 @@ var MmtpRouter = class {
|
|
|
25142
25722
|
}
|
|
25143
25723
|
// ── Private: FEC recovery routing ───────────────────────────────
|
|
25144
25724
|
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
25145
|
-
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
|
|
25725
|
+
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
25146
25726
|
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
25147
25727
|
if (ownerPacketId === void 0) {
|
|
25148
25728
|
throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
|
|
@@ -25159,7 +25739,7 @@ var MmtpRouter = class {
|
|
|
25159
25739
|
ownerPacketId,
|
|
25160
25740
|
blockProfile
|
|
25161
25741
|
});
|
|
25162
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
25742
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
25163
25743
|
}
|
|
25164
25744
|
/**
|
|
25165
25745
|
* A recovered source packet of a protected flow is admitted on its own
|
|
@@ -25171,9 +25751,24 @@ var MmtpRouter = class {
|
|
|
25171
25751
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
25172
25752
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
25173
25753
|
* outside a protected flow.
|
|
25174
|
-
|
|
25175
|
-
|
|
25176
|
-
|
|
25754
|
+
*
|
|
25755
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
25756
|
+
* without a record: while the flow is protected that decoder takes only
|
|
25757
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
25758
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
25759
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
25760
|
+
* unlisted MoQ repair and still needs a record.
|
|
25761
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
25762
|
+
* ever mixes verified and unverified live inputs.
|
|
25763
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
25764
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
25765
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
25766
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
25767
|
+
*/
|
|
25768
|
+
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
25769
|
+
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25770
|
+
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25771
|
+
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
25177
25772
|
let recovered = true;
|
|
25178
25773
|
if (receiver) {
|
|
25179
25774
|
const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -25185,6 +25780,8 @@ var MmtpRouter = class {
|
|
|
25185
25780
|
let admitted = false;
|
|
25186
25781
|
if (receiver === "ambiguous") {
|
|
25187
25782
|
this.#ambiguousRecoveredDrops++;
|
|
25783
|
+
} else if (userAgent) {
|
|
25784
|
+
admitted = true;
|
|
25188
25785
|
} else {
|
|
25189
25786
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
25190
25787
|
wire.set(sym.rawSymbol);
|
|
@@ -25212,22 +25809,22 @@ var MmtpRouter = class {
|
|
|
25212
25809
|
catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
|
|
25213
25810
|
);
|
|
25214
25811
|
if (flows.size > 1) return "ambiguous";
|
|
25215
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
25812
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
25216
25813
|
let match;
|
|
25217
|
-
for (const
|
|
25218
|
-
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
25814
|
+
for (const entry of this.#provenance.values()) {
|
|
25815
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
25219
25816
|
if (match) return "ambiguous";
|
|
25220
|
-
match =
|
|
25817
|
+
match = entry;
|
|
25221
25818
|
}
|
|
25222
25819
|
}
|
|
25223
25820
|
return match;
|
|
25224
25821
|
}
|
|
25225
|
-
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName) {
|
|
25822
|
+
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25226
25823
|
if (ownerPacketId == null) return;
|
|
25227
25824
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25228
|
-
const
|
|
25229
|
-
|
|
25230
|
-
);
|
|
25825
|
+
const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
25826
|
+
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25827
|
+
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25231
25828
|
if (ssIdSeqOffset !== void 0) {
|
|
25232
25829
|
result = {
|
|
25233
25830
|
...result,
|
|
@@ -25235,12 +25832,13 @@ var MmtpRouter = class {
|
|
|
25235
25832
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25236
25833
|
if (wire === sym.packetSequenceNumber) return sym;
|
|
25237
25834
|
this.#restampedRecoveredSeqs++;
|
|
25835
|
+
this.#restampedStreams.add(streamKey);
|
|
25238
25836
|
return { ...sym, packetSequenceNumber: wire };
|
|
25239
25837
|
})
|
|
25240
25838
|
};
|
|
25241
25839
|
}
|
|
25242
|
-
this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
|
|
25243
|
-
const deliveryProvenance = this.#fecManager?.blockDeliveryProvenance?.(
|
|
25840
|
+
if (!overlay) this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
|
|
25841
|
+
const deliveryProvenance = overlay?.deliveryProvenance ?? this.#fecManager?.blockDeliveryProvenance?.(
|
|
25244
25842
|
blockId,
|
|
25245
25843
|
ownerPacketId,
|
|
25246
25844
|
catalogTrackName
|
|
@@ -25318,7 +25916,7 @@ var MmtpRouter = class {
|
|
|
25318
25916
|
rapFlag: sym.fragment.rapFlag,
|
|
25319
25917
|
recovered: true,
|
|
25320
25918
|
deliveryProvenance,
|
|
25321
|
-
|
|
25919
|
+
...fecSbnOf(sym.index),
|
|
25322
25920
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
25323
25921
|
});
|
|
25324
25922
|
continue;
|
|
@@ -25430,7 +26028,7 @@ var MmtpRouter = class {
|
|
|
25430
26028
|
rapFlag: assembled.rapFlag,
|
|
25431
26029
|
recovered: true,
|
|
25432
26030
|
...assembled.deliveryProvenance === void 0 ? {} : { deliveryProvenance: assembled.deliveryProvenance },
|
|
25433
|
-
|
|
26031
|
+
...fecSbnOf(sym.index),
|
|
25434
26032
|
...recoveredCatalogTrackName === void 0 ? {} : { catalogTrackName: recoveredCatalogTrackName }
|
|
25435
26033
|
});
|
|
25436
26034
|
continue;
|
|
@@ -25567,7 +26165,7 @@ var MmtpRouter = class {
|
|
|
25567
26165
|
rapFlag: sym.fragment.rapFlag,
|
|
25568
26166
|
recovered: true,
|
|
25569
26167
|
deliveryProvenance,
|
|
25570
|
-
|
|
26168
|
+
...fecSbnOf(sym.index),
|
|
25571
26169
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
25572
26170
|
});
|
|
25573
26171
|
} else {
|
|
@@ -25646,7 +26244,7 @@ var MmtpRouter = class {
|
|
|
25646
26244
|
rapFlag: assembled.rapFlag,
|
|
25647
26245
|
recovered: true,
|
|
25648
26246
|
deliveryProvenance,
|
|
25649
|
-
|
|
26247
|
+
...fecSbnOf(sym.index),
|
|
25650
26248
|
...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName }
|
|
25651
26249
|
});
|
|
25652
26250
|
}
|
|
@@ -25681,7 +26279,7 @@ var MmtpRouter = class {
|
|
|
25681
26279
|
...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
|
|
25682
26280
|
recovered: true,
|
|
25683
26281
|
deliveryProvenance,
|
|
25684
|
-
|
|
26282
|
+
...fecSbnOf(sym.index),
|
|
25685
26283
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
25686
26284
|
});
|
|
25687
26285
|
continue;
|
|
@@ -25998,11 +26596,8 @@ function providerErrorOf(error) {
|
|
|
25998
26596
|
return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
|
|
25999
26597
|
}
|
|
26000
26598
|
var MAX_TRACKED_DROPPED_PIDS = 16;
|
|
26001
|
-
function getMulticastGlobals() {
|
|
26002
|
-
return globalThis;
|
|
26003
|
-
}
|
|
26004
26599
|
function hasMulticastGateway() {
|
|
26005
|
-
return typeof
|
|
26600
|
+
return typeof resolveMulticastGateway()?.subscribe === "function";
|
|
26006
26601
|
}
|
|
26007
26602
|
function senderOf(event) {
|
|
26008
26603
|
const sourceAddress = event.sourceAddress;
|
|
@@ -26094,6 +26689,9 @@ var MulticastTransport = class {
|
|
|
26094
26689
|
// listener closure, so an in-place admission change (setContext) applies
|
|
26095
26690
|
// from the next packet on without rejoining.
|
|
26096
26691
|
#context = void 0;
|
|
26692
|
+
#deliveredContext = void 0;
|
|
26693
|
+
/** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
|
|
26694
|
+
#userAgentGateway = void 0;
|
|
26097
26695
|
// The live join's provider-side filter. An in-place widening must reach it,
|
|
26098
26696
|
// or the provider never delivers the new ID; null without a live join.
|
|
26099
26697
|
#providerFilter = null;
|
|
@@ -26130,7 +26728,7 @@ var MulticastTransport = class {
|
|
|
26130
26728
|
this.#dataPlaneLive = false;
|
|
26131
26729
|
this.#sender = void 0;
|
|
26132
26730
|
if (this.#disposed) return this.#declined(generation, "disposed");
|
|
26133
|
-
const gateway = this.#gateway ??
|
|
26731
|
+
const gateway = this.#gateway ?? resolveMulticastGateway();
|
|
26134
26732
|
if (typeof gateway?.subscribe !== "function") {
|
|
26135
26733
|
console.warn("[MulticastTransport] Multicast API not available");
|
|
26136
26734
|
return this.#declined(generation, "unavailable");
|
|
@@ -26146,15 +26744,16 @@ var MulticastTransport = class {
|
|
|
26146
26744
|
console.log(
|
|
26147
26745
|
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
|
|
26148
26746
|
);
|
|
26747
|
+
const userAgent = isUserAgentVerifiedGateway(gateway);
|
|
26149
26748
|
const subscribeConfig = toWindowMulticastSubscribeConfig({
|
|
26150
26749
|
source: config.sourceAddress,
|
|
26151
26750
|
senderAddress: config.senderAddress,
|
|
26152
26751
|
group: config.groupAddress,
|
|
26153
26752
|
port: config.port,
|
|
26154
|
-
mode: config.mode,
|
|
26155
|
-
amtRelay: config.amtRelay,
|
|
26156
|
-
amtRelayPort: config.amtRelayPort,
|
|
26157
|
-
amtDiscovery: config.amtDiscovery,
|
|
26753
|
+
mode: userAgent ? "native" : config.mode,
|
|
26754
|
+
amtRelay: userAgent ? void 0 : config.amtRelay,
|
|
26755
|
+
amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
|
|
26756
|
+
amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
|
|
26158
26757
|
packetIds: config.packetIds,
|
|
26159
26758
|
tracks: config.tracks,
|
|
26160
26759
|
atsc3: config.atsc3
|
|
@@ -26200,7 +26799,8 @@ var MulticastTransport = class {
|
|
|
26200
26799
|
acknowledgedUpdate: 0,
|
|
26201
26800
|
inFlight: 0
|
|
26202
26801
|
};
|
|
26203
|
-
this.#
|
|
26802
|
+
this.#userAgentGateway = userAgent ? gateway : void 0;
|
|
26803
|
+
this.#setContext(context);
|
|
26204
26804
|
if (packetIds) {
|
|
26205
26805
|
for (const packetId of packetIds) {
|
|
26206
26806
|
if (!this.#acceptedBytesByPacketId.has(packetId)) {
|
|
@@ -26208,7 +26808,7 @@ var MulticastTransport = class {
|
|
|
26208
26808
|
}
|
|
26209
26809
|
}
|
|
26210
26810
|
}
|
|
26211
|
-
this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
26811
|
+
this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
26212
26812
|
const delivery = subscription.delivery ?? "datagram";
|
|
26213
26813
|
if (delivery !== "datagram" && delivery !== "ordered-relay") {
|
|
26214
26814
|
throw new Error(
|
|
@@ -26289,7 +26889,7 @@ var MulticastTransport = class {
|
|
|
26289
26889
|
}
|
|
26290
26890
|
if (!this.#acceptPacket(packet, sourceAddress)) return false;
|
|
26291
26891
|
const liveness = !this.#isRecordCandidate(packet);
|
|
26292
|
-
this.#onPacket(packet, this.#
|
|
26892
|
+
this.#onPacket(packet, this.#deliveredContext);
|
|
26293
26893
|
return liveness;
|
|
26294
26894
|
}
|
|
26295
26895
|
#isRecordCandidate(packet) {
|
|
@@ -26500,8 +27100,10 @@ var MulticastTransport = class {
|
|
|
26500
27100
|
return this.#subscription !== null;
|
|
26501
27101
|
}
|
|
26502
27102
|
/**
|
|
26503
|
-
*
|
|
26504
|
-
*
|
|
27103
|
+
* The live join's context as the caller gave it (the join's `context`, or
|
|
27104
|
+
* the last setContext), or undefined without a live join. On a native
|
|
27105
|
+
* provider each accepted packet is delivered with a marked copy of it
|
|
27106
|
+
* (`userAgentVerifiedIngress`); this getter never carries the mark.
|
|
26505
27107
|
*/
|
|
26506
27108
|
get context() {
|
|
26507
27109
|
return this.#context;
|
|
@@ -26525,7 +27127,16 @@ var MulticastTransport = class {
|
|
|
26525
27127
|
"[MulticastTransport] setContext needs a live join; join() carries the first context"
|
|
26526
27128
|
);
|
|
26527
27129
|
}
|
|
27130
|
+
this.#setContext(context);
|
|
27131
|
+
}
|
|
27132
|
+
/**
|
|
27133
|
+
* The join's context, and what the subscription delivers with each packet:
|
|
27134
|
+
* marked admitted by the user agent when the gateway is its native
|
|
27135
|
+
* MulticastReceiver provider. feedPacket never marks.
|
|
27136
|
+
*/
|
|
27137
|
+
#setContext(context) {
|
|
26528
27138
|
this.#context = context;
|
|
27139
|
+
this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
|
|
26529
27140
|
}
|
|
26530
27141
|
/**
|
|
26531
27142
|
* Replace the live join's packet-ID allow-list in place, without rejoining.
|
|
@@ -26657,6 +27268,8 @@ var MulticastTransport = class {
|
|
|
26657
27268
|
this.#matchedSource = void 0;
|
|
26658
27269
|
this.#providerFilter = null;
|
|
26659
27270
|
this.#context = void 0;
|
|
27271
|
+
this.#deliveredContext = void 0;
|
|
27272
|
+
this.#userAgentGateway = void 0;
|
|
26660
27273
|
this.#withdrawnPacketIds.clear();
|
|
26661
27274
|
}
|
|
26662
27275
|
/** The ingress filter; an accepted packet is counted here, once. */
|
|
@@ -26860,7 +27473,7 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
|
|
|
26860
27473
|
repairsSkippedRangeNoAnchor: 0,
|
|
26861
27474
|
repairsSkippedGroupCannotComplete: 0,
|
|
26862
27475
|
repairsSkippedMootRap: 0,
|
|
26863
|
-
repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
|
|
27476
|
+
repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0, "overlay-recovered": 0 },
|
|
26864
27477
|
lastRepairSkip: null,
|
|
26865
27478
|
keyframeRepairFetchesSent: 0,
|
|
26866
27479
|
rangeRepairFetchesSent: 0,
|
|
@@ -27193,9 +27806,7 @@ function repairTrackNamesBySource(tracks) {
|
|
|
27193
27806
|
}
|
|
27194
27807
|
function boundRepairNames(binding) {
|
|
27195
27808
|
if (binding?.status !== "ok") return [];
|
|
27196
|
-
|
|
27197
|
-
if (binding.overlay) names.push(binding.overlay.repairTrackName);
|
|
27198
|
-
return names;
|
|
27809
|
+
return binding.layers.map((layer) => layer.repairTrackName);
|
|
27199
27810
|
}
|
|
27200
27811
|
var MulticastManager = class {
|
|
27201
27812
|
#fec;
|
|
@@ -27209,6 +27820,10 @@ var MulticastManager = class {
|
|
|
27209
27820
|
/** Live joinRepairEndpoints() releases → the repair owners they registered,
|
|
27210
27821
|
* re-applied after configureMultiTrack() clears catalog-derived owners. */
|
|
27211
27822
|
#repairReleases = /* @__PURE__ */ new Map();
|
|
27823
|
+
/** Each source's catalog keyframe overlay, from its `ok` binding. */
|
|
27824
|
+
#overlayBindings = /* @__PURE__ */ new Map();
|
|
27825
|
+
/** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
|
|
27826
|
+
#overlayInstalls = /* @__PURE__ */ new Map();
|
|
27212
27827
|
#reportedFecDiagnosticsFailure = false;
|
|
27213
27828
|
// ── Stream-global FEC timing reduction ──
|
|
27214
27829
|
//
|
|
@@ -27449,6 +28064,63 @@ var MulticastManager = class {
|
|
|
27449
28064
|
this.#configureTrackMapper(packetId, fecConfig);
|
|
27450
28065
|
this.#router.setPacketIdType(packetId, trackType);
|
|
27451
28066
|
}
|
|
28067
|
+
/**
|
|
28068
|
+
* Decode the keyframe overlay of `sourcePacketId` (draft-ramadan-mmt-fec-02
|
|
28069
|
+
* 7.4) with `decoder`, a decoder of the same kind `setFecDecoder` takes.
|
|
28070
|
+
* Opt-in: nothing else installs one, so a receiver that never calls this
|
|
28071
|
+
* holds no overlay state and drops overlay repair. K_overlay, T, scheme
|
|
28072
|
+
* and block profile come from the source's `ok` catalog binding, which
|
|
28073
|
+
* must declare an overlay (`configureMultiTrack` first). The overlay is
|
|
28074
|
+
* decoded on its own FEC state, apart from the base: no repair FETCH, no
|
|
28075
|
+
* repair lifecycle event and no base statistic for its blocks. A second
|
|
28076
|
+
* install on the same source replaces the first; the returned disposer
|
|
28077
|
+
* removes only the install it created. Call it on releasing the overlay
|
|
28078
|
+
* join.
|
|
28079
|
+
*/
|
|
28080
|
+
setKeyframeOverlayDecoder(sourcePacketId, decoder, catalogTrackName) {
|
|
28081
|
+
const binding = this.#overlayBindings.get(sourcePacketId);
|
|
28082
|
+
if (!binding || catalogTrackName !== void 0 && binding.sourceCatalogTrackName !== catalogTrackName) {
|
|
28083
|
+
throw new Error(
|
|
28084
|
+
`[MulticastManager] packetId=${sourcePacketId} has no catalog keyframe overlay to decode`
|
|
28085
|
+
);
|
|
28086
|
+
}
|
|
28087
|
+
this.#overlayInstalls.get(sourcePacketId)?.dispose();
|
|
28088
|
+
let fec;
|
|
28089
|
+
let uninstall;
|
|
28090
|
+
const attach = () => {
|
|
28091
|
+
fec = new FECManager();
|
|
28092
|
+
fec.setFecDecoder(binding.repairPacketId, decoder, binding.k, {
|
|
28093
|
+
blockProfile: binding.blockProfile,
|
|
28094
|
+
k: binding.k,
|
|
28095
|
+
symbolSize: binding.symbolSize,
|
|
28096
|
+
trackKind: "video"
|
|
28097
|
+
}, binding.repairTrackName);
|
|
28098
|
+
uninstall = this.#router.installKeyframeOverlay(
|
|
28099
|
+
binding.repairPacketId,
|
|
28100
|
+
fec,
|
|
28101
|
+
binding.k,
|
|
28102
|
+
binding.repairTrackName
|
|
28103
|
+
);
|
|
28104
|
+
};
|
|
28105
|
+
const detach = () => {
|
|
28106
|
+
uninstall();
|
|
28107
|
+
fec.releasePacket(binding.repairPacketId);
|
|
28108
|
+
};
|
|
28109
|
+
attach();
|
|
28110
|
+
const install = {
|
|
28111
|
+
reattach: () => {
|
|
28112
|
+
detach();
|
|
28113
|
+
attach();
|
|
28114
|
+
},
|
|
28115
|
+
dispose: () => {
|
|
28116
|
+
if (this.#overlayInstalls.get(sourcePacketId) !== install) return;
|
|
28117
|
+
this.#overlayInstalls.delete(sourcePacketId);
|
|
28118
|
+
detach();
|
|
28119
|
+
}
|
|
28120
|
+
};
|
|
28121
|
+
this.#overlayInstalls.set(sourcePacketId, install);
|
|
28122
|
+
return install.dispose;
|
|
28123
|
+
}
|
|
27452
28124
|
/**
|
|
27453
28125
|
* Register a Worker-backed FEC decoder for a specific packetId (track).
|
|
27454
28126
|
* Worker-backed tracks decode asynchronously — recovered frames arrive via
|
|
@@ -27950,8 +28622,17 @@ var MulticastManager = class {
|
|
|
27950
28622
|
* repair roles and owners are registered here; in-band owners stay the
|
|
27951
28623
|
* caller's. Joining is best effort: a group that fails to join is logged
|
|
27952
28624
|
* and leaves the media join as it was.
|
|
27953
|
-
|
|
27954
|
-
|
|
28625
|
+
*
|
|
28626
|
+
* `options.gateway` names the gateway a subscribe callback reads through.
|
|
28627
|
+
* When it is the browser's own MulticastReceiver provider, the callback's
|
|
28628
|
+
* packets are fed as admitted by the user agent
|
|
28629
|
+
* (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
|
|
28630
|
+
* The mark rests on this statement, not on the data path: every packet the
|
|
28631
|
+
* callback delivers must come from a subscription opened on
|
|
28632
|
+
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
28633
|
+
* WebSocket repair feed) must not name a native provider here.
|
|
28634
|
+
*/
|
|
28635
|
+
joinRepairEndpoints(endpoints, provider, options) {
|
|
27955
28636
|
const releases = [];
|
|
27956
28637
|
let released = false;
|
|
27957
28638
|
const safely = (each) => {
|
|
@@ -27961,7 +28642,11 @@ var MulticastManager = class {
|
|
|
27961
28642
|
console.warn("[MulticastManager] repair group release failed", error);
|
|
27962
28643
|
}
|
|
27963
28644
|
};
|
|
27964
|
-
const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners)
|
|
28645
|
+
const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
|
|
28646
|
+
...owner,
|
|
28647
|
+
ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
|
|
28648
|
+
repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
|
|
28649
|
+
}));
|
|
27965
28650
|
this.#applyRepairOwners(repairOwners);
|
|
27966
28651
|
for (const endpoint of endpoints) {
|
|
27967
28652
|
const ingress = {
|
|
@@ -27969,17 +28654,19 @@ var MulticastManager = class {
|
|
|
27969
28654
|
...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
|
|
27970
28655
|
};
|
|
27971
28656
|
let sender;
|
|
27972
|
-
const
|
|
28657
|
+
const feed = (packet, sourceAddress, ingressToFeed) => {
|
|
27973
28658
|
if (released) return;
|
|
27974
28659
|
if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
|
|
27975
|
-
this.#router.feedPacket(packet, "multicast",
|
|
28660
|
+
this.#router.feedPacket(packet, "multicast", ingressToFeed);
|
|
27976
28661
|
};
|
|
27977
28662
|
const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
|
|
27978
28663
|
if (typeof provider === "function") {
|
|
28664
|
+
const marked = userAgentVerifiedIngress(ingress, options?.gateway);
|
|
28665
|
+
const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
|
|
27979
28666
|
Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
|
|
27980
28667
|
continue;
|
|
27981
28668
|
}
|
|
27982
|
-
const transport = new MulticastTransport((packet) =>
|
|
28669
|
+
const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
|
|
27983
28670
|
gateway: provider,
|
|
27984
28671
|
lockedTo: () => this.#transport.sender
|
|
27985
28672
|
});
|
|
@@ -27995,7 +28682,7 @@ var MulticastManager = class {
|
|
|
27995
28682
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
27996
28683
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
27997
28684
|
...endpoint.flow === void 0 ? {} : { protected: true }
|
|
27998
|
-
}).then((joined) => {
|
|
28685
|
+
}, ingress).then((joined) => {
|
|
27999
28686
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
28000
28687
|
});
|
|
28001
28688
|
}
|
|
@@ -28009,9 +28696,17 @@ var MulticastManager = class {
|
|
|
28009
28696
|
return release;
|
|
28010
28697
|
}
|
|
28011
28698
|
#applyRepairOwners(owners) {
|
|
28012
|
-
for (const { packetId, ownerPacketId } of owners) {
|
|
28699
|
+
for (const { packetId, ownerPacketId, instance, ownerTrackName, repairTrackName } of owners) {
|
|
28013
28700
|
this.#router.setPacketIdRole(packetId, "repair");
|
|
28014
|
-
|
|
28701
|
+
if (instance === "keyframe") {
|
|
28702
|
+
this.#router.setKeyframeOverlayLeg(packetId, {
|
|
28703
|
+
sourcePacketId: ownerPacketId,
|
|
28704
|
+
...ownerTrackName === void 0 ? {} : { sourceCatalogTrackName: ownerTrackName },
|
|
28705
|
+
...repairTrackName === void 0 ? {} : { repairTrackName }
|
|
28706
|
+
});
|
|
28707
|
+
} else {
|
|
28708
|
+
this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
|
|
28709
|
+
}
|
|
28015
28710
|
}
|
|
28016
28711
|
}
|
|
28017
28712
|
// ---------------------------------------------------------------------------
|
|
@@ -28275,8 +28970,8 @@ var MulticastManager = class {
|
|
|
28275
28970
|
this.#fec.setMulticastSymbolDelivery(delivery);
|
|
28276
28971
|
}
|
|
28277
28972
|
/** Process a repair packet from MoQ (reliable QUIC). */
|
|
28278
|
-
processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
|
|
28279
|
-
this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
|
|
28973
|
+
processRepairFromMoQ(packet, ownerPacketId, catalogTrackName, repairTrackName) {
|
|
28974
|
+
this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName, repairTrackName);
|
|
28280
28975
|
}
|
|
28281
28976
|
/** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
|
|
28282
28977
|
routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
|
|
@@ -28328,7 +29023,10 @@ var MulticastManager = class {
|
|
|
28328
29023
|
const ownedRepairNames = /* @__PURE__ */ new Set();
|
|
28329
29024
|
const claimed = /* @__PURE__ */ new Map();
|
|
28330
29025
|
for (const binding of bindings.values()) {
|
|
28331
|
-
|
|
29026
|
+
const overlay = binding.status === "ok" ? binding.overlay?.repairTrackName : void 0;
|
|
29027
|
+
for (const name of [...boundRepairNames(binding), ...overlay === void 0 ? [] : [overlay]]) {
|
|
29028
|
+
claimed.set(name, (claimed.get(name) ?? 0) + 1);
|
|
29029
|
+
}
|
|
28332
29030
|
}
|
|
28333
29031
|
for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
|
|
28334
29032
|
const repairOwnerGroup = /* @__PURE__ */ new Map();
|
|
@@ -28431,6 +29129,7 @@ var MulticastManager = class {
|
|
|
28431
29129
|
const catalogTracksByName = new Map(
|
|
28432
29130
|
catalog.tracks.map((track) => [track.name, track])
|
|
28433
29131
|
);
|
|
29132
|
+
const overlayBindings = /* @__PURE__ */ new Map();
|
|
28434
29133
|
for (let i = 0; i < catalog.tracks.length; i++) {
|
|
28435
29134
|
const track = catalog.tracks[i];
|
|
28436
29135
|
const role = resolvedRoles[i];
|
|
@@ -28459,6 +29158,29 @@ var MulticastManager = class {
|
|
|
28459
29158
|
this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
|
|
28460
29159
|
}
|
|
28461
29160
|
}
|
|
29161
|
+
const overlayName = binding.overlay?.repairTrackName;
|
|
29162
|
+
const overlayPacketId = overlayName === void 0 ? void 0 : catalogTracksByName.get(overlayName)?.packetId;
|
|
29163
|
+
if (overlayName !== void 0 && ownedRepairNames.has(overlayName)) {
|
|
29164
|
+
this.#router.setKeyframeOverlayLeg(overlayPacketId, {
|
|
29165
|
+
sourcePacketId: track.packetId,
|
|
29166
|
+
sourceCatalogTrackName: track.name,
|
|
29167
|
+
repairTrackName: overlayName
|
|
29168
|
+
});
|
|
29169
|
+
}
|
|
29170
|
+
if (overlayName !== void 0 && overlayPacketId !== void 0 && ownedRepairNames.has(overlayName)) {
|
|
29171
|
+
const geometry2 = binding.overlay.geometry;
|
|
29172
|
+
overlayBindings.set(track.packetId, {
|
|
29173
|
+
repairPacketId: overlayPacketId,
|
|
29174
|
+
repairTrackName: overlayName,
|
|
29175
|
+
sourceCatalogTrackName: track.name,
|
|
29176
|
+
k: binding.overlay.sourceSymbols,
|
|
29177
|
+
symbolSize: fecCfg.symbolSize,
|
|
29178
|
+
blockProfile: parseFecBlockProfile(
|
|
29179
|
+
geometry2.blockProfile,
|
|
29180
|
+
`tracks[${JSON.stringify(track.name)}].fec keyframe overlay blockProfile`
|
|
29181
|
+
)
|
|
29182
|
+
});
|
|
29183
|
+
}
|
|
28462
29184
|
}
|
|
28463
29185
|
const wasmDecoder = fecDecoders.get(i);
|
|
28464
29186
|
if (track.packetId !== void 0 && wasmDecoder !== void 0) {
|
|
@@ -28492,6 +29214,16 @@ var MulticastManager = class {
|
|
|
28492
29214
|
}
|
|
28493
29215
|
}
|
|
28494
29216
|
}
|
|
29217
|
+
for (const [sourcePacketId, install] of [...this.#overlayInstalls]) {
|
|
29218
|
+
const before = this.#overlayBindings.get(sourcePacketId);
|
|
29219
|
+
const after = overlayBindings.get(sourcePacketId);
|
|
29220
|
+
const same = before !== void 0 && after !== void 0 && Object.keys(after).every((key) => after[key] === before[key]);
|
|
29221
|
+
if (!same) install.dispose();
|
|
29222
|
+
else if (this.#router.hasKeyframeOverlay(after.repairPacketId)) this.#router.resetKeyframeOverlays(sourcePacketId);
|
|
29223
|
+
else install.reattach();
|
|
29224
|
+
}
|
|
29225
|
+
this.#overlayBindings.clear();
|
|
29226
|
+
for (const [sourcePacketId, binding] of overlayBindings) this.#overlayBindings.set(sourcePacketId, binding);
|
|
28495
29227
|
}
|
|
28496
29228
|
/**
|
|
28497
29229
|
* Why `configureMultiTrack` consumes the named catalog track without FEC
|
|
@@ -28663,6 +29395,10 @@ var MulticastManager = class {
|
|
|
28663
29395
|
addRapCompleteObserver(observer) {
|
|
28664
29396
|
return this.#router.addFecRapObserver(observer);
|
|
28665
29397
|
}
|
|
29398
|
+
/** Observe each base block whose RAP the keyframe overlay recovered (fec-multicast-00 §6.6). */
|
|
29399
|
+
addKeyframeOverlayRecoveredObserver(observer) {
|
|
29400
|
+
return this.#router.addKeyframeOverlayRecoveredObserver(observer);
|
|
29401
|
+
}
|
|
28666
29402
|
/** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
|
|
28667
29403
|
epochFor(packetId) {
|
|
28668
29404
|
return this.#fec.epochFor(packetId);
|
|
@@ -31754,12 +32490,27 @@ function wireRepairFetch(mc, opts) {
|
|
|
31754
32490
|
dispatch();
|
|
31755
32491
|
});
|
|
31756
32492
|
});
|
|
32493
|
+
const removeOverlayObserver = mc.addKeyframeOverlayRecoveredObserver?.((packetId, catalogTrackName, sbn) => {
|
|
32494
|
+
queueMicrotask(() => {
|
|
32495
|
+
if (disposed) return;
|
|
32496
|
+
for (const entry of [...entries.values()]) {
|
|
32497
|
+
if (entry.range || entry.priority !== 0 || entry.packetId !== packetId || entry.sbn !== sbn) continue;
|
|
32498
|
+
if (catalogTrackName !== void 0 && entry.catalogTrackName !== catalogTrackName) continue;
|
|
32499
|
+
stop(entry, "overlay-recovered");
|
|
32500
|
+
retire(entry);
|
|
32501
|
+
releaseHold(entry);
|
|
32502
|
+
clearEntry(entry);
|
|
32503
|
+
}
|
|
32504
|
+
dispatch();
|
|
32505
|
+
});
|
|
32506
|
+
});
|
|
31757
32507
|
return () => {
|
|
31758
32508
|
disposed = true;
|
|
31759
32509
|
mc.setUnicastRepair?.(false);
|
|
31760
32510
|
removeResetObserver();
|
|
31761
32511
|
removeLifecycleObserver();
|
|
31762
32512
|
removeRapObserver?.();
|
|
32513
|
+
removeOverlayObserver?.();
|
|
31763
32514
|
for (const entry of [...entries.values()]) {
|
|
31764
32515
|
clearEntry(entry);
|
|
31765
32516
|
if (entry.running) releaseHold(entry);
|
|
@@ -31793,6 +32544,7 @@ export {
|
|
|
31793
32544
|
HoldLastTimeline,
|
|
31794
32545
|
JOIN_DEFER_WARN_MS,
|
|
31795
32546
|
KEYFRAME_LOST_TIMEOUT_SLACK,
|
|
32547
|
+
KEYFRAME_OVERLAY_EXTENSION_TYPE,
|
|
31796
32548
|
LOCHeaderExtensionId,
|
|
31797
32549
|
MAX_ENCODING_SYMBOL_ID,
|
|
31798
32550
|
MAX_RAPTORQ_SOURCE_SYMBOLS,
|
|
@@ -31902,6 +32654,7 @@ export {
|
|
|
31902
32654
|
isTrackTimingConfig,
|
|
31903
32655
|
joinedRepairSymbolCount,
|
|
31904
32656
|
keyframeIntervalMediaTicks,
|
|
32657
|
+
keyframeOverlaySsId,
|
|
31905
32658
|
mergeOverrideIntoCmsfTracks,
|
|
31906
32659
|
mergeOverrideIntoHangRenditions,
|
|
31907
32660
|
mfuReassemblyTimeoutMs,
|