@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.a16efba1d85a
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 +630 -191
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +60 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +3 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +26 -4
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.js.map +1 -1
- 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 +35 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +97 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +6 -0
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +150 -44
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.js +176 -48
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +26 -16
- package/dist/multicast-transport.js.map +2 -2
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +1 -1
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +189 -179
- package/src/fec-manager.ts +26 -4
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +92 -7
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +46 -0
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
package/dist/browser-runtime.js
CHANGED
|
@@ -489,6 +489,8 @@ var PACKET_TYPE_SIGNALING = 2;
|
|
|
489
489
|
var PACKET_TYPE_REPAIR = 3;
|
|
490
490
|
var MMTP_SIGNALING_PACKET_ID = 0;
|
|
491
491
|
var MMTP_PACKET_ID_MAX = 65535;
|
|
492
|
+
var FEC_CODE_REED_SOLOMON = 1;
|
|
493
|
+
var FEC_CODE_RAPTORQ = 3;
|
|
492
494
|
function parseMMTPHeader(buffer) {
|
|
493
495
|
if (buffer.length < MMTP_HEADER_SIZE) {
|
|
494
496
|
throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
|
|
@@ -655,30 +657,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
|
|
|
655
657
|
const authLength = view2.getUint16(2, false);
|
|
656
658
|
return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
|
|
657
659
|
}
|
|
658
|
-
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options
|
|
660
|
+
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
|
|
659
661
|
if (declaredEnd > payload.length)
|
|
660
662
|
return null;
|
|
661
663
|
if (options?.skipFecId) {
|
|
662
|
-
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false
|
|
664
|
+
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
|
|
663
665
|
}
|
|
664
|
-
const overlay = options?.overlayDeclared === true && rapFlag;
|
|
665
666
|
if (fecType === 1) {
|
|
666
|
-
const fecStart = payload.length -
|
|
667
|
+
const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
667
668
|
if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
|
|
668
669
|
return null;
|
|
669
670
|
}
|
|
670
|
-
return { hasFecId: true
|
|
671
|
+
return { hasFecId: true };
|
|
671
672
|
}
|
|
672
|
-
if (fecType !== 0
|
|
673
|
+
if (fecType !== 0)
|
|
673
674
|
return null;
|
|
674
675
|
if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
|
|
675
|
-
return { hasFecId: false
|
|
676
|
+
return { hasFecId: false };
|
|
676
677
|
}
|
|
677
678
|
return null;
|
|
678
679
|
}
|
|
679
|
-
function sourceFecTrailerSize(overlay) {
|
|
680
|
-
return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
681
|
-
}
|
|
682
680
|
function extractNALUnit(mmtpPacket) {
|
|
683
681
|
const extraction = extractMFUPayload(mmtpPacket);
|
|
684
682
|
if (!extraction)
|
|
@@ -737,11 +735,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
|
737
735
|
if ("rejection" in layoutResult)
|
|
738
736
|
return layoutResult;
|
|
739
737
|
const { layout } = layoutResult;
|
|
740
|
-
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options
|
|
738
|
+
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
|
|
741
739
|
if (!trailerLayout) {
|
|
742
740
|
if (!options?.skipFecId && header.fecType === 1) {
|
|
743
741
|
const observed = payload.length - layout.dataEnd;
|
|
744
|
-
const expected =
|
|
742
|
+
const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
745
743
|
if (observed !== expected) {
|
|
746
744
|
return {
|
|
747
745
|
rejection: {
|
|
@@ -776,13 +774,10 @@ function extractPlannedMfu(mmtpPacket, result) {
|
|
|
776
774
|
const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
|
|
777
775
|
const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
|
|
778
776
|
const data = payload.subarray(mfuOffset, dataEnd);
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
|
|
784
|
-
const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
|
|
785
|
-
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
|
|
777
|
+
const fecPayloadId = trailerLayout.hasFecId ? {
|
|
778
|
+
ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
|
|
779
|
+
} : null;
|
|
780
|
+
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
|
|
786
781
|
}
|
|
787
782
|
function extractTimestamp(mmtpPacket) {
|
|
788
783
|
const { header } = unwrapMMTP(mmtpPacket);
|
|
@@ -903,6 +898,50 @@ var NTP_SHORT_BITS = 32;
|
|
|
903
898
|
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
904
899
|
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
905
900
|
|
|
901
|
+
// ../mmt-base/dist/signalling.js
|
|
902
|
+
var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
|
|
903
|
+
var SSBG_MODE_1 = 1;
|
|
904
|
+
var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
|
|
905
|
+
var REPAIR_SYMBOL_ID_MAX = 16777215;
|
|
906
|
+
var MIN_REPAIR_SYMBOL_SIZE = 14;
|
|
907
|
+
var REED_SOLOMON_MAX_BLOCK = 255;
|
|
908
|
+
function alFecRaptorQFlowConfig(flow) {
|
|
909
|
+
if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
|
|
910
|
+
return null;
|
|
911
|
+
if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
|
|
912
|
+
return null;
|
|
913
|
+
if (flow.packetIds.length !== 1)
|
|
914
|
+
return null;
|
|
915
|
+
const repair = flow.repairFlows[0];
|
|
916
|
+
const { fecCodeId, maximumK: k, maximumP: p } = repair;
|
|
917
|
+
const t = flow.repairSymbolSize;
|
|
918
|
+
if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
|
|
919
|
+
return null;
|
|
920
|
+
if (fecCodeId === FEC_CODE_RAPTORQ) {
|
|
921
|
+
if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
|
|
922
|
+
return null;
|
|
923
|
+
} else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
|
|
924
|
+
if (k + p > REED_SOLOMON_MAX_BLOCK)
|
|
925
|
+
return null;
|
|
926
|
+
} else {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
|
|
930
|
+
return null;
|
|
931
|
+
return {
|
|
932
|
+
sourcePacketIds: [...flow.packetIds],
|
|
933
|
+
repairFlowId: repair.repairFlowId,
|
|
934
|
+
fecCodeId,
|
|
935
|
+
ssbgMode: flow.ssbgMode,
|
|
936
|
+
fecPayloadIdMode: flow.fecPayloadIdMode,
|
|
937
|
+
repairSymbolSize: t,
|
|
938
|
+
maximumK: k,
|
|
939
|
+
maximumP: p,
|
|
940
|
+
protectionWindowTime: repair.protectionWindowTime,
|
|
941
|
+
protectionWindowSize: repair.protectionWindowSize
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
906
945
|
// ../mmt-base/dist/serial-number.js
|
|
907
946
|
var SERIAL32_SPAN = 4294967296;
|
|
908
947
|
var SERIAL32_HALF = 2147483648;
|
|
@@ -7949,8 +7988,8 @@ var MulticastSubscriptionPool = class {
|
|
|
7949
7988
|
record.claims--;
|
|
7950
7989
|
return this.acquire(gateway, config, consumer);
|
|
7951
7990
|
}
|
|
7952
|
-
const
|
|
7953
|
-
entry.consumers.set(
|
|
7991
|
+
const token2 = Symbol(key);
|
|
7992
|
+
entry.consumers.set(token2, consumer);
|
|
7954
7993
|
let released = false;
|
|
7955
7994
|
return {
|
|
7956
7995
|
key,
|
|
@@ -7958,7 +7997,7 @@ var MulticastSubscriptionPool = class {
|
|
|
7958
7997
|
if (released)
|
|
7959
7998
|
return;
|
|
7960
7999
|
released = true;
|
|
7961
|
-
entry.consumers.delete(
|
|
8000
|
+
entry.consumers.delete(token2);
|
|
7962
8001
|
record.claims--;
|
|
7963
8002
|
if (record.claims > 0)
|
|
7964
8003
|
return;
|
|
@@ -8707,14 +8746,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8707
8746
|
});
|
|
8708
8747
|
|
|
8709
8748
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
8749
|
+
var MAX_PENDING_PACKETS = 256;
|
|
8710
8750
|
var AMTGatewayManager = class {
|
|
8711
8751
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8712
8752
|
messageHandler;
|
|
8753
|
+
messageListenerRegistered = false;
|
|
8713
8754
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
8714
8755
|
constructor() {
|
|
8715
8756
|
this.messageHandler = (event) => {
|
|
8716
8757
|
const data = event.data;
|
|
8717
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8718
8758
|
if (data.type === "UDP_PACKET") {
|
|
8719
8759
|
this.handlePacket(data);
|
|
8720
8760
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8723,12 +8763,22 @@ var AMTGatewayManager = class {
|
|
|
8723
8763
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
8724
8764
|
}
|
|
8725
8765
|
};
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
console.warn("[AMT] Service worker not available");
|
|
8766
|
+
}
|
|
8767
|
+
ensureMessageListener() {
|
|
8768
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
8769
|
+
return;
|
|
8731
8770
|
}
|
|
8771
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
8772
|
+
this.messageListenerRegistered = true;
|
|
8773
|
+
console.log("[AMT] Registered service worker message handler");
|
|
8774
|
+
}
|
|
8775
|
+
removeMessageListener() {
|
|
8776
|
+
if (!this.messageListenerRegistered)
|
|
8777
|
+
return;
|
|
8778
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8779
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8780
|
+
}
|
|
8781
|
+
this.messageListenerRegistered = false;
|
|
8732
8782
|
}
|
|
8733
8783
|
/**
|
|
8734
8784
|
* Subscribe via AMT tunnel
|
|
@@ -8738,6 +8788,7 @@ var AMTGatewayManager = class {
|
|
|
8738
8788
|
if (config.protected) {
|
|
8739
8789
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
8740
8790
|
}
|
|
8791
|
+
this.ensureMessageListener();
|
|
8741
8792
|
const keyStr = this.getKeyString(key);
|
|
8742
8793
|
if (this.subscriptions.has(keyStr)) {
|
|
8743
8794
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -8773,6 +8824,8 @@ var AMTGatewayManager = class {
|
|
|
8773
8824
|
this.subscriptions.delete(keyStr);
|
|
8774
8825
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8775
8826
|
}
|
|
8827
|
+
if (this.subscriptions.size === 0)
|
|
8828
|
+
this.removeMessageListener();
|
|
8776
8829
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8777
8830
|
}
|
|
8778
8831
|
}
|
|
@@ -8794,6 +8847,8 @@ var AMTGatewayManager = class {
|
|
|
8794
8847
|
} finally {
|
|
8795
8848
|
this.subscriptions.delete(handle);
|
|
8796
8849
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8850
|
+
if (this.subscriptions.size === 0)
|
|
8851
|
+
this.removeMessageListener();
|
|
8797
8852
|
}
|
|
8798
8853
|
}
|
|
8799
8854
|
/**
|
|
@@ -8810,14 +8865,12 @@ var AMTGatewayManager = class {
|
|
|
8810
8865
|
if (!subscriptionId)
|
|
8811
8866
|
return;
|
|
8812
8867
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8813
|
-
if (!subscription)
|
|
8814
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8868
|
+
if (!subscription)
|
|
8815
8869
|
return;
|
|
8816
|
-
}
|
|
8817
8870
|
if (!subscription.handshakeComplete) {
|
|
8818
8871
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8819
8872
|
const packetData2 = data;
|
|
8820
|
-
if (queue) {
|
|
8873
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8821
8874
|
queue.push({
|
|
8822
8875
|
data: packetData2.data,
|
|
8823
8876
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8843,19 +8896,15 @@ var AMTGatewayManager = class {
|
|
|
8843
8896
|
if (!subscriptionId)
|
|
8844
8897
|
return;
|
|
8845
8898
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8846
|
-
if (!subscription)
|
|
8847
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8899
|
+
if (!subscription)
|
|
8848
8900
|
return;
|
|
8849
|
-
}
|
|
8850
8901
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8851
8902
|
}
|
|
8852
8903
|
/**
|
|
8853
8904
|
* Cleanup on destroy
|
|
8854
8905
|
*/
|
|
8855
8906
|
destroy() {
|
|
8856
|
-
|
|
8857
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8858
|
-
}
|
|
8907
|
+
this.removeMessageListener();
|
|
8859
8908
|
const handles = Array.from(this.subscriptions.keys());
|
|
8860
8909
|
for (const handle of handles) {
|
|
8861
8910
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -14013,6 +14062,10 @@ var FECManager = class _FECManager {
|
|
|
14013
14062
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
14014
14063
|
// One classifier per physical stream, built from that stream's geometry.
|
|
14015
14064
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
14065
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
14066
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
14067
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
14068
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
14016
14069
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
14017
14070
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
14018
14071
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -14212,6 +14265,7 @@ var FECManager = class _FECManager {
|
|
|
14212
14265
|
#orderedFrontiers;
|
|
14213
14266
|
#orderedPendingBlocks;
|
|
14214
14267
|
#classifiers;
|
|
14268
|
+
#noSpanRaps;
|
|
14215
14269
|
#streamGeometries;
|
|
14216
14270
|
#failedBlocks;
|
|
14217
14271
|
#resolvedBlockClasses;
|
|
@@ -14873,6 +14927,7 @@ var FECManager = class _FECManager {
|
|
|
14873
14927
|
};
|
|
14874
14928
|
const discontinuity = () => {
|
|
14875
14929
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
14930
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14876
14931
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14877
14932
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14878
14933
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -15285,6 +15340,9 @@ var FECManager = class _FECManager {
|
|
|
15285
15340
|
}
|
|
15286
15341
|
if (policyChanged) {
|
|
15287
15342
|
this.#bumpEpoch(packetId);
|
|
15343
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15344
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15345
|
+
}
|
|
15288
15346
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15289
15347
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15290
15348
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15520,6 +15578,7 @@ var FECManager = class _FECManager {
|
|
|
15520
15578
|
resetMap(this.#isoLocators);
|
|
15521
15579
|
resetMap(this.#sourceAnchors);
|
|
15522
15580
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15581
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15523
15582
|
this.#bumpEpoch(packetId);
|
|
15524
15583
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15525
15584
|
for (const key of keys.keys()) {
|
|
@@ -15626,6 +15685,7 @@ var FECManager = class _FECManager {
|
|
|
15626
15685
|
dropMap(this.#isoLocators);
|
|
15627
15686
|
dropMap(this.#sourceAnchors);
|
|
15628
15687
|
dropMap(this.#classifiers);
|
|
15688
|
+
dropMap(this.#noSpanRaps);
|
|
15629
15689
|
dropMap(this.#streamGeometries);
|
|
15630
15690
|
dropSet(this.#pendingWorkerCleanups);
|
|
15631
15691
|
dropSet(this.#completedBlocks);
|
|
@@ -16256,11 +16316,14 @@ var FECManager = class _FECManager {
|
|
|
16256
16316
|
/**
|
|
16257
16317
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16258
16318
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16259
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
16319
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
16320
|
+
* and neither has a stream without catalog timing until it releases two
|
|
16321
|
+
* RAPs inside one MPU.
|
|
16260
16322
|
*/
|
|
16261
16323
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16262
|
-
|
|
16263
|
-
return this.#
|
|
16324
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16325
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
16326
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
16264
16327
|
}
|
|
16265
16328
|
/** A block's class now, or frozen at its resolution. */
|
|
16266
16329
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16286,7 +16349,14 @@ var FECManager = class _FECManager {
|
|
|
16286
16349
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16287
16350
|
*/
|
|
16288
16351
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16289
|
-
if (!this.#trackConfigs.has(packetId)
|
|
16352
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
16353
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
16354
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16355
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
16356
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
16357
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
16358
|
+
return;
|
|
16359
|
+
}
|
|
16290
16360
|
this.#applyClassifierChanges(
|
|
16291
16361
|
packetId,
|
|
16292
16362
|
catalogTrackName,
|
|
@@ -17301,6 +17371,7 @@ var FECManager = class _FECManager {
|
|
|
17301
17371
|
this.#blockAnchorsMs.clear();
|
|
17302
17372
|
this.#blockCatalogTrackNames.clear();
|
|
17303
17373
|
this.#classifiers.clear();
|
|
17374
|
+
this.#noSpanRaps.clear();
|
|
17304
17375
|
this.#streamGeometries.clear();
|
|
17305
17376
|
this.#failedBlocks.clear();
|
|
17306
17377
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17831,9 +17902,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17831
17902
|
}
|
|
17832
17903
|
#track = null;
|
|
17833
17904
|
#fragments = /* @__PURE__ */ new Map();
|
|
17905
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
17906
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
17907
|
+
/**
|
|
17908
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
17909
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
17910
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
17911
|
+
*/
|
|
17912
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17834
17913
|
#pendingSamples = 0;
|
|
17835
17914
|
#evictedSamples = 0;
|
|
17836
17915
|
#lastMovieFragmentRejection = null;
|
|
17916
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
17917
|
+
get initialized() {
|
|
17918
|
+
return this.#track !== null;
|
|
17919
|
+
}
|
|
17837
17920
|
get pendingSampleCount() {
|
|
17838
17921
|
return this.#pendingSamples;
|
|
17839
17922
|
}
|
|
@@ -17857,7 +17940,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17857
17940
|
this.#track = timing;
|
|
17858
17941
|
return true;
|
|
17859
17942
|
}
|
|
17860
|
-
|
|
17943
|
+
/**
|
|
17944
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
17945
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
17946
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
17947
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
17948
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
17949
|
+
*/
|
|
17950
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17861
17951
|
this.#lastMovieFragmentRejection = null;
|
|
17862
17952
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17863
17953
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17883,6 +17973,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17883
17973
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17884
17974
|
);
|
|
17885
17975
|
}
|
|
17976
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
17977
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17886
17978
|
let freed = 0;
|
|
17887
17979
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17888
17980
|
freed += this.#evictOldestFragment();
|
|
@@ -17896,6 +17988,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17896
17988
|
return false;
|
|
17897
17989
|
}
|
|
17898
17990
|
this.#fragments.set(key, [...parsed.samples]);
|
|
17991
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17899
17992
|
this.#pendingSamples += parsed.samples.length;
|
|
17900
17993
|
return true;
|
|
17901
17994
|
}
|
|
@@ -17953,6 +18046,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17953
18046
|
let live = 0;
|
|
17954
18047
|
for (const sample of samples) if (sample !== null) live++;
|
|
17955
18048
|
this.#fragments.delete(oldest);
|
|
18049
|
+
this.#firstSamples.delete(oldest);
|
|
17956
18050
|
this.#pendingSamples -= live;
|
|
17957
18051
|
this.#evictedSamples += live;
|
|
17958
18052
|
return live;
|
|
@@ -17968,12 +18062,23 @@ var IsoBmffTimingResolver = class {
|
|
|
17968
18062
|
if (!samples || !samples[index]) return false;
|
|
17969
18063
|
samples[index] = null;
|
|
17970
18064
|
this.#pendingSamples--;
|
|
17971
|
-
if (samples.every((value) => value === null))
|
|
18065
|
+
if (samples.every((value) => value === null)) {
|
|
18066
|
+
this.#fragments.delete(key);
|
|
18067
|
+
const first = this.#firstSamples.get(key);
|
|
18068
|
+
this.#firstSamples.delete(key);
|
|
18069
|
+
this.#delivered.delete(key);
|
|
18070
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18071
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18072
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18073
|
+
}
|
|
18074
|
+
}
|
|
17972
18075
|
return true;
|
|
17973
18076
|
}
|
|
17974
18077
|
reset() {
|
|
17975
18078
|
this.#track = null;
|
|
17976
18079
|
this.#fragments.clear();
|
|
18080
|
+
this.#firstSamples.clear();
|
|
18081
|
+
this.#delivered.clear();
|
|
17977
18082
|
this.#pendingSamples = 0;
|
|
17978
18083
|
}
|
|
17979
18084
|
};
|
|
@@ -18455,6 +18560,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18455
18560
|
this.#reclaimedTimingSamples = 0;
|
|
18456
18561
|
this.#evictedTimingSamples = 0;
|
|
18457
18562
|
this.#rejectedMovieFragments = 0;
|
|
18563
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18564
|
+
held: 0,
|
|
18565
|
+
expired: 0,
|
|
18566
|
+
discarded: 0,
|
|
18567
|
+
superseded: 0
|
|
18568
|
+
};
|
|
18458
18569
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18459
18570
|
this.#fecGate = null;
|
|
18460
18571
|
this.#fecSequencer = null;
|
|
@@ -18563,6 +18674,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18563
18674
|
#reclaimedTimingSamples;
|
|
18564
18675
|
#evictedTimingSamples;
|
|
18565
18676
|
#rejectedMovieFragments;
|
|
18677
|
+
#recoveredHeadsAwaitingTiming;
|
|
18566
18678
|
#sequencers;
|
|
18567
18679
|
#fecGate;
|
|
18568
18680
|
#fecSequencer;
|
|
@@ -18683,6 +18795,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18683
18795
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18684
18796
|
};
|
|
18685
18797
|
}
|
|
18798
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
18799
|
+
get recoveredHeadsAwaitingTiming() {
|
|
18800
|
+
let pending = 0;
|
|
18801
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18802
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
18803
|
+
}
|
|
18804
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
18805
|
+
}
|
|
18686
18806
|
get reorderStats() {
|
|
18687
18807
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18688
18808
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18811,10 +18931,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18811
18931
|
get reassemblerDataUnitsReturned() {
|
|
18812
18932
|
return this.#reassemblerDataUnitsReturned;
|
|
18813
18933
|
}
|
|
18814
|
-
#countOutcome(
|
|
18815
|
-
const count = (this.#feedOutcomes.get(
|
|
18816
|
-
this.#feedOutcomes.set(
|
|
18817
|
-
this.#lastOutcome =
|
|
18934
|
+
#countOutcome(token2) {
|
|
18935
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
18936
|
+
this.#feedOutcomes.set(token2, count);
|
|
18937
|
+
this.#lastOutcome = token2;
|
|
18818
18938
|
this.#lastOutcomeCount = count;
|
|
18819
18939
|
}
|
|
18820
18940
|
#countDeferredOutcome(reason) {
|
|
@@ -18856,7 +18976,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18856
18976
|
const now = Date.now();
|
|
18857
18977
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18858
18978
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18859
|
-
awaitingTiming.shift();
|
|
18979
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18860
18980
|
}
|
|
18861
18981
|
}
|
|
18862
18982
|
}
|
|
@@ -18974,11 +19094,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18974
19094
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
18975
19095
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
18976
19096
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
18977
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19097
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
18978
19098
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
18979
19099
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
18980
19100
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
|
|
18981
|
-
const admitted = resolver2.addMovieFragment(
|
|
19101
|
+
const admitted = resolver2.addMovieFragment(
|
|
19102
|
+
mpu.mpuSequenceNumber,
|
|
19103
|
+
data,
|
|
19104
|
+
remaining,
|
|
19105
|
+
options?.recovered === true
|
|
19106
|
+
);
|
|
18982
19107
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
18983
19108
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
18984
19109
|
if (!admitted) {
|
|
@@ -18998,7 +19123,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18998
19123
|
};
|
|
18999
19124
|
}
|
|
19000
19125
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
19001
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19126
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
19002
19127
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
19003
19128
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
19004
19129
|
return this.#drop("mfu-empty-payload");
|
|
@@ -19019,6 +19144,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19019
19144
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19020
19145
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19021
19146
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19147
|
+
sequenceNumber: info.sequenceNumber,
|
|
19022
19148
|
arrivedAt: Date.now()
|
|
19023
19149
|
});
|
|
19024
19150
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19342,6 +19468,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19342
19468
|
}
|
|
19343
19469
|
this.#emitFrame(data, meta);
|
|
19344
19470
|
}
|
|
19471
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19472
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19473
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19474
|
+
}
|
|
19345
19475
|
/**
|
|
19346
19476
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19347
19477
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19403,6 +19533,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19403
19533
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19404
19534
|
this.#sequencers.clear();
|
|
19405
19535
|
this.#fecSequencer?.discard();
|
|
19536
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19537
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19538
|
+
}
|
|
19406
19539
|
this.#timingStreams.clear();
|
|
19407
19540
|
this.#pendingTimingSamples = 0;
|
|
19408
19541
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19421,10 +19554,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19421
19554
|
return resolver;
|
|
19422
19555
|
}
|
|
19423
19556
|
#awaitTiming(streamKey, head) {
|
|
19424
|
-
const
|
|
19425
|
-
if (!
|
|
19557
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19558
|
+
if (!state) return;
|
|
19559
|
+
if (!head.options?.recovered) {
|
|
19560
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
19561
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
19562
|
+
state.awaitingTiming = kept;
|
|
19563
|
+
}
|
|
19564
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19426
19565
|
awaitingTiming.push(head);
|
|
19427
|
-
if (
|
|
19566
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
19567
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
19568
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
19569
|
+
}
|
|
19570
|
+
}
|
|
19571
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
19572
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
19573
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19428
19574
|
}
|
|
19429
19575
|
/**
|
|
19430
19576
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19452,6 +19598,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19452
19598
|
const state = this.#timingStreams.get(streamKey);
|
|
19453
19599
|
if (!state) return;
|
|
19454
19600
|
this.#timingStreams.delete(streamKey);
|
|
19601
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19455
19602
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19456
19603
|
if (this.#pendingTimingSamples < 0) {
|
|
19457
19604
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -23457,6 +23604,10 @@ var MmtpRouter = class {
|
|
|
23457
23604
|
get keyframeOverlayStats() {
|
|
23458
23605
|
return { ...this.#overlayStats };
|
|
23459
23606
|
}
|
|
23607
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
23608
|
+
get recoveredHeadsAwaitingTiming() {
|
|
23609
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
23610
|
+
}
|
|
23460
23611
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23461
23612
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23462
23613
|
}
|
|
@@ -25866,6 +26017,29 @@ var MmtpRouter = class {
|
|
|
25866
26017
|
}
|
|
25867
26018
|
);
|
|
25868
26019
|
}
|
|
26020
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26021
|
+
ownerPacketId,
|
|
26022
|
+
sym.fragment.mpuSequenceNumber,
|
|
26023
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26024
|
+
sym.fragment.sampleNumber,
|
|
26025
|
+
catalogTrackName
|
|
26026
|
+
);
|
|
26027
|
+
for (const sym of result.symbols) {
|
|
26028
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26029
|
+
this.#feedViaParser(
|
|
26030
|
+
sym.rawSymbol,
|
|
26031
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26032
|
+
void 0,
|
|
26033
|
+
{
|
|
26034
|
+
skipFecPayloadId: true,
|
|
26035
|
+
recovered: true,
|
|
26036
|
+
deliveryProvenance,
|
|
26037
|
+
...fecSbnOf(sym.index),
|
|
26038
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26039
|
+
}
|
|
26040
|
+
);
|
|
26041
|
+
}
|
|
26042
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25869
26043
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25870
26044
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25871
26045
|
if (assembler) {
|
|
@@ -29627,7 +29801,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29627
29801
|
blockProfile,
|
|
29628
29802
|
k: opts.fecK,
|
|
29629
29803
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29630
|
-
// and CMCD `
|
|
29804
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29631
29805
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29632
29806
|
symbolSize: opts.fecSymbolSize,
|
|
29633
29807
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -30169,24 +30343,24 @@ var MediaRouter = class {
|
|
|
30169
30343
|
if (!this.#multicastVideoKeyed) return;
|
|
30170
30344
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
30171
30345
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
30172
|
-
const
|
|
30346
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
30173
30347
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
30174
30348
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
30175
|
-
this.#commitMulticastActivation(
|
|
30349
|
+
this.#commitMulticastActivation(token2);
|
|
30176
30350
|
return;
|
|
30177
30351
|
}
|
|
30178
30352
|
const ready = Promise.resolve(maybeReady);
|
|
30179
|
-
this.#pendingMulticastToken =
|
|
30353
|
+
this.#pendingMulticastToken = token2;
|
|
30180
30354
|
this.#sourceSwitchReady = ready;
|
|
30181
30355
|
void ready.then(
|
|
30182
|
-
() => this.#commitMulticastActivation(
|
|
30183
|
-
(err) => this.#rejectMulticastActivation(
|
|
30356
|
+
() => this.#commitMulticastActivation(token2),
|
|
30357
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
30184
30358
|
).finally(() => {
|
|
30185
30359
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
30186
30360
|
});
|
|
30187
30361
|
}
|
|
30188
|
-
#commitMulticastActivation(
|
|
30189
|
-
if (
|
|
30362
|
+
#commitMulticastActivation(token2) {
|
|
30363
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30190
30364
|
this.#active = "multicast";
|
|
30191
30365
|
this.#suppressed = true;
|
|
30192
30366
|
this.#pendingMulticastToken = null;
|
|
@@ -30197,9 +30371,9 @@ var MediaRouter = class {
|
|
|
30197
30371
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
30198
30372
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
30199
30373
|
}
|
|
30200
|
-
#rejectMulticastActivation(
|
|
30201
|
-
if (
|
|
30202
|
-
if (this.#pendingMulticastToken ===
|
|
30374
|
+
#rejectMulticastActivation(token2, err) {
|
|
30375
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30376
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
30203
30377
|
this.#pendingMulticastVideo = null;
|
|
30204
30378
|
this.#pendingMulticastAudio = null;
|
|
30205
30379
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -31168,63 +31342,65 @@ var FecStarvationDetector = class {
|
|
|
31168
31342
|
|
|
31169
31343
|
// src/cmcd-producer.ts
|
|
31170
31344
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31178
|
-
|
|
31179
|
-
|
|
31180
|
-
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
}
|
|
31190
|
-
if (snap.multicastSource != null) {
|
|
31191
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
31192
|
-
}
|
|
31193
|
-
if (snap.amtRelay != null) {
|
|
31194
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
31195
|
-
}
|
|
31196
|
-
return params;
|
|
31197
|
-
}
|
|
31198
|
-
function formatQueryString(params) {
|
|
31199
|
-
return Object.keys(params).sort().map((key) => {
|
|
31200
|
-
const v = params[key];
|
|
31201
|
-
if (typeof v === "string") return `${key}=${sanitize(v)}`;
|
|
31202
|
-
return `${key}=${v}`;
|
|
31345
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
31346
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
31347
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
31348
|
+
var TRANSPORT_TOKEN = {
|
|
31349
|
+
native: "ssm",
|
|
31350
|
+
amt: "amt",
|
|
31351
|
+
moq: "moq"
|
|
31352
|
+
};
|
|
31353
|
+
function sfString(value) {
|
|
31354
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
31355
|
+
}
|
|
31356
|
+
function encodeCmcdRecord(record) {
|
|
31357
|
+
return Object.keys(record).sort().map((key) => {
|
|
31358
|
+
const value = record[key];
|
|
31359
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
31360
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
31361
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
31362
|
+
return `${key}=${value.token}`;
|
|
31203
31363
|
}).join(",");
|
|
31204
31364
|
}
|
|
31365
|
+
var token = (value) => ({ token: value });
|
|
31366
|
+
function putCustom(record, name, value) {
|
|
31367
|
+
if (value === void 0) return;
|
|
31368
|
+
if (typeof value === "number") value = String(value);
|
|
31369
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
31370
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
31371
|
+
}
|
|
31205
31372
|
var CmcdProducer = class {
|
|
31206
|
-
#onCmcdParams;
|
|
31207
31373
|
#onCmcdPost;
|
|
31374
|
+
#intervalMs;
|
|
31375
|
+
#sessionId;
|
|
31376
|
+
#now;
|
|
31377
|
+
#sn = 0;
|
|
31378
|
+
#lastHeartbeat = null;
|
|
31208
31379
|
#transport;
|
|
31209
31380
|
#lastStats;
|
|
31210
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
31211
31381
|
#playMode = null;
|
|
31212
31382
|
#deltaFailRate = null;
|
|
31213
31383
|
#switchMiscount = 0;
|
|
31214
31384
|
#manifestStats = null;
|
|
31215
31385
|
#fecFailClosed = null;
|
|
31386
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
31387
|
+
#lifecycle = null;
|
|
31216
31388
|
constructor(options) {
|
|
31217
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
31218
31389
|
this.#onCmcdPost = options.onCmcdPost;
|
|
31390
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
31391
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
31392
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
31393
|
+
}
|
|
31394
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
31395
|
+
this.#now = options.now ?? Date.now;
|
|
31219
31396
|
}
|
|
31220
31397
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
31221
31398
|
updateTransport(snapshot) {
|
|
31222
31399
|
this.#transport = snapshot;
|
|
31223
31400
|
}
|
|
31224
31401
|
/**
|
|
31225
|
-
* Set the current play-mode
|
|
31226
|
-
*
|
|
31227
|
-
* (CLAUDE.md: error on bad init).
|
|
31402
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
31403
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
31228
31404
|
*/
|
|
31229
31405
|
updatePlayMode(mode) {
|
|
31230
31406
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -31235,9 +31411,8 @@ var CmcdProducer = class {
|
|
|
31235
31411
|
this.#playMode = mode;
|
|
31236
31412
|
}
|
|
31237
31413
|
/**
|
|
31238
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
31239
|
-
* `
|
|
31240
|
-
* Omitted on single-track streams.
|
|
31414
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
31415
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
31241
31416
|
*/
|
|
31242
31417
|
updateDeltaFailRate(rate) {
|
|
31243
31418
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -31247,112 +31422,139 @@ var CmcdProducer = class {
|
|
|
31247
31422
|
}
|
|
31248
31423
|
this.#deltaFailRate = rate;
|
|
31249
31424
|
}
|
|
31250
|
-
/**
|
|
31251
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
31252
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
31253
|
-
*/
|
|
31425
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
31254
31426
|
incrementSwitchMiscount() {
|
|
31255
31427
|
this.#switchMiscount += 1;
|
|
31256
31428
|
}
|
|
31257
31429
|
/**
|
|
31258
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
31259
|
-
*
|
|
31260
|
-
* `
|
|
31261
|
-
* stream playing without FEC is told apart from one
|
|
31430
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
31431
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
31432
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
31433
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
31434
|
+
* with no loss.
|
|
31262
31435
|
*/
|
|
31263
31436
|
updateFecFailClosed(reason) {
|
|
31437
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
31264
31438
|
this.#fecFailClosed = reason;
|
|
31439
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
31265
31440
|
}
|
|
31266
31441
|
updateManifestVerification(stats) {
|
|
31267
31442
|
this.#manifestStats = stats;
|
|
31268
31443
|
}
|
|
31269
31444
|
/**
|
|
31270
|
-
*
|
|
31271
|
-
*
|
|
31272
|
-
*
|
|
31273
|
-
*
|
|
31445
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
31446
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
31447
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
31448
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
31274
31449
|
*/
|
|
31450
|
+
onRepairLifecycle(event) {
|
|
31451
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
31452
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
31453
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
31454
|
+
else this.#lifecycle.fetch++;
|
|
31455
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
31456
|
+
this.#lifecycle.failedKeyframe++;
|
|
31457
|
+
}
|
|
31458
|
+
}
|
|
31459
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
31275
31460
|
emitSwitchDrift(driftUs) {
|
|
31276
31461
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
31277
31462
|
throw new Error(
|
|
31278
31463
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
31279
31464
|
);
|
|
31280
31465
|
}
|
|
31281
|
-
const
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31285
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
31286
|
-
this.#emit(params);
|
|
31466
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
31467
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
31468
|
+
this.#post([record]);
|
|
31287
31469
|
}
|
|
31288
|
-
/**
|
|
31289
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31290
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31291
|
-
*/
|
|
31470
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31292
31471
|
onSnapshot(data) {
|
|
31293
31472
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31294
31473
|
if (!videoTrack) return;
|
|
31295
31474
|
this.#lastStats = videoTrack;
|
|
31296
|
-
|
|
31297
|
-
...buildFecParams(videoTrack),
|
|
31298
|
-
...buildTransportParams(this.#transport)
|
|
31299
|
-
};
|
|
31300
|
-
if (this.#playMode !== null) {
|
|
31301
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
31302
|
-
}
|
|
31303
|
-
if (this.#fecFailClosed) {
|
|
31304
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
31305
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
31306
|
-
}
|
|
31307
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31308
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
31309
|
-
}
|
|
31310
|
-
if (this.#switchMiscount > 0) {
|
|
31311
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31312
|
-
}
|
|
31313
|
-
if (this.#manifestStats) {
|
|
31314
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
31315
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
31316
|
-
}
|
|
31317
|
-
this.#emit(params);
|
|
31475
|
+
this.#heartbeat();
|
|
31318
31476
|
}
|
|
31319
31477
|
/**
|
|
31320
|
-
*
|
|
31321
|
-
*
|
|
31478
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
31479
|
+
* heartbeat when due.
|
|
31322
31480
|
*/
|
|
31323
31481
|
onTransportSnapshot(data) {
|
|
31324
31482
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31325
31483
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31326
31484
|
}
|
|
31327
31485
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31328
|
-
if (data.switchMiscount !== void 0
|
|
31329
|
-
|
|
31486
|
+
if (data.switchMiscount !== void 0) {
|
|
31487
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
31488
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
31489
|
+
}
|
|
31490
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31330
31491
|
}
|
|
31331
31492
|
this.#transport = data.transport;
|
|
31332
|
-
|
|
31333
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
31334
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
31335
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
31336
|
-
this.#emit(params);
|
|
31493
|
+
this.#heartbeat();
|
|
31337
31494
|
}
|
|
31338
|
-
/**
|
|
31339
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
31340
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
31341
|
-
* x-bc-ct (one-shot).
|
|
31342
|
-
*/
|
|
31495
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31343
31496
|
onTransition(event) {
|
|
31344
|
-
const
|
|
31345
|
-
|
|
31346
|
-
|
|
31497
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
31498
|
+
putCustom(record, "ct", token(event));
|
|
31499
|
+
this.#putTransport(record);
|
|
31500
|
+
this.#post([record]);
|
|
31501
|
+
}
|
|
31502
|
+
#base(event) {
|
|
31503
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
31504
|
+
}
|
|
31505
|
+
#heartbeat() {
|
|
31506
|
+
const now = this.#now();
|
|
31507
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
31508
|
+
this.#lastHeartbeat = now;
|
|
31509
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
31510
|
+
}
|
|
31511
|
+
#putTransport(record) {
|
|
31512
|
+
const snap = this.#transport;
|
|
31513
|
+
if (!snap) return;
|
|
31514
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
31515
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
31516
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
31517
|
+
}
|
|
31518
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
31519
|
+
}
|
|
31520
|
+
/** The `e=ce` counter report. */
|
|
31521
|
+
#counters() {
|
|
31522
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
31523
|
+
const stats = this.#lastStats;
|
|
31524
|
+
if (stats) {
|
|
31525
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
31526
|
+
putCustom(record, "fk", stats.config.k);
|
|
31527
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
31528
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
31529
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
31530
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
31531
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
31532
|
+
}
|
|
31533
|
+
if (this.#lifecycle) {
|
|
31534
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
31535
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
31536
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
31537
|
+
}
|
|
31538
|
+
this.#putTransport(record);
|
|
31539
|
+
if (this.#playMode !== null) {
|
|
31540
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
31541
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31542
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
31543
|
+
}
|
|
31347
31544
|
}
|
|
31348
|
-
|
|
31349
|
-
|
|
31350
|
-
|
|
31545
|
+
if (this.#fecFailClosed) {
|
|
31546
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
31547
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
31548
|
+
}
|
|
31549
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
31550
|
+
if (this.#manifestStats) {
|
|
31551
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
31552
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
31553
|
+
}
|
|
31554
|
+
return record;
|
|
31351
31555
|
}
|
|
31352
|
-
|
|
31353
|
-
|
|
31354
|
-
this.#onCmcdParams?.(params);
|
|
31355
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
31556
|
+
#post(records) {
|
|
31557
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31356
31558
|
}
|
|
31357
31559
|
};
|
|
31358
31560
|
|
|
@@ -31448,18 +31650,31 @@ function resolveRepairLayers(catalog, source) {
|
|
|
31448
31650
|
totalBaseRepairSymbols: binding.totalBaseRepairSymbols
|
|
31449
31651
|
};
|
|
31450
31652
|
}
|
|
31653
|
+
function valueAt(catalog, pointer) {
|
|
31654
|
+
let node = catalog;
|
|
31655
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
31656
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
31657
|
+
node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
31658
|
+
}
|
|
31659
|
+
return node;
|
|
31660
|
+
}
|
|
31661
|
+
function catalogTrackDeliveryMode(catalog, source) {
|
|
31662
|
+
const binding = bindingOf(catalog, source);
|
|
31663
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
|
|
31664
|
+
return entry?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
31665
|
+
}
|
|
31451
31666
|
function moqtPriority2(value) {
|
|
31452
31667
|
return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
|
|
31453
31668
|
}
|
|
31454
31669
|
function catalogRepairTarget(catalog, source) {
|
|
31455
31670
|
const binding = bindingOf(catalog, source);
|
|
31456
31671
|
if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
|
|
31457
|
-
const
|
|
31672
|
+
const layerZero2 = binding.layers[0];
|
|
31458
31673
|
const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
|
|
31459
31674
|
const tracks = catalog.tracks;
|
|
31460
31675
|
const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
|
|
31461
31676
|
const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
|
|
31462
|
-
return { repairTrack:
|
|
31677
|
+
return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
|
|
31463
31678
|
}
|
|
31464
31679
|
function numericField(value) {
|
|
31465
31680
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
@@ -31491,6 +31706,7 @@ function fecParams(catalog, track, fps, sampleRate) {
|
|
|
31491
31706
|
repairTrackName: base.repairTrack,
|
|
31492
31707
|
repairLayers: repair.baseLayers,
|
|
31493
31708
|
repairOverlays: repair.overlays,
|
|
31709
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track),
|
|
31494
31710
|
...mediaRate
|
|
31495
31711
|
};
|
|
31496
31712
|
}
|
|
@@ -31541,7 +31757,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
|
|
|
31541
31757
|
var PATH_BITS = { moq: 1, multicast: 2 };
|
|
31542
31758
|
var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
|
|
31543
31759
|
var MmtpFecSession = class {
|
|
31760
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
31761
|
+
get repairSymbols() {
|
|
31762
|
+
return this.#repairSymbols;
|
|
31763
|
+
}
|
|
31544
31764
|
#decoder;
|
|
31765
|
+
#repairSymbols;
|
|
31766
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
31767
|
+
#message;
|
|
31545
31768
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
31546
31769
|
#blocks = /* @__PURE__ */ new Map();
|
|
31547
31770
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -31579,6 +31802,24 @@ var MmtpFecSession = class {
|
|
|
31579
31802
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
31580
31803
|
*/
|
|
31581
31804
|
constructor(config) {
|
|
31805
|
+
if (config && "alFecFlow" in config) {
|
|
31806
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow);
|
|
31807
|
+
if (!message) {
|
|
31808
|
+
throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
|
|
31809
|
+
}
|
|
31810
|
+
this.#message = message;
|
|
31811
|
+
this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
|
|
31812
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
|
|
31813
|
+
this.sourceSymbols = message.maximumK;
|
|
31814
|
+
this.#repairSymbols = message.maximumP;
|
|
31815
|
+
this.symbolSize = message.repairSymbolSize;
|
|
31816
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
|
|
31817
|
+
this.#repairPacketId = message.repairFlowId;
|
|
31818
|
+
this.#heldLayers = layerZero(message.maximumP);
|
|
31819
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
31820
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
31821
|
+
return;
|
|
31822
|
+
}
|
|
31582
31823
|
const fec = config?.fec;
|
|
31583
31824
|
if (!fec || typeof fec !== "object") {
|
|
31584
31825
|
throw new Error("MmtpFecSession: the catalog fec instance is required");
|
|
@@ -31589,7 +31830,7 @@ var MmtpFecSession = class {
|
|
|
31589
31830
|
}
|
|
31590
31831
|
this.algorithm = fec.scheme;
|
|
31591
31832
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
|
|
31592
|
-
this
|
|
31833
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
|
|
31593
31834
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
|
|
31594
31835
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
|
|
31595
31836
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
|
|
@@ -31609,17 +31850,30 @@ var MmtpFecSession = class {
|
|
|
31609
31850
|
config.heldLayers
|
|
31610
31851
|
);
|
|
31611
31852
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
31612
|
-
|
|
31613
|
-
|
|
31614
|
-
|
|
31615
|
-
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
31620
|
-
|
|
31621
|
-
|
|
31622
|
-
|
|
31853
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
31854
|
+
}
|
|
31855
|
+
/**
|
|
31856
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
31857
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
31858
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
31859
|
+
* previous one stays valid before the first block carrying the new total
|
|
31860
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
31861
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
31862
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
31863
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
31864
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
31865
|
+
*/
|
|
31866
|
+
updateAlFecFlow(flow) {
|
|
31867
|
+
const held = this.#message;
|
|
31868
|
+
if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
|
|
31869
|
+
const next = alFecRaptorQFlowConfig(flow);
|
|
31870
|
+
if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
|
|
31871
|
+
return "refused";
|
|
31872
|
+
}
|
|
31873
|
+
if (next.maximumP <= this.#repairSymbols) return "unchanged";
|
|
31874
|
+
this.#repairSymbols = next.maximumP;
|
|
31875
|
+
this.#heldLayers = layerZero(next.maximumP);
|
|
31876
|
+
return "raised";
|
|
31623
31877
|
}
|
|
31624
31878
|
/** True once release() ran. */
|
|
31625
31879
|
get released() {
|
|
@@ -31714,7 +31968,7 @@ var MmtpFecSession = class {
|
|
|
31714
31968
|
}
|
|
31715
31969
|
if (admission.reject) {
|
|
31716
31970
|
const verdict2 = this.#heldLayers.failsClosed(
|
|
31717
|
-
repairTrackName,
|
|
31971
|
+
this.#trackFor(repairTrackName),
|
|
31718
31972
|
frame.ssStart,
|
|
31719
31973
|
frame.rsbLength,
|
|
31720
31974
|
this.#anchorHorizon,
|
|
@@ -31729,7 +31983,7 @@ var MmtpFecSession = class {
|
|
|
31729
31983
|
this.#repairPacketId ??= frame.packetId;
|
|
31730
31984
|
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
31731
31985
|
const verdict = this.#heldLayers.admit(
|
|
31732
|
-
repairTrackName,
|
|
31986
|
+
this.#trackFor(repairTrackName),
|
|
31733
31987
|
frame.ssStart,
|
|
31734
31988
|
frame.rsbLength,
|
|
31735
31989
|
this.#anchorHorizon,
|
|
@@ -31788,6 +32042,10 @@ var MmtpFecSession = class {
|
|
|
31788
32042
|
decoder.free();
|
|
31789
32043
|
return true;
|
|
31790
32044
|
}
|
|
32045
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
32046
|
+
#trackFor(repairTrackName) {
|
|
32047
|
+
return this.#message ? void 0 : repairTrackName;
|
|
32048
|
+
}
|
|
31791
32049
|
#drop(reason) {
|
|
31792
32050
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
|
|
31793
32051
|
return [];
|
|
@@ -31909,6 +32167,22 @@ var MmtpFecSession = class {
|
|
|
31909
32167
|
}
|
|
31910
32168
|
}
|
|
31911
32169
|
};
|
|
32170
|
+
function layerZero(total) {
|
|
32171
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
|
|
32172
|
+
}
|
|
32173
|
+
function buildDecoder(Decoder, session) {
|
|
32174
|
+
if (typeof Decoder !== "function") {
|
|
32175
|
+
throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
|
|
32176
|
+
}
|
|
32177
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
|
|
32178
|
+
try {
|
|
32179
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize);
|
|
32180
|
+
} catch (error) {
|
|
32181
|
+
decoder.free();
|
|
32182
|
+
throw error;
|
|
32183
|
+
}
|
|
32184
|
+
return decoder;
|
|
32185
|
+
}
|
|
31912
32186
|
function requirePositiveInteger(value, path) {
|
|
31913
32187
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
31914
32188
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
|
|
@@ -31922,6 +32196,168 @@ function requireNumber(value, path) {
|
|
|
31922
32196
|
return value;
|
|
31923
32197
|
}
|
|
31924
32198
|
|
|
32199
|
+
// src/repair-layer-subscriptions.ts
|
|
32200
|
+
var SERIAL32_HALF2 = 2147483648;
|
|
32201
|
+
var RepairLayerSubscriptions = class {
|
|
32202
|
+
#opts;
|
|
32203
|
+
#windowPackets;
|
|
32204
|
+
#margin;
|
|
32205
|
+
#dropAfterWindows;
|
|
32206
|
+
#source;
|
|
32207
|
+
#layerCount = 0;
|
|
32208
|
+
#subs = /* @__PURE__ */ new Map();
|
|
32209
|
+
#disposed = false;
|
|
32210
|
+
#lastSeq;
|
|
32211
|
+
#expected = 0;
|
|
32212
|
+
#lost = 0;
|
|
32213
|
+
#wantFewer = 0;
|
|
32214
|
+
#lossRate = null;
|
|
32215
|
+
#repairPackets = 0;
|
|
32216
|
+
#repairBytes = 0;
|
|
32217
|
+
#staleDropped = 0;
|
|
32218
|
+
constructor(opts) {
|
|
32219
|
+
this.#opts = opts;
|
|
32220
|
+
this.#source = opts.source;
|
|
32221
|
+
this.#windowPackets = opts.windowPackets ?? 500;
|
|
32222
|
+
this.#margin = opts.margin ?? 2;
|
|
32223
|
+
this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
|
|
32224
|
+
}
|
|
32225
|
+
get layerCount() {
|
|
32226
|
+
return this.#layerCount;
|
|
32227
|
+
}
|
|
32228
|
+
/**
|
|
32229
|
+
* One source packet's MMTP `packet_sequence_number`, in arrival order.
|
|
32230
|
+
* The sequence is per `packet_id`: pass the packets of ONE packet_id only,
|
|
32231
|
+
* or interleaved sequences read as loss.
|
|
32232
|
+
*/
|
|
32233
|
+
noteSourceSequence(seq) {
|
|
32234
|
+
if (this.#disposed || this.#delivery() === "stream") return;
|
|
32235
|
+
seq >>>= 0;
|
|
32236
|
+
if (this.#lastSeq === void 0) {
|
|
32237
|
+
this.#lastSeq = seq;
|
|
32238
|
+
return;
|
|
32239
|
+
}
|
|
32240
|
+
const d = serial32Distance(this.#lastSeq, seq);
|
|
32241
|
+
if (d === 0) return;
|
|
32242
|
+
const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
|
|
32243
|
+
if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
|
|
32244
|
+
this.#lastSeq = seq;
|
|
32245
|
+
this.#expected = 0;
|
|
32246
|
+
this.#lost = 0;
|
|
32247
|
+
return;
|
|
32248
|
+
}
|
|
32249
|
+
if (back === 0) {
|
|
32250
|
+
this.#expected += d;
|
|
32251
|
+
this.#lost += d - 1;
|
|
32252
|
+
this.#lastSeq = seq;
|
|
32253
|
+
} else if (this.#lost > 0) {
|
|
32254
|
+
this.#lost -= 1;
|
|
32255
|
+
}
|
|
32256
|
+
if (this.#expected >= this.#windowPackets) this.#closeWindow();
|
|
32257
|
+
}
|
|
32258
|
+
/** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
|
|
32259
|
+
switchSource(next) {
|
|
32260
|
+
if (this.#disposed) return;
|
|
32261
|
+
this.#closeAll();
|
|
32262
|
+
this.#source = next;
|
|
32263
|
+
this.#lastSeq = void 0;
|
|
32264
|
+
this.#expected = 0;
|
|
32265
|
+
this.#lost = 0;
|
|
32266
|
+
this.#wantFewer = 0;
|
|
32267
|
+
this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
|
|
32268
|
+
this.#reconcile();
|
|
32269
|
+
}
|
|
32270
|
+
snapshot() {
|
|
32271
|
+
return {
|
|
32272
|
+
delivery: this.#delivery(),
|
|
32273
|
+
layerCount: this.#layerCount,
|
|
32274
|
+
subscribedTracks: [...this.#subs.keys()],
|
|
32275
|
+
repairPackets: this.#repairPackets,
|
|
32276
|
+
repairBytes: this.#repairBytes,
|
|
32277
|
+
staleDropped: this.#staleDropped,
|
|
32278
|
+
lossRate: this.#lossRate
|
|
32279
|
+
};
|
|
32280
|
+
}
|
|
32281
|
+
dispose() {
|
|
32282
|
+
if (this.#disposed) return;
|
|
32283
|
+
this.#closeAll();
|
|
32284
|
+
this.#disposed = true;
|
|
32285
|
+
}
|
|
32286
|
+
#delivery() {
|
|
32287
|
+
return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32288
|
+
}
|
|
32289
|
+
#layers() {
|
|
32290
|
+
return this.#source.repairLayers ?? [];
|
|
32291
|
+
}
|
|
32292
|
+
#closeWindow() {
|
|
32293
|
+
const rate = this.#lost / this.#expected;
|
|
32294
|
+
this.#lossRate = rate;
|
|
32295
|
+
this.#expected = 0;
|
|
32296
|
+
this.#lost = 0;
|
|
32297
|
+
const target = this.#targetLayers(rate);
|
|
32298
|
+
if (target > this.#layerCount) {
|
|
32299
|
+
this.#wantFewer = 0;
|
|
32300
|
+
this.#layerCount = target;
|
|
32301
|
+
} else if (target < this.#layerCount) {
|
|
32302
|
+
this.#wantFewer += 1;
|
|
32303
|
+
if (this.#wantFewer >= this.#dropAfterWindows) {
|
|
32304
|
+
this.#wantFewer = 0;
|
|
32305
|
+
this.#layerCount -= 1;
|
|
32306
|
+
}
|
|
32307
|
+
} else {
|
|
32308
|
+
this.#wantFewer = 0;
|
|
32309
|
+
}
|
|
32310
|
+
this.#reconcile();
|
|
32311
|
+
}
|
|
32312
|
+
/** Fewest base layers whose summed P covers margin x the measured loss of K. */
|
|
32313
|
+
#targetLayers(rate) {
|
|
32314
|
+
if (rate <= 0) return 0;
|
|
32315
|
+
const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
|
|
32316
|
+
const layers = this.#layers();
|
|
32317
|
+
let held = 0;
|
|
32318
|
+
for (let n = 0; n < layers.length; n++) {
|
|
32319
|
+
held += layers[n].repairSymbols;
|
|
32320
|
+
if (held >= needed) return n + 1;
|
|
32321
|
+
}
|
|
32322
|
+
return layers.length;
|
|
32323
|
+
}
|
|
32324
|
+
/** Stream delivery never leaves layer count 0, so it never subscribes. */
|
|
32325
|
+
#reconcile() {
|
|
32326
|
+
const want = /* @__PURE__ */ new Map();
|
|
32327
|
+
for (const layer of this.#layers().slice(0, this.#layerCount)) {
|
|
32328
|
+
want.set(layer.repairTrackName, layer.priority);
|
|
32329
|
+
}
|
|
32330
|
+
const overlay = this.#source.repairOverlays?.[0];
|
|
32331
|
+
if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
|
|
32332
|
+
for (const [name, entry] of this.#subs) {
|
|
32333
|
+
if (!want.has(name)) {
|
|
32334
|
+
entry.sub.close();
|
|
32335
|
+
this.#subs.delete(name);
|
|
32336
|
+
}
|
|
32337
|
+
}
|
|
32338
|
+
for (const [name, priority] of want) {
|
|
32339
|
+
if (this.#subs.has(name)) continue;
|
|
32340
|
+
const entry = {};
|
|
32341
|
+
entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
|
|
32342
|
+
this.#subs.set(name, entry);
|
|
32343
|
+
}
|
|
32344
|
+
}
|
|
32345
|
+
/** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
|
|
32346
|
+
#onPacket(name, entry, packet) {
|
|
32347
|
+
if (this.#disposed || this.#subs.get(name) !== entry) {
|
|
32348
|
+
this.#staleDropped += 1;
|
|
32349
|
+
return;
|
|
32350
|
+
}
|
|
32351
|
+
this.#repairPackets += 1;
|
|
32352
|
+
this.#repairBytes += packet.byteLength;
|
|
32353
|
+
this.#opts.feed(packet, name);
|
|
32354
|
+
}
|
|
32355
|
+
#closeAll() {
|
|
32356
|
+
for (const entry of this.#subs.values()) entry.sub.close();
|
|
32357
|
+
this.#subs.clear();
|
|
32358
|
+
}
|
|
32359
|
+
};
|
|
32360
|
+
|
|
31925
32361
|
// src/repair-fetch.ts
|
|
31926
32362
|
var MOQT_PRIORITY_MAX = 255;
|
|
31927
32363
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
@@ -32567,6 +33003,7 @@ export {
|
|
|
32567
33003
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
32568
33004
|
REPAIR_PRIORITY_MAX,
|
|
32569
33005
|
REPAIR_PRIORITY_MIN,
|
|
33006
|
+
RepairLayerSubscriptions,
|
|
32570
33007
|
SOURCE_MEDIA_PRIORITY_MAX,
|
|
32571
33008
|
SampleCombineError,
|
|
32572
33009
|
SampleCombiner,
|
|
@@ -32591,6 +33028,7 @@ export {
|
|
|
32591
33028
|
catalogGroupDurationMs,
|
|
32592
33029
|
catalogKeyframeCadenceMs,
|
|
32593
33030
|
catalogRepairTarget,
|
|
33031
|
+
catalogTrackDeliveryMode,
|
|
32594
33032
|
catalogTrackFecUnavailable,
|
|
32595
33033
|
classifyH264Keyframe,
|
|
32596
33034
|
classifyHevcKeyframe,
|
|
@@ -32611,6 +33049,7 @@ export {
|
|
|
32611
33049
|
detectH264Keyframe,
|
|
32612
33050
|
detectHevcKeyframe,
|
|
32613
33051
|
driftCheck,
|
|
33052
|
+
encodeCmcdRecord,
|
|
32614
33053
|
encodeDataSegment,
|
|
32615
33054
|
encodeFecSourceInfo,
|
|
32616
33055
|
encodeLocTimestamp,
|