@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +1949 -374
- 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 +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- 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 +6 -5
- 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 +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- 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 +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- 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 +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- 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 +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- 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 +336 -59
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- 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.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- 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 +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -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);
|
|
@@ -856,6 +851,17 @@ function readRepairFecPayloadIdFields(mmtpPacket, parsedHeader) {
|
|
|
856
851
|
dataOffset: off + REPAIR_FEC_PID_SIZE
|
|
857
852
|
};
|
|
858
853
|
}
|
|
854
|
+
function extractRepairFecPayloadId(mmtpPacket) {
|
|
855
|
+
const fields = readRepairFecPayloadIdFields(mmtpPacket);
|
|
856
|
+
if (!fields)
|
|
857
|
+
return null;
|
|
858
|
+
return {
|
|
859
|
+
ssStart: fields.ssStart,
|
|
860
|
+
rsbLength: fields.rsbLength,
|
|
861
|
+
rsId: fields.rsId,
|
|
862
|
+
ssbLength: fields.ssbLength
|
|
863
|
+
};
|
|
864
|
+
}
|
|
859
865
|
function parseMoqRepairFrame(frame) {
|
|
860
866
|
return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame));
|
|
861
867
|
}
|
|
@@ -903,6 +909,50 @@ var NTP_SHORT_BITS = 32;
|
|
|
903
909
|
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
904
910
|
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
905
911
|
|
|
912
|
+
// ../mmt-base/dist/signalling.js
|
|
913
|
+
var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
|
|
914
|
+
var SSBG_MODE_1 = 1;
|
|
915
|
+
var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
|
|
916
|
+
var REPAIR_SYMBOL_ID_MAX = 16777215;
|
|
917
|
+
var MIN_REPAIR_SYMBOL_SIZE = 14;
|
|
918
|
+
var REED_SOLOMON_MAX_BLOCK = 255;
|
|
919
|
+
function alFecRaptorQFlowConfig(flow) {
|
|
920
|
+
if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
|
|
921
|
+
return null;
|
|
922
|
+
if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
|
|
923
|
+
return null;
|
|
924
|
+
if (flow.packetIds.length !== 1)
|
|
925
|
+
return null;
|
|
926
|
+
const repair = flow.repairFlows[0];
|
|
927
|
+
const { fecCodeId, maximumK: k, maximumP: p } = repair;
|
|
928
|
+
const t = flow.repairSymbolSize;
|
|
929
|
+
if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
|
|
930
|
+
return null;
|
|
931
|
+
if (fecCodeId === FEC_CODE_RAPTORQ) {
|
|
932
|
+
if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
|
|
933
|
+
return null;
|
|
934
|
+
} else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
|
|
935
|
+
if (k + p > REED_SOLOMON_MAX_BLOCK)
|
|
936
|
+
return null;
|
|
937
|
+
} else {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
|
|
941
|
+
return null;
|
|
942
|
+
return {
|
|
943
|
+
sourcePacketIds: [...flow.packetIds],
|
|
944
|
+
repairFlowId: repair.repairFlowId,
|
|
945
|
+
fecCodeId,
|
|
946
|
+
ssbgMode: flow.ssbgMode,
|
|
947
|
+
fecPayloadIdMode: flow.fecPayloadIdMode,
|
|
948
|
+
repairSymbolSize: t,
|
|
949
|
+
maximumK: k,
|
|
950
|
+
maximumP: p,
|
|
951
|
+
protectionWindowTime: repair.protectionWindowTime,
|
|
952
|
+
protectionWindowSize: repair.protectionWindowSize
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
906
956
|
// ../mmt-base/dist/serial-number.js
|
|
907
957
|
var SERIAL32_SPAN = 4294967296;
|
|
908
958
|
var SERIAL32_HALF = 2147483648;
|
|
@@ -7457,11 +7507,33 @@ var AdaptiveBitrateController = class {
|
|
|
7457
7507
|
};
|
|
7458
7508
|
|
|
7459
7509
|
// node_modules/@blockcast/mmt-base/dist/mmtp.js
|
|
7510
|
+
var MMTP_HEADER_SIZE2 = 12;
|
|
7460
7511
|
var MPU_HEADER_SIZE2 = 8;
|
|
7461
7512
|
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
7462
7513
|
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
7463
7514
|
var MMTP_SIGNALING_PACKET_ID2 = 0;
|
|
7464
7515
|
var MMTP_PACKET_ID_MAX2 = 65535;
|
|
7516
|
+
function readMMTPPacketId2(buffer) {
|
|
7517
|
+
if (buffer.length < MMTP_HEADER_SIZE2)
|
|
7518
|
+
return null;
|
|
7519
|
+
const flags = buffer[0];
|
|
7520
|
+
if ((flags >> 6 & 3) !== 0)
|
|
7521
|
+
return null;
|
|
7522
|
+
let offset = MMTP_HEADER_SIZE2;
|
|
7523
|
+
if ((flags & 32) !== 0) {
|
|
7524
|
+
if (offset + 4 > buffer.length)
|
|
7525
|
+
return null;
|
|
7526
|
+
offset += 4;
|
|
7527
|
+
}
|
|
7528
|
+
if ((flags & 2) !== 0) {
|
|
7529
|
+
if (offset + 4 > buffer.length)
|
|
7530
|
+
return null;
|
|
7531
|
+
const length = buffer[offset + 2] << 8 | buffer[offset + 3];
|
|
7532
|
+
if (offset + 4 + length > buffer.length)
|
|
7533
|
+
return null;
|
|
7534
|
+
}
|
|
7535
|
+
return buffer[2] << 8 | buffer[3];
|
|
7536
|
+
}
|
|
7465
7537
|
|
|
7466
7538
|
// node_modules/@blockcast/mmt-base/dist/net-constants.js
|
|
7467
7539
|
var DEFAULT_AMT_RELAY_PORT2 = 2268;
|
|
@@ -7927,8 +7999,8 @@ var MulticastSubscriptionPool = class {
|
|
|
7927
7999
|
record.claims--;
|
|
7928
8000
|
return this.acquire(gateway, config, consumer);
|
|
7929
8001
|
}
|
|
7930
|
-
const
|
|
7931
|
-
entry.consumers.set(
|
|
8002
|
+
const token2 = Symbol(key);
|
|
8003
|
+
entry.consumers.set(token2, consumer);
|
|
7932
8004
|
let released = false;
|
|
7933
8005
|
return {
|
|
7934
8006
|
key,
|
|
@@ -7936,7 +8008,7 @@ var MulticastSubscriptionPool = class {
|
|
|
7936
8008
|
if (released)
|
|
7937
8009
|
return;
|
|
7938
8010
|
released = true;
|
|
7939
|
-
entry.consumers.delete(
|
|
8011
|
+
entry.consumers.delete(token2);
|
|
7940
8012
|
record.claims--;
|
|
7941
8013
|
if (record.claims > 0)
|
|
7942
8014
|
return;
|
|
@@ -8447,6 +8519,205 @@ var TransportManager = class {
|
|
|
8447
8519
|
};
|
|
8448
8520
|
var transportManager = new TransportManager();
|
|
8449
8521
|
|
|
8522
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
|
|
8523
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
|
|
8524
|
+
var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
|
|
8525
|
+
var INTEGRITY_SCHEME = "manifest-ed25519-2";
|
|
8526
|
+
var MulticastError = class extends Error {
|
|
8527
|
+
code;
|
|
8528
|
+
constructor(code, message, cause) {
|
|
8529
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
8530
|
+
this.name = "MulticastError";
|
|
8531
|
+
this.code = code;
|
|
8532
|
+
}
|
|
8533
|
+
};
|
|
8534
|
+
function datagramsOf(chunk) {
|
|
8535
|
+
const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
|
|
8536
|
+
const out = [];
|
|
8537
|
+
for (const d of list) {
|
|
8538
|
+
const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
|
|
8539
|
+
if (data instanceof Uint8Array)
|
|
8540
|
+
out.push(data);
|
|
8541
|
+
else if (data instanceof ArrayBuffer)
|
|
8542
|
+
out.push(new Uint8Array(data));
|
|
8543
|
+
}
|
|
8544
|
+
return out;
|
|
8545
|
+
}
|
|
8546
|
+
function reasonOf(value) {
|
|
8547
|
+
return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
|
|
8548
|
+
}
|
|
8549
|
+
function registry() {
|
|
8550
|
+
const g = globalThis;
|
|
8551
|
+
if (g[REGISTRY] === void 0) {
|
|
8552
|
+
const providers = /* @__PURE__ */ new WeakSet();
|
|
8553
|
+
const byScope = /* @__PURE__ */ new WeakMap();
|
|
8554
|
+
Object.defineProperty(g, REGISTRY, {
|
|
8555
|
+
value: Object.freeze({
|
|
8556
|
+
isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
|
|
8557
|
+
providerFor: (scope) => {
|
|
8558
|
+
let provider = byScope.get(scope);
|
|
8559
|
+
if (provider === void 0) {
|
|
8560
|
+
provider = createProvider(scope);
|
|
8561
|
+
providers.add(provider);
|
|
8562
|
+
byScope.set(scope, provider);
|
|
8563
|
+
}
|
|
8564
|
+
return provider;
|
|
8565
|
+
}
|
|
8566
|
+
}),
|
|
8567
|
+
writable: false,
|
|
8568
|
+
configurable: false,
|
|
8569
|
+
enumerable: false
|
|
8570
|
+
});
|
|
8571
|
+
}
|
|
8572
|
+
return g[REGISTRY];
|
|
8573
|
+
}
|
|
8574
|
+
function resolveMulticastGateway(scope = globalThis) {
|
|
8575
|
+
const s = scope;
|
|
8576
|
+
if (typeof s.MulticastReceiver === "function")
|
|
8577
|
+
return registry().providerFor(scope);
|
|
8578
|
+
if (typeof s.multicast?.subscribe === "function")
|
|
8579
|
+
return s.multicast;
|
|
8580
|
+
return void 0;
|
|
8581
|
+
}
|
|
8582
|
+
function isUserAgentVerifiedGateway(gateway) {
|
|
8583
|
+
return registry().isProvider(gateway);
|
|
8584
|
+
}
|
|
8585
|
+
function userAgentVerifiedIngress(ingress, gateway) {
|
|
8586
|
+
if (isUserAgentVerifiedGateway(gateway))
|
|
8587
|
+
return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
|
|
8588
|
+
if (!("admittedBy" in ingress))
|
|
8589
|
+
return ingress;
|
|
8590
|
+
const { admittedBy: _, ...rest } = ingress;
|
|
8591
|
+
return rest;
|
|
8592
|
+
}
|
|
8593
|
+
function unsupported(message, cause) {
|
|
8594
|
+
return new MulticastError("capability-unsupported", message, cause);
|
|
8595
|
+
}
|
|
8596
|
+
function createProvider(scope) {
|
|
8597
|
+
const Receiver = scope.MulticastReceiver;
|
|
8598
|
+
let gate = null;
|
|
8599
|
+
return {
|
|
8600
|
+
capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
|
|
8601
|
+
subscribe: async (config) => {
|
|
8602
|
+
const c = config;
|
|
8603
|
+
if (c.consume !== "bytes")
|
|
8604
|
+
throw unsupported(`consume mode ${String(c.consume)} is not supported`);
|
|
8605
|
+
const origin = c.origin ?? "ip-multicast";
|
|
8606
|
+
if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
|
|
8607
|
+
throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
|
|
8608
|
+
}
|
|
8609
|
+
if (c.relay !== void 0)
|
|
8610
|
+
throw unsupported("MulticastReceiver picks its own AMT relay");
|
|
8611
|
+
if (!c.source)
|
|
8612
|
+
throw unsupported("MulticastReceiver needs an SSM source");
|
|
8613
|
+
if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
|
|
8614
|
+
throw unsupported("MulticastReceiver filters by source only");
|
|
8615
|
+
}
|
|
8616
|
+
const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
|
|
8617
|
+
if (gate !== null)
|
|
8618
|
+
await gate;
|
|
8619
|
+
let rx;
|
|
8620
|
+
try {
|
|
8621
|
+
rx = new Receiver(options);
|
|
8622
|
+
} catch (error) {
|
|
8623
|
+
throw unsupported("MulticastReceiver refused the request", error);
|
|
8624
|
+
}
|
|
8625
|
+
const opened = rx.opened;
|
|
8626
|
+
if (gate === null)
|
|
8627
|
+
gate = opened.then(() => void 0, () => void 0);
|
|
8628
|
+
return openSubscription(rx, opened, options, c.packetIds);
|
|
8629
|
+
}
|
|
8630
|
+
};
|
|
8631
|
+
}
|
|
8632
|
+
async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
8633
|
+
let opened;
|
|
8634
|
+
try {
|
|
8635
|
+
opened = await openedPromise;
|
|
8636
|
+
} catch (error) {
|
|
8637
|
+
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8638
|
+
}
|
|
8639
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8640
|
+
await rx.close();
|
|
8641
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8642
|
+
}
|
|
8643
|
+
const events = new EventTarget();
|
|
8644
|
+
let state = "connected";
|
|
8645
|
+
let filter = packetIds?.length ? new Set(packetIds) : null;
|
|
8646
|
+
let closing = false;
|
|
8647
|
+
const setState = (next) => {
|
|
8648
|
+
if (state === next)
|
|
8649
|
+
return;
|
|
8650
|
+
state = next;
|
|
8651
|
+
events.dispatchEvent(new CustomEvent("state", { detail: next }));
|
|
8652
|
+
};
|
|
8653
|
+
const sourceAddress = options.sourceAddress;
|
|
8654
|
+
const subscription = {
|
|
8655
|
+
id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
|
|
8656
|
+
get state() {
|
|
8657
|
+
return state;
|
|
8658
|
+
},
|
|
8659
|
+
delivery: "datagram",
|
|
8660
|
+
eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
|
|
8661
|
+
setPacketIds(ids) {
|
|
8662
|
+
filter = ids.length ? new Set(ids) : null;
|
|
8663
|
+
},
|
|
8664
|
+
addEventListener: events.addEventListener.bind(events),
|
|
8665
|
+
removeEventListener: events.removeEventListener.bind(events),
|
|
8666
|
+
async unsubscribe() {
|
|
8667
|
+
closing = true;
|
|
8668
|
+
await rx.close();
|
|
8669
|
+
setState("closed");
|
|
8670
|
+
}
|
|
8671
|
+
};
|
|
8672
|
+
void (async () => {
|
|
8673
|
+
const reader = opened.readable.getReader();
|
|
8674
|
+
try {
|
|
8675
|
+
for (; ; ) {
|
|
8676
|
+
const { value, done } = await reader.read();
|
|
8677
|
+
if (done)
|
|
8678
|
+
break;
|
|
8679
|
+
const packets = [];
|
|
8680
|
+
for (const data of datagramsOf(value)) {
|
|
8681
|
+
if (filter !== null) {
|
|
8682
|
+
const id = readMMTPPacketId2(data);
|
|
8683
|
+
if (id === null || !filter.has(id))
|
|
8684
|
+
continue;
|
|
8685
|
+
}
|
|
8686
|
+
const copy = data.slice().buffer;
|
|
8687
|
+
packets.push(copy);
|
|
8688
|
+
events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
|
|
8689
|
+
}
|
|
8690
|
+
if (packets.length > 0)
|
|
8691
|
+
events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
|
|
8692
|
+
}
|
|
8693
|
+
} catch {
|
|
8694
|
+
}
|
|
8695
|
+
})();
|
|
8696
|
+
void rx.closed.then((value) => {
|
|
8697
|
+
const reason = reasonOf(value);
|
|
8698
|
+
if (!closing) {
|
|
8699
|
+
if (reason === "consent-withdrawn") {
|
|
8700
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8701
|
+
detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
|
|
8702
|
+
}));
|
|
8703
|
+
} else if (reason === "oversubscribed" || reason === "network") {
|
|
8704
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8705
|
+
detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
|
|
8706
|
+
}));
|
|
8707
|
+
}
|
|
8708
|
+
}
|
|
8709
|
+
setState("closed");
|
|
8710
|
+
}, (error) => {
|
|
8711
|
+
if (!closing) {
|
|
8712
|
+
events.dispatchEvent(new CustomEvent("error", {
|
|
8713
|
+
detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
|
|
8714
|
+
}));
|
|
8715
|
+
}
|
|
8716
|
+
setState("closed");
|
|
8717
|
+
});
|
|
8718
|
+
return subscription;
|
|
8719
|
+
}
|
|
8720
|
+
|
|
8450
8721
|
// node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
|
|
8451
8722
|
var MAX_MOQT_PRIORITY = 255;
|
|
8452
8723
|
function moqtPriority(value, field) {
|
|
@@ -8486,14 +8757,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8486
8757
|
});
|
|
8487
8758
|
|
|
8488
8759
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
8760
|
+
var MAX_PENDING_PACKETS = 256;
|
|
8489
8761
|
var AMTGatewayManager = class {
|
|
8490
8762
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8491
8763
|
messageHandler;
|
|
8764
|
+
messageListenerRegistered = false;
|
|
8492
8765
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
8493
8766
|
constructor() {
|
|
8494
8767
|
this.messageHandler = (event) => {
|
|
8495
8768
|
const data = event.data;
|
|
8496
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8497
8769
|
if (data.type === "UDP_PACKET") {
|
|
8498
8770
|
this.handlePacket(data);
|
|
8499
8771
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8502,12 +8774,22 @@ var AMTGatewayManager = class {
|
|
|
8502
8774
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
8503
8775
|
}
|
|
8504
8776
|
};
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8777
|
+
}
|
|
8778
|
+
ensureMessageListener() {
|
|
8779
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
8780
|
+
return;
|
|
8781
|
+
}
|
|
8782
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
8783
|
+
this.messageListenerRegistered = true;
|
|
8784
|
+
console.log("[AMT] Registered service worker message handler");
|
|
8785
|
+
}
|
|
8786
|
+
removeMessageListener() {
|
|
8787
|
+
if (!this.messageListenerRegistered)
|
|
8788
|
+
return;
|
|
8789
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8790
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8510
8791
|
}
|
|
8792
|
+
this.messageListenerRegistered = false;
|
|
8511
8793
|
}
|
|
8512
8794
|
/**
|
|
8513
8795
|
* Subscribe via AMT tunnel
|
|
@@ -8517,6 +8799,7 @@ var AMTGatewayManager = class {
|
|
|
8517
8799
|
if (config.protected) {
|
|
8518
8800
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
8519
8801
|
}
|
|
8802
|
+
this.ensureMessageListener();
|
|
8520
8803
|
const keyStr = this.getKeyString(key);
|
|
8521
8804
|
if (this.subscriptions.has(keyStr)) {
|
|
8522
8805
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -8552,6 +8835,8 @@ var AMTGatewayManager = class {
|
|
|
8552
8835
|
this.subscriptions.delete(keyStr);
|
|
8553
8836
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8554
8837
|
}
|
|
8838
|
+
if (this.subscriptions.size === 0)
|
|
8839
|
+
this.removeMessageListener();
|
|
8555
8840
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8556
8841
|
}
|
|
8557
8842
|
}
|
|
@@ -8573,6 +8858,8 @@ var AMTGatewayManager = class {
|
|
|
8573
8858
|
} finally {
|
|
8574
8859
|
this.subscriptions.delete(handle);
|
|
8575
8860
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8861
|
+
if (this.subscriptions.size === 0)
|
|
8862
|
+
this.removeMessageListener();
|
|
8576
8863
|
}
|
|
8577
8864
|
}
|
|
8578
8865
|
/**
|
|
@@ -8589,14 +8876,12 @@ var AMTGatewayManager = class {
|
|
|
8589
8876
|
if (!subscriptionId)
|
|
8590
8877
|
return;
|
|
8591
8878
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8592
|
-
if (!subscription)
|
|
8593
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8879
|
+
if (!subscription)
|
|
8594
8880
|
return;
|
|
8595
|
-
}
|
|
8596
8881
|
if (!subscription.handshakeComplete) {
|
|
8597
8882
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8598
8883
|
const packetData2 = data;
|
|
8599
|
-
if (queue) {
|
|
8884
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8600
8885
|
queue.push({
|
|
8601
8886
|
data: packetData2.data,
|
|
8602
8887
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8622,19 +8907,15 @@ var AMTGatewayManager = class {
|
|
|
8622
8907
|
if (!subscriptionId)
|
|
8623
8908
|
return;
|
|
8624
8909
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8625
|
-
if (!subscription)
|
|
8626
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8910
|
+
if (!subscription)
|
|
8627
8911
|
return;
|
|
8628
|
-
}
|
|
8629
8912
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8630
8913
|
}
|
|
8631
8914
|
/**
|
|
8632
8915
|
* Cleanup on destroy
|
|
8633
8916
|
*/
|
|
8634
8917
|
destroy() {
|
|
8635
|
-
|
|
8636
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8637
|
-
}
|
|
8918
|
+
this.removeMessageListener();
|
|
8638
8919
|
const handles = Array.from(this.subscriptions.keys());
|
|
8639
8920
|
for (const handle of handles) {
|
|
8640
8921
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -8733,9 +9014,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
|
|
|
8733
9014
|
}
|
|
8734
9015
|
function validateMmtpDashTemplate(template, path) {
|
|
8735
9016
|
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 $${
|
|
9017
|
+
const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
|
|
9018
|
+
if (unsupported2 !== void 0) {
|
|
9019
|
+
throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
|
|
8739
9020
|
}
|
|
8740
9021
|
if (variables.filter((variable) => variable === "Number").length !== 1) {
|
|
8741
9022
|
throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
|
|
@@ -12165,6 +12446,7 @@ var TrackSession = class {
|
|
|
12165
12446
|
this.#state = "idle";
|
|
12166
12447
|
this.#subscription = null;
|
|
12167
12448
|
this.#disarmFec();
|
|
12449
|
+
this.#unsuppress();
|
|
12168
12450
|
}
|
|
12169
12451
|
// -----------------------------------------------------------------------
|
|
12170
12452
|
// Internal
|
|
@@ -12658,6 +12940,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12658
12940
|
this.#heldEvicted = 0;
|
|
12659
12941
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
12660
12942
|
this.#holes = [];
|
|
12943
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
12944
|
+
this.#expired = [];
|
|
12945
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
12946
|
+
this.#newlyExpired = [];
|
|
12947
|
+
/**
|
|
12948
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
12949
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
12950
|
+
*/
|
|
12951
|
+
this.#closedBehind = [];
|
|
12661
12952
|
/**
|
|
12662
12953
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
12663
12954
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -12679,7 +12970,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12679
12970
|
#retentionBlocks;
|
|
12680
12971
|
#heldEvicted;
|
|
12681
12972
|
#newest;
|
|
12973
|
+
#newestSource;
|
|
12682
12974
|
#holes;
|
|
12975
|
+
#expired;
|
|
12976
|
+
#newlyExpired;
|
|
12977
|
+
#closedBehind;
|
|
12683
12978
|
static #checkedRetention(retentionBlocks) {
|
|
12684
12979
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
12685
12980
|
throw new Error(
|
|
@@ -12716,6 +13011,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12716
13011
|
observe(position) {
|
|
12717
13012
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12718
13013
|
}
|
|
13014
|
+
/**
|
|
13015
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
13016
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
13017
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
13018
|
+
* reordering.
|
|
13019
|
+
*/
|
|
13020
|
+
get newestSource() {
|
|
13021
|
+
return this.#newestSource;
|
|
13022
|
+
}
|
|
13023
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
13024
|
+
noteSource(ssId) {
|
|
13025
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
13026
|
+
this.#newestSource = ssId;
|
|
13027
|
+
return true;
|
|
13028
|
+
}
|
|
12719
13029
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12720
13030
|
get heldEvicted() {
|
|
12721
13031
|
return this.#heldEvicted;
|
|
@@ -12734,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12734
13044
|
}
|
|
12735
13045
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
12736
13046
|
locate(ssId) {
|
|
12737
|
-
for (
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
13047
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
13048
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
13049
|
+
const block = blocks[i];
|
|
13050
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
13051
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
13052
|
+
}
|
|
12741
13053
|
}
|
|
12742
13054
|
}
|
|
12743
13055
|
return null;
|
|
12744
13056
|
}
|
|
12745
|
-
/** Number of pending
|
|
13057
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
12746
13058
|
get holeCount() {
|
|
12747
|
-
return this.#holes.length;
|
|
13059
|
+
return this.#holes.length + this.#expired.length;
|
|
13060
|
+
}
|
|
13061
|
+
/**
|
|
13062
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
13063
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
13064
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
13065
|
+
* release gate does not wait for it. Its owner must close it
|
|
13066
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
13067
|
+
*/
|
|
13068
|
+
takeExpiredHoles() {
|
|
13069
|
+
return this.#newlyExpired.splice(0);
|
|
12748
13070
|
}
|
|
12749
13071
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
12750
13072
|
blockEndingAt(end) {
|
|
@@ -12800,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12800
13122
|
/**
|
|
12801
13123
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
12802
13124
|
* block states its range and places every held source in it; later repair
|
|
12803
|
-
* symbols of the same block change nothing. A range
|
|
12804
|
-
*
|
|
12805
|
-
*
|
|
13125
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
13126
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
13127
|
+
* a known block (a different length, or an overlap with another block or
|
|
13128
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
13129
|
+
* block changes.
|
|
12806
13130
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
12807
13131
|
* A block so far past `newest` that stating it would retire the position
|
|
12808
13132
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -12821,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12821
13145
|
const known = this.blockAt(start);
|
|
12822
13146
|
if (known !== void 0) {
|
|
12823
13147
|
if (known.span) {
|
|
12824
|
-
|
|
13148
|
+
if (known.length === length) {
|
|
13149
|
+
const block2 = { start, length, span: false };
|
|
13150
|
+
this.#blocks[this.#blocks.indexOf(known)] = block2;
|
|
13151
|
+
return { status: "placed", block: block2, placed: [] };
|
|
13152
|
+
}
|
|
13153
|
+
return length < known.length ? {
|
|
13154
|
+
status: "overlap",
|
|
13155
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
13156
|
+
ofSpan: known
|
|
13157
|
+
} : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
|
|
12825
13158
|
}
|
|
12826
13159
|
if (known.length !== length) {
|
|
12827
13160
|
return {
|
|
@@ -12889,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12889
13222
|
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
12890
13223
|
}
|
|
12891
13224
|
/**
|
|
12892
|
-
* Turn every
|
|
12893
|
-
*
|
|
12894
|
-
*
|
|
13225
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
13226
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
13227
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
13228
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
13229
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
13230
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
12895
13231
|
*/
|
|
12896
13232
|
closeHolesWithin(start, length) {
|
|
12897
13233
|
const closed = [];
|
|
12898
|
-
for (const
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
13234
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
13235
|
+
for (const hole of [...holes]) {
|
|
13236
|
+
if (!isoBlockContains(start, length, hole.start)) continue;
|
|
13237
|
+
holes.splice(holes.indexOf(hole), 1);
|
|
13238
|
+
const spanBlock = { start: hole.start, length: hole.length, span: true };
|
|
13239
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
|
|
13240
|
+
if (holes === this.#holes) {
|
|
13241
|
+
this.#blocks.push(spanBlock);
|
|
13242
|
+
} else {
|
|
13243
|
+
this.#closedBehind.push(spanBlock);
|
|
13244
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
|
|
13245
|
+
}
|
|
13246
|
+
}
|
|
12904
13247
|
}
|
|
12905
13248
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
|
|
12906
13249
|
}
|
|
@@ -12914,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12914
13257
|
return void 0;
|
|
12915
13258
|
}
|
|
12916
13259
|
/**
|
|
12917
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
12918
|
-
* lies more than `retentionBlocks` blocks behind
|
|
12919
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
12920
|
-
* horizon uses
|
|
13260
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
13261
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
13262
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
13263
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
13264
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
12921
13265
|
*/
|
|
12922
13266
|
retireBehind(position) {
|
|
12923
13267
|
this.observe(position);
|
|
12924
13268
|
const horizon = this.#retentionBlocks;
|
|
12925
13269
|
const retired = [];
|
|
13270
|
+
this.#closedBehind.length = 0;
|
|
12926
13271
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
12927
13272
|
const block = this.#blocks[i];
|
|
12928
13273
|
const last = serial32Add(block.start, block.length - 1);
|
|
@@ -12934,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12934
13279
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
12935
13280
|
const hole = this.#holes[i];
|
|
12936
13281
|
const last = serial32Add(hole.start, hole.length - 1);
|
|
12937
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
13282
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
|
|
13283
|
+
this.#holes.splice(i, 1);
|
|
13284
|
+
this.#expired.push(hole);
|
|
13285
|
+
this.#newlyExpired.push(hole);
|
|
12938
13286
|
}
|
|
12939
13287
|
for (const ssId of [...this.#held.keys()]) {
|
|
12940
|
-
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#
|
|
13288
|
+
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon && !this.#holes.some((hole) => isoBlockContains(hole.start, hole.length, ssId)) && !this.#expired.some((hole) => isoBlockContains(hole.start, hole.length, ssId))) {
|
|
13289
|
+
this.#held.delete(ssId);
|
|
13290
|
+
}
|
|
12941
13291
|
}
|
|
12942
13292
|
return retired;
|
|
12943
13293
|
}
|
|
@@ -12945,9 +13295,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12945
13295
|
reset() {
|
|
12946
13296
|
this.#blocks.length = 0;
|
|
12947
13297
|
this.#holes.length = 0;
|
|
13298
|
+
this.#expired.length = 0;
|
|
13299
|
+
this.#newlyExpired.length = 0;
|
|
13300
|
+
this.#closedBehind.length = 0;
|
|
12948
13301
|
this.#held.clear();
|
|
12949
13302
|
this.#coverageEnd = void 0;
|
|
12950
13303
|
this.#newest = void 0;
|
|
13304
|
+
this.#newestSource = void 0;
|
|
12951
13305
|
}
|
|
12952
13306
|
#claim(block) {
|
|
12953
13307
|
const placed = [];
|
|
@@ -13567,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13567
13921
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13568
13922
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13569
13923
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13924
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13570
13925
|
function isMulticastSymbolDelivery(value) {
|
|
13571
13926
|
return value === "datagram" || value === "ordered-relay";
|
|
13572
13927
|
}
|
|
@@ -13660,10 +14015,18 @@ var FECManager = class _FECManager {
|
|
|
13660
14015
|
// 'pending' forever, which would park every later frame of its stream in
|
|
13661
14016
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
13662
14017
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
14018
|
+
/**
|
|
14019
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
14020
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
14021
|
+
*/
|
|
13663
14022
|
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
13664
14023
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13665
14024
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13666
14025
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
14026
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
14027
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
14028
|
+
// not network arrivals and never make a block clean.
|
|
14029
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13667
14030
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13668
14031
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13669
14032
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -13672,6 +14035,9 @@ var FECManager = class _FECManager {
|
|
|
13672
14035
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
13673
14036
|
// symbols; waiting for K fails every one of them.
|
|
13674
14037
|
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
14038
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
14039
|
+
// a repair states the block there or the span resolves.
|
|
14040
|
+
this.#isoSpanKeys = /* @__PURE__ */ new Set();
|
|
13675
14041
|
this.#ssbLengthConflictCount = 0;
|
|
13676
14042
|
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
13677
14043
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -13792,6 +14158,10 @@ var FECManager = class _FECManager {
|
|
|
13792
14158
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
13793
14159
|
// One classifier per physical stream, built from that stream's geometry.
|
|
13794
14160
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
14161
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
14162
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
14163
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
14164
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
13795
14165
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
13796
14166
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
13797
14167
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -13865,7 +14235,10 @@ var FECManager = class _FECManager {
|
|
|
13865
14235
|
// arrives via onBlockRecovered callback.
|
|
13866
14236
|
this.onBlockStarted = null;
|
|
13867
14237
|
this.onBlockComplete = null;
|
|
13868
|
-
/**
|
|
14238
|
+
/**
|
|
14239
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
14240
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
14241
|
+
*/
|
|
13869
14242
|
this.onBlockFailed = null;
|
|
13870
14243
|
/**
|
|
13871
14244
|
* Fired once per stream when its Worker decoder stops being routable.
|
|
@@ -13907,6 +14280,8 @@ var FECManager = class _FECManager {
|
|
|
13907
14280
|
* for it, which it must be.
|
|
13908
14281
|
*/
|
|
13909
14282
|
this.#onUndelimitedLoss = null;
|
|
14283
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
14284
|
+
this.#onIsoHoleExpired = null;
|
|
13910
14285
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
13911
14286
|
this.onBlockVanished = null;
|
|
13912
14287
|
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
@@ -13949,7 +14324,9 @@ var FECManager = class _FECManager {
|
|
|
13949
14324
|
#pendingWorkerCleanupBounds;
|
|
13950
14325
|
#blockSourceCounts;
|
|
13951
14326
|
#blockSourceEsis;
|
|
14327
|
+
#blockRebuiltEsis;
|
|
13952
14328
|
#blockAuthoredLengths;
|
|
14329
|
+
#isoSpanKeys;
|
|
13953
14330
|
#ssbLengthConflictCount;
|
|
13954
14331
|
#blockRepairRsIds;
|
|
13955
14332
|
#blockRsbLengths;
|
|
@@ -13991,6 +14368,7 @@ var FECManager = class _FECManager {
|
|
|
13991
14368
|
#orderedFrontiers;
|
|
13992
14369
|
#orderedPendingBlocks;
|
|
13993
14370
|
#classifiers;
|
|
14371
|
+
#noSpanRaps;
|
|
13994
14372
|
#streamGeometries;
|
|
13995
14373
|
#failedBlocks;
|
|
13996
14374
|
#resolvedBlockClasses;
|
|
@@ -14015,6 +14393,7 @@ var FECManager = class _FECManager {
|
|
|
14015
14393
|
#decoderUnavailableEvents;
|
|
14016
14394
|
#repairSymbolSizeMismatchCount;
|
|
14017
14395
|
#onUndelimitedLoss;
|
|
14396
|
+
#onIsoHoleExpired;
|
|
14018
14397
|
#encodeTrackName(catalogTrackName) {
|
|
14019
14398
|
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
14020
14399
|
if (encoded === void 0) {
|
|
@@ -14486,7 +14865,10 @@ var FECManager = class _FECManager {
|
|
|
14486
14865
|
timer.handle = null;
|
|
14487
14866
|
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
14488
14867
|
};
|
|
14489
|
-
timer.handle = setTimeout(
|
|
14868
|
+
timer.handle = setTimeout(
|
|
14869
|
+
onFire,
|
|
14870
|
+
this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
|
|
14871
|
+
);
|
|
14490
14872
|
this.#repairDeadlines.set(key, timer);
|
|
14491
14873
|
}
|
|
14492
14874
|
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
@@ -14630,8 +15012,14 @@ var FECManager = class _FECManager {
|
|
|
14630
15012
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
14631
15013
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
14632
15014
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
15015
|
+
*
|
|
15016
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
15017
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
15018
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
15019
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
15020
|
+
* it (BLO-52085).
|
|
14633
15021
|
*/
|
|
14634
|
-
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
|
|
15022
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
|
|
14635
15023
|
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
14636
15024
|
const now = performance.now();
|
|
14637
15025
|
const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
|
|
@@ -14652,6 +15040,7 @@ var FECManager = class _FECManager {
|
|
|
14652
15040
|
};
|
|
14653
15041
|
const discontinuity = () => {
|
|
14654
15042
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
15043
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14655
15044
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14656
15045
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14657
15046
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -14669,7 +15058,7 @@ var FECManager = class _FECManager {
|
|
|
14669
15058
|
return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
|
|
14670
15059
|
};
|
|
14671
15060
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
14672
|
-
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
15061
|
+
if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
|
|
14673
15062
|
discontinuity();
|
|
14674
15063
|
return;
|
|
14675
15064
|
}
|
|
@@ -14931,17 +15320,21 @@ var FECManager = class _FECManager {
|
|
|
14931
15320
|
this.#blockWaitDeadlinesMs.delete(key);
|
|
14932
15321
|
this.#repairHoldUntilMs.delete(key);
|
|
14933
15322
|
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
14934
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
15323
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
|
|
14935
15324
|
};
|
|
14936
|
-
timer.handle = setTimeout(
|
|
15325
|
+
timer.handle = setTimeout(
|
|
15326
|
+
onFire,
|
|
15327
|
+
this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
|
|
15328
|
+
);
|
|
14937
15329
|
this.#activeBlocks.set(key, timer);
|
|
14938
15330
|
this.#indexBlockKey(key, sbn, packetId);
|
|
14939
15331
|
}
|
|
14940
15332
|
/**
|
|
14941
15333
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
14942
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
15334
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
15335
|
+
* is when it was due: its deadline, or now.
|
|
14943
15336
|
*/
|
|
14944
|
-
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
15337
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
|
|
14945
15338
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
14946
15339
|
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
14947
15340
|
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
@@ -14957,7 +15350,7 @@ var FECManager = class _FECManager {
|
|
|
14957
15350
|
if (workerClient) {
|
|
14958
15351
|
this.#pendingWorkerCleanups.add(key);
|
|
14959
15352
|
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
14960
|
-
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
15353
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
|
|
14961
15354
|
workerClient.cleanup(sbn);
|
|
14962
15355
|
} else {
|
|
14963
15356
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
@@ -15064,6 +15457,9 @@ var FECManager = class _FECManager {
|
|
|
15064
15457
|
}
|
|
15065
15458
|
if (policyChanged) {
|
|
15066
15459
|
this.#bumpEpoch(packetId);
|
|
15460
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15461
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15462
|
+
}
|
|
15067
15463
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15068
15464
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15069
15465
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15266,7 +15662,9 @@ var FECManager = class _FECManager {
|
|
|
15266
15662
|
}
|
|
15267
15663
|
resetMap(this.#blockSourceCounts);
|
|
15268
15664
|
resetMap(this.#blockSourceEsis);
|
|
15665
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15269
15666
|
resetMap(this.#blockAuthoredLengths);
|
|
15667
|
+
resetSet(this.#isoSpanKeys);
|
|
15270
15668
|
resetMap(this.#blockRepairRsIds);
|
|
15271
15669
|
resetMap(this.#blockRsbLengths);
|
|
15272
15670
|
resetMap(this.#heldLayerStates);
|
|
@@ -15299,6 +15697,7 @@ var FECManager = class _FECManager {
|
|
|
15299
15697
|
resetMap(this.#isoLocators);
|
|
15300
15698
|
resetMap(this.#sourceAnchors);
|
|
15301
15699
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15700
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15302
15701
|
this.#bumpEpoch(packetId);
|
|
15303
15702
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15304
15703
|
for (const key of keys.keys()) {
|
|
@@ -15389,7 +15788,9 @@ var FECManager = class _FECManager {
|
|
|
15389
15788
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15390
15789
|
dropMap(this.#blockSourceCounts);
|
|
15391
15790
|
dropMap(this.#blockSourceEsis);
|
|
15791
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15392
15792
|
dropMap(this.#blockAuthoredLengths);
|
|
15793
|
+
dropSet(this.#isoSpanKeys);
|
|
15393
15794
|
dropMap(this.#blockRepairRsIds);
|
|
15394
15795
|
dropMap(this.#blockRsbLengths);
|
|
15395
15796
|
dropMap(this.#blockPathCounts);
|
|
@@ -15405,6 +15806,7 @@ var FECManager = class _FECManager {
|
|
|
15405
15806
|
dropMap(this.#isoLocators);
|
|
15406
15807
|
dropMap(this.#sourceAnchors);
|
|
15407
15808
|
dropMap(this.#classifiers);
|
|
15809
|
+
dropMap(this.#noSpanRaps);
|
|
15408
15810
|
dropMap(this.#streamGeometries);
|
|
15409
15811
|
dropSet(this.#pendingWorkerCleanups);
|
|
15410
15812
|
dropSet(this.#completedBlocks);
|
|
@@ -15550,6 +15952,9 @@ var FECManager = class _FECManager {
|
|
|
15550
15952
|
this.#unindexBlockKey(key);
|
|
15551
15953
|
}
|
|
15552
15954
|
}
|
|
15955
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15956
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15957
|
+
}
|
|
15553
15958
|
return true;
|
|
15554
15959
|
}
|
|
15555
15960
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -15728,8 +16133,9 @@ var FECManager = class _FECManager {
|
|
|
15728
16133
|
return true;
|
|
15729
16134
|
}
|
|
15730
16135
|
/**
|
|
15731
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
15732
|
-
*
|
|
16136
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
16137
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
16138
|
+
* multicast wait, which may already have passed.
|
|
15733
16139
|
*/
|
|
15734
16140
|
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
15735
16141
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -15867,6 +16273,8 @@ var FECManager = class _FECManager {
|
|
|
15867
16273
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
15868
16274
|
const evictedBefore = locator.heldEvicted;
|
|
15869
16275
|
const held = locator.hold(ssId, source);
|
|
16276
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
16277
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
15870
16278
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
15871
16279
|
if (evicted > 0) {
|
|
15872
16280
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -15887,17 +16295,20 @@ var FECManager = class _FECManager {
|
|
|
15887
16295
|
* contradicts a known block is refused (counted) and must be dropped.
|
|
15888
16296
|
*/
|
|
15889
16297
|
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
15890
|
-
const
|
|
16298
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16299
|
+
const placement = locator.placeRepair(ssStart, ssbLength);
|
|
16300
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
|
|
15891
16301
|
if (placement.status !== "placed") {
|
|
15892
16302
|
this.#isoRepairRefusedCount++;
|
|
15893
16303
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
15894
16304
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
15895
16305
|
}
|
|
16306
|
+
if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
|
|
15896
16307
|
return placement;
|
|
15897
16308
|
}
|
|
16309
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
|
|
15898
16310
|
if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
|
|
15899
16311
|
if (placement.hole !== void 0) {
|
|
15900
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
15901
16312
|
const hole = placement.hole.block;
|
|
15902
16313
|
const previous = locator.blockEndingAt(hole.start);
|
|
15903
16314
|
const loss = {
|
|
@@ -15918,17 +16329,29 @@ var FECManager = class _FECManager {
|
|
|
15918
16329
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
15919
16330
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
15920
16331
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
15921
|
-
* nobody to close it.
|
|
16332
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
16333
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
16334
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
16335
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
15922
16336
|
*/
|
|
15923
|
-
setUndelimitedLossHandler(handler) {
|
|
16337
|
+
setUndelimitedLossHandler(handler, onExpired = null) {
|
|
15924
16338
|
this.#onUndelimitedLoss = handler;
|
|
16339
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired;
|
|
15925
16340
|
this.#deferIsoGaps = handler !== null;
|
|
15926
16341
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
|
|
15927
16342
|
}
|
|
16343
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
16344
|
+
#settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
|
|
16345
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
16346
|
+
const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
|
|
16347
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
|
|
16348
|
+
}
|
|
16349
|
+
}
|
|
15928
16350
|
/**
|
|
15929
|
-
* The range repair for a hole ended: every hole
|
|
15930
|
-
*
|
|
15931
|
-
* with the held sources they placed, for the caller to feed
|
|
16351
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
16352
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
16353
|
+
* spans with the held sources they placed, for the caller to feed
|
|
16354
|
+
* (MmtpRouter).
|
|
15932
16355
|
*/
|
|
15933
16356
|
closeIsoHoles(packetId, catalogTrackName, start, length) {
|
|
15934
16357
|
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
@@ -15947,7 +16370,10 @@ var FECManager = class _FECManager {
|
|
|
15947
16370
|
*/
|
|
15948
16371
|
#openIsoSpan(span, packetId, catalogTrackName) {
|
|
15949
16372
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
|
|
15950
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
16373
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
16374
|
+
this.#blockAuthoredLengths.set(key, span.block.length);
|
|
16375
|
+
this.#isoSpanKeys.add(key);
|
|
16376
|
+
}
|
|
15951
16377
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
|
|
15952
16378
|
}
|
|
15953
16379
|
/**
|
|
@@ -16035,11 +16461,14 @@ var FECManager = class _FECManager {
|
|
|
16035
16461
|
/**
|
|
16036
16462
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16037
16463
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16038
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
16464
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
16465
|
+
* and neither has a stream without catalog timing until it releases two
|
|
16466
|
+
* RAPs inside one MPU.
|
|
16039
16467
|
*/
|
|
16040
16468
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16041
|
-
|
|
16042
|
-
return this.#
|
|
16469
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16470
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
16471
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
16043
16472
|
}
|
|
16044
16473
|
/** A block's class now, or frozen at its resolution. */
|
|
16045
16474
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16065,7 +16494,14 @@ var FECManager = class _FECManager {
|
|
|
16065
16494
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16066
16495
|
*/
|
|
16067
16496
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16068
|
-
if (!this.#trackConfigs.has(packetId)
|
|
16497
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
16498
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
16499
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16500
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
16501
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
16502
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
16503
|
+
return;
|
|
16504
|
+
}
|
|
16069
16505
|
this.#applyClassifierChanges(
|
|
16070
16506
|
packetId,
|
|
16071
16507
|
catalogTrackName,
|
|
@@ -16278,11 +16714,14 @@ var FECManager = class _FECManager {
|
|
|
16278
16714
|
});
|
|
16279
16715
|
client.onCleanupComplete((sbn) => {
|
|
16280
16716
|
if (this.#releasedWorkerClients.has(client)) return;
|
|
16281
|
-
const
|
|
16717
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
|
|
16718
|
+
const cleanupKeys = owed.filter((key) => {
|
|
16282
16719
|
const block = this.#parseBlockKey(key);
|
|
16283
|
-
return block.packetId === packetId && block.sbn === sbn
|
|
16720
|
+
return block.packetId === packetId && block.sbn === sbn;
|
|
16284
16721
|
});
|
|
16285
|
-
|
|
16722
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
|
|
16723
|
+
const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
|
|
16724
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
16286
16725
|
});
|
|
16287
16726
|
client.onError((msg, code) => {
|
|
16288
16727
|
const requestedKind = code ?? msg;
|
|
@@ -16352,9 +16791,11 @@ var FECManager = class _FECManager {
|
|
|
16352
16791
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
16353
16792
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
16354
16793
|
*/
|
|
16355
|
-
#boundOwedCleanup(key, packetId, client) {
|
|
16794
|
+
#boundOwedCleanup(key, packetId, client, dueAtMs) {
|
|
16356
16795
|
this.#clearOwedCleanupBound(key);
|
|
16357
16796
|
const bound = {
|
|
16797
|
+
dueAtMs,
|
|
16798
|
+
askedAtMs: performance.now(),
|
|
16358
16799
|
handle: setTimeout(() => {
|
|
16359
16800
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
16360
16801
|
this.#pendingWorkerCleanupBounds.delete(key);
|
|
@@ -16452,7 +16893,15 @@ var FECManager = class _FECManager {
|
|
|
16452
16893
|
this.#isoSourceOversizeCount++;
|
|
16453
16894
|
return null;
|
|
16454
16895
|
}
|
|
16455
|
-
|
|
16896
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16897
|
+
}
|
|
16898
|
+
/**
|
|
16899
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16900
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16901
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16902
|
+
* stated for `ssId`.
|
|
16903
|
+
*/
|
|
16904
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16456
16905
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16457
16906
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16458
16907
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16481,6 +16930,74 @@ var FECManager = class _FECManager {
|
|
|
16481
16930
|
}
|
|
16482
16931
|
return recovered;
|
|
16483
16932
|
}
|
|
16933
|
+
/**
|
|
16934
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16935
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16936
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16937
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16938
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16939
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16940
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16941
|
+
*
|
|
16942
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16943
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16944
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16945
|
+
* feedSourceSymbol takes it.
|
|
16946
|
+
*
|
|
16947
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16948
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16949
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16950
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16951
|
+
* block as clean on its own side), the block is completed here as
|
|
16952
|
+
* recovered, with nothing left to route.
|
|
16953
|
+
*/
|
|
16954
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16955
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16956
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16957
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16958
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16959
|
+
const sbn = located.block.start;
|
|
16960
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16961
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16962
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16963
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16964
|
+
if (rebuilt === void 0) {
|
|
16965
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16966
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16967
|
+
}
|
|
16968
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16969
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16970
|
+
rebuilt.add(located.esi);
|
|
16971
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16972
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
16973
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
16974
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16977
|
+
return { taken: true, recovered };
|
|
16978
|
+
}
|
|
16979
|
+
/**
|
|
16980
|
+
* A block that holds every one of its `length` sources, received or
|
|
16981
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16982
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16983
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16984
|
+
*/
|
|
16985
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16986
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16987
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16988
|
+
let present = rebuilt.size;
|
|
16989
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16990
|
+
if (present < length) return false;
|
|
16991
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16992
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16993
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16994
|
+
if (catalogTrackName === void 0) {
|
|
16995
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16996
|
+
} else {
|
|
16997
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16998
|
+
}
|
|
16999
|
+
return true;
|
|
17000
|
+
}
|
|
16484
17001
|
/**
|
|
16485
17002
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16486
17003
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16683,8 +17200,75 @@ var FECManager = class _FECManager {
|
|
|
16683
17200
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16684
17201
|
return null;
|
|
16685
17202
|
}
|
|
17203
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16686
17204
|
return recovered;
|
|
16687
17205
|
}
|
|
17206
|
+
/**
|
|
17207
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17208
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17209
|
+
* position past a block leaves the sources that block still misses
|
|
17210
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17211
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17212
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17213
|
+
* sent before it.
|
|
17214
|
+
*/
|
|
17215
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17216
|
+
const previous = locator.newestSource;
|
|
17217
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17218
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17219
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17220
|
+
this.#shortenUnrecoverableWait(
|
|
17221
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17222
|
+
block.start,
|
|
17223
|
+
packetId,
|
|
17224
|
+
catalogTrackName
|
|
17225
|
+
);
|
|
17226
|
+
}
|
|
17227
|
+
}
|
|
17228
|
+
/**
|
|
17229
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17230
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17231
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17232
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17233
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17234
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17235
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17236
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17237
|
+
* block.
|
|
17238
|
+
*/
|
|
17239
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17240
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17241
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17242
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17243
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
17244
|
+
const received = this.#blockSourceEsis.get(key);
|
|
17245
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
17246
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
17247
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17248
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17249
|
+
return void 0;
|
|
17250
|
+
}
|
|
17251
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17252
|
+
}
|
|
17253
|
+
/**
|
|
17254
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17255
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17256
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17257
|
+
*/
|
|
17258
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17259
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17260
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17261
|
+
if (config === void 0) return;
|
|
17262
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17263
|
+
if (waitMs === void 0) return;
|
|
17264
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17265
|
+
this.#armBlockTimeout(
|
|
17266
|
+
sbn,
|
|
17267
|
+
packetId,
|
|
17268
|
+
catalogTrackName,
|
|
17269
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17270
|
+
);
|
|
17271
|
+
}
|
|
16688
17272
|
/**
|
|
16689
17273
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16690
17274
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -16782,12 +17366,13 @@ var FECManager = class _FECManager {
|
|
|
16782
17366
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
16783
17367
|
* classification evidence, and marks the block clean when all of its
|
|
16784
17368
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
16785
|
-
* otherwise K.
|
|
17369
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
17370
|
+
* placed only now (see #noteSbnSeen).
|
|
16786
17371
|
*
|
|
16787
17372
|
* Throws for a packetId that was never configured, so a classified block
|
|
16788
17373
|
* never reads the manager-global K or timeout.
|
|
16789
17374
|
*/
|
|
16790
|
-
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
17375
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
|
|
16791
17376
|
const trackConfig = this.#trackConfigs.get(packetId);
|
|
16792
17377
|
if (trackConfig === void 0) {
|
|
16793
17378
|
throw new Error(
|
|
@@ -16803,7 +17388,8 @@ var FECManager = class _FECManager {
|
|
|
16803
17388
|
catalogTrackName,
|
|
16804
17389
|
"source",
|
|
16805
17390
|
evidence.packetSequenceNumber,
|
|
16806
|
-
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
|
|
17391
|
+
Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
|
|
17392
|
+
held
|
|
16807
17393
|
);
|
|
16808
17394
|
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
16809
17395
|
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
@@ -16839,6 +17425,15 @@ var FECManager = class _FECManager {
|
|
|
16839
17425
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
16840
17426
|
return;
|
|
16841
17427
|
}
|
|
17428
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17429
|
+
key,
|
|
17430
|
+
sbn,
|
|
17431
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17432
|
+
packetId,
|
|
17433
|
+
catalogTrackName
|
|
17434
|
+
)) {
|
|
17435
|
+
return;
|
|
17436
|
+
}
|
|
16842
17437
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
16843
17438
|
}
|
|
16844
17439
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -16847,7 +17442,9 @@ var FECManager = class _FECManager {
|
|
|
16847
17442
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16848
17443
|
this.#blockSourceCounts.delete(key);
|
|
16849
17444
|
this.#blockSourceEsis.delete(key);
|
|
17445
|
+
this.#blockRebuiltEsis.delete(key);
|
|
16850
17446
|
this.#blockAuthoredLengths.delete(key);
|
|
17447
|
+
this.#isoSpanKeys.delete(key);
|
|
16851
17448
|
this.#blockRepairRsIds.delete(key);
|
|
16852
17449
|
this.#blockRsbLengths.delete(key);
|
|
16853
17450
|
this.#markTerminal(key, sbn, packetId, true);
|
|
@@ -16878,6 +17475,19 @@ var FECManager = class _FECManager {
|
|
|
16878
17475
|
#receiptNow() {
|
|
16879
17476
|
return this.#receiptAtMs ?? performance.now();
|
|
16880
17477
|
}
|
|
17478
|
+
/**
|
|
17479
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
17480
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
17481
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
17482
|
+
* that timer would have, never later than a delay counted from now, and
|
|
17483
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
17484
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
17485
|
+
* from another fraction of a ms (BLO-52085).
|
|
17486
|
+
*/
|
|
17487
|
+
#receiptTimerDelayMs(deadlineMs, minMs) {
|
|
17488
|
+
const receivedAtMs = this.#receiptAtMs;
|
|
17489
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
|
|
17490
|
+
}
|
|
16881
17491
|
/**
|
|
16882
17492
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
16883
17493
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -16913,10 +17523,20 @@ var FECManager = class _FECManager {
|
|
|
16913
17523
|
this.#repairDeadlineMs(trackConfig)
|
|
16914
17524
|
);
|
|
16915
17525
|
}
|
|
16916
|
-
this.#
|
|
17526
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17527
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17528
|
+
return;
|
|
17529
|
+
}
|
|
17530
|
+
this.#armBlockTimeout(
|
|
17531
|
+
sbn,
|
|
17532
|
+
packetId,
|
|
17533
|
+
catalogTrackName,
|
|
17534
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17535
|
+
);
|
|
16917
17536
|
}
|
|
16918
17537
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
16919
17538
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
17539
|
+
const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
|
|
16920
17540
|
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
16921
17541
|
this.#clearRepairDeadline(key);
|
|
16922
17542
|
this.#clearBlockTimer(key);
|
|
@@ -16926,6 +17546,7 @@ var FECManager = class _FECManager {
|
|
|
16926
17546
|
}
|
|
16927
17547
|
this.#blockSourceCounts.delete(key);
|
|
16928
17548
|
this.#blockSourceEsis.delete(key);
|
|
17549
|
+
this.#blockRebuiltEsis.delete(key);
|
|
16929
17550
|
this.#blockAuthoredLengths.delete(key);
|
|
16930
17551
|
this.#blockRepairRsIds.delete(key);
|
|
16931
17552
|
this.#blockRsbLengths.delete(key);
|
|
@@ -16935,7 +17556,8 @@ var FECManager = class _FECManager {
|
|
|
16935
17556
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
16936
17557
|
this.#fecStats.blocksFailed++;
|
|
16937
17558
|
this.#statsForPacket(packetId).blocksFailed++;
|
|
16938
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17559
|
+
if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
17560
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
16939
17561
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
16940
17562
|
}
|
|
16941
17563
|
/**
|
|
@@ -16964,7 +17586,9 @@ var FECManager = class _FECManager {
|
|
|
16964
17586
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
16965
17587
|
this.#blockSourceCounts.delete(key);
|
|
16966
17588
|
this.#blockSourceEsis.delete(key);
|
|
17589
|
+
this.#blockRebuiltEsis.delete(key);
|
|
16967
17590
|
this.#blockAuthoredLengths.delete(key);
|
|
17591
|
+
this.#isoSpanKeys.delete(key);
|
|
16968
17592
|
this.#blockRepairRsIds.delete(key);
|
|
16969
17593
|
this.#blockRsbLengths.delete(key);
|
|
16970
17594
|
this.#clearBlockTimer(key);
|
|
@@ -17055,7 +17679,9 @@ var FECManager = class _FECManager {
|
|
|
17055
17679
|
this.#blockKeysBySbn.clear();
|
|
17056
17680
|
this.#blockSourceCounts.clear();
|
|
17057
17681
|
this.#blockSourceEsis.clear();
|
|
17682
|
+
this.#blockRebuiltEsis.clear();
|
|
17058
17683
|
this.#blockAuthoredLengths.clear();
|
|
17684
|
+
this.#isoSpanKeys.clear();
|
|
17059
17685
|
this.#blockRepairRsIds.clear();
|
|
17060
17686
|
this.#blockRsbLengths.clear();
|
|
17061
17687
|
this.#heldLayerStates.clear();
|
|
@@ -17080,6 +17706,7 @@ var FECManager = class _FECManager {
|
|
|
17080
17706
|
this.#blockAnchorsMs.clear();
|
|
17081
17707
|
this.#blockCatalogTrackNames.clear();
|
|
17082
17708
|
this.#classifiers.clear();
|
|
17709
|
+
this.#noSpanRaps.clear();
|
|
17083
17710
|
this.#streamGeometries.clear();
|
|
17084
17711
|
this.#failedBlocks.clear();
|
|
17085
17712
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17098,17 +17725,17 @@ var FECManager = class _FECManager {
|
|
|
17098
17725
|
|
|
17099
17726
|
// src/mfu-reassembly-policy.ts
|
|
17100
17727
|
function validateReliableMoqOptionShape(value) {
|
|
17101
|
-
const
|
|
17728
|
+
const unsupported2 = Object.keys(value).filter(
|
|
17102
17729
|
(key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
|
|
17103
17730
|
);
|
|
17104
|
-
if (
|
|
17105
|
-
if (
|
|
17731
|
+
if (unsupported2.length > 0) {
|
|
17732
|
+
if (unsupported2.includes("maxRacingGroups")) {
|
|
17106
17733
|
throw new Error(
|
|
17107
17734
|
"[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
|
|
17108
17735
|
);
|
|
17109
17736
|
}
|
|
17110
17737
|
throw new Error(
|
|
17111
|
-
`[MFU reassembly] unsupported reliable MoQ option(s): ${
|
|
17738
|
+
`[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
|
|
17112
17739
|
);
|
|
17113
17740
|
}
|
|
17114
17741
|
const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
|
|
@@ -17152,12 +17779,12 @@ function assertValidMfuReassemblyPolicy(policy) {
|
|
|
17152
17779
|
return;
|
|
17153
17780
|
}
|
|
17154
17781
|
if (value["transport"] === "datagram") {
|
|
17155
|
-
const
|
|
17782
|
+
const unsupported2 = Object.keys(value).filter(
|
|
17156
17783
|
(key) => key !== "transport" && key !== "deadlineMs"
|
|
17157
17784
|
);
|
|
17158
|
-
if (
|
|
17785
|
+
if (unsupported2.length > 0) {
|
|
17159
17786
|
throw new Error(
|
|
17160
|
-
`[MFU reassembly] unsupported datagram option(s): ${
|
|
17787
|
+
`[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
|
|
17161
17788
|
);
|
|
17162
17789
|
}
|
|
17163
17790
|
assertValidDatagramDeadline(value["deadlineMs"]);
|
|
@@ -17610,9 +18237,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17610
18237
|
}
|
|
17611
18238
|
#track = null;
|
|
17612
18239
|
#fragments = /* @__PURE__ */ new Map();
|
|
18240
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
18241
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
18242
|
+
/**
|
|
18243
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
18244
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
18245
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
18246
|
+
*/
|
|
18247
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17613
18248
|
#pendingSamples = 0;
|
|
17614
18249
|
#evictedSamples = 0;
|
|
17615
18250
|
#lastMovieFragmentRejection = null;
|
|
18251
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
18252
|
+
get initialized() {
|
|
18253
|
+
return this.#track !== null;
|
|
18254
|
+
}
|
|
17616
18255
|
get pendingSampleCount() {
|
|
17617
18256
|
return this.#pendingSamples;
|
|
17618
18257
|
}
|
|
@@ -17636,7 +18275,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17636
18275
|
this.#track = timing;
|
|
17637
18276
|
return true;
|
|
17638
18277
|
}
|
|
17639
|
-
|
|
18278
|
+
/**
|
|
18279
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
18280
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
18281
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
18282
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
18283
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
18284
|
+
*/
|
|
18285
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17640
18286
|
this.#lastMovieFragmentRejection = null;
|
|
17641
18287
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17642
18288
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17662,6 +18308,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17662
18308
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17663
18309
|
);
|
|
17664
18310
|
}
|
|
18311
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
18312
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17665
18313
|
let freed = 0;
|
|
17666
18314
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17667
18315
|
freed += this.#evictOldestFragment();
|
|
@@ -17675,6 +18323,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17675
18323
|
return false;
|
|
17676
18324
|
}
|
|
17677
18325
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18326
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17678
18327
|
this.#pendingSamples += parsed.samples.length;
|
|
17679
18328
|
return true;
|
|
17680
18329
|
}
|
|
@@ -17732,6 +18381,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17732
18381
|
let live = 0;
|
|
17733
18382
|
for (const sample of samples) if (sample !== null) live++;
|
|
17734
18383
|
this.#fragments.delete(oldest);
|
|
18384
|
+
this.#firstSamples.delete(oldest);
|
|
17735
18385
|
this.#pendingSamples -= live;
|
|
17736
18386
|
this.#evictedSamples += live;
|
|
17737
18387
|
return live;
|
|
@@ -17747,12 +18397,23 @@ var IsoBmffTimingResolver = class {
|
|
|
17747
18397
|
if (!samples || !samples[index]) return false;
|
|
17748
18398
|
samples[index] = null;
|
|
17749
18399
|
this.#pendingSamples--;
|
|
17750
|
-
if (samples.every((value) => value === null))
|
|
18400
|
+
if (samples.every((value) => value === null)) {
|
|
18401
|
+
this.#fragments.delete(key);
|
|
18402
|
+
const first = this.#firstSamples.get(key);
|
|
18403
|
+
this.#firstSamples.delete(key);
|
|
18404
|
+
this.#delivered.delete(key);
|
|
18405
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18406
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18407
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18408
|
+
}
|
|
18409
|
+
}
|
|
17751
18410
|
return true;
|
|
17752
18411
|
}
|
|
17753
18412
|
reset() {
|
|
17754
18413
|
this.#track = null;
|
|
17755
18414
|
this.#fragments.clear();
|
|
18415
|
+
this.#firstSamples.clear();
|
|
18416
|
+
this.#delivered.clear();
|
|
17756
18417
|
this.#pendingSamples = 0;
|
|
17757
18418
|
}
|
|
17758
18419
|
};
|
|
@@ -17922,6 +18583,9 @@ function authoredGap(before, after) {
|
|
|
17922
18583
|
function ssIdAfter(a, b) {
|
|
17923
18584
|
return (a - b | 0) > 0;
|
|
17924
18585
|
}
|
|
18586
|
+
function holdsRebuilt(stream) {
|
|
18587
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
18588
|
+
}
|
|
17925
18589
|
var FecReleaseSequencer = class {
|
|
17926
18590
|
#streams = /* @__PURE__ */ new Map();
|
|
17927
18591
|
#gate;
|
|
@@ -17984,6 +18648,7 @@ var FecReleaseSequencer = class {
|
|
|
17984
18648
|
stream.lastSbn = void 0;
|
|
17985
18649
|
stream.discontinuity = false;
|
|
17986
18650
|
stream.rapSsId = void 0;
|
|
18651
|
+
stream.sourced = false;
|
|
17987
18652
|
}
|
|
17988
18653
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
17989
18654
|
this.#recordStat("droppedStale");
|
|
@@ -18010,6 +18675,7 @@ var FecReleaseSequencer = class {
|
|
|
18010
18675
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
18011
18676
|
else hi = mid;
|
|
18012
18677
|
}
|
|
18678
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
18013
18679
|
stream.buf.splice(lo, 0, frame);
|
|
18014
18680
|
this.#drainUnlessDeferred(stream);
|
|
18015
18681
|
}
|
|
@@ -18045,6 +18711,41 @@ var FecReleaseSequencer = class {
|
|
|
18045
18711
|
}
|
|
18046
18712
|
return false;
|
|
18047
18713
|
}
|
|
18714
|
+
/**
|
|
18715
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
18716
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
18717
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
18718
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
18719
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
18720
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
18721
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
18722
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
18723
|
+
* released.
|
|
18724
|
+
*/
|
|
18725
|
+
unsource(packetId, catalogTrackName) {
|
|
18726
|
+
for (const stream of this.#streams.values()) {
|
|
18727
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
18728
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
18729
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
18730
|
+
else this.#drain(stream, true);
|
|
18731
|
+
}
|
|
18732
|
+
stream.sourced = false;
|
|
18733
|
+
}
|
|
18734
|
+
}
|
|
18735
|
+
}
|
|
18736
|
+
/**
|
|
18737
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
18738
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
18739
|
+
* packetId.
|
|
18740
|
+
*/
|
|
18741
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
18742
|
+
for (const stream of this.#streams.values()) {
|
|
18743
|
+
if (stream.packetId !== packetId) continue;
|
|
18744
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
18745
|
+
if (holdsRebuilt(stream)) return true;
|
|
18746
|
+
}
|
|
18747
|
+
return false;
|
|
18748
|
+
}
|
|
18048
18749
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
18049
18750
|
poke(streamKey) {
|
|
18050
18751
|
if (streamKey !== void 0) {
|
|
@@ -18136,16 +18837,32 @@ var FecReleaseSequencer = class {
|
|
|
18136
18837
|
last: null,
|
|
18137
18838
|
lastSbn: void 0,
|
|
18138
18839
|
discontinuity: false,
|
|
18139
|
-
rapSsId: void 0
|
|
18840
|
+
rapSsId: void 0,
|
|
18841
|
+
sourced: false,
|
|
18842
|
+
draining: 0
|
|
18140
18843
|
};
|
|
18141
18844
|
this.#streams.set(streamKey, stream);
|
|
18142
18845
|
}
|
|
18143
18846
|
return stream;
|
|
18144
18847
|
}
|
|
18145
|
-
#
|
|
18848
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
18849
|
+
#drain(stream, asUnanchored = false) {
|
|
18850
|
+
stream.draining++;
|
|
18851
|
+
try {
|
|
18852
|
+
this.#drainLoop(stream, asUnanchored);
|
|
18853
|
+
} finally {
|
|
18854
|
+
stream.draining--;
|
|
18855
|
+
}
|
|
18856
|
+
}
|
|
18857
|
+
#drainLoop(stream, asUnanchored) {
|
|
18146
18858
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
18147
18859
|
const head = stream.buf[0];
|
|
18148
18860
|
const last = stream.last;
|
|
18861
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
18862
|
+
stream.buf.shift();
|
|
18863
|
+
this.#release(stream, head, void 0);
|
|
18864
|
+
continue;
|
|
18865
|
+
}
|
|
18149
18866
|
if (stream.discontinuity) {
|
|
18150
18867
|
stream.discontinuity = false;
|
|
18151
18868
|
stream.buf.shift();
|
|
@@ -18159,9 +18876,10 @@ var FecReleaseSequencer = class {
|
|
|
18159
18876
|
}
|
|
18160
18877
|
const headSbn = head.meta.fecSbn;
|
|
18161
18878
|
const from = stream.lastSbn ?? headSbn;
|
|
18162
|
-
const to = headSbn ?? stream.lastSbn;
|
|
18879
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
18163
18880
|
let lost = [];
|
|
18164
18881
|
const unanchored = from === void 0 && to === void 0;
|
|
18882
|
+
let unresolved = false;
|
|
18165
18883
|
if (from !== void 0 && to !== void 0) {
|
|
18166
18884
|
const resolved = this.#gate.resolvedLoss(
|
|
18167
18885
|
stream.packetId,
|
|
@@ -18170,7 +18888,8 @@ var FecReleaseSequencer = class {
|
|
|
18170
18888
|
Math.max(from, to),
|
|
18171
18889
|
from
|
|
18172
18890
|
);
|
|
18173
|
-
|
|
18891
|
+
unresolved = resolved === null;
|
|
18892
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
18174
18893
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
18175
18894
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
18176
18895
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -18178,22 +18897,56 @@ var FecReleaseSequencer = class {
|
|
|
18178
18897
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
18179
18898
|
continue;
|
|
18180
18899
|
}
|
|
18181
|
-
lost = resolved;
|
|
18900
|
+
lost = resolved ?? [];
|
|
18182
18901
|
}
|
|
18183
18902
|
stream.buf.shift();
|
|
18184
18903
|
this.#release(
|
|
18185
18904
|
stream,
|
|
18186
18905
|
head,
|
|
18187
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18906
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
18188
18907
|
);
|
|
18189
18908
|
}
|
|
18190
18909
|
}
|
|
18910
|
+
/**
|
|
18911
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
18912
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
18913
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
18914
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
18915
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
18916
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
18917
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
18918
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
18919
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
18920
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
18921
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
18922
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
18923
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
18924
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
18925
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
18926
|
+
*/
|
|
18927
|
+
#cursorPassed(stream, asUnanchored) {
|
|
18928
|
+
let at2 = -1;
|
|
18929
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
18930
|
+
const frame = stream.buf[i];
|
|
18931
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
18932
|
+
frame.passed = true;
|
|
18933
|
+
if (at2 < 0) at2 = i;
|
|
18934
|
+
} else if (frame.passed !== true) break;
|
|
18935
|
+
}
|
|
18936
|
+
if (at2 < 0) return false;
|
|
18937
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
18938
|
+
}
|
|
18191
18939
|
#release(stream, frame, decision) {
|
|
18940
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
18941
|
+
if (stale) this.#recordStat("droppedStale");
|
|
18192
18942
|
if (decision) frame.meta.gapBefore = decision;
|
|
18193
18943
|
stream.last = frame;
|
|
18194
|
-
if (frame.meta.fecSbn !== void 0
|
|
18944
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
18945
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
18946
|
+
}
|
|
18947
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
18195
18948
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
18196
|
-
this.#emit(frame.data, frame.meta);
|
|
18949
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
18197
18950
|
}
|
|
18198
18951
|
};
|
|
18199
18952
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -18234,6 +18987,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18234
18987
|
this.#reclaimedTimingSamples = 0;
|
|
18235
18988
|
this.#evictedTimingSamples = 0;
|
|
18236
18989
|
this.#rejectedMovieFragments = 0;
|
|
18990
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18991
|
+
held: 0,
|
|
18992
|
+
expired: 0,
|
|
18993
|
+
discarded: 0,
|
|
18994
|
+
superseded: 0
|
|
18995
|
+
};
|
|
18237
18996
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18238
18997
|
this.#fecGate = null;
|
|
18239
18998
|
this.#fecSequencer = null;
|
|
@@ -18342,6 +19101,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18342
19101
|
#reclaimedTimingSamples;
|
|
18343
19102
|
#evictedTimingSamples;
|
|
18344
19103
|
#rejectedMovieFragments;
|
|
19104
|
+
#recoveredHeadsAwaitingTiming;
|
|
18345
19105
|
#sequencers;
|
|
18346
19106
|
#fecGate;
|
|
18347
19107
|
#fecSequencer;
|
|
@@ -18447,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18447
19207
|
/**
|
|
18448
19208
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18449
19209
|
*
|
|
18450
|
-
*
|
|
18451
|
-
*
|
|
18452
|
-
*
|
|
18453
|
-
*
|
|
18454
|
-
*
|
|
19210
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
19211
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
19212
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
19213
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
19214
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
19215
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
18455
19216
|
*/
|
|
18456
19217
|
get timingBudgetStats() {
|
|
18457
19218
|
return {
|
|
@@ -18462,6 +19223,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18462
19223
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18463
19224
|
};
|
|
18464
19225
|
}
|
|
19226
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
19227
|
+
get recoveredHeadsAwaitingTiming() {
|
|
19228
|
+
let pending = 0;
|
|
19229
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19230
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
19231
|
+
}
|
|
19232
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
19233
|
+
}
|
|
18465
19234
|
get reorderStats() {
|
|
18466
19235
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18467
19236
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18503,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18503
19272
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
18504
19273
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
18505
19274
|
}
|
|
19275
|
+
/** See FecReleaseSequencer.unsource. */
|
|
19276
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
19277
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
19278
|
+
}
|
|
19279
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
19280
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
19281
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
19282
|
+
}
|
|
18506
19283
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
18507
19284
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
18508
19285
|
if (!this.#fecSequencer) return;
|
|
@@ -18590,10 +19367,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18590
19367
|
get reassemblerDataUnitsReturned() {
|
|
18591
19368
|
return this.#reassemblerDataUnitsReturned;
|
|
18592
19369
|
}
|
|
18593
|
-
#countOutcome(
|
|
18594
|
-
const count = (this.#feedOutcomes.get(
|
|
18595
|
-
this.#feedOutcomes.set(
|
|
18596
|
-
this.#lastOutcome =
|
|
19370
|
+
#countOutcome(token2) {
|
|
19371
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
19372
|
+
this.#feedOutcomes.set(token2, count);
|
|
19373
|
+
this.#lastOutcome = token2;
|
|
18597
19374
|
this.#lastOutcomeCount = count;
|
|
18598
19375
|
}
|
|
18599
19376
|
#countDeferredOutcome(reason) {
|
|
@@ -18635,7 +19412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18635
19412
|
const now = Date.now();
|
|
18636
19413
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18637
19414
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18638
|
-
awaitingTiming.shift();
|
|
19415
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18639
19416
|
}
|
|
18640
19417
|
}
|
|
18641
19418
|
}
|
|
@@ -18753,11 +19530,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18753
19530
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
18754
19531
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
18755
19532
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
18756
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19533
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
18757
19534
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
18758
19535
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
18759
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
18760
|
-
const admitted = resolver2.addMovieFragment(
|
|
19536
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19537
|
+
const admitted = resolver2.addMovieFragment(
|
|
19538
|
+
mpu.mpuSequenceNumber,
|
|
19539
|
+
data,
|
|
19540
|
+
remaining,
|
|
19541
|
+
options?.recovered === true
|
|
19542
|
+
);
|
|
18761
19543
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
18762
19544
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
18763
19545
|
if (!admitted) {
|
|
@@ -18777,7 +19559,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18777
19559
|
};
|
|
18778
19560
|
}
|
|
18779
19561
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
18780
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19562
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
18781
19563
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
18782
19564
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
18783
19565
|
return this.#drop("mfu-empty-payload");
|
|
@@ -18798,6 +19580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18798
19580
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
18799
19581
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
18800
19582
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19583
|
+
sequenceNumber: info.sequenceNumber,
|
|
18801
19584
|
arrivedAt: Date.now()
|
|
18802
19585
|
});
|
|
18803
19586
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -18823,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18823
19606
|
info,
|
|
18824
19607
|
resolver,
|
|
18825
19608
|
options?.recovered === true,
|
|
18826
|
-
options?.fecSbn
|
|
19609
|
+
options?.fecSbn,
|
|
19610
|
+
options?.fecSbnFirst
|
|
18827
19611
|
);
|
|
18828
19612
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
18829
19613
|
}
|
|
@@ -18916,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18916
19700
|
info,
|
|
18917
19701
|
resolver,
|
|
18918
19702
|
options?.recovered === true,
|
|
18919
|
-
options?.fecSbn
|
|
19703
|
+
options?.fecSbn,
|
|
19704
|
+
options?.fecSbnFirst
|
|
18920
19705
|
);
|
|
18921
19706
|
if (outcome.record !== void 0) return outcome.record;
|
|
18922
19707
|
return this.#drop(outcome.reason);
|
|
@@ -18936,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18936
19721
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
18937
19722
|
* is what keeps both true.
|
|
18938
19723
|
*/
|
|
18939
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
19724
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
18940
19725
|
const timing = resolver.sampleTiming(
|
|
18941
19726
|
combined.mpuSequenceNumber,
|
|
18942
19727
|
combined.movieFragmentSequenceNumber,
|
|
@@ -18953,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18953
19738
|
);
|
|
18954
19739
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
18955
19740
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
19741
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
18956
19742
|
if (!resolver.consumeSample(
|
|
18957
19743
|
combined.mpuSequenceNumber,
|
|
18958
19744
|
combined.movieFragmentSequenceNumber,
|
|
@@ -19121,6 +19907,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19121
19907
|
}
|
|
19122
19908
|
this.#emitFrame(data, meta);
|
|
19123
19909
|
}
|
|
19910
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19911
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19912
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19913
|
+
}
|
|
19124
19914
|
/**
|
|
19125
19915
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19126
19916
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19182,6 +19972,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19182
19972
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19183
19973
|
this.#sequencers.clear();
|
|
19184
19974
|
this.#fecSequencer?.discard();
|
|
19975
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19976
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19977
|
+
}
|
|
19185
19978
|
this.#timingStreams.clear();
|
|
19186
19979
|
this.#pendingTimingSamples = 0;
|
|
19187
19980
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19200,10 +19993,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19200
19993
|
return resolver;
|
|
19201
19994
|
}
|
|
19202
19995
|
#awaitTiming(streamKey, head) {
|
|
19203
|
-
const
|
|
19204
|
-
if (!
|
|
19996
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19997
|
+
if (!state) return;
|
|
19998
|
+
if (!head.options?.recovered) {
|
|
19999
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
20000
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
20001
|
+
state.awaitingTiming = kept;
|
|
20002
|
+
}
|
|
20003
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19205
20004
|
awaitingTiming.push(head);
|
|
19206
|
-
if (
|
|
20005
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
20006
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
20007
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
20008
|
+
}
|
|
20009
|
+
}
|
|
20010
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
20011
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
20012
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19207
20013
|
}
|
|
19208
20014
|
/**
|
|
19209
20015
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19231,6 +20037,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19231
20037
|
const state = this.#timingStreams.get(streamKey);
|
|
19232
20038
|
if (!state) return;
|
|
19233
20039
|
this.#timingStreams.delete(streamKey);
|
|
20040
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19234
20041
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19235
20042
|
if (this.#pendingTimingSamples < 0) {
|
|
19236
20043
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22437,7 +23244,18 @@ function keyframeOverlaySsId(header) {
|
|
|
22437
23244
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22438
23245
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22439
23246
|
}
|
|
23247
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23248
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23249
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23250
|
+
}
|
|
23251
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23252
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23253
|
+
return headerPsn + offset >>> 0;
|
|
23254
|
+
}
|
|
23255
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23256
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22440
23257
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
23258
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
22441
23259
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22442
23260
|
function assertStreamIdentity(role, stream) {
|
|
22443
23261
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -22548,6 +23366,7 @@ var MmtpRouter = class {
|
|
|
22548
23366
|
#provenance = /* @__PURE__ */ new Map();
|
|
22549
23367
|
#onFlowProtected = null;
|
|
22550
23368
|
#unadmittedListedDrops = 0;
|
|
23369
|
+
#admittedSourceDatagrams = 0;
|
|
22551
23370
|
#unlistedRepairSetAside = 0;
|
|
22552
23371
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22553
23372
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22604,6 +23423,26 @@ var MmtpRouter = class {
|
|
|
22604
23423
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22605
23424
|
*/
|
|
22606
23425
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23426
|
+
/**
|
|
23427
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23428
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23429
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23430
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23431
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23432
|
+
* an MPU no member of which arrived).
|
|
23433
|
+
*/
|
|
23434
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23435
|
+
/**
|
|
23436
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
23437
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
23438
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
23439
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
23440
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
23441
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
23442
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
23443
|
+
* this offset did.
|
|
23444
|
+
*/
|
|
23445
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22607
23446
|
/**
|
|
22608
23447
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22609
23448
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22673,7 +23512,9 @@ var MmtpRouter = class {
|
|
|
22673
23512
|
recovered: 0,
|
|
22674
23513
|
restarts: 0,
|
|
22675
23514
|
unsequencedDropped: 0,
|
|
22676
|
-
protectedDropped: 0
|
|
23515
|
+
protectedDropped: 0,
|
|
23516
|
+
baseFed: 0,
|
|
23517
|
+
baseUndetermined: 0
|
|
22677
23518
|
};
|
|
22678
23519
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22679
23520
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -22846,7 +23687,7 @@ var MmtpRouter = class {
|
|
|
22846
23687
|
const cb = this.#getFrameCallback(packetId);
|
|
22847
23688
|
if (isVideo && !assembler) {
|
|
22848
23689
|
if (frameMeta.recovered) {
|
|
22849
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23690
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
22850
23691
|
} else {
|
|
22851
23692
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
22852
23693
|
}
|
|
@@ -22879,13 +23720,18 @@ var MmtpRouter = class {
|
|
|
22879
23720
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
22880
23721
|
}
|
|
22881
23722
|
};
|
|
22882
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
23723
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
22883
23724
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
22884
23725
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
22885
23726
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
22886
23727
|
}
|
|
22887
|
-
|
|
22888
|
-
|
|
23728
|
+
if (span === void 0) {
|
|
23729
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23730
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
23731
|
+
} else {
|
|
23732
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23733
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
23734
|
+
}
|
|
22889
23735
|
};
|
|
22890
23736
|
}
|
|
22891
23737
|
}
|
|
@@ -22930,8 +23776,9 @@ var MmtpRouter = class {
|
|
|
22930
23776
|
}
|
|
22931
23777
|
/**
|
|
22932
23778
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
22933
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
22934
|
-
*
|
|
23779
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
23780
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
23781
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
22935
23782
|
*/
|
|
22936
23783
|
get restampedRecoveredSequenceCount() {
|
|
22937
23784
|
return this.#restampedRecoveredSeqs;
|
|
@@ -23110,6 +23957,7 @@ var MmtpRouter = class {
|
|
|
23110
23957
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
23111
23958
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
23112
23959
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
23960
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
23113
23961
|
if (previous) {
|
|
23114
23962
|
previous.flush();
|
|
23115
23963
|
previous.dispose();
|
|
@@ -23236,6 +24084,10 @@ var MmtpRouter = class {
|
|
|
23236
24084
|
get keyframeOverlayStats() {
|
|
23237
24085
|
return { ...this.#overlayStats };
|
|
23238
24086
|
}
|
|
24087
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
24088
|
+
get recoveredHeadsAwaitingTiming() {
|
|
24089
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
24090
|
+
}
|
|
23239
24091
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23240
24092
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23241
24093
|
}
|
|
@@ -23256,7 +24108,8 @@ var MmtpRouter = class {
|
|
|
23256
24108
|
fec,
|
|
23257
24109
|
k,
|
|
23258
24110
|
offsets: /* @__PURE__ */ new Map(),
|
|
23259
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
24111
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
24112
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
23260
24113
|
};
|
|
23261
24114
|
this.#overlays.set(repairPacketId, inst);
|
|
23262
24115
|
return () => {
|
|
@@ -23286,12 +24139,18 @@ var MmtpRouter = class {
|
|
|
23286
24139
|
*/
|
|
23287
24140
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23288
24141
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
24142
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
24143
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
24144
|
+
stream.overlayToBase.routed.clear();
|
|
24145
|
+
stream.overlayToBase.pending.clear();
|
|
24146
|
+
}
|
|
23289
24147
|
}
|
|
23290
24148
|
#resetOverlay(inst) {
|
|
23291
24149
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
23292
24150
|
inst.last = void 0;
|
|
23293
24151
|
inst.offsets.clear();
|
|
23294
24152
|
inst.moqBlocks.clear();
|
|
24153
|
+
inst.rebuilt.clear();
|
|
23295
24154
|
}
|
|
23296
24155
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
23297
24156
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -23342,18 +24201,33 @@ var MmtpRouter = class {
|
|
|
23342
24201
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23343
24202
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23344
24203
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23345
|
-
* included (7.1, 7.4.3).
|
|
23346
|
-
|
|
23347
|
-
|
|
24204
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
24205
|
+
* FEC reads it.
|
|
24206
|
+
*
|
|
24207
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
24208
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
24209
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
24210
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
24211
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
24212
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
24213
|
+
*/
|
|
24214
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23348
24215
|
if (this.#overlays.size === 0) return;
|
|
23349
24216
|
const ssId = keyframeOverlaySsId(header);
|
|
23350
24217
|
if (ssId === void 0) return;
|
|
24218
|
+
let rebuiltPsn;
|
|
23351
24219
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
23352
24220
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
24221
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
24222
|
+
inst.rebuilt.delete(ssId);
|
|
23353
24223
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23354
24224
|
if (identity !== null) {
|
|
23355
24225
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23356
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
24226
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
24227
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
24228
|
+
overlaySsId: ssId,
|
|
24229
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
24230
|
+
});
|
|
23357
24231
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23358
24232
|
}
|
|
23359
24233
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23375,6 +24249,14 @@ var MmtpRouter = class {
|
|
|
23375
24249
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
23376
24250
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
23377
24251
|
}
|
|
24252
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
24253
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
24254
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
24255
|
+
this.#restampedRecoveredSeqs++;
|
|
24256
|
+
} else if (baseSsId !== void 0) {
|
|
24257
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24258
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24259
|
+
}
|
|
23378
24260
|
}
|
|
23379
24261
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
23380
24262
|
inst.moqBlocks.add(blockStart);
|
|
@@ -23408,11 +24290,16 @@ var MmtpRouter = class {
|
|
|
23408
24290
|
}
|
|
23409
24291
|
/**
|
|
23410
24292
|
* Route an overlay-recovered block's packets as source media of the
|
|
23411
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
23412
|
-
*
|
|
23413
|
-
*
|
|
23414
|
-
*
|
|
23415
|
-
*
|
|
24293
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
24294
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
24295
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
24296
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
24297
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
24298
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
24299
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
24300
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
24301
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
24302
|
+
* header's sequence and any other drops the packet.
|
|
23416
24303
|
*/
|
|
23417
24304
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
23418
24305
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23432,22 +24319,139 @@ var MmtpRouter = class {
|
|
|
23432
24319
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23433
24320
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23434
24321
|
const restored = /* @__PURE__ */ new Set();
|
|
24322
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
24323
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
23435
24324
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23436
|
-
const
|
|
23437
|
-
|
|
23438
|
-
|
|
24325
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24326
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24327
|
+
if (anchor !== void 0) {
|
|
24328
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23439
24329
|
restored.add(packetSequenceNumber);
|
|
23440
|
-
|
|
24330
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24331
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24332
|
+
if (baseSsId !== void 0) {
|
|
24333
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24334
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24335
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
24336
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
24337
|
+
}
|
|
24338
|
+
}
|
|
24339
|
+
return [out];
|
|
24340
|
+
}
|
|
24341
|
+
if (!restamped) {
|
|
24342
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
24343
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24344
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
24345
|
+
if (baseSsId !== void 0) {
|
|
24346
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
24347
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
24348
|
+
} else {
|
|
24349
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
24350
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
24351
|
+
}
|
|
24352
|
+
return [sym];
|
|
23441
24353
|
}
|
|
23442
|
-
if (!restamped) return [sym];
|
|
23443
24354
|
this.#overlayStats.unsequencedDropped++;
|
|
23444
24355
|
return [];
|
|
23445
24356
|
});
|
|
23446
24357
|
this.#overlayStats.recovered++;
|
|
24358
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
24359
|
+
for (const sym of symbols) {
|
|
24360
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24361
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
24362
|
+
const first = mpuFirst.get(mpu);
|
|
24363
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
24364
|
+
}
|
|
24365
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
24366
|
+
for (const sym of symbols) {
|
|
24367
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
24368
|
+
}
|
|
23447
24369
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
23448
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
24370
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
24371
|
+
baseSsIds,
|
|
24372
|
+
firstSsIds
|
|
23449
24373
|
});
|
|
23450
24374
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24375
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
24376
|
+
}
|
|
24377
|
+
/**
|
|
24378
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24379
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24380
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
24381
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
24382
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24383
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24384
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24385
|
+
* A base block no repair has stated yet takes it when one does
|
|
24386
|
+
* (#feedPendingRebuilt).
|
|
24387
|
+
*
|
|
24388
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24389
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24390
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24391
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24392
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24393
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24394
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24395
|
+
* block's own arrivals cannot show.
|
|
24396
|
+
*/
|
|
24397
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24398
|
+
const fec = this.#fecManager;
|
|
24399
|
+
const owner = inst.leg.sourcePacketId;
|
|
24400
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24401
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24402
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24403
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24404
|
+
const feeds = [];
|
|
24405
|
+
for (const sym of symbols) {
|
|
24406
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24407
|
+
const ssId = baseSsIds.get(sym.index);
|
|
24408
|
+
if (ssId === void 0) {
|
|
24409
|
+
this.#overlayStats.baseUndetermined++;
|
|
24410
|
+
continue;
|
|
24411
|
+
}
|
|
24412
|
+
routed.delete(ssId);
|
|
24413
|
+
routed.add(ssId);
|
|
24414
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24415
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24416
|
+
}
|
|
24417
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24418
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24419
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24420
|
+
}
|
|
24421
|
+
}
|
|
24422
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24423
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24424
|
+
const fec = this.#fecManager;
|
|
24425
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24426
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24427
|
+
if (coords == null) {
|
|
24428
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24429
|
+
pending.delete(ssId);
|
|
24430
|
+
pending.set(ssId, symbolData.slice());
|
|
24431
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24432
|
+
return;
|
|
24433
|
+
}
|
|
24434
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24435
|
+
if (!fed.taken) return;
|
|
24436
|
+
this.#overlayStats.baseFed++;
|
|
24437
|
+
if (fed.recovered === null) return;
|
|
24438
|
+
const sbn = coords.sbn;
|
|
24439
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24440
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24441
|
+
fec.countRecovery(owner, sbn, name);
|
|
24442
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24443
|
+
});
|
|
24444
|
+
}
|
|
24445
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24446
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24447
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24448
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24449
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24450
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24451
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24452
|
+
pending.delete(ssId);
|
|
24453
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24454
|
+
}
|
|
23451
24455
|
}
|
|
23452
24456
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23453
24457
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23579,13 +24583,18 @@ var MmtpRouter = class {
|
|
|
23579
24583
|
if (!entry) return;
|
|
23580
24584
|
entry.receiver.release();
|
|
23581
24585
|
entry.protected = false;
|
|
24586
|
+
entry.userAgent = false;
|
|
23582
24587
|
this.#assistBlocks.clear();
|
|
23583
24588
|
this.#assistFailures.clear();
|
|
23584
24589
|
}
|
|
23585
|
-
/**
|
|
24590
|
+
/** 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
24591
|
get unadmittedListedDropCount() {
|
|
23587
24592
|
return this.#unadmittedListedDrops;
|
|
23588
24593
|
}
|
|
24594
|
+
/** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
|
|
24595
|
+
get admittedSourceDatagramCount() {
|
|
24596
|
+
return this.#admittedSourceDatagrams;
|
|
24597
|
+
}
|
|
23589
24598
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23590
24599
|
get unlistedRepairSetAsideCount() {
|
|
23591
24600
|
return this.#unlistedRepairSetAside;
|
|
@@ -23623,6 +24632,13 @@ var MmtpRouter = class {
|
|
|
23623
24632
|
}
|
|
23624
24633
|
return false;
|
|
23625
24634
|
}
|
|
24635
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
24636
|
+
#userAgentGoverns(packetId) {
|
|
24637
|
+
for (const entry of this.#provenance.values()) {
|
|
24638
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
24639
|
+
}
|
|
24640
|
+
return false;
|
|
24641
|
+
}
|
|
23626
24642
|
/**
|
|
23627
24643
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
23628
24644
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -23713,7 +24729,7 @@ var MmtpRouter = class {
|
|
|
23713
24729
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
23714
24730
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
23715
24731
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
23716
|
-
})) {
|
|
24732
|
+
}, true)) {
|
|
23717
24733
|
this.#unlistedAssistedRecoveries++;
|
|
23718
24734
|
this.#assistBlocks.delete(key);
|
|
23719
24735
|
return;
|
|
@@ -24065,6 +25081,8 @@ var MmtpRouter = class {
|
|
|
24065
25081
|
)) return;
|
|
24066
25082
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24067
25083
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
25084
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
25085
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24068
25086
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24069
25087
|
const recovered = fec.feedRepairSymbol(
|
|
24070
25088
|
ownerPacketId,
|
|
@@ -24115,7 +25133,7 @@ var MmtpRouter = class {
|
|
|
24115
25133
|
catalogTrackName
|
|
24116
25134
|
)) continue;
|
|
24117
25135
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
24118
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
25136
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
24119
25137
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
24120
25138
|
}
|
|
24121
25139
|
if (payload.oversized) continue;
|
|
@@ -24202,6 +25220,8 @@ var MmtpRouter = class {
|
|
|
24202
25220
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24203
25221
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24204
25222
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
25223
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
25224
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24205
25225
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24206
25226
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24207
25227
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -24328,6 +25348,12 @@ var MmtpRouter = class {
|
|
|
24328
25348
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
24329
25349
|
* own RAP cadence, so no timer is involved.
|
|
24330
25350
|
*
|
|
25351
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
25352
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
25353
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
25354
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
25355
|
+
* frames of the outgoing stream.
|
|
25356
|
+
*
|
|
24331
25357
|
* The commit is one synchronous transition inside the frame path: the
|
|
24332
25358
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
24333
25359
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -24378,6 +25404,9 @@ var MmtpRouter = class {
|
|
|
24378
25404
|
droppedIncomingFrames: 0
|
|
24379
25405
|
};
|
|
24380
25406
|
this.#staged = state;
|
|
25407
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
25408
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
25409
|
+
}
|
|
24381
25410
|
return {
|
|
24382
25411
|
cancel: () => {
|
|
24383
25412
|
if (this.#staged !== state) return false;
|
|
@@ -24460,7 +25489,17 @@ var MmtpRouter = class {
|
|
|
24460
25489
|
*/
|
|
24461
25490
|
#routeParsedFrame(data, meta) {
|
|
24462
25491
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
24463
|
-
if (this.#staged !== null
|
|
25492
|
+
if (this.#staged !== null) {
|
|
25493
|
+
let consumed = false;
|
|
25494
|
+
this.#fecPokeDepth++;
|
|
25495
|
+
try {
|
|
25496
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
25497
|
+
} finally {
|
|
25498
|
+
this.#fecPokeDepth--;
|
|
25499
|
+
this.#flushPendingFecPokes();
|
|
25500
|
+
}
|
|
25501
|
+
if (consumed) return;
|
|
25502
|
+
}
|
|
24464
25503
|
this.#deliverParsedFrame(data, meta);
|
|
24465
25504
|
}
|
|
24466
25505
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -24612,12 +25651,16 @@ var MmtpRouter = class {
|
|
|
24612
25651
|
this.#parser.deferFecRelease(work);
|
|
24613
25652
|
} finally {
|
|
24614
25653
|
this.#fecPokeDepth--;
|
|
24615
|
-
|
|
24616
|
-
|
|
24617
|
-
|
|
24618
|
-
|
|
24619
|
-
|
|
24620
|
-
|
|
25654
|
+
this.#flushPendingFecPokes();
|
|
25655
|
+
}
|
|
25656
|
+
}
|
|
25657
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
25658
|
+
#flushPendingFecPokes() {
|
|
25659
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
25660
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
25661
|
+
this.#pendingFecPokes.clear();
|
|
25662
|
+
for (const poke of pokes) {
|
|
25663
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
24621
25664
|
}
|
|
24622
25665
|
}
|
|
24623
25666
|
}
|
|
@@ -24695,6 +25738,14 @@ var MmtpRouter = class {
|
|
|
24695
25738
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
24696
25739
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
24697
25740
|
},
|
|
25741
|
+
// A staged switch commits on when each side's frames reach it
|
|
25742
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
25743
|
+
// they did unanchored.
|
|
25744
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
25745
|
+
const staged = this.#staged;
|
|
25746
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
25747
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
25748
|
+
},
|
|
24698
25749
|
onReleased: (data, meta) => {
|
|
24699
25750
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
24700
25751
|
const timing = meta.authoredTiming;
|
|
@@ -24836,6 +25887,7 @@ var MmtpRouter = class {
|
|
|
24836
25887
|
const parsed = this.#parser.parsePacket(packet);
|
|
24837
25888
|
if (!parsed) return;
|
|
24838
25889
|
const info = parsed.info;
|
|
25890
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
24839
25891
|
if (source === "multicast" && !manifestAdmitted) {
|
|
24840
25892
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
24841
25893
|
}
|
|
@@ -24869,6 +25921,12 @@ var MmtpRouter = class {
|
|
|
24869
25921
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
24870
25922
|
return;
|
|
24871
25923
|
}
|
|
25924
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
25925
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
25926
|
+
protectedFlow.userAgent = true;
|
|
25927
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
25928
|
+
return;
|
|
25929
|
+
}
|
|
24872
25930
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
24873
25931
|
if (verdict === "admitted") {
|
|
24874
25932
|
this.#noteFlowAdmitted(ingress.flow);
|
|
@@ -24886,6 +25944,11 @@ var MmtpRouter = class {
|
|
|
24886
25944
|
return;
|
|
24887
25945
|
}
|
|
24888
25946
|
}
|
|
25947
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
|
|
25948
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
25949
|
+
this.#unadmittedListedDrops++;
|
|
25950
|
+
return;
|
|
25951
|
+
}
|
|
24889
25952
|
if (info.isRepair) {
|
|
24890
25953
|
const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
|
|
24891
25954
|
if (overlay !== void 0) {
|
|
@@ -24903,7 +25966,7 @@ var MmtpRouter = class {
|
|
|
24903
25966
|
);
|
|
24904
25967
|
if (!repair) return;
|
|
24905
25968
|
if (ownerPacketId != null && this.#fecManager) {
|
|
24906
|
-
if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
25969
|
+
if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
24907
25970
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
24908
25971
|
const count = this.#repairPidCounts[info.packetId];
|
|
24909
25972
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -25106,7 +26169,8 @@ var MmtpRouter = class {
|
|
|
25106
26169
|
parsed.header,
|
|
25107
26170
|
info.sequenceNumber,
|
|
25108
26171
|
info.rapFlag,
|
|
25109
|
-
source
|
|
26172
|
+
source,
|
|
26173
|
+
fecPayloadId?.ssId
|
|
25110
26174
|
);
|
|
25111
26175
|
}
|
|
25112
26176
|
}
|
|
@@ -25132,7 +26196,8 @@ var MmtpRouter = class {
|
|
|
25132
26196
|
source: parserOptions?.source,
|
|
25133
26197
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
25134
26198
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
25135
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
26199
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
26200
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
25136
26201
|
};
|
|
25137
26202
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
25138
26203
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -25256,7 +26321,8 @@ var MmtpRouter = class {
|
|
|
25256
26321
|
if (stream === void 0) {
|
|
25257
26322
|
stream = {
|
|
25258
26323
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25259
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
26324
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
26325
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25260
26326
|
};
|
|
25261
26327
|
byName.set(catalogTrackName, stream);
|
|
25262
26328
|
}
|
|
@@ -25371,8 +26437,16 @@ var MmtpRouter = class {
|
|
|
25371
26437
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25372
26438
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25373
26439
|
}
|
|
25374
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26440
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25375
26441
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26442
|
+
if (this.#isFecTrack(packetId)) {
|
|
26443
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26444
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26445
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26446
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26447
|
+
return;
|
|
26448
|
+
}
|
|
26449
|
+
}
|
|
25376
26450
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25377
26451
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25378
26452
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25482,7 +26556,7 @@ var MmtpRouter = class {
|
|
|
25482
26556
|
}
|
|
25483
26557
|
// ── Private: FEC recovery routing ───────────────────────────────
|
|
25484
26558
|
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
25485
|
-
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
|
|
26559
|
+
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
25486
26560
|
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
25487
26561
|
if (ownerPacketId === void 0) {
|
|
25488
26562
|
throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
|
|
@@ -25499,7 +26573,7 @@ var MmtpRouter = class {
|
|
|
25499
26573
|
ownerPacketId,
|
|
25500
26574
|
blockProfile
|
|
25501
26575
|
});
|
|
25502
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
26576
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
25503
26577
|
}
|
|
25504
26578
|
/**
|
|
25505
26579
|
* A recovered source packet of a protected flow is admitted on its own
|
|
@@ -25511,9 +26585,31 @@ var MmtpRouter = class {
|
|
|
25511
26585
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
25512
26586
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
25513
26587
|
* outside a protected flow.
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
26588
|
+
*
|
|
26589
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
26590
|
+
* without a record: while the flow is protected that decoder takes only
|
|
26591
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
26592
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
26593
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
26594
|
+
* unlisted MoQ repair and still needs a record.
|
|
26595
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
26596
|
+
* ever mixes verified and unverified live inputs.
|
|
26597
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
26598
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
26599
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
26600
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
26601
|
+
*/
|
|
26602
|
+
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26603
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26604
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26605
|
+
result = {
|
|
26606
|
+
...result,
|
|
26607
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26608
|
+
};
|
|
26609
|
+
}
|
|
26610
|
+
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
26611
|
+
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
26612
|
+
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
25517
26613
|
let recovered = true;
|
|
25518
26614
|
if (receiver) {
|
|
25519
26615
|
const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -25525,6 +26621,8 @@ var MmtpRouter = class {
|
|
|
25525
26621
|
let admitted = false;
|
|
25526
26622
|
if (receiver === "ambiguous") {
|
|
25527
26623
|
this.#ambiguousRecoveredDrops++;
|
|
26624
|
+
} else if (userAgent) {
|
|
26625
|
+
admitted = true;
|
|
25528
26626
|
} else {
|
|
25529
26627
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
25530
26628
|
wire.set(sym.rawSymbol);
|
|
@@ -25552,12 +26650,12 @@ var MmtpRouter = class {
|
|
|
25552
26650
|
catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
|
|
25553
26651
|
);
|
|
25554
26652
|
if (flows.size > 1) return "ambiguous";
|
|
25555
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
26653
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
25556
26654
|
let match;
|
|
25557
|
-
for (const
|
|
25558
|
-
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
26655
|
+
for (const entry of this.#provenance.values()) {
|
|
26656
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
25559
26657
|
if (match) return "ambiguous";
|
|
25560
|
-
match =
|
|
26658
|
+
match = entry;
|
|
25561
26659
|
}
|
|
25562
26660
|
}
|
|
25563
26661
|
return match;
|
|
@@ -25565,7 +26663,12 @@ var MmtpRouter = class {
|
|
|
25565
26663
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
25566
26664
|
if (ownerPacketId == null) return;
|
|
25567
26665
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
25568
|
-
const fecSbnOf = (index) =>
|
|
26666
|
+
const fecSbnOf = (index) => {
|
|
26667
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
26668
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
26669
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
26670
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
26671
|
+
};
|
|
25569
26672
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
25570
26673
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
25571
26674
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -25573,7 +26676,10 @@ var MmtpRouter = class {
|
|
|
25573
26676
|
...result,
|
|
25574
26677
|
symbols: result.symbols.map((sym) => {
|
|
25575
26678
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25576
|
-
if (wire === sym.packetSequenceNumber)
|
|
26679
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26680
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26681
|
+
return sym;
|
|
26682
|
+
}
|
|
25577
26683
|
this.#restampedRecoveredSeqs++;
|
|
25578
26684
|
this.#restampedStreams.add(streamKey);
|
|
25579
26685
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25609,6 +26715,29 @@ var MmtpRouter = class {
|
|
|
25609
26715
|
}
|
|
25610
26716
|
);
|
|
25611
26717
|
}
|
|
26718
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26719
|
+
ownerPacketId,
|
|
26720
|
+
sym.fragment.mpuSequenceNumber,
|
|
26721
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26722
|
+
sym.fragment.sampleNumber,
|
|
26723
|
+
catalogTrackName
|
|
26724
|
+
);
|
|
26725
|
+
for (const sym of result.symbols) {
|
|
26726
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26727
|
+
this.#feedViaParser(
|
|
26728
|
+
sym.rawSymbol,
|
|
26729
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26730
|
+
void 0,
|
|
26731
|
+
{
|
|
26732
|
+
skipFecPayloadId: true,
|
|
26733
|
+
recovered: true,
|
|
26734
|
+
deliveryProvenance,
|
|
26735
|
+
...fecSbnOf(sym.index),
|
|
26736
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26737
|
+
}
|
|
26738
|
+
);
|
|
26739
|
+
}
|
|
26740
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25612
26741
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25613
26742
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25614
26743
|
if (assembler) {
|
|
@@ -26339,11 +27468,8 @@ function providerErrorOf(error) {
|
|
|
26339
27468
|
return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
|
|
26340
27469
|
}
|
|
26341
27470
|
var MAX_TRACKED_DROPPED_PIDS = 16;
|
|
26342
|
-
function getMulticastGlobals() {
|
|
26343
|
-
return globalThis;
|
|
26344
|
-
}
|
|
26345
27471
|
function hasMulticastGateway() {
|
|
26346
|
-
return typeof
|
|
27472
|
+
return typeof resolveMulticastGateway()?.subscribe === "function";
|
|
26347
27473
|
}
|
|
26348
27474
|
function senderOf(event) {
|
|
26349
27475
|
const sourceAddress = event.sourceAddress;
|
|
@@ -26435,6 +27561,9 @@ var MulticastTransport = class {
|
|
|
26435
27561
|
// listener closure, so an in-place admission change (setContext) applies
|
|
26436
27562
|
// from the next packet on without rejoining.
|
|
26437
27563
|
#context = void 0;
|
|
27564
|
+
#deliveredContext = void 0;
|
|
27565
|
+
/** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
|
|
27566
|
+
#userAgentGateway = void 0;
|
|
26438
27567
|
// The live join's provider-side filter. An in-place widening must reach it,
|
|
26439
27568
|
// or the provider never delivers the new ID; null without a live join.
|
|
26440
27569
|
#providerFilter = null;
|
|
@@ -26471,7 +27600,7 @@ var MulticastTransport = class {
|
|
|
26471
27600
|
this.#dataPlaneLive = false;
|
|
26472
27601
|
this.#sender = void 0;
|
|
26473
27602
|
if (this.#disposed) return this.#declined(generation, "disposed");
|
|
26474
|
-
const gateway = this.#gateway ??
|
|
27603
|
+
const gateway = this.#gateway ?? resolveMulticastGateway();
|
|
26475
27604
|
if (typeof gateway?.subscribe !== "function") {
|
|
26476
27605
|
console.warn("[MulticastTransport] Multicast API not available");
|
|
26477
27606
|
return this.#declined(generation, "unavailable");
|
|
@@ -26487,15 +27616,16 @@ var MulticastTransport = class {
|
|
|
26487
27616
|
console.log(
|
|
26488
27617
|
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
|
|
26489
27618
|
);
|
|
27619
|
+
const userAgent = isUserAgentVerifiedGateway(gateway);
|
|
26490
27620
|
const subscribeConfig = toWindowMulticastSubscribeConfig({
|
|
26491
27621
|
source: config.sourceAddress,
|
|
26492
27622
|
senderAddress: config.senderAddress,
|
|
26493
27623
|
group: config.groupAddress,
|
|
26494
27624
|
port: config.port,
|
|
26495
|
-
mode: config.mode,
|
|
26496
|
-
amtRelay: config.amtRelay,
|
|
26497
|
-
amtRelayPort: config.amtRelayPort,
|
|
26498
|
-
amtDiscovery: config.amtDiscovery,
|
|
27625
|
+
mode: userAgent ? "native" : config.mode,
|
|
27626
|
+
amtRelay: userAgent ? void 0 : config.amtRelay,
|
|
27627
|
+
amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
|
|
27628
|
+
amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
|
|
26499
27629
|
packetIds: config.packetIds,
|
|
26500
27630
|
tracks: config.tracks,
|
|
26501
27631
|
atsc3: config.atsc3
|
|
@@ -26541,7 +27671,8 @@ var MulticastTransport = class {
|
|
|
26541
27671
|
acknowledgedUpdate: 0,
|
|
26542
27672
|
inFlight: 0
|
|
26543
27673
|
};
|
|
26544
|
-
this.#
|
|
27674
|
+
this.#userAgentGateway = userAgent ? gateway : void 0;
|
|
27675
|
+
this.#setContext(context);
|
|
26545
27676
|
if (packetIds) {
|
|
26546
27677
|
for (const packetId of packetIds) {
|
|
26547
27678
|
if (!this.#acceptedBytesByPacketId.has(packetId)) {
|
|
@@ -26549,7 +27680,7 @@ var MulticastTransport = class {
|
|
|
26549
27680
|
}
|
|
26550
27681
|
}
|
|
26551
27682
|
}
|
|
26552
|
-
this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
27683
|
+
this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
|
|
26553
27684
|
const delivery = subscription.delivery ?? "datagram";
|
|
26554
27685
|
if (delivery !== "datagram" && delivery !== "ordered-relay") {
|
|
26555
27686
|
throw new Error(
|
|
@@ -26630,7 +27761,7 @@ var MulticastTransport = class {
|
|
|
26630
27761
|
}
|
|
26631
27762
|
if (!this.#acceptPacket(packet, sourceAddress)) return false;
|
|
26632
27763
|
const liveness = !this.#isRecordCandidate(packet);
|
|
26633
|
-
this.#onPacket(packet, this.#
|
|
27764
|
+
this.#onPacket(packet, this.#deliveredContext);
|
|
26634
27765
|
return liveness;
|
|
26635
27766
|
}
|
|
26636
27767
|
#isRecordCandidate(packet) {
|
|
@@ -26841,8 +27972,10 @@ var MulticastTransport = class {
|
|
|
26841
27972
|
return this.#subscription !== null;
|
|
26842
27973
|
}
|
|
26843
27974
|
/**
|
|
26844
|
-
*
|
|
26845
|
-
*
|
|
27975
|
+
* The live join's context as the caller gave it (the join's `context`, or
|
|
27976
|
+
* the last setContext), or undefined without a live join. On a native
|
|
27977
|
+
* provider each accepted packet is delivered with a marked copy of it
|
|
27978
|
+
* (`userAgentVerifiedIngress`); this getter never carries the mark.
|
|
26846
27979
|
*/
|
|
26847
27980
|
get context() {
|
|
26848
27981
|
return this.#context;
|
|
@@ -26866,7 +27999,16 @@ var MulticastTransport = class {
|
|
|
26866
27999
|
"[MulticastTransport] setContext needs a live join; join() carries the first context"
|
|
26867
28000
|
);
|
|
26868
28001
|
}
|
|
28002
|
+
this.#setContext(context);
|
|
28003
|
+
}
|
|
28004
|
+
/**
|
|
28005
|
+
* The join's context, and what the subscription delivers with each packet:
|
|
28006
|
+
* marked admitted by the user agent when the gateway is its native
|
|
28007
|
+
* MulticastReceiver provider. feedPacket never marks.
|
|
28008
|
+
*/
|
|
28009
|
+
#setContext(context) {
|
|
26869
28010
|
this.#context = context;
|
|
28011
|
+
this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
|
|
26870
28012
|
}
|
|
26871
28013
|
/**
|
|
26872
28014
|
* Replace the live join's packet-ID allow-list in place, without rejoining.
|
|
@@ -26998,6 +28140,8 @@ var MulticastTransport = class {
|
|
|
26998
28140
|
this.#matchedSource = void 0;
|
|
26999
28141
|
this.#providerFilter = null;
|
|
27000
28142
|
this.#context = void 0;
|
|
28143
|
+
this.#deliveredContext = void 0;
|
|
28144
|
+
this.#userAgentGateway = void 0;
|
|
27001
28145
|
this.#withdrawnPacketIds.clear();
|
|
27002
28146
|
}
|
|
27003
28147
|
/** The ingress filter; an accepted packet is counted here, once. */
|
|
@@ -27320,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
|
|
|
27320
28464
|
function isSymbolCounts(value) {
|
|
27321
28465
|
return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
|
|
27322
28466
|
}
|
|
28467
|
+
function isSpanFailure(value) {
|
|
28468
|
+
if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
|
|
28469
|
+
const length = value["length"];
|
|
28470
|
+
const sources = value["sources"];
|
|
28471
|
+
return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
|
|
28472
|
+
}
|
|
27323
28473
|
function isNullableNonNegative(value) {
|
|
27324
28474
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
27325
28475
|
}
|
|
@@ -27339,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
|
|
|
27339
28489
|
case "recovered":
|
|
27340
28490
|
return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
|
|
27341
28491
|
case "failed":
|
|
27342
|
-
return hasExactKeys(
|
|
28492
|
+
return hasExactKeys(
|
|
28493
|
+
value,
|
|
28494
|
+
"span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
|
|
28495
|
+
) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
|
|
27343
28496
|
case "repair-preempted":
|
|
27344
28497
|
case "repair-deferred":
|
|
27345
28498
|
return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
|
|
@@ -27605,7 +28758,7 @@ var MulticastManager = class {
|
|
|
27605
28758
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
27606
28759
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
27607
28760
|
},
|
|
27608
|
-
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
|
|
28761
|
+
onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
27609
28762
|
const pathCount = this.#fec.blockPathCount(
|
|
27610
28763
|
sbn,
|
|
27611
28764
|
packetId,
|
|
@@ -27623,7 +28776,8 @@ var MulticastManager = class {
|
|
|
27623
28776
|
symbols: {
|
|
27624
28777
|
multicast: pathCount?.multicastSymbols ?? 0,
|
|
27625
28778
|
moq: pathCount?.moqSymbols ?? 0
|
|
27626
|
-
}
|
|
28779
|
+
},
|
|
28780
|
+
...span === void 0 ? {} : { span }
|
|
27627
28781
|
});
|
|
27628
28782
|
},
|
|
27629
28783
|
onFrameGap: (decision) => {
|
|
@@ -28350,8 +29504,17 @@ var MulticastManager = class {
|
|
|
28350
29504
|
* repair roles and owners are registered here; in-band owners stay the
|
|
28351
29505
|
* caller's. Joining is best effort: a group that fails to join is logged
|
|
28352
29506
|
* and leaves the media join as it was.
|
|
28353
|
-
|
|
28354
|
-
|
|
29507
|
+
*
|
|
29508
|
+
* `options.gateway` names the gateway a subscribe callback reads through.
|
|
29509
|
+
* When it is the browser's own MulticastReceiver provider, the callback's
|
|
29510
|
+
* packets are fed as admitted by the user agent
|
|
29511
|
+
* (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
|
|
29512
|
+
* The mark rests on this statement, not on the data path: every packet the
|
|
29513
|
+
* callback delivers must come from a subscription opened on
|
|
29514
|
+
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
29515
|
+
* WebSocket repair feed) must not name a native provider here.
|
|
29516
|
+
*/
|
|
29517
|
+
joinRepairEndpoints(endpoints, provider, options) {
|
|
28355
29518
|
const releases = [];
|
|
28356
29519
|
let released = false;
|
|
28357
29520
|
const safely = (each) => {
|
|
@@ -28373,17 +29536,19 @@ var MulticastManager = class {
|
|
|
28373
29536
|
...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
|
|
28374
29537
|
};
|
|
28375
29538
|
let sender;
|
|
28376
|
-
const
|
|
29539
|
+
const feed = (packet, sourceAddress, ingressToFeed) => {
|
|
28377
29540
|
if (released) return;
|
|
28378
29541
|
if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
|
|
28379
|
-
this.#router.feedPacket(packet, "multicast",
|
|
29542
|
+
this.#router.feedPacket(packet, "multicast", ingressToFeed);
|
|
28380
29543
|
};
|
|
28381
29544
|
const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
|
|
28382
29545
|
if (typeof provider === "function") {
|
|
29546
|
+
const marked = userAgentVerifiedIngress(ingress, options?.gateway);
|
|
29547
|
+
const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
|
|
28383
29548
|
Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
|
|
28384
29549
|
continue;
|
|
28385
29550
|
}
|
|
28386
|
-
const transport = new MulticastTransport((packet) =>
|
|
29551
|
+
const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
|
|
28387
29552
|
gateway: provider,
|
|
28388
29553
|
lockedTo: () => this.#transport.sender
|
|
28389
29554
|
});
|
|
@@ -28395,11 +29560,12 @@ var MulticastManager = class {
|
|
|
28395
29560
|
mode: endpoint.mode,
|
|
28396
29561
|
amtRelay: endpoint.amtRelay,
|
|
28397
29562
|
amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
|
|
29563
|
+
amtDiscovery: endpoint.amtDiscovery,
|
|
28398
29564
|
packetIds: endpoint.packetIds,
|
|
28399
29565
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
28400
29566
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
28401
29567
|
...endpoint.flow === void 0 ? {} : { protected: true }
|
|
28402
|
-
}).then((joined) => {
|
|
29568
|
+
}, ingress).then((joined) => {
|
|
28403
29569
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
28404
29570
|
});
|
|
28405
29571
|
}
|
|
@@ -28991,10 +30157,12 @@ var MulticastManager = class {
|
|
|
28991
30157
|
* Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
|
|
28992
30158
|
* on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
|
|
28993
30159
|
* pending hole and reported here, and `closeIsoHoles` must be called for it
|
|
28994
|
-
* once its range FETCH ends (or none is sent).
|
|
30160
|
+
* once its range FETCH ends (or none is sent). `onExpired` is told of a
|
|
30161
|
+
* pending hole the stream runs past the locator's horizon: returning true,
|
|
30162
|
+
* the caller closes it (`closeIsoHoles`); otherwise it is discarded.
|
|
28995
30163
|
*/
|
|
28996
|
-
onUndelimitedLoss(cb) {
|
|
28997
|
-
this.#fec.setUndelimitedLossHandler(cb);
|
|
30164
|
+
onUndelimitedLoss(cb, onExpired) {
|
|
30165
|
+
this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
|
|
28998
30166
|
}
|
|
28999
30167
|
/**
|
|
29000
30168
|
* Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
|
|
@@ -29230,7 +30398,8 @@ var MulticastManager = class {
|
|
|
29230
30398
|
"stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
|
|
29231
30399
|
...event,
|
|
29232
30400
|
block: Object.freeze({ ...event.block }),
|
|
29233
|
-
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
|
|
30401
|
+
..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
|
|
30402
|
+
..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
|
|
29234
30403
|
}
|
|
29235
30404
|
);
|
|
29236
30405
|
if (this.#logRepairLifecycle) {
|
|
@@ -29343,7 +30512,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29343
30512
|
blockProfile,
|
|
29344
30513
|
k: opts.fecK,
|
|
29345
30514
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29346
|
-
// and CMCD `
|
|
30515
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29347
30516
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29348
30517
|
symbolSize: opts.fecSymbolSize,
|
|
29349
30518
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -29885,24 +31054,24 @@ var MediaRouter = class {
|
|
|
29885
31054
|
if (!this.#multicastVideoKeyed) return;
|
|
29886
31055
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
29887
31056
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
29888
|
-
const
|
|
31057
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
29889
31058
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
29890
31059
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
29891
|
-
this.#commitMulticastActivation(
|
|
31060
|
+
this.#commitMulticastActivation(token2);
|
|
29892
31061
|
return;
|
|
29893
31062
|
}
|
|
29894
31063
|
const ready = Promise.resolve(maybeReady);
|
|
29895
|
-
this.#pendingMulticastToken =
|
|
31064
|
+
this.#pendingMulticastToken = token2;
|
|
29896
31065
|
this.#sourceSwitchReady = ready;
|
|
29897
31066
|
void ready.then(
|
|
29898
|
-
() => this.#commitMulticastActivation(
|
|
29899
|
-
(err) => this.#rejectMulticastActivation(
|
|
31067
|
+
() => this.#commitMulticastActivation(token2),
|
|
31068
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
29900
31069
|
).finally(() => {
|
|
29901
31070
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
29902
31071
|
});
|
|
29903
31072
|
}
|
|
29904
|
-
#commitMulticastActivation(
|
|
29905
|
-
if (
|
|
31073
|
+
#commitMulticastActivation(token2) {
|
|
31074
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
29906
31075
|
this.#active = "multicast";
|
|
29907
31076
|
this.#suppressed = true;
|
|
29908
31077
|
this.#pendingMulticastToken = null;
|
|
@@ -29913,9 +31082,9 @@ var MediaRouter = class {
|
|
|
29913
31082
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
29914
31083
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
29915
31084
|
}
|
|
29916
|
-
#rejectMulticastActivation(
|
|
29917
|
-
if (
|
|
29918
|
-
if (this.#pendingMulticastToken ===
|
|
31085
|
+
#rejectMulticastActivation(token2, err) {
|
|
31086
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
31087
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
29919
31088
|
this.#pendingMulticastVideo = null;
|
|
29920
31089
|
this.#pendingMulticastAudio = null;
|
|
29921
31090
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -30884,63 +32053,65 @@ var FecStarvationDetector = class {
|
|
|
30884
32053
|
|
|
30885
32054
|
// src/cmcd-producer.ts
|
|
30886
32055
|
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}`;
|
|
32056
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
32057
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
32058
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
32059
|
+
var TRANSPORT_TOKEN = {
|
|
32060
|
+
native: "ssm",
|
|
32061
|
+
amt: "amt",
|
|
32062
|
+
moq: "moq"
|
|
32063
|
+
};
|
|
32064
|
+
function sfString(value) {
|
|
32065
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
32066
|
+
}
|
|
32067
|
+
function encodeCmcdRecord(record) {
|
|
32068
|
+
return Object.keys(record).sort().map((key) => {
|
|
32069
|
+
const value = record[key];
|
|
32070
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
32071
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
32072
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
32073
|
+
return `${key}=${value.token}`;
|
|
30919
32074
|
}).join(",");
|
|
30920
32075
|
}
|
|
32076
|
+
var token = (value) => ({ token: value });
|
|
32077
|
+
function putCustom(record, name, value) {
|
|
32078
|
+
if (value === void 0) return;
|
|
32079
|
+
if (typeof value === "number") value = String(value);
|
|
32080
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
32081
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
32082
|
+
}
|
|
30921
32083
|
var CmcdProducer = class {
|
|
30922
|
-
#onCmcdParams;
|
|
30923
32084
|
#onCmcdPost;
|
|
32085
|
+
#intervalMs;
|
|
32086
|
+
#sessionId;
|
|
32087
|
+
#now;
|
|
32088
|
+
#sn = 0;
|
|
32089
|
+
#lastHeartbeat = null;
|
|
30924
32090
|
#transport;
|
|
30925
32091
|
#lastStats;
|
|
30926
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
30927
32092
|
#playMode = null;
|
|
30928
32093
|
#deltaFailRate = null;
|
|
30929
32094
|
#switchMiscount = 0;
|
|
30930
32095
|
#manifestStats = null;
|
|
30931
32096
|
#fecFailClosed = null;
|
|
32097
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
32098
|
+
#lifecycle = null;
|
|
30932
32099
|
constructor(options) {
|
|
30933
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
30934
32100
|
this.#onCmcdPost = options.onCmcdPost;
|
|
32101
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
32102
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
32103
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
32104
|
+
}
|
|
32105
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
32106
|
+
this.#now = options.now ?? Date.now;
|
|
30935
32107
|
}
|
|
30936
32108
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
30937
32109
|
updateTransport(snapshot) {
|
|
30938
32110
|
this.#transport = snapshot;
|
|
30939
32111
|
}
|
|
30940
32112
|
/**
|
|
30941
|
-
* Set the current play-mode
|
|
30942
|
-
*
|
|
30943
|
-
* (CLAUDE.md: error on bad init).
|
|
32113
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
32114
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
30944
32115
|
*/
|
|
30945
32116
|
updatePlayMode(mode) {
|
|
30946
32117
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -30951,9 +32122,8 @@ var CmcdProducer = class {
|
|
|
30951
32122
|
this.#playMode = mode;
|
|
30952
32123
|
}
|
|
30953
32124
|
/**
|
|
30954
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
30955
|
-
* `
|
|
30956
|
-
* Omitted on single-track streams.
|
|
32125
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
32126
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
30957
32127
|
*/
|
|
30958
32128
|
updateDeltaFailRate(rate) {
|
|
30959
32129
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -30963,112 +32133,139 @@ var CmcdProducer = class {
|
|
|
30963
32133
|
}
|
|
30964
32134
|
this.#deltaFailRate = rate;
|
|
30965
32135
|
}
|
|
30966
|
-
/**
|
|
30967
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
30968
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
30969
|
-
*/
|
|
32136
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
30970
32137
|
incrementSwitchMiscount() {
|
|
30971
32138
|
this.#switchMiscount += 1;
|
|
30972
32139
|
}
|
|
30973
32140
|
/**
|
|
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
|
|
32141
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
32142
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
32143
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
32144
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
32145
|
+
* with no loss.
|
|
30978
32146
|
*/
|
|
30979
32147
|
updateFecFailClosed(reason) {
|
|
32148
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
30980
32149
|
this.#fecFailClosed = reason;
|
|
32150
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
30981
32151
|
}
|
|
30982
32152
|
updateManifestVerification(stats) {
|
|
30983
32153
|
this.#manifestStats = stats;
|
|
30984
32154
|
}
|
|
30985
32155
|
/**
|
|
30986
|
-
*
|
|
30987
|
-
*
|
|
30988
|
-
*
|
|
30989
|
-
*
|
|
32156
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
32157
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
32158
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
32159
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
30990
32160
|
*/
|
|
32161
|
+
onRepairLifecycle(event) {
|
|
32162
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
32163
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
32164
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
32165
|
+
else this.#lifecycle.fetch++;
|
|
32166
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
32167
|
+
this.#lifecycle.failedKeyframe++;
|
|
32168
|
+
}
|
|
32169
|
+
}
|
|
32170
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
30991
32171
|
emitSwitchDrift(driftUs) {
|
|
30992
32172
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
30993
32173
|
throw new Error(
|
|
30994
32174
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
30995
32175
|
);
|
|
30996
32176
|
}
|
|
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);
|
|
32177
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
32178
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
32179
|
+
this.#post([record]);
|
|
31003
32180
|
}
|
|
31004
|
-
/**
|
|
31005
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31006
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31007
|
-
*/
|
|
32181
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31008
32182
|
onSnapshot(data) {
|
|
31009
32183
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31010
32184
|
if (!videoTrack) return;
|
|
31011
32185
|
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);
|
|
32186
|
+
this.#heartbeat();
|
|
31034
32187
|
}
|
|
31035
32188
|
/**
|
|
31036
|
-
*
|
|
31037
|
-
*
|
|
32189
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
32190
|
+
* heartbeat when due.
|
|
31038
32191
|
*/
|
|
31039
32192
|
onTransportSnapshot(data) {
|
|
31040
32193
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31041
32194
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31042
32195
|
}
|
|
31043
32196
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31044
|
-
if (data.switchMiscount !== void 0
|
|
31045
|
-
|
|
32197
|
+
if (data.switchMiscount !== void 0) {
|
|
32198
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
32199
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
32200
|
+
}
|
|
32201
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31046
32202
|
}
|
|
31047
32203
|
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);
|
|
32204
|
+
this.#heartbeat();
|
|
31053
32205
|
}
|
|
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
|
-
*/
|
|
32206
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31059
32207
|
onTransition(event) {
|
|
31060
|
-
const
|
|
31061
|
-
|
|
31062
|
-
|
|
32208
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
32209
|
+
putCustom(record, "ct", token(event));
|
|
32210
|
+
this.#putTransport(record);
|
|
32211
|
+
this.#post([record]);
|
|
32212
|
+
}
|
|
32213
|
+
#base(event) {
|
|
32214
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
32215
|
+
}
|
|
32216
|
+
#heartbeat() {
|
|
32217
|
+
const now = this.#now();
|
|
32218
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
32219
|
+
this.#lastHeartbeat = now;
|
|
32220
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
32221
|
+
}
|
|
32222
|
+
#putTransport(record) {
|
|
32223
|
+
const snap = this.#transport;
|
|
32224
|
+
if (!snap) return;
|
|
32225
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
32226
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
32227
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
32228
|
+
}
|
|
32229
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
32230
|
+
}
|
|
32231
|
+
/** The `e=ce` counter report. */
|
|
32232
|
+
#counters() {
|
|
32233
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
32234
|
+
const stats = this.#lastStats;
|
|
32235
|
+
if (stats) {
|
|
32236
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
32237
|
+
putCustom(record, "fk", stats.config.k);
|
|
32238
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
32239
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
32240
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
32241
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
32242
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
32243
|
+
}
|
|
32244
|
+
if (this.#lifecycle) {
|
|
32245
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
32246
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
32247
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
32248
|
+
}
|
|
32249
|
+
this.#putTransport(record);
|
|
32250
|
+
if (this.#playMode !== null) {
|
|
32251
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
32252
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
32253
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
32254
|
+
}
|
|
31063
32255
|
}
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
32256
|
+
if (this.#fecFailClosed) {
|
|
32257
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
32258
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
32259
|
+
}
|
|
32260
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
32261
|
+
if (this.#manifestStats) {
|
|
32262
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
32263
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
32264
|
+
}
|
|
32265
|
+
return record;
|
|
31067
32266
|
}
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
this.#onCmcdParams?.(params);
|
|
31071
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
32267
|
+
#post(records) {
|
|
32268
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31072
32269
|
}
|
|
31073
32270
|
};
|
|
31074
32271
|
|
|
@@ -31164,18 +32361,31 @@ function resolveRepairLayers(catalog, source) {
|
|
|
31164
32361
|
totalBaseRepairSymbols: binding.totalBaseRepairSymbols
|
|
31165
32362
|
};
|
|
31166
32363
|
}
|
|
32364
|
+
function valueAt(catalog, pointer) {
|
|
32365
|
+
let node = catalog;
|
|
32366
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
32367
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
32368
|
+
node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
32369
|
+
}
|
|
32370
|
+
return node;
|
|
32371
|
+
}
|
|
32372
|
+
function catalogTrackDeliveryMode(catalog, source) {
|
|
32373
|
+
const binding = bindingOf(catalog, source);
|
|
32374
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
|
|
32375
|
+
return entry?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32376
|
+
}
|
|
31167
32377
|
function moqtPriority2(value) {
|
|
31168
32378
|
return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
|
|
31169
32379
|
}
|
|
31170
32380
|
function catalogRepairTarget(catalog, source) {
|
|
31171
32381
|
const binding = bindingOf(catalog, source);
|
|
31172
32382
|
if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
|
|
31173
|
-
const
|
|
32383
|
+
const layerZero2 = binding.layers[0];
|
|
31174
32384
|
const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
|
|
31175
32385
|
const tracks = catalog.tracks;
|
|
31176
32386
|
const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
|
|
31177
32387
|
const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
|
|
31178
|
-
return { repairTrack:
|
|
32388
|
+
return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
|
|
31179
32389
|
}
|
|
31180
32390
|
function numericField(value) {
|
|
31181
32391
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
@@ -31207,6 +32417,7 @@ function fecParams(catalog, track, fps, sampleRate) {
|
|
|
31207
32417
|
repairTrackName: base.repairTrack,
|
|
31208
32418
|
repairLayers: repair.baseLayers,
|
|
31209
32419
|
repairOverlays: repair.overlays,
|
|
32420
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track),
|
|
31210
32421
|
...mediaRate
|
|
31211
32422
|
};
|
|
31212
32423
|
}
|
|
@@ -31257,7 +32468,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
|
|
|
31257
32468
|
var PATH_BITS = { moq: 1, multicast: 2 };
|
|
31258
32469
|
var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
|
|
31259
32470
|
var MmtpFecSession = class {
|
|
32471
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
32472
|
+
get repairSymbols() {
|
|
32473
|
+
return this.#repairSymbols;
|
|
32474
|
+
}
|
|
31260
32475
|
#decoder;
|
|
32476
|
+
#repairSymbols;
|
|
32477
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
32478
|
+
#message;
|
|
31261
32479
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
31262
32480
|
#blocks = /* @__PURE__ */ new Map();
|
|
31263
32481
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -31295,6 +32513,24 @@ var MmtpFecSession = class {
|
|
|
31295
32513
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
31296
32514
|
*/
|
|
31297
32515
|
constructor(config) {
|
|
32516
|
+
if (config && "alFecFlow" in config) {
|
|
32517
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow);
|
|
32518
|
+
if (!message) {
|
|
32519
|
+
throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
|
|
32520
|
+
}
|
|
32521
|
+
this.#message = message;
|
|
32522
|
+
this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
|
|
32523
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
|
|
32524
|
+
this.sourceSymbols = message.maximumK;
|
|
32525
|
+
this.#repairSymbols = message.maximumP;
|
|
32526
|
+
this.symbolSize = message.repairSymbolSize;
|
|
32527
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
|
|
32528
|
+
this.#repairPacketId = message.repairFlowId;
|
|
32529
|
+
this.#heldLayers = layerZero(message.maximumP);
|
|
32530
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
32531
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
32532
|
+
return;
|
|
32533
|
+
}
|
|
31298
32534
|
const fec = config?.fec;
|
|
31299
32535
|
if (!fec || typeof fec !== "object") {
|
|
31300
32536
|
throw new Error("MmtpFecSession: the catalog fec instance is required");
|
|
@@ -31305,7 +32541,7 @@ var MmtpFecSession = class {
|
|
|
31305
32541
|
}
|
|
31306
32542
|
this.algorithm = fec.scheme;
|
|
31307
32543
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
|
|
31308
|
-
this
|
|
32544
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
|
|
31309
32545
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
|
|
31310
32546
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
|
|
31311
32547
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
|
|
@@ -31325,17 +32561,30 @@ var MmtpFecSession = class {
|
|
|
31325
32561
|
config.heldLayers
|
|
31326
32562
|
);
|
|
31327
32563
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
|
|
31334
|
-
|
|
31335
|
-
|
|
31336
|
-
|
|
31337
|
-
|
|
31338
|
-
|
|
32564
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
32565
|
+
}
|
|
32566
|
+
/**
|
|
32567
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
32568
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
32569
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
32570
|
+
* previous one stays valid before the first block carrying the new total
|
|
32571
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
32572
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
32573
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
32574
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
32575
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
32576
|
+
*/
|
|
32577
|
+
updateAlFecFlow(flow) {
|
|
32578
|
+
const held = this.#message;
|
|
32579
|
+
if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
|
|
32580
|
+
const next = alFecRaptorQFlowConfig(flow);
|
|
32581
|
+
if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
|
|
32582
|
+
return "refused";
|
|
32583
|
+
}
|
|
32584
|
+
if (next.maximumP <= this.#repairSymbols) return "unchanged";
|
|
32585
|
+
this.#repairSymbols = next.maximumP;
|
|
32586
|
+
this.#heldLayers = layerZero(next.maximumP);
|
|
32587
|
+
return "raised";
|
|
31339
32588
|
}
|
|
31340
32589
|
/** True once release() ran. */
|
|
31341
32590
|
get released() {
|
|
@@ -31430,7 +32679,7 @@ var MmtpFecSession = class {
|
|
|
31430
32679
|
}
|
|
31431
32680
|
if (admission.reject) {
|
|
31432
32681
|
const verdict2 = this.#heldLayers.failsClosed(
|
|
31433
|
-
repairTrackName,
|
|
32682
|
+
this.#trackFor(repairTrackName),
|
|
31434
32683
|
frame.ssStart,
|
|
31435
32684
|
frame.rsbLength,
|
|
31436
32685
|
this.#anchorHorizon,
|
|
@@ -31445,7 +32694,7 @@ var MmtpFecSession = class {
|
|
|
31445
32694
|
this.#repairPacketId ??= frame.packetId;
|
|
31446
32695
|
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
31447
32696
|
const verdict = this.#heldLayers.admit(
|
|
31448
|
-
repairTrackName,
|
|
32697
|
+
this.#trackFor(repairTrackName),
|
|
31449
32698
|
frame.ssStart,
|
|
31450
32699
|
frame.rsbLength,
|
|
31451
32700
|
this.#anchorHorizon,
|
|
@@ -31504,6 +32753,10 @@ var MmtpFecSession = class {
|
|
|
31504
32753
|
decoder.free();
|
|
31505
32754
|
return true;
|
|
31506
32755
|
}
|
|
32756
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
32757
|
+
#trackFor(repairTrackName) {
|
|
32758
|
+
return this.#message ? void 0 : repairTrackName;
|
|
32759
|
+
}
|
|
31507
32760
|
#drop(reason) {
|
|
31508
32761
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
|
|
31509
32762
|
return [];
|
|
@@ -31625,6 +32878,22 @@ var MmtpFecSession = class {
|
|
|
31625
32878
|
}
|
|
31626
32879
|
}
|
|
31627
32880
|
};
|
|
32881
|
+
function layerZero(total) {
|
|
32882
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
|
|
32883
|
+
}
|
|
32884
|
+
function buildDecoder(Decoder, session) {
|
|
32885
|
+
if (typeof Decoder !== "function") {
|
|
32886
|
+
throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
|
|
32887
|
+
}
|
|
32888
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
|
|
32889
|
+
try {
|
|
32890
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize);
|
|
32891
|
+
} catch (error) {
|
|
32892
|
+
decoder.free();
|
|
32893
|
+
throw error;
|
|
32894
|
+
}
|
|
32895
|
+
return decoder;
|
|
32896
|
+
}
|
|
31628
32897
|
function requirePositiveInteger(value, path) {
|
|
31629
32898
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
31630
32899
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
|
|
@@ -31638,8 +32907,171 @@ function requireNumber(value, path) {
|
|
|
31638
32907
|
return value;
|
|
31639
32908
|
}
|
|
31640
32909
|
|
|
32910
|
+
// src/repair-layer-subscriptions.ts
|
|
32911
|
+
var SERIAL32_HALF2 = 2147483648;
|
|
32912
|
+
var RepairLayerSubscriptions = class {
|
|
32913
|
+
#opts;
|
|
32914
|
+
#windowPackets;
|
|
32915
|
+
#margin;
|
|
32916
|
+
#dropAfterWindows;
|
|
32917
|
+
#source;
|
|
32918
|
+
#layerCount = 0;
|
|
32919
|
+
#subs = /* @__PURE__ */ new Map();
|
|
32920
|
+
#disposed = false;
|
|
32921
|
+
#lastSeq;
|
|
32922
|
+
#expected = 0;
|
|
32923
|
+
#lost = 0;
|
|
32924
|
+
#wantFewer = 0;
|
|
32925
|
+
#lossRate = null;
|
|
32926
|
+
#repairPackets = 0;
|
|
32927
|
+
#repairBytes = 0;
|
|
32928
|
+
#staleDropped = 0;
|
|
32929
|
+
constructor(opts) {
|
|
32930
|
+
this.#opts = opts;
|
|
32931
|
+
this.#source = opts.source;
|
|
32932
|
+
this.#windowPackets = opts.windowPackets ?? 500;
|
|
32933
|
+
this.#margin = opts.margin ?? 2;
|
|
32934
|
+
this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
|
|
32935
|
+
}
|
|
32936
|
+
get layerCount() {
|
|
32937
|
+
return this.#layerCount;
|
|
32938
|
+
}
|
|
32939
|
+
/**
|
|
32940
|
+
* One source packet's MMTP `packet_sequence_number`, in arrival order.
|
|
32941
|
+
* The sequence is per `packet_id`: pass the packets of ONE packet_id only,
|
|
32942
|
+
* or interleaved sequences read as loss.
|
|
32943
|
+
*/
|
|
32944
|
+
noteSourceSequence(seq) {
|
|
32945
|
+
if (this.#disposed || this.#delivery() === "stream") return;
|
|
32946
|
+
seq >>>= 0;
|
|
32947
|
+
if (this.#lastSeq === void 0) {
|
|
32948
|
+
this.#lastSeq = seq;
|
|
32949
|
+
return;
|
|
32950
|
+
}
|
|
32951
|
+
const d = serial32Distance(this.#lastSeq, seq);
|
|
32952
|
+
if (d === 0) return;
|
|
32953
|
+
const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
|
|
32954
|
+
if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
|
|
32955
|
+
this.#lastSeq = seq;
|
|
32956
|
+
this.#expected = 0;
|
|
32957
|
+
this.#lost = 0;
|
|
32958
|
+
return;
|
|
32959
|
+
}
|
|
32960
|
+
if (back === 0) {
|
|
32961
|
+
this.#expected += d;
|
|
32962
|
+
this.#lost += d - 1;
|
|
32963
|
+
this.#lastSeq = seq;
|
|
32964
|
+
} else if (this.#lost > 0) {
|
|
32965
|
+
this.#lost -= 1;
|
|
32966
|
+
}
|
|
32967
|
+
if (this.#expected >= this.#windowPackets) this.#closeWindow();
|
|
32968
|
+
}
|
|
32969
|
+
/** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
|
|
32970
|
+
switchSource(next) {
|
|
32971
|
+
if (this.#disposed) return;
|
|
32972
|
+
this.#closeAll();
|
|
32973
|
+
this.#source = next;
|
|
32974
|
+
this.#lastSeq = void 0;
|
|
32975
|
+
this.#expected = 0;
|
|
32976
|
+
this.#lost = 0;
|
|
32977
|
+
this.#wantFewer = 0;
|
|
32978
|
+
this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
|
|
32979
|
+
this.#reconcile();
|
|
32980
|
+
}
|
|
32981
|
+
snapshot() {
|
|
32982
|
+
return {
|
|
32983
|
+
delivery: this.#delivery(),
|
|
32984
|
+
layerCount: this.#layerCount,
|
|
32985
|
+
subscribedTracks: [...this.#subs.keys()],
|
|
32986
|
+
repairPackets: this.#repairPackets,
|
|
32987
|
+
repairBytes: this.#repairBytes,
|
|
32988
|
+
staleDropped: this.#staleDropped,
|
|
32989
|
+
lossRate: this.#lossRate
|
|
32990
|
+
};
|
|
32991
|
+
}
|
|
32992
|
+
dispose() {
|
|
32993
|
+
if (this.#disposed) return;
|
|
32994
|
+
this.#closeAll();
|
|
32995
|
+
this.#disposed = true;
|
|
32996
|
+
}
|
|
32997
|
+
#delivery() {
|
|
32998
|
+
return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32999
|
+
}
|
|
33000
|
+
#layers() {
|
|
33001
|
+
return this.#source.repairLayers ?? [];
|
|
33002
|
+
}
|
|
33003
|
+
#closeWindow() {
|
|
33004
|
+
const rate = this.#lost / this.#expected;
|
|
33005
|
+
this.#lossRate = rate;
|
|
33006
|
+
this.#expected = 0;
|
|
33007
|
+
this.#lost = 0;
|
|
33008
|
+
const target = this.#targetLayers(rate);
|
|
33009
|
+
if (target > this.#layerCount) {
|
|
33010
|
+
this.#wantFewer = 0;
|
|
33011
|
+
this.#layerCount = target;
|
|
33012
|
+
} else if (target < this.#layerCount) {
|
|
33013
|
+
this.#wantFewer += 1;
|
|
33014
|
+
if (this.#wantFewer >= this.#dropAfterWindows) {
|
|
33015
|
+
this.#wantFewer = 0;
|
|
33016
|
+
this.#layerCount -= 1;
|
|
33017
|
+
}
|
|
33018
|
+
} else {
|
|
33019
|
+
this.#wantFewer = 0;
|
|
33020
|
+
}
|
|
33021
|
+
this.#reconcile();
|
|
33022
|
+
}
|
|
33023
|
+
/** Fewest base layers whose summed P covers margin x the measured loss of K. */
|
|
33024
|
+
#targetLayers(rate) {
|
|
33025
|
+
if (rate <= 0) return 0;
|
|
33026
|
+
const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
|
|
33027
|
+
const layers = this.#layers();
|
|
33028
|
+
let held = 0;
|
|
33029
|
+
for (let n = 0; n < layers.length; n++) {
|
|
33030
|
+
held += layers[n].repairSymbols;
|
|
33031
|
+
if (held >= needed) return n + 1;
|
|
33032
|
+
}
|
|
33033
|
+
return layers.length;
|
|
33034
|
+
}
|
|
33035
|
+
/** Stream delivery never leaves layer count 0, so it never subscribes. */
|
|
33036
|
+
#reconcile() {
|
|
33037
|
+
const want = /* @__PURE__ */ new Map();
|
|
33038
|
+
for (const layer of this.#layers().slice(0, this.#layerCount)) {
|
|
33039
|
+
want.set(layer.repairTrackName, layer.priority);
|
|
33040
|
+
}
|
|
33041
|
+
const overlay = this.#source.repairOverlays?.[0];
|
|
33042
|
+
if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
|
|
33043
|
+
for (const [name, entry] of this.#subs) {
|
|
33044
|
+
if (!want.has(name)) {
|
|
33045
|
+
entry.sub.close();
|
|
33046
|
+
this.#subs.delete(name);
|
|
33047
|
+
}
|
|
33048
|
+
}
|
|
33049
|
+
for (const [name, priority] of want) {
|
|
33050
|
+
if (this.#subs.has(name)) continue;
|
|
33051
|
+
const entry = {};
|
|
33052
|
+
entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
|
|
33053
|
+
this.#subs.set(name, entry);
|
|
33054
|
+
}
|
|
33055
|
+
}
|
|
33056
|
+
/** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
|
|
33057
|
+
#onPacket(name, entry, packet) {
|
|
33058
|
+
if (this.#disposed || this.#subs.get(name) !== entry) {
|
|
33059
|
+
this.#staleDropped += 1;
|
|
33060
|
+
return;
|
|
33061
|
+
}
|
|
33062
|
+
this.#repairPackets += 1;
|
|
33063
|
+
this.#repairBytes += packet.byteLength;
|
|
33064
|
+
this.#opts.feed(packet, name);
|
|
33065
|
+
}
|
|
33066
|
+
#closeAll() {
|
|
33067
|
+
for (const entry of this.#subs.values()) entry.sub.close();
|
|
33068
|
+
this.#subs.clear();
|
|
33069
|
+
}
|
|
33070
|
+
};
|
|
33071
|
+
|
|
31641
33072
|
// src/repair-fetch.ts
|
|
31642
33073
|
var MOQT_PRIORITY_MAX = 255;
|
|
33074
|
+
var WALK_LEAD_MS = 3;
|
|
31643
33075
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
31644
33076
|
function isMoqtPriority(value) {
|
|
31645
33077
|
return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
|
|
@@ -31690,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
|
|
|
31690
33122
|
mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
31691
33123
|
};
|
|
31692
33124
|
const clearEntry = (entry) => {
|
|
31693
|
-
|
|
31694
|
-
|
|
33125
|
+
const { range } = entry;
|
|
33126
|
+
if (range) {
|
|
33127
|
+
atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
|
|
33128
|
+
} else {
|
|
33129
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
|
|
33130
|
+
}
|
|
33131
|
+
};
|
|
33132
|
+
const atReport = (range, work) => {
|
|
33133
|
+
const fec = mc.fecManager;
|
|
33134
|
+
if (range.groups !== null || fec.withReceiptTime === void 0) work();
|
|
33135
|
+
else fec.withReceiptTime(range.reportedAt, work);
|
|
31695
33136
|
};
|
|
31696
33137
|
const releaseHold = (entry) => {
|
|
31697
33138
|
if (entry.range) return;
|
|
@@ -31758,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
|
|
|
31758
33199
|
const admit = (entry, now) => {
|
|
31759
33200
|
const budgetMs = entry.deadlineAt - now;
|
|
31760
33201
|
const rttMs = readRtt();
|
|
31761
|
-
const reason =
|
|
33202
|
+
const reason = refusal(entry, now, rttMs);
|
|
31762
33203
|
if (reason === null) return rttMs;
|
|
31763
33204
|
skip(entry, reason, rttMs, budgetMs);
|
|
31764
33205
|
return null;
|
|
31765
33206
|
};
|
|
33207
|
+
const refusal = (entry, now, rttMs) => !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < entry.deadlineAt - now ? null : "rtt-exceeds-budget";
|
|
31766
33208
|
const retire = (entry) => {
|
|
31767
33209
|
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
31768
33210
|
if (entry.expiry !== void 0) clearTimeout(entry.expiry);
|
|
31769
33211
|
};
|
|
31770
|
-
const
|
|
33212
|
+
const holdsBack = (entry, priority, videoWalks = true) => {
|
|
33213
|
+
if (entry.priority >= priority) return false;
|
|
33214
|
+
const { walk } = entry;
|
|
33215
|
+
if (walk === void 0) return true;
|
|
33216
|
+
if (entry.priority !== 0 || !videoWalks) return false;
|
|
33217
|
+
return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
|
|
33218
|
+
};
|
|
33219
|
+
const outstandingBelow = (priority, videoWalks = true) => {
|
|
31771
33220
|
for (const entry of entries.values()) {
|
|
31772
|
-
if (entry
|
|
33221
|
+
if (holdsBack(entry, priority, videoWalks)) return true;
|
|
31773
33222
|
}
|
|
31774
33223
|
return false;
|
|
31775
33224
|
};
|
|
33225
|
+
const yieldVideoWalks = (queued) => {
|
|
33226
|
+
const now = performance.now();
|
|
33227
|
+
const rttMs = readRtt();
|
|
33228
|
+
const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
|
|
33229
|
+
if (!due) return;
|
|
33230
|
+
for (const entry of entries.values()) {
|
|
33231
|
+
const walk = entry.walk;
|
|
33232
|
+
if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
|
|
33233
|
+
if (walk.group.controller.signal.aborted) continue;
|
|
33234
|
+
walk.yielded = true;
|
|
33235
|
+
walk.group.controller.abort();
|
|
33236
|
+
}
|
|
33237
|
+
};
|
|
31776
33238
|
const preemptBestEffort = () => {
|
|
31777
33239
|
for (const entry of entries.values()) {
|
|
31778
33240
|
if (entry.priority === 0 || !entry.running) continue;
|
|
@@ -31801,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
|
|
|
31801
33263
|
}
|
|
31802
33264
|
const rttMs = admit(entry, now);
|
|
31803
33265
|
if (rttMs === null) return;
|
|
31804
|
-
if (entry.priority === 0) preemptBestEffort();
|
|
33266
|
+
if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
|
|
33267
|
+
if (entry.range === void 0) leaveWalkedGroup(entry);
|
|
31805
33268
|
if (counted) entry.attempts++;
|
|
31806
33269
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
31807
33270
|
rttMs,
|
|
@@ -31823,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
|
|
|
31823
33286
|
}
|
|
31824
33287
|
void execute(entry, run);
|
|
31825
33288
|
};
|
|
33289
|
+
const leaveWalkedGroup = (block) => {
|
|
33290
|
+
for (const entry of entries.values()) {
|
|
33291
|
+
const group = entry.walk?.group;
|
|
33292
|
+
if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
|
|
33293
|
+
group.controller.abort();
|
|
33294
|
+
}
|
|
33295
|
+
}
|
|
33296
|
+
};
|
|
31826
33297
|
const dispatch = () => {
|
|
31827
33298
|
if (disposed) return;
|
|
31828
33299
|
const now = performance.now();
|
|
@@ -31847,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
31847
33318
|
for (const entry of queued) {
|
|
31848
33319
|
if (entry.priority === 0) start(entry);
|
|
31849
33320
|
}
|
|
33321
|
+
yieldVideoWalks(queued);
|
|
31850
33322
|
if (outstandingBelow(1)) return;
|
|
31851
33323
|
for (const entry of queued) {
|
|
31852
33324
|
if (entry.priority === 1) start(entry);
|
|
@@ -31873,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
31873
33345
|
}
|
|
31874
33346
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength;
|
|
31875
33347
|
};
|
|
33348
|
+
const groupServed = (sbn, packetId, catalogTrackName) => {
|
|
33349
|
+
if (mc.fecManager.blockSymbolCounts === void 0) return false;
|
|
33350
|
+
const counts = symbolCounts(sbn, packetId, catalogTrackName);
|
|
33351
|
+
if (counts === void 0 || counts.authoredLength === void 0) return true;
|
|
33352
|
+
return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
|
|
33353
|
+
};
|
|
33354
|
+
const statedEnd = (sbn, packetId, catalogTrackName) => {
|
|
33355
|
+
const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
|
|
33356
|
+
return length === void 0 ? void 0 : serial32Add(sbn, length);
|
|
33357
|
+
};
|
|
31876
33358
|
const finish = (entry, run, fed, error) => {
|
|
31877
33359
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
31878
33360
|
return;
|
|
@@ -31917,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
|
|
|
31917
33399
|
signal: run.controller.signal,
|
|
31918
33400
|
subscriberPriority: entry.subscriberPriority
|
|
31919
33401
|
};
|
|
33402
|
+
const { range } = entry;
|
|
33403
|
+
const feed = (packet) => {
|
|
33404
|
+
if (entry.catalogTrackName === void 0) {
|
|
33405
|
+
mc.processRepairFromMoQ(packet, entry.packetId);
|
|
33406
|
+
} else {
|
|
33407
|
+
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
33408
|
+
}
|
|
33409
|
+
};
|
|
31920
33410
|
try {
|
|
31921
|
-
const packets = entry.
|
|
33411
|
+
const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
|
|
31922
33412
|
...common,
|
|
31923
|
-
startGroup:
|
|
31924
|
-
endGroup:
|
|
31925
|
-
lostSsId:
|
|
31926
|
-
})
|
|
33413
|
+
startGroup: range.groups.startGroup,
|
|
33414
|
+
endGroup: range.groups.endGroup,
|
|
33415
|
+
lostSsId: range.lostSsId
|
|
33416
|
+
});
|
|
31927
33417
|
for await (const packet of packets) {
|
|
31928
33418
|
if (run.stopped !== null || entry.running !== run) break;
|
|
31929
33419
|
fed++;
|
|
31930
|
-
if (
|
|
31931
|
-
|
|
31932
|
-
} else {
|
|
31933
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
|
|
31934
|
-
}
|
|
33420
|
+
if (range === void 0) feed(packet);
|
|
33421
|
+
else atReport(range, () => feed(packet));
|
|
31935
33422
|
}
|
|
31936
33423
|
finish(entry, run, fed, void 0);
|
|
31937
33424
|
} catch (error) {
|
|
31938
33425
|
finish(entry, run, fed, error ?? new Error("repair transport rejected"));
|
|
31939
33426
|
}
|
|
31940
33427
|
};
|
|
33428
|
+
async function* walkHole(entry, walk, hole, common) {
|
|
33429
|
+
const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
|
|
33430
|
+
for (let first = true; !common.signal.aborted; first = false) {
|
|
33431
|
+
if (!first && entry.priority !== 0 && outstandingBelow(1)) {
|
|
33432
|
+
stop(entry, "preempted");
|
|
33433
|
+
note("repair-preempted", entry);
|
|
33434
|
+
return;
|
|
33435
|
+
}
|
|
33436
|
+
if (!first && entry.priority === 0) {
|
|
33437
|
+
dispatch();
|
|
33438
|
+
if (common.signal.aborted) return;
|
|
33439
|
+
}
|
|
33440
|
+
const sbn = walk.next;
|
|
33441
|
+
let next;
|
|
33442
|
+
if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
|
|
33443
|
+
const rsIds = /* @__PURE__ */ new Set();
|
|
33444
|
+
const group = new AbortController();
|
|
33445
|
+
const cancel = () => group.abort();
|
|
33446
|
+
common.signal.addEventListener("abort", cancel, { once: true });
|
|
33447
|
+
walk.group = { sbn, controller: group };
|
|
33448
|
+
try {
|
|
33449
|
+
for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
|
|
33450
|
+
yield packet;
|
|
33451
|
+
const id = extractRepairFecPayloadId(packet);
|
|
33452
|
+
if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
|
|
33453
|
+
next ??= serial32Add(sbn, id.ssbLength);
|
|
33454
|
+
rsIds.add(id.rsId);
|
|
33455
|
+
if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
|
|
33456
|
+
group.abort();
|
|
33457
|
+
break;
|
|
33458
|
+
}
|
|
33459
|
+
}
|
|
33460
|
+
} catch (error) {
|
|
33461
|
+
if (!group.signal.aborted || common.signal.aborted) throw error;
|
|
33462
|
+
} finally {
|
|
33463
|
+
common.signal.removeEventListener("abort", cancel);
|
|
33464
|
+
walk.group = null;
|
|
33465
|
+
}
|
|
33466
|
+
if (walk.yielded) {
|
|
33467
|
+
walk.yielded = false;
|
|
33468
|
+
continue;
|
|
33469
|
+
}
|
|
33470
|
+
}
|
|
33471
|
+
next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
|
|
33472
|
+
if (next === void 0) return;
|
|
33473
|
+
if (serial32Distance(entry.sbn, next) > lostAt) return;
|
|
33474
|
+
walk.next = next;
|
|
33475
|
+
}
|
|
33476
|
+
}
|
|
31941
33477
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
31942
33478
|
for (const entry of [...entries.values()]) {
|
|
31943
33479
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
|
|
@@ -32133,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
|
|
|
32133
33669
|
throw error;
|
|
32134
33670
|
}
|
|
32135
33671
|
};
|
|
32136
|
-
|
|
33672
|
+
const trackKind = (packetId) => {
|
|
33673
|
+
try {
|
|
33674
|
+
return mc.fecManager.trackKindFor?.(packetId);
|
|
33675
|
+
} catch {
|
|
33676
|
+
return void 0;
|
|
33677
|
+
}
|
|
33678
|
+
};
|
|
33679
|
+
const walkDeadlineAt = (packetId, now, deadlineAt) => {
|
|
33680
|
+
let repairDeadlineMs;
|
|
33681
|
+
try {
|
|
33682
|
+
repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
|
|
33683
|
+
} catch {
|
|
33684
|
+
repairDeadlineMs = void 0;
|
|
33685
|
+
}
|
|
33686
|
+
if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
|
|
33687
|
+
return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
|
|
33688
|
+
};
|
|
33689
|
+
const onHoleExpired = (hole) => {
|
|
33690
|
+
if (disposed) return false;
|
|
33691
|
+
const entry = [...entries.values()].find((candidate) => candidate.walk !== void 0 && candidate.packetId === hole.packetId && candidate.catalogTrackName === hole.catalogTrackName && serial32Distance(candidate.sbn, hole.holeStart) < candidate.range.holeLength);
|
|
33692
|
+
if (entry === void 0) return false;
|
|
33693
|
+
queueMicrotask(() => {
|
|
33694
|
+
if (entries.get(entry.key) !== entry) return;
|
|
33695
|
+
retire(entry);
|
|
33696
|
+
stop(entry, "expired");
|
|
33697
|
+
clearEntry(entry);
|
|
33698
|
+
dispatch();
|
|
33699
|
+
});
|
|
33700
|
+
return true;
|
|
33701
|
+
};
|
|
33702
|
+
if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
|
|
32137
33703
|
mc.onUndelimitedLoss((loss) => {
|
|
32138
33704
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss;
|
|
32139
33705
|
const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
|
|
@@ -32141,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
|
|
|
32141
33707
|
const blockProfile = blockProfileOf(packetId);
|
|
32142
33708
|
const target = opts.resolveTarget(packetId, catalogTrackName);
|
|
32143
33709
|
if (!target) return close();
|
|
32144
|
-
const
|
|
32145
|
-
const
|
|
32146
|
-
|
|
33710
|
+
const walked = opts.rangeTransport === void 0;
|
|
33711
|
+
const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
|
|
33712
|
+
const priority = repairPriorityForClass(holeClass);
|
|
33713
|
+
const groups = walked ? null : rangeFor(loss, target.repairTrack);
|
|
33714
|
+
if (!walked && groups === null) {
|
|
32147
33715
|
close();
|
|
32148
33716
|
mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
|
|
32149
33717
|
return;
|
|
@@ -32154,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
|
|
|
32154
33722
|
packetId,
|
|
32155
33723
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
32156
33724
|
};
|
|
32157
|
-
const wire = wirePriority(identity,
|
|
32158
|
-
|
|
33725
|
+
const wire = wirePriority(identity, holeClass);
|
|
33726
|
+
if (wire === null) return close();
|
|
33727
|
+
const now = performance.now();
|
|
33728
|
+
const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
|
|
32159
33729
|
if (deadline === null) {
|
|
32160
33730
|
close();
|
|
32161
33731
|
mc.noteRepairSkipped(
|
|
@@ -32167,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32167
33737
|
);
|
|
32168
33738
|
return;
|
|
32169
33739
|
}
|
|
33740
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
|
|
32170
33741
|
const entry = {
|
|
32171
33742
|
key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
|
|
32172
33743
|
...identity,
|
|
@@ -32174,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
|
|
|
32174
33745
|
blockProfile,
|
|
32175
33746
|
priority,
|
|
32176
33747
|
subscriberPriority: wire.subscriberPriority,
|
|
32177
|
-
deadlineAt
|
|
32178
|
-
playoutDeadlineAt:
|
|
33748
|
+
deadlineAt,
|
|
33749
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
32179
33750
|
anchorAt: deadline.anchorAt,
|
|
32180
33751
|
delayMs: deadline.delayMs,
|
|
32181
33752
|
bound: deadline.bound,
|
|
32182
33753
|
order: enqueueOrder++,
|
|
32183
33754
|
attempts: 0,
|
|
32184
33755
|
running: null,
|
|
32185
|
-
range: {
|
|
33756
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
33757
|
+
...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
|
|
32186
33758
|
};
|
|
32187
33759
|
const previous = entries.get(entry.key);
|
|
32188
33760
|
if (previous) {
|
|
@@ -32192,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
|
|
|
32192
33764
|
entries.set(entry.key, entry);
|
|
32193
33765
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
|
|
32194
33766
|
dispatch();
|
|
32195
|
-
});
|
|
33767
|
+
}, onHoleExpired);
|
|
32196
33768
|
}
|
|
32197
33769
|
const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
|
|
32198
33770
|
queueMicrotask(() => {
|
|
@@ -32283,6 +33855,7 @@ export {
|
|
|
32283
33855
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
32284
33856
|
REPAIR_PRIORITY_MAX,
|
|
32285
33857
|
REPAIR_PRIORITY_MIN,
|
|
33858
|
+
RepairLayerSubscriptions,
|
|
32286
33859
|
SOURCE_MEDIA_PRIORITY_MAX,
|
|
32287
33860
|
SampleCombineError,
|
|
32288
33861
|
SampleCombiner,
|
|
@@ -32307,6 +33880,7 @@ export {
|
|
|
32307
33880
|
catalogGroupDurationMs,
|
|
32308
33881
|
catalogKeyframeCadenceMs,
|
|
32309
33882
|
catalogRepairTarget,
|
|
33883
|
+
catalogTrackDeliveryMode,
|
|
32310
33884
|
catalogTrackFecUnavailable,
|
|
32311
33885
|
classifyH264Keyframe,
|
|
32312
33886
|
classifyHevcKeyframe,
|
|
@@ -32327,6 +33901,7 @@ export {
|
|
|
32327
33901
|
detectH264Keyframe,
|
|
32328
33902
|
detectHevcKeyframe,
|
|
32329
33903
|
driftCheck,
|
|
33904
|
+
encodeCmcdRecord,
|
|
32330
33905
|
encodeDataSegment,
|
|
32331
33906
|
encodeFecSourceInfo,
|
|
32332
33907
|
encodeLocTimestamp,
|