@blockcast/mmt-container 2.1.0 → 2.2.0-main.02321582f1ca
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -17
- package/dist/browser-runtime.js +1302 -249
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +213 -6
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +37 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +98 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +25 -2
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1749 -56
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +15 -2
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15683 -14941
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +256 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +96 -8
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +414 -29
- package/src/multicast-manager.ts +26 -5
- package/src/multicast-transport.ts +44 -20
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
package/dist/browser-runtime.js
CHANGED
|
@@ -489,6 +489,8 @@ var PACKET_TYPE_SIGNALING = 2;
|
|
|
489
489
|
var PACKET_TYPE_REPAIR = 3;
|
|
490
490
|
var MMTP_SIGNALING_PACKET_ID = 0;
|
|
491
491
|
var MMTP_PACKET_ID_MAX = 65535;
|
|
492
|
+
var FEC_CODE_REED_SOLOMON = 1;
|
|
493
|
+
var FEC_CODE_RAPTORQ = 3;
|
|
492
494
|
function parseMMTPHeader(buffer) {
|
|
493
495
|
if (buffer.length < MMTP_HEADER_SIZE) {
|
|
494
496
|
throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
|
|
@@ -655,30 +657,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
|
|
|
655
657
|
const authLength = view2.getUint16(2, false);
|
|
656
658
|
return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
|
|
657
659
|
}
|
|
658
|
-
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options
|
|
660
|
+
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
|
|
659
661
|
if (declaredEnd > payload.length)
|
|
660
662
|
return null;
|
|
661
663
|
if (options?.skipFecId) {
|
|
662
|
-
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false
|
|
664
|
+
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
|
|
663
665
|
}
|
|
664
|
-
const overlay = options?.overlayDeclared === true && rapFlag;
|
|
665
666
|
if (fecType === 1) {
|
|
666
|
-
const fecStart = payload.length -
|
|
667
|
+
const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
667
668
|
if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
|
|
668
669
|
return null;
|
|
669
670
|
}
|
|
670
|
-
return { hasFecId: true
|
|
671
|
+
return { hasFecId: true };
|
|
671
672
|
}
|
|
672
|
-
if (fecType !== 0
|
|
673
|
+
if (fecType !== 0)
|
|
673
674
|
return null;
|
|
674
675
|
if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
|
|
675
|
-
return { hasFecId: false
|
|
676
|
+
return { hasFecId: false };
|
|
676
677
|
}
|
|
677
678
|
return null;
|
|
678
679
|
}
|
|
679
|
-
function sourceFecTrailerSize(overlay) {
|
|
680
|
-
return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
681
|
-
}
|
|
682
680
|
function extractNALUnit(mmtpPacket) {
|
|
683
681
|
const extraction = extractMFUPayload(mmtpPacket);
|
|
684
682
|
if (!extraction)
|
|
@@ -737,11 +735,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
|
737
735
|
if ("rejection" in layoutResult)
|
|
738
736
|
return layoutResult;
|
|
739
737
|
const { layout } = layoutResult;
|
|
740
|
-
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options
|
|
738
|
+
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
|
|
741
739
|
if (!trailerLayout) {
|
|
742
740
|
if (!options?.skipFecId && header.fecType === 1) {
|
|
743
741
|
const observed = payload.length - layout.dataEnd;
|
|
744
|
-
const expected =
|
|
742
|
+
const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
745
743
|
if (observed !== expected) {
|
|
746
744
|
return {
|
|
747
745
|
rejection: {
|
|
@@ -776,13 +774,10 @@ function extractPlannedMfu(mmtpPacket, result) {
|
|
|
776
774
|
const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
|
|
777
775
|
const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
|
|
778
776
|
const data = payload.subarray(mfuOffset, dataEnd);
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
|
|
784
|
-
const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
|
|
785
|
-
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
|
|
777
|
+
const fecPayloadId = trailerLayout.hasFecId ? {
|
|
778
|
+
ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
|
|
779
|
+
} : null;
|
|
780
|
+
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
|
|
786
781
|
}
|
|
787
782
|
function extractTimestamp(mmtpPacket) {
|
|
788
783
|
const { header } = unwrapMMTP(mmtpPacket);
|
|
@@ -903,6 +898,50 @@ var NTP_SHORT_BITS = 32;
|
|
|
903
898
|
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
904
899
|
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
905
900
|
|
|
901
|
+
// ../mmt-base/dist/signalling.js
|
|
902
|
+
var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
|
|
903
|
+
var SSBG_MODE_1 = 1;
|
|
904
|
+
var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
|
|
905
|
+
var REPAIR_SYMBOL_ID_MAX = 16777215;
|
|
906
|
+
var MIN_REPAIR_SYMBOL_SIZE = 14;
|
|
907
|
+
var REED_SOLOMON_MAX_BLOCK = 255;
|
|
908
|
+
function alFecRaptorQFlowConfig(flow) {
|
|
909
|
+
if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
|
|
910
|
+
return null;
|
|
911
|
+
if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
|
|
912
|
+
return null;
|
|
913
|
+
if (flow.packetIds.length !== 1)
|
|
914
|
+
return null;
|
|
915
|
+
const repair = flow.repairFlows[0];
|
|
916
|
+
const { fecCodeId, maximumK: k, maximumP: p } = repair;
|
|
917
|
+
const t = flow.repairSymbolSize;
|
|
918
|
+
if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
|
|
919
|
+
return null;
|
|
920
|
+
if (fecCodeId === FEC_CODE_RAPTORQ) {
|
|
921
|
+
if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
|
|
922
|
+
return null;
|
|
923
|
+
} else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
|
|
924
|
+
if (k + p > REED_SOLOMON_MAX_BLOCK)
|
|
925
|
+
return null;
|
|
926
|
+
} else {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
|
|
930
|
+
return null;
|
|
931
|
+
return {
|
|
932
|
+
sourcePacketIds: [...flow.packetIds],
|
|
933
|
+
repairFlowId: repair.repairFlowId,
|
|
934
|
+
fecCodeId,
|
|
935
|
+
ssbgMode: flow.ssbgMode,
|
|
936
|
+
fecPayloadIdMode: flow.fecPayloadIdMode,
|
|
937
|
+
repairSymbolSize: t,
|
|
938
|
+
maximumK: k,
|
|
939
|
+
maximumP: p,
|
|
940
|
+
protectionWindowTime: repair.protectionWindowTime,
|
|
941
|
+
protectionWindowSize: repair.protectionWindowSize
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
906
945
|
// ../mmt-base/dist/serial-number.js
|
|
907
946
|
var SERIAL32_SPAN = 4294967296;
|
|
908
947
|
var SERIAL32_HALF = 2147483648;
|
|
@@ -7457,11 +7496,33 @@ var AdaptiveBitrateController = class {
|
|
|
7457
7496
|
};
|
|
7458
7497
|
|
|
7459
7498
|
// node_modules/@blockcast/mmt-base/dist/mmtp.js
|
|
7499
|
+
var MMTP_HEADER_SIZE2 = 12;
|
|
7460
7500
|
var MPU_HEADER_SIZE2 = 8;
|
|
7461
7501
|
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
7462
7502
|
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
7463
7503
|
var MMTP_SIGNALING_PACKET_ID2 = 0;
|
|
7464
7504
|
var MMTP_PACKET_ID_MAX2 = 65535;
|
|
7505
|
+
function readMMTPPacketId2(buffer) {
|
|
7506
|
+
if (buffer.length < MMTP_HEADER_SIZE2)
|
|
7507
|
+
return null;
|
|
7508
|
+
const flags = buffer[0];
|
|
7509
|
+
if ((flags >> 6 & 3) !== 0)
|
|
7510
|
+
return null;
|
|
7511
|
+
let offset = MMTP_HEADER_SIZE2;
|
|
7512
|
+
if ((flags & 32) !== 0) {
|
|
7513
|
+
if (offset + 4 > buffer.length)
|
|
7514
|
+
return null;
|
|
7515
|
+
offset += 4;
|
|
7516
|
+
}
|
|
7517
|
+
if ((flags & 2) !== 0) {
|
|
7518
|
+
if (offset + 4 > buffer.length)
|
|
7519
|
+
return null;
|
|
7520
|
+
const length = buffer[offset + 2] << 8 | buffer[offset + 3];
|
|
7521
|
+
if (offset + 4 + length > buffer.length)
|
|
7522
|
+
return null;
|
|
7523
|
+
}
|
|
7524
|
+
return buffer[2] << 8 | buffer[3];
|
|
7525
|
+
}
|
|
7465
7526
|
|
|
7466
7527
|
// node_modules/@blockcast/mmt-base/dist/net-constants.js
|
|
7467
7528
|
var DEFAULT_AMT_RELAY_PORT2 = 2268;
|
|
@@ -7927,8 +7988,8 @@ var MulticastSubscriptionPool = class {
|
|
|
7927
7988
|
record.claims--;
|
|
7928
7989
|
return this.acquire(gateway, config, consumer);
|
|
7929
7990
|
}
|
|
7930
|
-
const
|
|
7931
|
-
entry.consumers.set(
|
|
7991
|
+
const token2 = Symbol(key);
|
|
7992
|
+
entry.consumers.set(token2, consumer);
|
|
7932
7993
|
let released = false;
|
|
7933
7994
|
return {
|
|
7934
7995
|
key,
|
|
@@ -7936,7 +7997,7 @@ var MulticastSubscriptionPool = class {
|
|
|
7936
7997
|
if (released)
|
|
7937
7998
|
return;
|
|
7938
7999
|
released = true;
|
|
7939
|
-
entry.consumers.delete(
|
|
8000
|
+
entry.consumers.delete(token2);
|
|
7940
8001
|
record.claims--;
|
|
7941
8002
|
if (record.claims > 0)
|
|
7942
8003
|
return;
|
|
@@ -8447,6 +8508,205 @@ var TransportManager = class {
|
|
|
8447
8508
|
};
|
|
8448
8509
|
var transportManager = new TransportManager();
|
|
8449
8510
|
|
|
8511
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
|
|
8512
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
|
|
8513
|
+
var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
|
|
8514
|
+
var INTEGRITY_SCHEME = "manifest-ed25519-2";
|
|
8515
|
+
var MulticastError = class extends Error {
|
|
8516
|
+
code;
|
|
8517
|
+
constructor(code, message, cause) {
|
|
8518
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
8519
|
+
this.name = "MulticastError";
|
|
8520
|
+
this.code = code;
|
|
8521
|
+
}
|
|
8522
|
+
};
|
|
8523
|
+
function datagramsOf(chunk) {
|
|
8524
|
+
const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
|
|
8525
|
+
const out = [];
|
|
8526
|
+
for (const d of list) {
|
|
8527
|
+
const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
|
|
8528
|
+
if (data instanceof Uint8Array)
|
|
8529
|
+
out.push(data);
|
|
8530
|
+
else if (data instanceof ArrayBuffer)
|
|
8531
|
+
out.push(new Uint8Array(data));
|
|
8532
|
+
}
|
|
8533
|
+
return out;
|
|
8534
|
+
}
|
|
8535
|
+
function reasonOf(value) {
|
|
8536
|
+
return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
|
|
8537
|
+
}
|
|
8538
|
+
function registry() {
|
|
8539
|
+
const g = globalThis;
|
|
8540
|
+
if (g[REGISTRY] === void 0) {
|
|
8541
|
+
const providers = /* @__PURE__ */ new WeakSet();
|
|
8542
|
+
const byScope = /* @__PURE__ */ new WeakMap();
|
|
8543
|
+
Object.defineProperty(g, REGISTRY, {
|
|
8544
|
+
value: Object.freeze({
|
|
8545
|
+
isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
|
|
8546
|
+
providerFor: (scope) => {
|
|
8547
|
+
let provider = byScope.get(scope);
|
|
8548
|
+
if (provider === void 0) {
|
|
8549
|
+
provider = createProvider(scope);
|
|
8550
|
+
providers.add(provider);
|
|
8551
|
+
byScope.set(scope, provider);
|
|
8552
|
+
}
|
|
8553
|
+
return provider;
|
|
8554
|
+
}
|
|
8555
|
+
}),
|
|
8556
|
+
writable: false,
|
|
8557
|
+
configurable: false,
|
|
8558
|
+
enumerable: false
|
|
8559
|
+
});
|
|
8560
|
+
}
|
|
8561
|
+
return g[REGISTRY];
|
|
8562
|
+
}
|
|
8563
|
+
function resolveMulticastGateway(scope = globalThis) {
|
|
8564
|
+
const s = scope;
|
|
8565
|
+
if (typeof s.MulticastReceiver === "function")
|
|
8566
|
+
return registry().providerFor(scope);
|
|
8567
|
+
if (typeof s.multicast?.subscribe === "function")
|
|
8568
|
+
return s.multicast;
|
|
8569
|
+
return void 0;
|
|
8570
|
+
}
|
|
8571
|
+
function isUserAgentVerifiedGateway(gateway) {
|
|
8572
|
+
return registry().isProvider(gateway);
|
|
8573
|
+
}
|
|
8574
|
+
function userAgentVerifiedIngress(ingress, gateway) {
|
|
8575
|
+
if (isUserAgentVerifiedGateway(gateway))
|
|
8576
|
+
return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
|
|
8577
|
+
if (!("admittedBy" in ingress))
|
|
8578
|
+
return ingress;
|
|
8579
|
+
const { admittedBy: _, ...rest } = ingress;
|
|
8580
|
+
return rest;
|
|
8581
|
+
}
|
|
8582
|
+
function unsupported(message, cause) {
|
|
8583
|
+
return new MulticastError("capability-unsupported", message, cause);
|
|
8584
|
+
}
|
|
8585
|
+
function createProvider(scope) {
|
|
8586
|
+
const Receiver = scope.MulticastReceiver;
|
|
8587
|
+
let gate = null;
|
|
8588
|
+
return {
|
|
8589
|
+
capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
|
|
8590
|
+
subscribe: async (config) => {
|
|
8591
|
+
const c = config;
|
|
8592
|
+
if (c.consume !== "bytes")
|
|
8593
|
+
throw unsupported(`consume mode ${String(c.consume)} is not supported`);
|
|
8594
|
+
const origin = c.origin ?? "ip-multicast";
|
|
8595
|
+
if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
|
|
8596
|
+
throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
|
|
8597
|
+
}
|
|
8598
|
+
if (c.relay !== void 0)
|
|
8599
|
+
throw unsupported("MulticastReceiver picks its own AMT relay");
|
|
8600
|
+
if (!c.source)
|
|
8601
|
+
throw unsupported("MulticastReceiver needs an SSM source");
|
|
8602
|
+
if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
|
|
8603
|
+
throw unsupported("MulticastReceiver filters by source only");
|
|
8604
|
+
}
|
|
8605
|
+
const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
|
|
8606
|
+
if (gate !== null)
|
|
8607
|
+
await gate;
|
|
8608
|
+
let rx;
|
|
8609
|
+
try {
|
|
8610
|
+
rx = new Receiver(options);
|
|
8611
|
+
} catch (error) {
|
|
8612
|
+
throw unsupported("MulticastReceiver refused the request", error);
|
|
8613
|
+
}
|
|
8614
|
+
const opened = rx.opened;
|
|
8615
|
+
if (gate === null)
|
|
8616
|
+
gate = opened.then(() => void 0, () => void 0);
|
|
8617
|
+
return openSubscription(rx, opened, options, c.packetIds);
|
|
8618
|
+
}
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
8622
|
+
let opened;
|
|
8623
|
+
try {
|
|
8624
|
+
opened = await openedPromise;
|
|
8625
|
+
} catch (error) {
|
|
8626
|
+
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8627
|
+
}
|
|
8628
|
+
if (rx.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8629
|
+
await rx.close();
|
|
8630
|
+
throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8631
|
+
}
|
|
8632
|
+
const events = new EventTarget();
|
|
8633
|
+
let state = "connected";
|
|
8634
|
+
let filter = packetIds?.length ? new Set(packetIds) : null;
|
|
8635
|
+
let closing = false;
|
|
8636
|
+
const setState = (next) => {
|
|
8637
|
+
if (state === next)
|
|
8638
|
+
return;
|
|
8639
|
+
state = next;
|
|
8640
|
+
events.dispatchEvent(new CustomEvent("state", { detail: next }));
|
|
8641
|
+
};
|
|
8642
|
+
const sourceAddress = options.sourceAddress;
|
|
8643
|
+
const subscription = {
|
|
8644
|
+
id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
|
|
8645
|
+
get state() {
|
|
8646
|
+
return state;
|
|
8647
|
+
},
|
|
8648
|
+
delivery: "datagram",
|
|
8649
|
+
eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
|
|
8650
|
+
setPacketIds(ids) {
|
|
8651
|
+
filter = ids.length ? new Set(ids) : null;
|
|
8652
|
+
},
|
|
8653
|
+
addEventListener: events.addEventListener.bind(events),
|
|
8654
|
+
removeEventListener: events.removeEventListener.bind(events),
|
|
8655
|
+
async unsubscribe() {
|
|
8656
|
+
closing = true;
|
|
8657
|
+
await rx.close();
|
|
8658
|
+
setState("closed");
|
|
8659
|
+
}
|
|
8660
|
+
};
|
|
8661
|
+
void (async () => {
|
|
8662
|
+
const reader = opened.readable.getReader();
|
|
8663
|
+
try {
|
|
8664
|
+
for (; ; ) {
|
|
8665
|
+
const { value, done } = await reader.read();
|
|
8666
|
+
if (done)
|
|
8667
|
+
break;
|
|
8668
|
+
const packets = [];
|
|
8669
|
+
for (const data of datagramsOf(value)) {
|
|
8670
|
+
if (filter !== null) {
|
|
8671
|
+
const id = readMMTPPacketId2(data);
|
|
8672
|
+
if (id === null || !filter.has(id))
|
|
8673
|
+
continue;
|
|
8674
|
+
}
|
|
8675
|
+
const copy = data.slice().buffer;
|
|
8676
|
+
packets.push(copy);
|
|
8677
|
+
events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
|
|
8678
|
+
}
|
|
8679
|
+
if (packets.length > 0)
|
|
8680
|
+
events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
|
|
8681
|
+
}
|
|
8682
|
+
} catch {
|
|
8683
|
+
}
|
|
8684
|
+
})();
|
|
8685
|
+
void rx.closed.then((value) => {
|
|
8686
|
+
const reason = reasonOf(value);
|
|
8687
|
+
if (!closing) {
|
|
8688
|
+
if (reason === "consent-withdrawn") {
|
|
8689
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8690
|
+
detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
|
|
8691
|
+
}));
|
|
8692
|
+
} else if (reason === "oversubscribed" || reason === "network") {
|
|
8693
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8694
|
+
detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
|
|
8695
|
+
}));
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
setState("closed");
|
|
8699
|
+
}, (error) => {
|
|
8700
|
+
if (!closing) {
|
|
8701
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8702
|
+
detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
|
|
8703
|
+
}));
|
|
8704
|
+
}
|
|
8705
|
+
setState("closed");
|
|
8706
|
+
});
|
|
8707
|
+
return subscription;
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8450
8710
|
// node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
|
|
8451
8711
|
var MAX_MOQT_PRIORITY = 255;
|
|
8452
8712
|
function moqtPriority(value, field) {
|
|
@@ -8486,14 +8746,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8486
8746
|
});
|
|
8487
8747
|
|
|
8488
8748
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
8749
|
+
var MAX_PENDING_PACKETS = 256;
|
|
8489
8750
|
var AMTGatewayManager = class {
|
|
8490
8751
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8491
8752
|
messageHandler;
|
|
8753
|
+
messageListenerRegistered = false;
|
|
8492
8754
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
8493
8755
|
constructor() {
|
|
8494
8756
|
this.messageHandler = (event) => {
|
|
8495
8757
|
const data = event.data;
|
|
8496
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8497
8758
|
if (data.type === "UDP_PACKET") {
|
|
8498
8759
|
this.handlePacket(data);
|
|
8499
8760
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8502,12 +8763,22 @@ var AMTGatewayManager = class {
|
|
|
8502
8763
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
8503
8764
|
}
|
|
8504
8765
|
};
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8766
|
+
}
|
|
8767
|
+
ensureMessageListener() {
|
|
8768
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
8769
|
+
return;
|
|
8770
|
+
}
|
|
8771
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
8772
|
+
this.messageListenerRegistered = true;
|
|
8773
|
+
console.log("[AMT] Registered service worker message handler");
|
|
8774
|
+
}
|
|
8775
|
+
removeMessageListener() {
|
|
8776
|
+
if (!this.messageListenerRegistered)
|
|
8777
|
+
return;
|
|
8778
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8779
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8510
8780
|
}
|
|
8781
|
+
this.messageListenerRegistered = false;
|
|
8511
8782
|
}
|
|
8512
8783
|
/**
|
|
8513
8784
|
* Subscribe via AMT tunnel
|
|
@@ -8517,6 +8788,7 @@ var AMTGatewayManager = class {
|
|
|
8517
8788
|
if (config.protected) {
|
|
8518
8789
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
8519
8790
|
}
|
|
8791
|
+
this.ensureMessageListener();
|
|
8520
8792
|
const keyStr = this.getKeyString(key);
|
|
8521
8793
|
if (this.subscriptions.has(keyStr)) {
|
|
8522
8794
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -8552,6 +8824,8 @@ var AMTGatewayManager = class {
|
|
|
8552
8824
|
this.subscriptions.delete(keyStr);
|
|
8553
8825
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8554
8826
|
}
|
|
8827
|
+
if (this.subscriptions.size === 0)
|
|
8828
|
+
this.removeMessageListener();
|
|
8555
8829
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8556
8830
|
}
|
|
8557
8831
|
}
|
|
@@ -8573,6 +8847,8 @@ var AMTGatewayManager = class {
|
|
|
8573
8847
|
} finally {
|
|
8574
8848
|
this.subscriptions.delete(handle);
|
|
8575
8849
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8850
|
+
if (this.subscriptions.size === 0)
|
|
8851
|
+
this.removeMessageListener();
|
|
8576
8852
|
}
|
|
8577
8853
|
}
|
|
8578
8854
|
/**
|
|
@@ -8589,14 +8865,12 @@ var AMTGatewayManager = class {
|
|
|
8589
8865
|
if (!subscriptionId)
|
|
8590
8866
|
return;
|
|
8591
8867
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8592
|
-
if (!subscription)
|
|
8593
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8868
|
+
if (!subscription)
|
|
8594
8869
|
return;
|
|
8595
|
-
}
|
|
8596
8870
|
if (!subscription.handshakeComplete) {
|
|
8597
8871
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8598
8872
|
const packetData2 = data;
|
|
8599
|
-
if (queue) {
|
|
8873
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8600
8874
|
queue.push({
|
|
8601
8875
|
data: packetData2.data,
|
|
8602
8876
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8622,19 +8896,15 @@ var AMTGatewayManager = class {
|
|
|
8622
8896
|
if (!subscriptionId)
|
|
8623
8897
|
return;
|
|
8624
8898
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8625
|
-
if (!subscription)
|
|
8626
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8899
|
+
if (!subscription)
|
|
8627
8900
|
return;
|
|
8628
|
-
}
|
|
8629
8901
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8630
8902
|
}
|
|
8631
8903
|
/**
|
|
8632
8904
|
* Cleanup on destroy
|
|
8633
8905
|
*/
|
|
8634
8906
|
destroy() {
|
|
8635
|
-
|
|
8636
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8637
|
-
}
|
|
8907
|
+
this.removeMessageListener();
|
|
8638
8908
|
const handles = Array.from(this.subscriptions.keys());
|
|
8639
8909
|
for (const handle of handles) {
|
|
8640
8910
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -8733,9 +9003,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
|
|
|
8733
9003
|
}
|
|
8734
9004
|
function validateMmtpDashTemplate(template, path) {
|
|
8735
9005
|
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 $${
|
|
9006
|
+
const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
|
|
9007
|
+
if (unsupported2 !== void 0) {
|
|
9008
|
+
throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
|
|
8739
9009
|
}
|
|
8740
9010
|
if (variables.filter((variable) => variable === "Number").length !== 1) {
|
|
8741
9011
|
throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
|
|
@@ -12679,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12679
12949
|
#retentionBlocks;
|
|
12680
12950
|
#heldEvicted;
|
|
12681
12951
|
#newest;
|
|
12952
|
+
#newestSource;
|
|
12682
12953
|
#holes;
|
|
12683
12954
|
static #checkedRetention(retentionBlocks) {
|
|
12684
12955
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
@@ -12716,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12716
12987
|
observe(position) {
|
|
12717
12988
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12718
12989
|
}
|
|
12990
|
+
/**
|
|
12991
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
12992
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
12993
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
12994
|
+
* reordering.
|
|
12995
|
+
*/
|
|
12996
|
+
get newestSource() {
|
|
12997
|
+
return this.#newestSource;
|
|
12998
|
+
}
|
|
12999
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
13000
|
+
noteSource(ssId) {
|
|
13001
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
13002
|
+
this.#newestSource = ssId;
|
|
13003
|
+
return true;
|
|
13004
|
+
}
|
|
12719
13005
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12720
13006
|
get heldEvicted() {
|
|
12721
13007
|
return this.#heldEvicted;
|
|
@@ -12948,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12948
13234
|
this.#held.clear();
|
|
12949
13235
|
this.#coverageEnd = void 0;
|
|
12950
13236
|
this.#newest = void 0;
|
|
13237
|
+
this.#newestSource = void 0;
|
|
12951
13238
|
}
|
|
12952
13239
|
#claim(block) {
|
|
12953
13240
|
const placed = [];
|
|
@@ -13567,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13567
13854
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13568
13855
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13569
13856
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13857
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13570
13858
|
function isMulticastSymbolDelivery(value) {
|
|
13571
13859
|
return value === "datagram" || value === "ordered-relay";
|
|
13572
13860
|
}
|
|
@@ -13664,6 +13952,10 @@ var FECManager = class _FECManager {
|
|
|
13664
13952
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13665
13953
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13666
13954
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
13955
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
13956
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
13957
|
+
// not network arrivals and never make a block clean.
|
|
13958
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13667
13959
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13668
13960
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13669
13961
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -13792,6 +14084,10 @@ var FECManager = class _FECManager {
|
|
|
13792
14084
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
13793
14085
|
// One classifier per physical stream, built from that stream's geometry.
|
|
13794
14086
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
14087
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
14088
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
14089
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
14090
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
13795
14091
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
13796
14092
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
13797
14093
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -13949,6 +14245,7 @@ var FECManager = class _FECManager {
|
|
|
13949
14245
|
#pendingWorkerCleanupBounds;
|
|
13950
14246
|
#blockSourceCounts;
|
|
13951
14247
|
#blockSourceEsis;
|
|
14248
|
+
#blockRebuiltEsis;
|
|
13952
14249
|
#blockAuthoredLengths;
|
|
13953
14250
|
#ssbLengthConflictCount;
|
|
13954
14251
|
#blockRepairRsIds;
|
|
@@ -13991,6 +14288,7 @@ var FECManager = class _FECManager {
|
|
|
13991
14288
|
#orderedFrontiers;
|
|
13992
14289
|
#orderedPendingBlocks;
|
|
13993
14290
|
#classifiers;
|
|
14291
|
+
#noSpanRaps;
|
|
13994
14292
|
#streamGeometries;
|
|
13995
14293
|
#failedBlocks;
|
|
13996
14294
|
#resolvedBlockClasses;
|
|
@@ -14652,6 +14950,7 @@ var FECManager = class _FECManager {
|
|
|
14652
14950
|
};
|
|
14653
14951
|
const discontinuity = () => {
|
|
14654
14952
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
14953
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14655
14954
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14656
14955
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14657
14956
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -15064,6 +15363,9 @@ var FECManager = class _FECManager {
|
|
|
15064
15363
|
}
|
|
15065
15364
|
if (policyChanged) {
|
|
15066
15365
|
this.#bumpEpoch(packetId);
|
|
15366
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15367
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15368
|
+
}
|
|
15067
15369
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15068
15370
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15069
15371
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15266,6 +15568,7 @@ var FECManager = class _FECManager {
|
|
|
15266
15568
|
}
|
|
15267
15569
|
resetMap(this.#blockSourceCounts);
|
|
15268
15570
|
resetMap(this.#blockSourceEsis);
|
|
15571
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15269
15572
|
resetMap(this.#blockAuthoredLengths);
|
|
15270
15573
|
resetMap(this.#blockRepairRsIds);
|
|
15271
15574
|
resetMap(this.#blockRsbLengths);
|
|
@@ -15299,6 +15602,7 @@ var FECManager = class _FECManager {
|
|
|
15299
15602
|
resetMap(this.#isoLocators);
|
|
15300
15603
|
resetMap(this.#sourceAnchors);
|
|
15301
15604
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15605
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15302
15606
|
this.#bumpEpoch(packetId);
|
|
15303
15607
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15304
15608
|
for (const key of keys.keys()) {
|
|
@@ -15389,6 +15693,7 @@ var FECManager = class _FECManager {
|
|
|
15389
15693
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15390
15694
|
dropMap(this.#blockSourceCounts);
|
|
15391
15695
|
dropMap(this.#blockSourceEsis);
|
|
15696
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15392
15697
|
dropMap(this.#blockAuthoredLengths);
|
|
15393
15698
|
dropMap(this.#blockRepairRsIds);
|
|
15394
15699
|
dropMap(this.#blockRsbLengths);
|
|
@@ -15405,6 +15710,7 @@ var FECManager = class _FECManager {
|
|
|
15405
15710
|
dropMap(this.#isoLocators);
|
|
15406
15711
|
dropMap(this.#sourceAnchors);
|
|
15407
15712
|
dropMap(this.#classifiers);
|
|
15713
|
+
dropMap(this.#noSpanRaps);
|
|
15408
15714
|
dropMap(this.#streamGeometries);
|
|
15409
15715
|
dropSet(this.#pendingWorkerCleanups);
|
|
15410
15716
|
dropSet(this.#completedBlocks);
|
|
@@ -15550,6 +15856,9 @@ var FECManager = class _FECManager {
|
|
|
15550
15856
|
this.#unindexBlockKey(key);
|
|
15551
15857
|
}
|
|
15552
15858
|
}
|
|
15859
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15860
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15861
|
+
}
|
|
15553
15862
|
return true;
|
|
15554
15863
|
}
|
|
15555
15864
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -15867,6 +16176,7 @@ var FECManager = class _FECManager {
|
|
|
15867
16176
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
15868
16177
|
const evictedBefore = locator.heldEvicted;
|
|
15869
16178
|
const held = locator.hold(ssId, source);
|
|
16179
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
15870
16180
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
15871
16181
|
if (evicted > 0) {
|
|
15872
16182
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16035,11 +16345,14 @@ var FECManager = class _FECManager {
|
|
|
16035
16345
|
/**
|
|
16036
16346
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16037
16347
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16038
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
16348
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
16349
|
+
* and neither has a stream without catalog timing until it releases two
|
|
16350
|
+
* RAPs inside one MPU.
|
|
16039
16351
|
*/
|
|
16040
16352
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16041
|
-
|
|
16042
|
-
return this.#
|
|
16353
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16354
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
16355
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
16043
16356
|
}
|
|
16044
16357
|
/** A block's class now, or frozen at its resolution. */
|
|
16045
16358
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16065,7 +16378,14 @@ var FECManager = class _FECManager {
|
|
|
16065
16378
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16066
16379
|
*/
|
|
16067
16380
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16068
|
-
if (!this.#trackConfigs.has(packetId)
|
|
16381
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
16382
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
16383
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16384
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
16385
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
16386
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
16387
|
+
return;
|
|
16388
|
+
}
|
|
16069
16389
|
this.#applyClassifierChanges(
|
|
16070
16390
|
packetId,
|
|
16071
16391
|
catalogTrackName,
|
|
@@ -16452,7 +16772,15 @@ var FECManager = class _FECManager {
|
|
|
16452
16772
|
this.#isoSourceOversizeCount++;
|
|
16453
16773
|
return null;
|
|
16454
16774
|
}
|
|
16455
|
-
|
|
16775
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16776
|
+
}
|
|
16777
|
+
/**
|
|
16778
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16779
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16780
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16781
|
+
* stated for `ssId`.
|
|
16782
|
+
*/
|
|
16783
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16456
16784
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16457
16785
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16458
16786
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16481,6 +16809,68 @@ var FECManager = class _FECManager {
|
|
|
16481
16809
|
}
|
|
16482
16810
|
return recovered;
|
|
16483
16811
|
}
|
|
16812
|
+
/**
|
|
16813
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16814
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16815
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16816
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16817
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16818
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16819
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16820
|
+
*
|
|
16821
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16822
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16823
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16824
|
+
* feedSourceSymbol takes it.
|
|
16825
|
+
*
|
|
16826
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16827
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16828
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16829
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16830
|
+
* block as clean on its own side), the block is completed here as
|
|
16831
|
+
* recovered, with nothing left to route.
|
|
16832
|
+
*/
|
|
16833
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16834
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16835
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16836
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16837
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16838
|
+
const sbn = located.block.start;
|
|
16839
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16840
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16841
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16842
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16843
|
+
if (rebuilt === void 0) {
|
|
16844
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16845
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16846
|
+
}
|
|
16847
|
+
rebuilt.add(located.esi);
|
|
16848
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16849
|
+
if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName);
|
|
16850
|
+
return { taken: true, recovered };
|
|
16851
|
+
}
|
|
16852
|
+
/**
|
|
16853
|
+
* A block that holds every one of its `length` sources, received or
|
|
16854
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16855
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16856
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16857
|
+
*/
|
|
16858
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16859
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16860
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16861
|
+
let present = rebuilt.size;
|
|
16862
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16863
|
+
if (present < length) return false;
|
|
16864
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16865
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16866
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16867
|
+
if (catalogTrackName === void 0) {
|
|
16868
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16869
|
+
} else {
|
|
16870
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16871
|
+
}
|
|
16872
|
+
return true;
|
|
16873
|
+
}
|
|
16484
16874
|
/**
|
|
16485
16875
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16486
16876
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16683,8 +17073,72 @@ var FECManager = class _FECManager {
|
|
|
16683
17073
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16684
17074
|
return null;
|
|
16685
17075
|
}
|
|
17076
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16686
17077
|
return recovered;
|
|
16687
17078
|
}
|
|
17079
|
+
/**
|
|
17080
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17081
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17082
|
+
* position past a block leaves the sources that block still misses
|
|
17083
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17084
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17085
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17086
|
+
* sent before it.
|
|
17087
|
+
*/
|
|
17088
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17089
|
+
const previous = locator.newestSource;
|
|
17090
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17091
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17092
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17093
|
+
this.#shortenUnrecoverableWait(
|
|
17094
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17095
|
+
block.start,
|
|
17096
|
+
packetId,
|
|
17097
|
+
catalogTrackName
|
|
17098
|
+
);
|
|
17099
|
+
}
|
|
17100
|
+
}
|
|
17101
|
+
/**
|
|
17102
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17103
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17104
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17105
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17106
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17107
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17108
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17109
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17110
|
+
* block.
|
|
17111
|
+
*/
|
|
17112
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17113
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17114
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17115
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17116
|
+
if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
|
|
17117
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17118
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17119
|
+
return void 0;
|
|
17120
|
+
}
|
|
17121
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17122
|
+
}
|
|
17123
|
+
/**
|
|
17124
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17125
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17126
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17127
|
+
*/
|
|
17128
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17129
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17130
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17131
|
+
if (config === void 0) return;
|
|
17132
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17133
|
+
if (waitMs === void 0) return;
|
|
17134
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17135
|
+
this.#armBlockTimeout(
|
|
17136
|
+
sbn,
|
|
17137
|
+
packetId,
|
|
17138
|
+
catalogTrackName,
|
|
17139
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17140
|
+
);
|
|
17141
|
+
}
|
|
16688
17142
|
/**
|
|
16689
17143
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16690
17144
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -16839,6 +17293,15 @@ var FECManager = class _FECManager {
|
|
|
16839
17293
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
16840
17294
|
return;
|
|
16841
17295
|
}
|
|
17296
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17297
|
+
key,
|
|
17298
|
+
sbn,
|
|
17299
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17300
|
+
packetId,
|
|
17301
|
+
catalogTrackName
|
|
17302
|
+
)) {
|
|
17303
|
+
return;
|
|
17304
|
+
}
|
|
16842
17305
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
16843
17306
|
}
|
|
16844
17307
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -16847,6 +17310,7 @@ var FECManager = class _FECManager {
|
|
|
16847
17310
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16848
17311
|
this.#blockSourceCounts.delete(key);
|
|
16849
17312
|
this.#blockSourceEsis.delete(key);
|
|
17313
|
+
this.#blockRebuiltEsis.delete(key);
|
|
16850
17314
|
this.#blockAuthoredLengths.delete(key);
|
|
16851
17315
|
this.#blockRepairRsIds.delete(key);
|
|
16852
17316
|
this.#blockRsbLengths.delete(key);
|
|
@@ -16913,7 +17377,16 @@ var FECManager = class _FECManager {
|
|
|
16913
17377
|
this.#repairDeadlineMs(trackConfig)
|
|
16914
17378
|
);
|
|
16915
17379
|
}
|
|
16916
|
-
this.#
|
|
17380
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17381
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17382
|
+
return;
|
|
17383
|
+
}
|
|
17384
|
+
this.#armBlockTimeout(
|
|
17385
|
+
sbn,
|
|
17386
|
+
packetId,
|
|
17387
|
+
catalogTrackName,
|
|
17388
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17389
|
+
);
|
|
16917
17390
|
}
|
|
16918
17391
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
16919
17392
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -16926,6 +17399,7 @@ var FECManager = class _FECManager {
|
|
|
16926
17399
|
}
|
|
16927
17400
|
this.#blockSourceCounts.delete(key);
|
|
16928
17401
|
this.#blockSourceEsis.delete(key);
|
|
17402
|
+
this.#blockRebuiltEsis.delete(key);
|
|
16929
17403
|
this.#blockAuthoredLengths.delete(key);
|
|
16930
17404
|
this.#blockRepairRsIds.delete(key);
|
|
16931
17405
|
this.#blockRsbLengths.delete(key);
|
|
@@ -16964,6 +17438,7 @@ var FECManager = class _FECManager {
|
|
|
16964
17438
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
16965
17439
|
this.#blockSourceCounts.delete(key);
|
|
16966
17440
|
this.#blockSourceEsis.delete(key);
|
|
17441
|
+
this.#blockRebuiltEsis.delete(key);
|
|
16967
17442
|
this.#blockAuthoredLengths.delete(key);
|
|
16968
17443
|
this.#blockRepairRsIds.delete(key);
|
|
16969
17444
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17055,6 +17530,7 @@ var FECManager = class _FECManager {
|
|
|
17055
17530
|
this.#blockKeysBySbn.clear();
|
|
17056
17531
|
this.#blockSourceCounts.clear();
|
|
17057
17532
|
this.#blockSourceEsis.clear();
|
|
17533
|
+
this.#blockRebuiltEsis.clear();
|
|
17058
17534
|
this.#blockAuthoredLengths.clear();
|
|
17059
17535
|
this.#blockRepairRsIds.clear();
|
|
17060
17536
|
this.#blockRsbLengths.clear();
|
|
@@ -17080,6 +17556,7 @@ var FECManager = class _FECManager {
|
|
|
17080
17556
|
this.#blockAnchorsMs.clear();
|
|
17081
17557
|
this.#blockCatalogTrackNames.clear();
|
|
17082
17558
|
this.#classifiers.clear();
|
|
17559
|
+
this.#noSpanRaps.clear();
|
|
17083
17560
|
this.#streamGeometries.clear();
|
|
17084
17561
|
this.#failedBlocks.clear();
|
|
17085
17562
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17098,17 +17575,17 @@ var FECManager = class _FECManager {
|
|
|
17098
17575
|
|
|
17099
17576
|
// src/mfu-reassembly-policy.ts
|
|
17100
17577
|
function validateReliableMoqOptionShape(value) {
|
|
17101
|
-
const
|
|
17578
|
+
const unsupported2 = Object.keys(value).filter(
|
|
17102
17579
|
(key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
|
|
17103
17580
|
);
|
|
17104
|
-
if (
|
|
17105
|
-
if (
|
|
17581
|
+
if (unsupported2.length > 0) {
|
|
17582
|
+
if (unsupported2.includes("maxRacingGroups")) {
|
|
17106
17583
|
throw new Error(
|
|
17107
17584
|
"[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
|
|
17108
17585
|
);
|
|
17109
17586
|
}
|
|
17110
17587
|
throw new Error(
|
|
17111
|
-
`[MFU reassembly] unsupported reliable MoQ option(s): ${
|
|
17588
|
+
`[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
|
|
17112
17589
|
);
|
|
17113
17590
|
}
|
|
17114
17591
|
const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
|
|
@@ -17152,12 +17629,12 @@ function assertValidMfuReassemblyPolicy(policy) {
|
|
|
17152
17629
|
return;
|
|
17153
17630
|
}
|
|
17154
17631
|
if (value["transport"] === "datagram") {
|
|
17155
|
-
const
|
|
17632
|
+
const unsupported2 = Object.keys(value).filter(
|
|
17156
17633
|
(key) => key !== "transport" && key !== "deadlineMs"
|
|
17157
17634
|
);
|
|
17158
|
-
if (
|
|
17635
|
+
if (unsupported2.length > 0) {
|
|
17159
17636
|
throw new Error(
|
|
17160
|
-
`[MFU reassembly] unsupported datagram option(s): ${
|
|
17637
|
+
`[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
|
|
17161
17638
|
);
|
|
17162
17639
|
}
|
|
17163
17640
|
assertValidDatagramDeadline(value["deadlineMs"]);
|
|
@@ -17610,9 +18087,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17610
18087
|
}
|
|
17611
18088
|
#track = null;
|
|
17612
18089
|
#fragments = /* @__PURE__ */ new Map();
|
|
18090
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
18091
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
18092
|
+
/**
|
|
18093
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
18094
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
18095
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
18096
|
+
*/
|
|
18097
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17613
18098
|
#pendingSamples = 0;
|
|
17614
18099
|
#evictedSamples = 0;
|
|
17615
18100
|
#lastMovieFragmentRejection = null;
|
|
18101
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
18102
|
+
get initialized() {
|
|
18103
|
+
return this.#track !== null;
|
|
18104
|
+
}
|
|
17616
18105
|
get pendingSampleCount() {
|
|
17617
18106
|
return this.#pendingSamples;
|
|
17618
18107
|
}
|
|
@@ -17636,7 +18125,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17636
18125
|
this.#track = timing;
|
|
17637
18126
|
return true;
|
|
17638
18127
|
}
|
|
17639
|
-
|
|
18128
|
+
/**
|
|
18129
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
18130
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
18131
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
18132
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
18133
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
18134
|
+
*/
|
|
18135
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17640
18136
|
this.#lastMovieFragmentRejection = null;
|
|
17641
18137
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17642
18138
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17662,6 +18158,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17662
18158
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17663
18159
|
);
|
|
17664
18160
|
}
|
|
18161
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
18162
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17665
18163
|
let freed = 0;
|
|
17666
18164
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17667
18165
|
freed += this.#evictOldestFragment();
|
|
@@ -17675,6 +18173,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17675
18173
|
return false;
|
|
17676
18174
|
}
|
|
17677
18175
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18176
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17678
18177
|
this.#pendingSamples += parsed.samples.length;
|
|
17679
18178
|
return true;
|
|
17680
18179
|
}
|
|
@@ -17732,6 +18231,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17732
18231
|
let live = 0;
|
|
17733
18232
|
for (const sample of samples) if (sample !== null) live++;
|
|
17734
18233
|
this.#fragments.delete(oldest);
|
|
18234
|
+
this.#firstSamples.delete(oldest);
|
|
17735
18235
|
this.#pendingSamples -= live;
|
|
17736
18236
|
this.#evictedSamples += live;
|
|
17737
18237
|
return live;
|
|
@@ -17747,12 +18247,23 @@ var IsoBmffTimingResolver = class {
|
|
|
17747
18247
|
if (!samples || !samples[index]) return false;
|
|
17748
18248
|
samples[index] = null;
|
|
17749
18249
|
this.#pendingSamples--;
|
|
17750
|
-
if (samples.every((value) => value === null))
|
|
18250
|
+
if (samples.every((value) => value === null)) {
|
|
18251
|
+
this.#fragments.delete(key);
|
|
18252
|
+
const first = this.#firstSamples.get(key);
|
|
18253
|
+
this.#firstSamples.delete(key);
|
|
18254
|
+
this.#delivered.delete(key);
|
|
18255
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18256
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18257
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18258
|
+
}
|
|
18259
|
+
}
|
|
17751
18260
|
return true;
|
|
17752
18261
|
}
|
|
17753
18262
|
reset() {
|
|
17754
18263
|
this.#track = null;
|
|
17755
18264
|
this.#fragments.clear();
|
|
18265
|
+
this.#firstSamples.clear();
|
|
18266
|
+
this.#delivered.clear();
|
|
17756
18267
|
this.#pendingSamples = 0;
|
|
17757
18268
|
}
|
|
17758
18269
|
};
|
|
@@ -18234,6 +18745,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18234
18745
|
this.#reclaimedTimingSamples = 0;
|
|
18235
18746
|
this.#evictedTimingSamples = 0;
|
|
18236
18747
|
this.#rejectedMovieFragments = 0;
|
|
18748
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18749
|
+
held: 0,
|
|
18750
|
+
expired: 0,
|
|
18751
|
+
discarded: 0,
|
|
18752
|
+
superseded: 0
|
|
18753
|
+
};
|
|
18237
18754
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18238
18755
|
this.#fecGate = null;
|
|
18239
18756
|
this.#fecSequencer = null;
|
|
@@ -18342,6 +18859,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18342
18859
|
#reclaimedTimingSamples;
|
|
18343
18860
|
#evictedTimingSamples;
|
|
18344
18861
|
#rejectedMovieFragments;
|
|
18862
|
+
#recoveredHeadsAwaitingTiming;
|
|
18345
18863
|
#sequencers;
|
|
18346
18864
|
#fecGate;
|
|
18347
18865
|
#fecSequencer;
|
|
@@ -18462,6 +18980,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18462
18980
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18463
18981
|
};
|
|
18464
18982
|
}
|
|
18983
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
18984
|
+
get recoveredHeadsAwaitingTiming() {
|
|
18985
|
+
let pending = 0;
|
|
18986
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18987
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
18988
|
+
}
|
|
18989
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
18990
|
+
}
|
|
18465
18991
|
get reorderStats() {
|
|
18466
18992
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18467
18993
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18590,10 +19116,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18590
19116
|
get reassemblerDataUnitsReturned() {
|
|
18591
19117
|
return this.#reassemblerDataUnitsReturned;
|
|
18592
19118
|
}
|
|
18593
|
-
#countOutcome(
|
|
18594
|
-
const count = (this.#feedOutcomes.get(
|
|
18595
|
-
this.#feedOutcomes.set(
|
|
18596
|
-
this.#lastOutcome =
|
|
19119
|
+
#countOutcome(token2) {
|
|
19120
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
19121
|
+
this.#feedOutcomes.set(token2, count);
|
|
19122
|
+
this.#lastOutcome = token2;
|
|
18597
19123
|
this.#lastOutcomeCount = count;
|
|
18598
19124
|
}
|
|
18599
19125
|
#countDeferredOutcome(reason) {
|
|
@@ -18635,7 +19161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18635
19161
|
const now = Date.now();
|
|
18636
19162
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18637
19163
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18638
|
-
awaitingTiming.shift();
|
|
19164
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18639
19165
|
}
|
|
18640
19166
|
}
|
|
18641
19167
|
}
|
|
@@ -18753,11 +19279,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18753
19279
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
18754
19280
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
18755
19281
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
18756
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19282
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
18757
19283
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
18758
19284
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
18759
19285
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
|
|
18760
|
-
const admitted = resolver2.addMovieFragment(
|
|
19286
|
+
const admitted = resolver2.addMovieFragment(
|
|
19287
|
+
mpu.mpuSequenceNumber,
|
|
19288
|
+
data,
|
|
19289
|
+
remaining,
|
|
19290
|
+
options?.recovered === true
|
|
19291
|
+
);
|
|
18761
19292
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
18762
19293
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
18763
19294
|
if (!admitted) {
|
|
@@ -18777,7 +19308,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18777
19308
|
};
|
|
18778
19309
|
}
|
|
18779
19310
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
18780
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19311
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
18781
19312
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
18782
19313
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
18783
19314
|
return this.#drop("mfu-empty-payload");
|
|
@@ -18798,6 +19329,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18798
19329
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
18799
19330
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
18800
19331
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19332
|
+
sequenceNumber: info.sequenceNumber,
|
|
18801
19333
|
arrivedAt: Date.now()
|
|
18802
19334
|
});
|
|
18803
19335
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19121,6 +19653,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19121
19653
|
}
|
|
19122
19654
|
this.#emitFrame(data, meta);
|
|
19123
19655
|
}
|
|
19656
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19657
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19658
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19659
|
+
}
|
|
19124
19660
|
/**
|
|
19125
19661
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19126
19662
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19182,6 +19718,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19182
19718
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19183
19719
|
this.#sequencers.clear();
|
|
19184
19720
|
this.#fecSequencer?.discard();
|
|
19721
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19722
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19723
|
+
}
|
|
19185
19724
|
this.#timingStreams.clear();
|
|
19186
19725
|
this.#pendingTimingSamples = 0;
|
|
19187
19726
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19200,10 +19739,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19200
19739
|
return resolver;
|
|
19201
19740
|
}
|
|
19202
19741
|
#awaitTiming(streamKey, head) {
|
|
19203
|
-
const
|
|
19204
|
-
if (!
|
|
19742
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19743
|
+
if (!state) return;
|
|
19744
|
+
if (!head.options?.recovered) {
|
|
19745
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
19746
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
19747
|
+
state.awaitingTiming = kept;
|
|
19748
|
+
}
|
|
19749
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19205
19750
|
awaitingTiming.push(head);
|
|
19206
|
-
if (
|
|
19751
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
19752
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
19753
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
19754
|
+
}
|
|
19755
|
+
}
|
|
19756
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
19757
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
19758
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19207
19759
|
}
|
|
19208
19760
|
/**
|
|
19209
19761
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19231,6 +19783,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19231
19783
|
const state = this.#timingStreams.get(streamKey);
|
|
19232
19784
|
if (!state) return;
|
|
19233
19785
|
this.#timingStreams.delete(streamKey);
|
|
19786
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19234
19787
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19235
19788
|
if (this.#pendingTimingSamples < 0) {
|
|
19236
19789
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22437,6 +22990,16 @@ function keyframeOverlaySsId(header) {
|
|
|
22437
22990
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22438
22991
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22439
22992
|
}
|
|
22993
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
22994
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
22995
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
22996
|
+
}
|
|
22997
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
22998
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
22999
|
+
return headerPsn + offset >>> 0;
|
|
23000
|
+
}
|
|
23001
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23002
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22440
23003
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
22441
23004
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22442
23005
|
function assertStreamIdentity(role, stream) {
|
|
@@ -22604,6 +23167,14 @@ var MmtpRouter = class {
|
|
|
22604
23167
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22605
23168
|
*/
|
|
22606
23169
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23170
|
+
/**
|
|
23171
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23172
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23173
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23174
|
+
* current offset, no hop re-stamps the stream and a packet's protected psn
|
|
23175
|
+
* names its base SS_ID (joint decoding of an MPU no member of which arrived).
|
|
23176
|
+
*/
|
|
23177
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22607
23178
|
/**
|
|
22608
23179
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22609
23180
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22673,7 +23244,9 @@ var MmtpRouter = class {
|
|
|
22673
23244
|
recovered: 0,
|
|
22674
23245
|
restarts: 0,
|
|
22675
23246
|
unsequencedDropped: 0,
|
|
22676
|
-
protectedDropped: 0
|
|
23247
|
+
protectedDropped: 0,
|
|
23248
|
+
baseFed: 0,
|
|
23249
|
+
baseUndetermined: 0
|
|
22677
23250
|
};
|
|
22678
23251
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22679
23252
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -22846,7 +23419,7 @@ var MmtpRouter = class {
|
|
|
22846
23419
|
const cb = this.#getFrameCallback(packetId);
|
|
22847
23420
|
if (isVideo && !assembler) {
|
|
22848
23421
|
if (frameMeta.recovered) {
|
|
22849
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23422
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
22850
23423
|
} else {
|
|
22851
23424
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
22852
23425
|
}
|
|
@@ -23236,6 +23809,10 @@ var MmtpRouter = class {
|
|
|
23236
23809
|
get keyframeOverlayStats() {
|
|
23237
23810
|
return { ...this.#overlayStats };
|
|
23238
23811
|
}
|
|
23812
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
23813
|
+
get recoveredHeadsAwaitingTiming() {
|
|
23814
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
23815
|
+
}
|
|
23239
23816
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23240
23817
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23241
23818
|
}
|
|
@@ -23286,6 +23863,11 @@ var MmtpRouter = class {
|
|
|
23286
23863
|
*/
|
|
23287
23864
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23288
23865
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
23866
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
23867
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
23868
|
+
stream.overlayToBase.routed.clear();
|
|
23869
|
+
stream.overlayToBase.pending.clear();
|
|
23870
|
+
}
|
|
23289
23871
|
}
|
|
23290
23872
|
#resetOverlay(inst) {
|
|
23291
23873
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
@@ -23342,9 +23924,10 @@ var MmtpRouter = class {
|
|
|
23342
23924
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23343
23925
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23344
23926
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23345
|
-
* included (7.1, 7.4.3).
|
|
23927
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
23928
|
+
* FEC reads it.
|
|
23346
23929
|
*/
|
|
23347
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
23930
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23348
23931
|
if (this.#overlays.size === 0) return;
|
|
23349
23932
|
const ssId = keyframeOverlaySsId(header);
|
|
23350
23933
|
if (ssId === void 0) return;
|
|
@@ -23353,7 +23936,11 @@ var MmtpRouter = class {
|
|
|
23353
23936
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23354
23937
|
if (identity !== null) {
|
|
23355
23938
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23356
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
23939
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
23940
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
23941
|
+
overlaySsId: ssId,
|
|
23942
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
23943
|
+
});
|
|
23357
23944
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23358
23945
|
}
|
|
23359
23946
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23432,14 +24019,27 @@ var MmtpRouter = class {
|
|
|
23432
24019
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23433
24020
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23434
24021
|
const restored = /* @__PURE__ */ new Set();
|
|
24022
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
23435
24023
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23436
|
-
const
|
|
23437
|
-
if (
|
|
23438
|
-
const
|
|
24024
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24025
|
+
if (anchor !== void 0) {
|
|
24026
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24027
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23439
24028
|
restored.add(packetSequenceNumber);
|
|
23440
|
-
|
|
24029
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24030
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24031
|
+
if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
|
|
24032
|
+
return [out];
|
|
24033
|
+
}
|
|
24034
|
+
if (!restamped) {
|
|
24035
|
+
const baseSsId = unstampedBaseSsId(
|
|
24036
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
24037
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
24038
|
+
sym.packetSequenceNumber
|
|
24039
|
+
);
|
|
24040
|
+
if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
|
|
24041
|
+
return [sym];
|
|
23441
24042
|
}
|
|
23442
|
-
if (!restamped) return [sym];
|
|
23443
24043
|
this.#overlayStats.unsequencedDropped++;
|
|
23444
24044
|
return [];
|
|
23445
24045
|
});
|
|
@@ -23448,6 +24048,85 @@ var MmtpRouter = class {
|
|
|
23448
24048
|
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
23449
24049
|
});
|
|
23450
24050
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24051
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24052
|
+
}
|
|
24053
|
+
/**
|
|
24054
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24055
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24056
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
24057
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24058
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24059
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24060
|
+
* A base block no repair has stated yet takes it when one does
|
|
24061
|
+
* (#feedPendingRebuilt).
|
|
24062
|
+
*
|
|
24063
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24064
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24065
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24066
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24067
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24068
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24069
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24070
|
+
* block's own arrivals cannot show.
|
|
24071
|
+
*/
|
|
24072
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24073
|
+
const fec = this.#fecManager;
|
|
24074
|
+
const owner = inst.leg.sourcePacketId;
|
|
24075
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24076
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24077
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24078
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24079
|
+
const feeds = [];
|
|
24080
|
+
for (const sym of symbols) {
|
|
24081
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24082
|
+
const ssId = baseSsIds.get(sym);
|
|
24083
|
+
if (ssId === void 0) {
|
|
24084
|
+
this.#overlayStats.baseUndetermined++;
|
|
24085
|
+
continue;
|
|
24086
|
+
}
|
|
24087
|
+
routed.delete(ssId);
|
|
24088
|
+
routed.add(ssId);
|
|
24089
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24090
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24091
|
+
}
|
|
24092
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24093
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24094
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24095
|
+
}
|
|
24096
|
+
}
|
|
24097
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24098
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24099
|
+
const fec = this.#fecManager;
|
|
24100
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24101
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24102
|
+
if (coords == null) {
|
|
24103
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24104
|
+
pending.delete(ssId);
|
|
24105
|
+
pending.set(ssId, symbolData.slice());
|
|
24106
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24107
|
+
return;
|
|
24108
|
+
}
|
|
24109
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24110
|
+
if (!fed.taken) return;
|
|
24111
|
+
this.#overlayStats.baseFed++;
|
|
24112
|
+
if (fed.recovered === null) return;
|
|
24113
|
+
const sbn = coords.sbn;
|
|
24114
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24115
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24116
|
+
fec.countRecovery(owner, sbn, name);
|
|
24117
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24118
|
+
});
|
|
24119
|
+
}
|
|
24120
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24121
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24122
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24123
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24124
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24125
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24126
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24127
|
+
pending.delete(ssId);
|
|
24128
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24129
|
+
}
|
|
23451
24130
|
}
|
|
23452
24131
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23453
24132
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23579,10 +24258,11 @@ var MmtpRouter = class {
|
|
|
23579
24258
|
if (!entry) return;
|
|
23580
24259
|
entry.receiver.release();
|
|
23581
24260
|
entry.protected = false;
|
|
24261
|
+
entry.userAgent = false;
|
|
23582
24262
|
this.#assistBlocks.clear();
|
|
23583
24263
|
this.#assistFailures.clear();
|
|
23584
24264
|
}
|
|
23585
|
-
/**
|
|
24265
|
+
/** 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. */
|
|
23586
24266
|
get unadmittedListedDropCount() {
|
|
23587
24267
|
return this.#unadmittedListedDrops;
|
|
23588
24268
|
}
|
|
@@ -23623,6 +24303,13 @@ var MmtpRouter = class {
|
|
|
23623
24303
|
}
|
|
23624
24304
|
return false;
|
|
23625
24305
|
}
|
|
24306
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
24307
|
+
#userAgentGoverns(packetId) {
|
|
24308
|
+
for (const entry of this.#provenance.values()) {
|
|
24309
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
24310
|
+
}
|
|
24311
|
+
return false;
|
|
24312
|
+
}
|
|
23626
24313
|
/**
|
|
23627
24314
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
23628
24315
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -23713,7 +24400,7 @@ var MmtpRouter = class {
|
|
|
23713
24400
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
23714
24401
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
23715
24402
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
23716
|
-
})) {
|
|
24403
|
+
}, true)) {
|
|
23717
24404
|
this.#unlistedAssistedRecoveries++;
|
|
23718
24405
|
this.#assistBlocks.delete(key);
|
|
23719
24406
|
return;
|
|
@@ -24065,6 +24752,8 @@ var MmtpRouter = class {
|
|
|
24065
24752
|
)) return;
|
|
24066
24753
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24067
24754
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
24755
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
24756
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24068
24757
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24069
24758
|
const recovered = fec.feedRepairSymbol(
|
|
24070
24759
|
ownerPacketId,
|
|
@@ -24202,6 +24891,7 @@ var MmtpRouter = class {
|
|
|
24202
24891
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24203
24892
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24204
24893
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
24894
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24205
24895
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24206
24896
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24207
24897
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24869,6 +25559,12 @@ var MmtpRouter = class {
|
|
|
24869
25559
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
24870
25560
|
return;
|
|
24871
25561
|
}
|
|
25562
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
25563
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
25564
|
+
protectedFlow.userAgent = true;
|
|
25565
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
25566
|
+
return;
|
|
25567
|
+
}
|
|
24872
25568
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
24873
25569
|
if (verdict === "admitted") {
|
|
24874
25570
|
this.#noteFlowAdmitted(ingress.flow);
|
|
@@ -24886,6 +25582,11 @@ var MmtpRouter = class {
|
|
|
24886
25582
|
return;
|
|
24887
25583
|
}
|
|
24888
25584
|
}
|
|
25585
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
|
|
25586
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
25587
|
+
this.#unadmittedListedDrops++;
|
|
25588
|
+
return;
|
|
25589
|
+
}
|
|
24889
25590
|
if (info.isRepair) {
|
|
24890
25591
|
const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
|
|
24891
25592
|
if (overlay !== void 0) {
|
|
@@ -24903,7 +25604,7 @@ var MmtpRouter = class {
|
|
|
24903
25604
|
);
|
|
24904
25605
|
if (!repair) return;
|
|
24905
25606
|
if (ownerPacketId != null && this.#fecManager) {
|
|
24906
|
-
if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
25607
|
+
if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
24907
25608
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
24908
25609
|
const count = this.#repairPidCounts[info.packetId];
|
|
24909
25610
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -25106,7 +25807,8 @@ var MmtpRouter = class {
|
|
|
25106
25807
|
parsed.header,
|
|
25107
25808
|
info.sequenceNumber,
|
|
25108
25809
|
info.rapFlag,
|
|
25109
|
-
source
|
|
25810
|
+
source,
|
|
25811
|
+
fecPayloadId?.ssId
|
|
25110
25812
|
);
|
|
25111
25813
|
}
|
|
25112
25814
|
}
|
|
@@ -25256,7 +25958,8 @@ var MmtpRouter = class {
|
|
|
25256
25958
|
if (stream === void 0) {
|
|
25257
25959
|
stream = {
|
|
25258
25960
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25259
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
25961
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
25962
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25260
25963
|
};
|
|
25261
25964
|
byName.set(catalogTrackName, stream);
|
|
25262
25965
|
}
|
|
@@ -25371,8 +26074,16 @@ var MmtpRouter = class {
|
|
|
25371
26074
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25372
26075
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25373
26076
|
}
|
|
25374
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26077
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25375
26078
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26079
|
+
if (this.#isFecTrack(packetId)) {
|
|
26080
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26081
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26082
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26083
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26084
|
+
return;
|
|
26085
|
+
}
|
|
26086
|
+
}
|
|
25376
26087
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25377
26088
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25378
26089
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25482,7 +26193,7 @@ var MmtpRouter = class {
|
|
|
25482
26193
|
}
|
|
25483
26194
|
// ── Private: FEC recovery routing ───────────────────────────────
|
|
25484
26195
|
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
25485
|
-
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
|
|
26196
|
+
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
25486
26197
|
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
25487
26198
|
if (ownerPacketId === void 0) {
|
|
25488
26199
|
throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
|
|
@@ -25499,7 +26210,7 @@ var MmtpRouter = class {
|
|
|
25499
26210
|
ownerPacketId,
|
|
25500
26211
|
blockProfile
|
|
25501
26212
|
});
|
|
25502
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
26213
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
25503
26214
|
}
|
|
25504
26215
|
/**
|
|
25505
26216
|
* A recovered source packet of a protected flow is admitted on its own
|
|
@@ -25511,9 +26222,31 @@ var MmtpRouter = class {
|
|
|
25511
26222
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
25512
26223
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
25513
26224
|
* outside a protected flow.
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
26225
|
+
*
|
|
26226
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
26227
|
+
* without a record: while the flow is protected that decoder takes only
|
|
26228
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
26229
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
26230
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
26231
|
+
* unlisted MoQ repair and still needs a record.
|
|
26232
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
26233
|
+
* ever mixes verified and unverified live inputs.
|
|
26234
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
26235
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
26236
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
26237
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
26238
|
+
*/
|
|
26239
|
+
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26240
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26241
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26242
|
+
result = {
|
|
26243
|
+
...result,
|
|
26244
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26245
|
+
};
|
|
26246
|
+
}
|
|
26247
|
+
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
26248
|
+
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
26249
|
+
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
25517
26250
|
let recovered = true;
|
|
25518
26251
|
if (receiver) {
|
|
25519
26252
|
const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -25525,6 +26258,8 @@ var MmtpRouter = class {
|
|
|
25525
26258
|
let admitted = false;
|
|
25526
26259
|
if (receiver === "ambiguous") {
|
|
25527
26260
|
this.#ambiguousRecoveredDrops++;
|
|
26261
|
+
} else if (userAgent) {
|
|
26262
|
+
admitted = true;
|
|
25528
26263
|
} else {
|
|
25529
26264
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
25530
26265
|
wire.set(sym.rawSymbol);
|
|
@@ -25552,12 +26287,12 @@ var MmtpRouter = class {
|
|
|
25552
26287
|
catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
|
|
25553
26288
|
);
|
|
25554
26289
|
if (flows.size > 1) return "ambiguous";
|
|
25555
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
26290
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
25556
26291
|
let match;
|
|
25557
|
-
for (const
|
|
25558
|
-
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
26292
|
+
for (const entry of this.#provenance.values()) {
|
|
26293
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
25559
26294
|
if (match) return "ambiguous";
|
|
25560
|
-
match =
|
|
26295
|
+
match = entry;
|
|
25561
26296
|
}
|
|
25562
26297
|
}
|
|
25563
26298
|
return match;
|
|
@@ -25573,7 +26308,10 @@ var MmtpRouter = class {
|
|
|
25573
26308
|
...result,
|
|
25574
26309
|
symbols: result.symbols.map((sym) => {
|
|
25575
26310
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25576
|
-
if (wire === sym.packetSequenceNumber)
|
|
26311
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26312
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26313
|
+
return sym;
|
|
26314
|
+
}
|
|
25577
26315
|
this.#restampedRecoveredSeqs++;
|
|
25578
26316
|
this.#restampedStreams.add(streamKey);
|
|
25579
26317
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25609,6 +26347,29 @@ var MmtpRouter = class {
|
|
|
25609
26347
|
}
|
|
25610
26348
|
);
|
|
25611
26349
|
}
|
|
26350
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26351
|
+
ownerPacketId,
|
|
26352
|
+
sym.fragment.mpuSequenceNumber,
|
|
26353
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26354
|
+
sym.fragment.sampleNumber,
|
|
26355
|
+
catalogTrackName
|
|
26356
|
+
);
|
|
26357
|
+
for (const sym of result.symbols) {
|
|
26358
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26359
|
+
this.#feedViaParser(
|
|
26360
|
+
sym.rawSymbol,
|
|
26361
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26362
|
+
void 0,
|
|
26363
|
+
{
|
|
26364
|
+
skipFecPayloadId: true,
|
|
26365
|
+
recovered: true,
|
|
26366
|
+
deliveryProvenance,
|
|
26367
|
+
...fecSbnOf(sym.index),
|
|
26368
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26369
|
+
}
|
|
26370
|
+
);
|
|
26371
|
+
}
|
|
26372
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25612
26373
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25613
26374
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25614
26375
|
if (assembler) {
|
|
@@ -26339,11 +27100,8 @@ function providerErrorOf(error) {
|
|
|
26339
27100
|
return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
|
|
26340
27101
|
}
|
|
26341
27102
|
var MAX_TRACKED_DROPPED_PIDS = 16;
|
|
26342
|
-
function getMulticastGlobals() {
|
|
26343
|
-
return globalThis;
|
|
26344
|
-
}
|
|
26345
27103
|
function hasMulticastGateway() {
|
|
26346
|
-
return typeof
|
|
27104
|
+
return typeof resolveMulticastGateway()?.subscribe === "function";
|
|
26347
27105
|
}
|
|
26348
27106
|
function senderOf(event) {
|
|
26349
27107
|
const sourceAddress = event.sourceAddress;
|
|
@@ -26435,6 +27193,9 @@ var MulticastTransport = class {
|
|
|
26435
27193
|
// listener closure, so an in-place admission change (setContext) applies
|
|
26436
27194
|
// from the next packet on without rejoining.
|
|
26437
27195
|
#context = void 0;
|
|
27196
|
+
#deliveredContext = void 0;
|
|
27197
|
+
/** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
|
|
27198
|
+
#userAgentGateway = void 0;
|
|
26438
27199
|
// The live join's provider-side filter. An in-place widening must reach it,
|
|
26439
27200
|
// or the provider never delivers the new ID; null without a live join.
|
|
26440
27201
|
#providerFilter = null;
|
|
@@ -26471,7 +27232,7 @@ var MulticastTransport = class {
|
|
|
26471
27232
|
this.#dataPlaneLive = false;
|
|
26472
27233
|
this.#sender = void 0;
|
|
26473
27234
|
if (this.#disposed) return this.#declined(generation, "disposed");
|
|
26474
|
-
const gateway = this.#gateway ??
|
|
27235
|
+
const gateway = this.#gateway ?? resolveMulticastGateway();
|
|
26475
27236
|
if (typeof gateway?.subscribe !== "function") {
|
|
26476
27237
|
console.warn("[MulticastTransport] Multicast API not available");
|
|
26477
27238
|
return this.#declined(generation, "unavailable");
|
|
@@ -26487,15 +27248,16 @@ var MulticastTransport = class {
|
|
|
26487
27248
|
console.log(
|
|
26488
27249
|
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
|
|
26489
27250
|
);
|
|
27251
|
+
const userAgent = isUserAgentVerifiedGateway(gateway);
|
|
26490
27252
|
const subscribeConfig = toWindowMulticastSubscribeConfig({
|
|
26491
27253
|
source: config.sourceAddress,
|
|
26492
27254
|
senderAddress: config.senderAddress,
|
|
26493
27255
|
group: config.groupAddress,
|
|
26494
27256
|
port: config.port,
|
|
26495
|
-
mode: config.mode,
|
|
26496
|
-
amtRelay: config.amtRelay,
|
|
26497
|
-
amtRelayPort: config.amtRelayPort,
|
|
26498
|
-
amtDiscovery: config.amtDiscovery,
|
|
27257
|
+
mode: userAgent ? "native" : config.mode,
|
|
27258
|
+
amtRelay: userAgent ? void 0 : config.amtRelay,
|
|
27259
|
+
amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
|
|
27260
|
+
amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
|
|
26499
27261
|
packetIds: config.packetIds,
|
|
26500
27262
|
tracks: config.tracks,
|
|
26501
27263
|
atsc3: config.atsc3
|
|
@@ -26541,7 +27303,8 @@ var MulticastTransport = class {
|
|
|
26541
27303
|
acknowledgedUpdate: 0,
|
|
26542
27304
|
inFlight: 0
|
|
26543
27305
|
};
|
|
26544
|
-
this.#
|
|
27306
|
+
this.#userAgentGateway = userAgent ? gateway : void 0;
|
|
27307
|
+
this.#setContext(context);
|
|
26545
27308
|
if (packetIds) {
|
|
26546
27309
|
for (const packetId of packetIds) {
|
|
26547
27310
|
if (!this.#acceptedBytesByPacketId.has(packetId)) {
|
|
@@ -26549,7 +27312,7 @@ var MulticastTransport = class {
|
|
|
26549
27312
|
}
|
|
26550
27313
|
}
|
|
26551
27314
|
}
|
|
26552
|
-
this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
27315
|
+
this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
26553
27316
|
const delivery = subscription.delivery ?? "datagram";
|
|
26554
27317
|
if (delivery !== "datagram" && delivery !== "ordered-relay") {
|
|
26555
27318
|
throw new Error(
|
|
@@ -26630,7 +27393,7 @@ var MulticastTransport = class {
|
|
|
26630
27393
|
}
|
|
26631
27394
|
if (!this.#acceptPacket(packet, sourceAddress)) return false;
|
|
26632
27395
|
const liveness = !this.#isRecordCandidate(packet);
|
|
26633
|
-
this.#onPacket(packet, this.#
|
|
27396
|
+
this.#onPacket(packet, this.#deliveredContext);
|
|
26634
27397
|
return liveness;
|
|
26635
27398
|
}
|
|
26636
27399
|
#isRecordCandidate(packet) {
|
|
@@ -26841,8 +27604,10 @@ var MulticastTransport = class {
|
|
|
26841
27604
|
return this.#subscription !== null;
|
|
26842
27605
|
}
|
|
26843
27606
|
/**
|
|
26844
|
-
*
|
|
26845
|
-
*
|
|
27607
|
+
* The live join's context as the caller gave it (the join's `context`, or
|
|
27608
|
+
* the last setContext), or undefined without a live join. On a native
|
|
27609
|
+
* provider each accepted packet is delivered with a marked copy of it
|
|
27610
|
+
* (`userAgentVerifiedIngress`); this getter never carries the mark.
|
|
26846
27611
|
*/
|
|
26847
27612
|
get context() {
|
|
26848
27613
|
return this.#context;
|
|
@@ -26866,7 +27631,16 @@ var MulticastTransport = class {
|
|
|
26866
27631
|
"[MulticastTransport] setContext needs a live join; join() carries the first context"
|
|
26867
27632
|
);
|
|
26868
27633
|
}
|
|
27634
|
+
this.#setContext(context);
|
|
27635
|
+
}
|
|
27636
|
+
/**
|
|
27637
|
+
* The join's context, and what the subscription delivers with each packet:
|
|
27638
|
+
* marked admitted by the user agent when the gateway is its native
|
|
27639
|
+
* MulticastReceiver provider. feedPacket never marks.
|
|
27640
|
+
*/
|
|
27641
|
+
#setContext(context) {
|
|
26869
27642
|
this.#context = context;
|
|
27643
|
+
this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
|
|
26870
27644
|
}
|
|
26871
27645
|
/**
|
|
26872
27646
|
* Replace the live join's packet-ID allow-list in place, without rejoining.
|
|
@@ -26998,6 +27772,8 @@ var MulticastTransport = class {
|
|
|
26998
27772
|
this.#matchedSource = void 0;
|
|
26999
27773
|
this.#providerFilter = null;
|
|
27000
27774
|
this.#context = void 0;
|
|
27775
|
+
this.#deliveredContext = void 0;
|
|
27776
|
+
this.#userAgentGateway = void 0;
|
|
27001
27777
|
this.#withdrawnPacketIds.clear();
|
|
27002
27778
|
}
|
|
27003
27779
|
/** The ingress filter; an accepted packet is counted here, once. */
|
|
@@ -28350,8 +29126,17 @@ var MulticastManager = class {
|
|
|
28350
29126
|
* repair roles and owners are registered here; in-band owners stay the
|
|
28351
29127
|
* caller's. Joining is best effort: a group that fails to join is logged
|
|
28352
29128
|
* and leaves the media join as it was.
|
|
28353
|
-
|
|
28354
|
-
|
|
29129
|
+
*
|
|
29130
|
+
* `options.gateway` names the gateway a subscribe callback reads through.
|
|
29131
|
+
* When it is the browser's own MulticastReceiver provider, the callback's
|
|
29132
|
+
* packets are fed as admitted by the user agent
|
|
29133
|
+
* (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
|
|
29134
|
+
* The mark rests on this statement, not on the data path: every packet the
|
|
29135
|
+
* callback delivers must come from a subscription opened on
|
|
29136
|
+
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
29137
|
+
* WebSocket repair feed) must not name a native provider here.
|
|
29138
|
+
*/
|
|
29139
|
+
joinRepairEndpoints(endpoints, provider, options) {
|
|
28355
29140
|
const releases = [];
|
|
28356
29141
|
let released = false;
|
|
28357
29142
|
const safely = (each) => {
|
|
@@ -28373,17 +29158,19 @@ var MulticastManager = class {
|
|
|
28373
29158
|
...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
|
|
28374
29159
|
};
|
|
28375
29160
|
let sender;
|
|
28376
|
-
const
|
|
29161
|
+
const feed = (packet, sourceAddress, ingressToFeed) => {
|
|
28377
29162
|
if (released) return;
|
|
28378
29163
|
if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
|
|
28379
|
-
this.#router.feedPacket(packet, "multicast",
|
|
29164
|
+
this.#router.feedPacket(packet, "multicast", ingressToFeed);
|
|
28380
29165
|
};
|
|
28381
29166
|
const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
|
|
28382
29167
|
if (typeof provider === "function") {
|
|
29168
|
+
const marked = userAgentVerifiedIngress(ingress, options?.gateway);
|
|
29169
|
+
const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
|
|
28383
29170
|
Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
|
|
28384
29171
|
continue;
|
|
28385
29172
|
}
|
|
28386
|
-
const transport = new MulticastTransport((packet) =>
|
|
29173
|
+
const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
|
|
28387
29174
|
gateway: provider,
|
|
28388
29175
|
lockedTo: () => this.#transport.sender
|
|
28389
29176
|
});
|
|
@@ -28395,11 +29182,12 @@ var MulticastManager = class {
|
|
|
28395
29182
|
mode: endpoint.mode,
|
|
28396
29183
|
amtRelay: endpoint.amtRelay,
|
|
28397
29184
|
amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
|
|
29185
|
+
amtDiscovery: endpoint.amtDiscovery,
|
|
28398
29186
|
packetIds: endpoint.packetIds,
|
|
28399
29187
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
28400
29188
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
28401
29189
|
...endpoint.flow === void 0 ? {} : { protected: true }
|
|
28402
|
-
}).then((joined) => {
|
|
29190
|
+
}, ingress).then((joined) => {
|
|
28403
29191
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
28404
29192
|
});
|
|
28405
29193
|
}
|
|
@@ -29343,7 +30131,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29343
30131
|
blockProfile,
|
|
29344
30132
|
k: opts.fecK,
|
|
29345
30133
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29346
|
-
// and CMCD `
|
|
30134
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29347
30135
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29348
30136
|
symbolSize: opts.fecSymbolSize,
|
|
29349
30137
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -29885,24 +30673,24 @@ var MediaRouter = class {
|
|
|
29885
30673
|
if (!this.#multicastVideoKeyed) return;
|
|
29886
30674
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
29887
30675
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
29888
|
-
const
|
|
30676
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
29889
30677
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
29890
30678
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
29891
|
-
this.#commitMulticastActivation(
|
|
30679
|
+
this.#commitMulticastActivation(token2);
|
|
29892
30680
|
return;
|
|
29893
30681
|
}
|
|
29894
30682
|
const ready = Promise.resolve(maybeReady);
|
|
29895
|
-
this.#pendingMulticastToken =
|
|
30683
|
+
this.#pendingMulticastToken = token2;
|
|
29896
30684
|
this.#sourceSwitchReady = ready;
|
|
29897
30685
|
void ready.then(
|
|
29898
|
-
() => this.#commitMulticastActivation(
|
|
29899
|
-
(err) => this.#rejectMulticastActivation(
|
|
30686
|
+
() => this.#commitMulticastActivation(token2),
|
|
30687
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
29900
30688
|
).finally(() => {
|
|
29901
30689
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
29902
30690
|
});
|
|
29903
30691
|
}
|
|
29904
|
-
#commitMulticastActivation(
|
|
29905
|
-
if (
|
|
30692
|
+
#commitMulticastActivation(token2) {
|
|
30693
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
29906
30694
|
this.#active = "multicast";
|
|
29907
30695
|
this.#suppressed = true;
|
|
29908
30696
|
this.#pendingMulticastToken = null;
|
|
@@ -29913,9 +30701,9 @@ var MediaRouter = class {
|
|
|
29913
30701
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
29914
30702
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
29915
30703
|
}
|
|
29916
|
-
#rejectMulticastActivation(
|
|
29917
|
-
if (
|
|
29918
|
-
if (this.#pendingMulticastToken ===
|
|
30704
|
+
#rejectMulticastActivation(token2, err) {
|
|
30705
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30706
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
29919
30707
|
this.#pendingMulticastVideo = null;
|
|
29920
30708
|
this.#pendingMulticastAudio = null;
|
|
29921
30709
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -30884,63 +31672,65 @@ var FecStarvationDetector = class {
|
|
|
30884
31672
|
|
|
30885
31673
|
// src/cmcd-producer.ts
|
|
30886
31674
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
30887
|
-
|
|
30888
|
-
|
|
30889
|
-
|
|
30890
|
-
|
|
30891
|
-
|
|
30892
|
-
|
|
30893
|
-
|
|
30894
|
-
|
|
30895
|
-
|
|
30896
|
-
|
|
30897
|
-
|
|
30898
|
-
|
|
30899
|
-
|
|
30900
|
-
|
|
30901
|
-
|
|
30902
|
-
|
|
30903
|
-
|
|
30904
|
-
|
|
30905
|
-
}
|
|
30906
|
-
if (snap.multicastSource != null) {
|
|
30907
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
30908
|
-
}
|
|
30909
|
-
if (snap.amtRelay != null) {
|
|
30910
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
30911
|
-
}
|
|
30912
|
-
return params;
|
|
30913
|
-
}
|
|
30914
|
-
function formatQueryString(params) {
|
|
30915
|
-
return Object.keys(params).sort().map((key) => {
|
|
30916
|
-
const v = params[key];
|
|
30917
|
-
if (typeof v === "string") return `${key}=${sanitize(v)}`;
|
|
30918
|
-
return `${key}=${v}`;
|
|
31675
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
31676
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
31677
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
31678
|
+
var TRANSPORT_TOKEN = {
|
|
31679
|
+
native: "ssm",
|
|
31680
|
+
amt: "amt",
|
|
31681
|
+
moq: "moq"
|
|
31682
|
+
};
|
|
31683
|
+
function sfString(value) {
|
|
31684
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
31685
|
+
}
|
|
31686
|
+
function encodeCmcdRecord(record) {
|
|
31687
|
+
return Object.keys(record).sort().map((key) => {
|
|
31688
|
+
const value = record[key];
|
|
31689
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
31690
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
31691
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
31692
|
+
return `${key}=${value.token}`;
|
|
30919
31693
|
}).join(",");
|
|
30920
31694
|
}
|
|
31695
|
+
var token = (value) => ({ token: value });
|
|
31696
|
+
function putCustom(record, name, value) {
|
|
31697
|
+
if (value === void 0) return;
|
|
31698
|
+
if (typeof value === "number") value = String(value);
|
|
31699
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
31700
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
31701
|
+
}
|
|
30921
31702
|
var CmcdProducer = class {
|
|
30922
|
-
#onCmcdParams;
|
|
30923
31703
|
#onCmcdPost;
|
|
31704
|
+
#intervalMs;
|
|
31705
|
+
#sessionId;
|
|
31706
|
+
#now;
|
|
31707
|
+
#sn = 0;
|
|
31708
|
+
#lastHeartbeat = null;
|
|
30924
31709
|
#transport;
|
|
30925
31710
|
#lastStats;
|
|
30926
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
30927
31711
|
#playMode = null;
|
|
30928
31712
|
#deltaFailRate = null;
|
|
30929
31713
|
#switchMiscount = 0;
|
|
30930
31714
|
#manifestStats = null;
|
|
30931
31715
|
#fecFailClosed = null;
|
|
31716
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
31717
|
+
#lifecycle = null;
|
|
30932
31718
|
constructor(options) {
|
|
30933
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
30934
31719
|
this.#onCmcdPost = options.onCmcdPost;
|
|
31720
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
31721
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
31722
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
31723
|
+
}
|
|
31724
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
31725
|
+
this.#now = options.now ?? Date.now;
|
|
30935
31726
|
}
|
|
30936
31727
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
30937
31728
|
updateTransport(snapshot) {
|
|
30938
31729
|
this.#transport = snapshot;
|
|
30939
31730
|
}
|
|
30940
31731
|
/**
|
|
30941
|
-
* Set the current play-mode
|
|
30942
|
-
*
|
|
30943
|
-
* (CLAUDE.md: error on bad init).
|
|
31732
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
31733
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
30944
31734
|
*/
|
|
30945
31735
|
updatePlayMode(mode) {
|
|
30946
31736
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -30951,9 +31741,8 @@ var CmcdProducer = class {
|
|
|
30951
31741
|
this.#playMode = mode;
|
|
30952
31742
|
}
|
|
30953
31743
|
/**
|
|
30954
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
30955
|
-
* `
|
|
30956
|
-
* Omitted on single-track streams.
|
|
31744
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
31745
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
30957
31746
|
*/
|
|
30958
31747
|
updateDeltaFailRate(rate) {
|
|
30959
31748
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -30963,112 +31752,139 @@ var CmcdProducer = class {
|
|
|
30963
31752
|
}
|
|
30964
31753
|
this.#deltaFailRate = rate;
|
|
30965
31754
|
}
|
|
30966
|
-
/**
|
|
30967
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
30968
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
30969
|
-
*/
|
|
31755
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
30970
31756
|
incrementSwitchMiscount() {
|
|
30971
31757
|
this.#switchMiscount += 1;
|
|
30972
31758
|
}
|
|
30973
31759
|
/**
|
|
30974
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
30975
|
-
*
|
|
30976
|
-
* `
|
|
30977
|
-
* stream playing without FEC is told apart from one
|
|
31760
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
31761
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
31762
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
31763
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
31764
|
+
* with no loss.
|
|
30978
31765
|
*/
|
|
30979
31766
|
updateFecFailClosed(reason) {
|
|
31767
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
30980
31768
|
this.#fecFailClosed = reason;
|
|
31769
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
30981
31770
|
}
|
|
30982
31771
|
updateManifestVerification(stats) {
|
|
30983
31772
|
this.#manifestStats = stats;
|
|
30984
31773
|
}
|
|
30985
31774
|
/**
|
|
30986
|
-
*
|
|
30987
|
-
*
|
|
30988
|
-
*
|
|
30989
|
-
*
|
|
31775
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
31776
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
31777
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
31778
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
30990
31779
|
*/
|
|
31780
|
+
onRepairLifecycle(event) {
|
|
31781
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
31782
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
31783
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
31784
|
+
else this.#lifecycle.fetch++;
|
|
31785
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
31786
|
+
this.#lifecycle.failedKeyframe++;
|
|
31787
|
+
}
|
|
31788
|
+
}
|
|
31789
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
30991
31790
|
emitSwitchDrift(driftUs) {
|
|
30992
31791
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
30993
31792
|
throw new Error(
|
|
30994
31793
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
30995
31794
|
);
|
|
30996
31795
|
}
|
|
30997
|
-
const
|
|
30998
|
-
|
|
30999
|
-
|
|
31000
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31001
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
31002
|
-
this.#emit(params);
|
|
31796
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
31797
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
31798
|
+
this.#post([record]);
|
|
31003
31799
|
}
|
|
31004
|
-
/**
|
|
31005
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31006
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31007
|
-
*/
|
|
31800
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31008
31801
|
onSnapshot(data) {
|
|
31009
31802
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31010
31803
|
if (!videoTrack) return;
|
|
31011
31804
|
this.#lastStats = videoTrack;
|
|
31012
|
-
|
|
31013
|
-
...buildFecParams(videoTrack),
|
|
31014
|
-
...buildTransportParams(this.#transport)
|
|
31015
|
-
};
|
|
31016
|
-
if (this.#playMode !== null) {
|
|
31017
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
31018
|
-
}
|
|
31019
|
-
if (this.#fecFailClosed) {
|
|
31020
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
31021
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
31022
|
-
}
|
|
31023
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31024
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
31025
|
-
}
|
|
31026
|
-
if (this.#switchMiscount > 0) {
|
|
31027
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31028
|
-
}
|
|
31029
|
-
if (this.#manifestStats) {
|
|
31030
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
31031
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
31032
|
-
}
|
|
31033
|
-
this.#emit(params);
|
|
31805
|
+
this.#heartbeat();
|
|
31034
31806
|
}
|
|
31035
31807
|
/**
|
|
31036
|
-
*
|
|
31037
|
-
*
|
|
31808
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
31809
|
+
* heartbeat when due.
|
|
31038
31810
|
*/
|
|
31039
31811
|
onTransportSnapshot(data) {
|
|
31040
31812
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31041
31813
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31042
31814
|
}
|
|
31043
31815
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31044
|
-
if (data.switchMiscount !== void 0
|
|
31045
|
-
|
|
31816
|
+
if (data.switchMiscount !== void 0) {
|
|
31817
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
31818
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
31819
|
+
}
|
|
31820
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31046
31821
|
}
|
|
31047
31822
|
this.#transport = data.transport;
|
|
31048
|
-
|
|
31049
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
31050
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
31051
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
31052
|
-
this.#emit(params);
|
|
31823
|
+
this.#heartbeat();
|
|
31053
31824
|
}
|
|
31054
|
-
/**
|
|
31055
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
31056
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
31057
|
-
* x-bc-ct (one-shot).
|
|
31058
|
-
*/
|
|
31825
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31059
31826
|
onTransition(event) {
|
|
31060
|
-
const
|
|
31061
|
-
|
|
31062
|
-
|
|
31827
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
31828
|
+
putCustom(record, "ct", token(event));
|
|
31829
|
+
this.#putTransport(record);
|
|
31830
|
+
this.#post([record]);
|
|
31831
|
+
}
|
|
31832
|
+
#base(event) {
|
|
31833
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
31834
|
+
}
|
|
31835
|
+
#heartbeat() {
|
|
31836
|
+
const now = this.#now();
|
|
31837
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
31838
|
+
this.#lastHeartbeat = now;
|
|
31839
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
31840
|
+
}
|
|
31841
|
+
#putTransport(record) {
|
|
31842
|
+
const snap = this.#transport;
|
|
31843
|
+
if (!snap) return;
|
|
31844
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
31845
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
31846
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
31847
|
+
}
|
|
31848
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
31849
|
+
}
|
|
31850
|
+
/** The `e=ce` counter report. */
|
|
31851
|
+
#counters() {
|
|
31852
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
31853
|
+
const stats = this.#lastStats;
|
|
31854
|
+
if (stats) {
|
|
31855
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
31856
|
+
putCustom(record, "fk", stats.config.k);
|
|
31857
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
31858
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
31859
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
31860
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
31861
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
31862
|
+
}
|
|
31863
|
+
if (this.#lifecycle) {
|
|
31864
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
31865
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
31866
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
31867
|
+
}
|
|
31868
|
+
this.#putTransport(record);
|
|
31869
|
+
if (this.#playMode !== null) {
|
|
31870
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
31871
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31872
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
31873
|
+
}
|
|
31874
|
+
}
|
|
31875
|
+
if (this.#fecFailClosed) {
|
|
31876
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
31877
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
31878
|
+
}
|
|
31879
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
31880
|
+
if (this.#manifestStats) {
|
|
31881
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
31882
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
31063
31883
|
}
|
|
31064
|
-
|
|
31065
|
-
params["x-bc-ct"] = event;
|
|
31066
|
-
this.#emit(params);
|
|
31884
|
+
return record;
|
|
31067
31885
|
}
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
this.#onCmcdParams?.(params);
|
|
31071
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
31886
|
+
#post(records) {
|
|
31887
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31072
31888
|
}
|
|
31073
31889
|
};
|
|
31074
31890
|
|
|
@@ -31164,18 +31980,31 @@ function resolveRepairLayers(catalog, source) {
|
|
|
31164
31980
|
totalBaseRepairSymbols: binding.totalBaseRepairSymbols
|
|
31165
31981
|
};
|
|
31166
31982
|
}
|
|
31983
|
+
function valueAt(catalog, pointer) {
|
|
31984
|
+
let node = catalog;
|
|
31985
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
31986
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
31987
|
+
node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
31988
|
+
}
|
|
31989
|
+
return node;
|
|
31990
|
+
}
|
|
31991
|
+
function catalogTrackDeliveryMode(catalog, source) {
|
|
31992
|
+
const binding = bindingOf(catalog, source);
|
|
31993
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
|
|
31994
|
+
return entry?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
31995
|
+
}
|
|
31167
31996
|
function moqtPriority2(value) {
|
|
31168
31997
|
return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
|
|
31169
31998
|
}
|
|
31170
31999
|
function catalogRepairTarget(catalog, source) {
|
|
31171
32000
|
const binding = bindingOf(catalog, source);
|
|
31172
32001
|
if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
|
|
31173
|
-
const
|
|
32002
|
+
const layerZero2 = binding.layers[0];
|
|
31174
32003
|
const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
|
|
31175
32004
|
const tracks = catalog.tracks;
|
|
31176
32005
|
const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
|
|
31177
32006
|
const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
|
|
31178
|
-
return { repairTrack:
|
|
32007
|
+
return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
|
|
31179
32008
|
}
|
|
31180
32009
|
function numericField(value) {
|
|
31181
32010
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
@@ -31207,6 +32036,7 @@ function fecParams(catalog, track, fps, sampleRate) {
|
|
|
31207
32036
|
repairTrackName: base.repairTrack,
|
|
31208
32037
|
repairLayers: repair.baseLayers,
|
|
31209
32038
|
repairOverlays: repair.overlays,
|
|
32039
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track),
|
|
31210
32040
|
...mediaRate
|
|
31211
32041
|
};
|
|
31212
32042
|
}
|
|
@@ -31257,7 +32087,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
|
|
|
31257
32087
|
var PATH_BITS = { moq: 1, multicast: 2 };
|
|
31258
32088
|
var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
|
|
31259
32089
|
var MmtpFecSession = class {
|
|
32090
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
32091
|
+
get repairSymbols() {
|
|
32092
|
+
return this.#repairSymbols;
|
|
32093
|
+
}
|
|
31260
32094
|
#decoder;
|
|
32095
|
+
#repairSymbols;
|
|
32096
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
32097
|
+
#message;
|
|
31261
32098
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
31262
32099
|
#blocks = /* @__PURE__ */ new Map();
|
|
31263
32100
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -31295,6 +32132,24 @@ var MmtpFecSession = class {
|
|
|
31295
32132
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
31296
32133
|
*/
|
|
31297
32134
|
constructor(config) {
|
|
32135
|
+
if (config && "alFecFlow" in config) {
|
|
32136
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow);
|
|
32137
|
+
if (!message) {
|
|
32138
|
+
throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
|
|
32139
|
+
}
|
|
32140
|
+
this.#message = message;
|
|
32141
|
+
this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
|
|
32142
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
|
|
32143
|
+
this.sourceSymbols = message.maximumK;
|
|
32144
|
+
this.#repairSymbols = message.maximumP;
|
|
32145
|
+
this.symbolSize = message.repairSymbolSize;
|
|
32146
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
|
|
32147
|
+
this.#repairPacketId = message.repairFlowId;
|
|
32148
|
+
this.#heldLayers = layerZero(message.maximumP);
|
|
32149
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
32150
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
32151
|
+
return;
|
|
32152
|
+
}
|
|
31298
32153
|
const fec = config?.fec;
|
|
31299
32154
|
if (!fec || typeof fec !== "object") {
|
|
31300
32155
|
throw new Error("MmtpFecSession: the catalog fec instance is required");
|
|
@@ -31305,7 +32160,7 @@ var MmtpFecSession = class {
|
|
|
31305
32160
|
}
|
|
31306
32161
|
this.algorithm = fec.scheme;
|
|
31307
32162
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
|
|
31308
|
-
this
|
|
32163
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
|
|
31309
32164
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
|
|
31310
32165
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
|
|
31311
32166
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
|
|
@@ -31325,17 +32180,30 @@ var MmtpFecSession = class {
|
|
|
31325
32180
|
config.heldLayers
|
|
31326
32181
|
);
|
|
31327
32182
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
|
|
31334
|
-
|
|
31335
|
-
|
|
31336
|
-
|
|
31337
|
-
|
|
31338
|
-
|
|
32183
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
32184
|
+
}
|
|
32185
|
+
/**
|
|
32186
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
32187
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
32188
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
32189
|
+
* previous one stays valid before the first block carrying the new total
|
|
32190
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
32191
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
32192
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
32193
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
32194
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
32195
|
+
*/
|
|
32196
|
+
updateAlFecFlow(flow) {
|
|
32197
|
+
const held = this.#message;
|
|
32198
|
+
if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
|
|
32199
|
+
const next = alFecRaptorQFlowConfig(flow);
|
|
32200
|
+
if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
|
|
32201
|
+
return "refused";
|
|
32202
|
+
}
|
|
32203
|
+
if (next.maximumP <= this.#repairSymbols) return "unchanged";
|
|
32204
|
+
this.#repairSymbols = next.maximumP;
|
|
32205
|
+
this.#heldLayers = layerZero(next.maximumP);
|
|
32206
|
+
return "raised";
|
|
31339
32207
|
}
|
|
31340
32208
|
/** True once release() ran. */
|
|
31341
32209
|
get released() {
|
|
@@ -31430,7 +32298,7 @@ var MmtpFecSession = class {
|
|
|
31430
32298
|
}
|
|
31431
32299
|
if (admission.reject) {
|
|
31432
32300
|
const verdict2 = this.#heldLayers.failsClosed(
|
|
31433
|
-
repairTrackName,
|
|
32301
|
+
this.#trackFor(repairTrackName),
|
|
31434
32302
|
frame.ssStart,
|
|
31435
32303
|
frame.rsbLength,
|
|
31436
32304
|
this.#anchorHorizon,
|
|
@@ -31445,7 +32313,7 @@ var MmtpFecSession = class {
|
|
|
31445
32313
|
this.#repairPacketId ??= frame.packetId;
|
|
31446
32314
|
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
31447
32315
|
const verdict = this.#heldLayers.admit(
|
|
31448
|
-
repairTrackName,
|
|
32316
|
+
this.#trackFor(repairTrackName),
|
|
31449
32317
|
frame.ssStart,
|
|
31450
32318
|
frame.rsbLength,
|
|
31451
32319
|
this.#anchorHorizon,
|
|
@@ -31504,6 +32372,10 @@ var MmtpFecSession = class {
|
|
|
31504
32372
|
decoder.free();
|
|
31505
32373
|
return true;
|
|
31506
32374
|
}
|
|
32375
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
32376
|
+
#trackFor(repairTrackName) {
|
|
32377
|
+
return this.#message ? void 0 : repairTrackName;
|
|
32378
|
+
}
|
|
31507
32379
|
#drop(reason) {
|
|
31508
32380
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
|
|
31509
32381
|
return [];
|
|
@@ -31625,6 +32497,22 @@ var MmtpFecSession = class {
|
|
|
31625
32497
|
}
|
|
31626
32498
|
}
|
|
31627
32499
|
};
|
|
32500
|
+
function layerZero(total) {
|
|
32501
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
|
|
32502
|
+
}
|
|
32503
|
+
function buildDecoder(Decoder, session) {
|
|
32504
|
+
if (typeof Decoder !== "function") {
|
|
32505
|
+
throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
|
|
32506
|
+
}
|
|
32507
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
|
|
32508
|
+
try {
|
|
32509
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize);
|
|
32510
|
+
} catch (error) {
|
|
32511
|
+
decoder.free();
|
|
32512
|
+
throw error;
|
|
32513
|
+
}
|
|
32514
|
+
return decoder;
|
|
32515
|
+
}
|
|
31628
32516
|
function requirePositiveInteger(value, path) {
|
|
31629
32517
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
31630
32518
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
|
|
@@ -31638,6 +32526,168 @@ function requireNumber(value, path) {
|
|
|
31638
32526
|
return value;
|
|
31639
32527
|
}
|
|
31640
32528
|
|
|
32529
|
+
// src/repair-layer-subscriptions.ts
|
|
32530
|
+
var SERIAL32_HALF2 = 2147483648;
|
|
32531
|
+
var RepairLayerSubscriptions = class {
|
|
32532
|
+
#opts;
|
|
32533
|
+
#windowPackets;
|
|
32534
|
+
#margin;
|
|
32535
|
+
#dropAfterWindows;
|
|
32536
|
+
#source;
|
|
32537
|
+
#layerCount = 0;
|
|
32538
|
+
#subs = /* @__PURE__ */ new Map();
|
|
32539
|
+
#disposed = false;
|
|
32540
|
+
#lastSeq;
|
|
32541
|
+
#expected = 0;
|
|
32542
|
+
#lost = 0;
|
|
32543
|
+
#wantFewer = 0;
|
|
32544
|
+
#lossRate = null;
|
|
32545
|
+
#repairPackets = 0;
|
|
32546
|
+
#repairBytes = 0;
|
|
32547
|
+
#staleDropped = 0;
|
|
32548
|
+
constructor(opts) {
|
|
32549
|
+
this.#opts = opts;
|
|
32550
|
+
this.#source = opts.source;
|
|
32551
|
+
this.#windowPackets = opts.windowPackets ?? 500;
|
|
32552
|
+
this.#margin = opts.margin ?? 2;
|
|
32553
|
+
this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
|
|
32554
|
+
}
|
|
32555
|
+
get layerCount() {
|
|
32556
|
+
return this.#layerCount;
|
|
32557
|
+
}
|
|
32558
|
+
/**
|
|
32559
|
+
* One source packet's MMTP `packet_sequence_number`, in arrival order.
|
|
32560
|
+
* The sequence is per `packet_id`: pass the packets of ONE packet_id only,
|
|
32561
|
+
* or interleaved sequences read as loss.
|
|
32562
|
+
*/
|
|
32563
|
+
noteSourceSequence(seq) {
|
|
32564
|
+
if (this.#disposed || this.#delivery() === "stream") return;
|
|
32565
|
+
seq >>>= 0;
|
|
32566
|
+
if (this.#lastSeq === void 0) {
|
|
32567
|
+
this.#lastSeq = seq;
|
|
32568
|
+
return;
|
|
32569
|
+
}
|
|
32570
|
+
const d = serial32Distance(this.#lastSeq, seq);
|
|
32571
|
+
if (d === 0) return;
|
|
32572
|
+
const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
|
|
32573
|
+
if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
|
|
32574
|
+
this.#lastSeq = seq;
|
|
32575
|
+
this.#expected = 0;
|
|
32576
|
+
this.#lost = 0;
|
|
32577
|
+
return;
|
|
32578
|
+
}
|
|
32579
|
+
if (back === 0) {
|
|
32580
|
+
this.#expected += d;
|
|
32581
|
+
this.#lost += d - 1;
|
|
32582
|
+
this.#lastSeq = seq;
|
|
32583
|
+
} else if (this.#lost > 0) {
|
|
32584
|
+
this.#lost -= 1;
|
|
32585
|
+
}
|
|
32586
|
+
if (this.#expected >= this.#windowPackets) this.#closeWindow();
|
|
32587
|
+
}
|
|
32588
|
+
/** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
|
|
32589
|
+
switchSource(next) {
|
|
32590
|
+
if (this.#disposed) return;
|
|
32591
|
+
this.#closeAll();
|
|
32592
|
+
this.#source = next;
|
|
32593
|
+
this.#lastSeq = void 0;
|
|
32594
|
+
this.#expected = 0;
|
|
32595
|
+
this.#lost = 0;
|
|
32596
|
+
this.#wantFewer = 0;
|
|
32597
|
+
this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
|
|
32598
|
+
this.#reconcile();
|
|
32599
|
+
}
|
|
32600
|
+
snapshot() {
|
|
32601
|
+
return {
|
|
32602
|
+
delivery: this.#delivery(),
|
|
32603
|
+
layerCount: this.#layerCount,
|
|
32604
|
+
subscribedTracks: [...this.#subs.keys()],
|
|
32605
|
+
repairPackets: this.#repairPackets,
|
|
32606
|
+
repairBytes: this.#repairBytes,
|
|
32607
|
+
staleDropped: this.#staleDropped,
|
|
32608
|
+
lossRate: this.#lossRate
|
|
32609
|
+
};
|
|
32610
|
+
}
|
|
32611
|
+
dispose() {
|
|
32612
|
+
if (this.#disposed) return;
|
|
32613
|
+
this.#closeAll();
|
|
32614
|
+
this.#disposed = true;
|
|
32615
|
+
}
|
|
32616
|
+
#delivery() {
|
|
32617
|
+
return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32618
|
+
}
|
|
32619
|
+
#layers() {
|
|
32620
|
+
return this.#source.repairLayers ?? [];
|
|
32621
|
+
}
|
|
32622
|
+
#closeWindow() {
|
|
32623
|
+
const rate = this.#lost / this.#expected;
|
|
32624
|
+
this.#lossRate = rate;
|
|
32625
|
+
this.#expected = 0;
|
|
32626
|
+
this.#lost = 0;
|
|
32627
|
+
const target = this.#targetLayers(rate);
|
|
32628
|
+
if (target > this.#layerCount) {
|
|
32629
|
+
this.#wantFewer = 0;
|
|
32630
|
+
this.#layerCount = target;
|
|
32631
|
+
} else if (target < this.#layerCount) {
|
|
32632
|
+
this.#wantFewer += 1;
|
|
32633
|
+
if (this.#wantFewer >= this.#dropAfterWindows) {
|
|
32634
|
+
this.#wantFewer = 0;
|
|
32635
|
+
this.#layerCount -= 1;
|
|
32636
|
+
}
|
|
32637
|
+
} else {
|
|
32638
|
+
this.#wantFewer = 0;
|
|
32639
|
+
}
|
|
32640
|
+
this.#reconcile();
|
|
32641
|
+
}
|
|
32642
|
+
/** Fewest base layers whose summed P covers margin x the measured loss of K. */
|
|
32643
|
+
#targetLayers(rate) {
|
|
32644
|
+
if (rate <= 0) return 0;
|
|
32645
|
+
const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
|
|
32646
|
+
const layers = this.#layers();
|
|
32647
|
+
let held = 0;
|
|
32648
|
+
for (let n = 0; n < layers.length; n++) {
|
|
32649
|
+
held += layers[n].repairSymbols;
|
|
32650
|
+
if (held >= needed) return n + 1;
|
|
32651
|
+
}
|
|
32652
|
+
return layers.length;
|
|
32653
|
+
}
|
|
32654
|
+
/** Stream delivery never leaves layer count 0, so it never subscribes. */
|
|
32655
|
+
#reconcile() {
|
|
32656
|
+
const want = /* @__PURE__ */ new Map();
|
|
32657
|
+
for (const layer of this.#layers().slice(0, this.#layerCount)) {
|
|
32658
|
+
want.set(layer.repairTrackName, layer.priority);
|
|
32659
|
+
}
|
|
32660
|
+
const overlay = this.#source.repairOverlays?.[0];
|
|
32661
|
+
if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
|
|
32662
|
+
for (const [name, entry] of this.#subs) {
|
|
32663
|
+
if (!want.has(name)) {
|
|
32664
|
+
entry.sub.close();
|
|
32665
|
+
this.#subs.delete(name);
|
|
32666
|
+
}
|
|
32667
|
+
}
|
|
32668
|
+
for (const [name, priority] of want) {
|
|
32669
|
+
if (this.#subs.has(name)) continue;
|
|
32670
|
+
const entry = {};
|
|
32671
|
+
entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
|
|
32672
|
+
this.#subs.set(name, entry);
|
|
32673
|
+
}
|
|
32674
|
+
}
|
|
32675
|
+
/** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
|
|
32676
|
+
#onPacket(name, entry, packet) {
|
|
32677
|
+
if (this.#disposed || this.#subs.get(name) !== entry) {
|
|
32678
|
+
this.#staleDropped += 1;
|
|
32679
|
+
return;
|
|
32680
|
+
}
|
|
32681
|
+
this.#repairPackets += 1;
|
|
32682
|
+
this.#repairBytes += packet.byteLength;
|
|
32683
|
+
this.#opts.feed(packet, name);
|
|
32684
|
+
}
|
|
32685
|
+
#closeAll() {
|
|
32686
|
+
for (const entry of this.#subs.values()) entry.sub.close();
|
|
32687
|
+
this.#subs.clear();
|
|
32688
|
+
}
|
|
32689
|
+
};
|
|
32690
|
+
|
|
31641
32691
|
// src/repair-fetch.ts
|
|
31642
32692
|
var MOQT_PRIORITY_MAX = 255;
|
|
31643
32693
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
@@ -32283,6 +33333,7 @@ export {
|
|
|
32283
33333
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
32284
33334
|
REPAIR_PRIORITY_MAX,
|
|
32285
33335
|
REPAIR_PRIORITY_MIN,
|
|
33336
|
+
RepairLayerSubscriptions,
|
|
32286
33337
|
SOURCE_MEDIA_PRIORITY_MAX,
|
|
32287
33338
|
SampleCombineError,
|
|
32288
33339
|
SampleCombiner,
|
|
@@ -32307,6 +33358,7 @@ export {
|
|
|
32307
33358
|
catalogGroupDurationMs,
|
|
32308
33359
|
catalogKeyframeCadenceMs,
|
|
32309
33360
|
catalogRepairTarget,
|
|
33361
|
+
catalogTrackDeliveryMode,
|
|
32310
33362
|
catalogTrackFecUnavailable,
|
|
32311
33363
|
classifyH264Keyframe,
|
|
32312
33364
|
classifyHevcKeyframe,
|
|
@@ -32327,6 +33379,7 @@ export {
|
|
|
32327
33379
|
detectH264Keyframe,
|
|
32328
33380
|
detectHevcKeyframe,
|
|
32329
33381
|
driftCheck,
|
|
33382
|
+
encodeCmcdRecord,
|
|
32330
33383
|
encodeDataSegment,
|
|
32331
33384
|
encodeFecSourceInfo,
|
|
32332
33385
|
encodeLocTimestamp,
|