@blockcast/mmt-container 2.2.0-main.a88c3a0aa4f3 → 2.2.0-main.bdefba2a99c2
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 +491 -177
- 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 +24 -15
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- 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 +4 -3
- 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/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.js +12 -19
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +133 -34
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +12 -19
- 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.js +36 -34
- 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.d.ts +2 -0
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +37 -34
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +24 -15
- 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 +130 -33
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +12 -19
- 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-worker-setup.ts +2 -2
- package/src/index.ts +17 -2
- package/src/mmtp-fec-session.ts +115 -15
- package/src/multicast-manager.ts +3 -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;
|
|
@@ -8710,11 +8749,11 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8710
8749
|
var AMTGatewayManager = class {
|
|
8711
8750
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8712
8751
|
messageHandler;
|
|
8752
|
+
messageListenerRegistered = false;
|
|
8713
8753
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
8714
8754
|
constructor() {
|
|
8715
8755
|
this.messageHandler = (event) => {
|
|
8716
8756
|
const data = event.data;
|
|
8717
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8718
8757
|
if (data.type === "UDP_PACKET") {
|
|
8719
8758
|
this.handlePacket(data);
|
|
8720
8759
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8723,12 +8762,22 @@ var AMTGatewayManager = class {
|
|
|
8723
8762
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
8724
8763
|
}
|
|
8725
8764
|
};
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8765
|
+
}
|
|
8766
|
+
ensureMessageListener() {
|
|
8767
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
8768
|
+
return;
|
|
8769
|
+
}
|
|
8770
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
8771
|
+
this.messageListenerRegistered = true;
|
|
8772
|
+
console.log("[AMT] Registered service worker message handler");
|
|
8773
|
+
}
|
|
8774
|
+
removeMessageListener() {
|
|
8775
|
+
if (!this.messageListenerRegistered)
|
|
8776
|
+
return;
|
|
8777
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8778
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8731
8779
|
}
|
|
8780
|
+
this.messageListenerRegistered = false;
|
|
8732
8781
|
}
|
|
8733
8782
|
/**
|
|
8734
8783
|
* Subscribe via AMT tunnel
|
|
@@ -8738,6 +8787,7 @@ var AMTGatewayManager = class {
|
|
|
8738
8787
|
if (config.protected) {
|
|
8739
8788
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
8740
8789
|
}
|
|
8790
|
+
this.ensureMessageListener();
|
|
8741
8791
|
const keyStr = this.getKeyString(key);
|
|
8742
8792
|
if (this.subscriptions.has(keyStr)) {
|
|
8743
8793
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -8773,6 +8823,8 @@ var AMTGatewayManager = class {
|
|
|
8773
8823
|
this.subscriptions.delete(keyStr);
|
|
8774
8824
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8775
8825
|
}
|
|
8826
|
+
if (this.subscriptions.size === 0)
|
|
8827
|
+
this.removeMessageListener();
|
|
8776
8828
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8777
8829
|
}
|
|
8778
8830
|
}
|
|
@@ -8794,6 +8846,8 @@ var AMTGatewayManager = class {
|
|
|
8794
8846
|
} finally {
|
|
8795
8847
|
this.subscriptions.delete(handle);
|
|
8796
8848
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8849
|
+
if (this.subscriptions.size === 0)
|
|
8850
|
+
this.removeMessageListener();
|
|
8797
8851
|
}
|
|
8798
8852
|
}
|
|
8799
8853
|
/**
|
|
@@ -8810,10 +8864,8 @@ var AMTGatewayManager = class {
|
|
|
8810
8864
|
if (!subscriptionId)
|
|
8811
8865
|
return;
|
|
8812
8866
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8813
|
-
if (!subscription)
|
|
8814
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8867
|
+
if (!subscription)
|
|
8815
8868
|
return;
|
|
8816
|
-
}
|
|
8817
8869
|
if (!subscription.handshakeComplete) {
|
|
8818
8870
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8819
8871
|
const packetData2 = data;
|
|
@@ -8843,19 +8895,15 @@ var AMTGatewayManager = class {
|
|
|
8843
8895
|
if (!subscriptionId)
|
|
8844
8896
|
return;
|
|
8845
8897
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8846
|
-
if (!subscription)
|
|
8847
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8898
|
+
if (!subscription)
|
|
8848
8899
|
return;
|
|
8849
|
-
}
|
|
8850
8900
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8851
8901
|
}
|
|
8852
8902
|
/**
|
|
8853
8903
|
* Cleanup on destroy
|
|
8854
8904
|
*/
|
|
8855
8905
|
destroy() {
|
|
8856
|
-
|
|
8857
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8858
|
-
}
|
|
8906
|
+
this.removeMessageListener();
|
|
8859
8907
|
const handles = Array.from(this.subscriptions.keys());
|
|
8860
8908
|
for (const handle of handles) {
|
|
8861
8909
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -18811,10 +18859,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18811
18859
|
get reassemblerDataUnitsReturned() {
|
|
18812
18860
|
return this.#reassemblerDataUnitsReturned;
|
|
18813
18861
|
}
|
|
18814
|
-
#countOutcome(
|
|
18815
|
-
const count = (this.#feedOutcomes.get(
|
|
18816
|
-
this.#feedOutcomes.set(
|
|
18817
|
-
this.#lastOutcome =
|
|
18862
|
+
#countOutcome(token2) {
|
|
18863
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
18864
|
+
this.#feedOutcomes.set(token2, count);
|
|
18865
|
+
this.#lastOutcome = token2;
|
|
18818
18866
|
this.#lastOutcomeCount = count;
|
|
18819
18867
|
}
|
|
18820
18868
|
#countDeferredOutcome(reason) {
|
|
@@ -28678,6 +28726,7 @@ var MulticastManager = class {
|
|
|
28678
28726
|
mode: endpoint.mode,
|
|
28679
28727
|
amtRelay: endpoint.amtRelay,
|
|
28680
28728
|
amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
|
|
28729
|
+
amtDiscovery: endpoint.amtDiscovery,
|
|
28681
28730
|
packetIds: endpoint.packetIds,
|
|
28682
28731
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
28683
28732
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
@@ -29626,7 +29675,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29626
29675
|
blockProfile,
|
|
29627
29676
|
k: opts.fecK,
|
|
29628
29677
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29629
|
-
// and CMCD `
|
|
29678
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29630
29679
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29631
29680
|
symbolSize: opts.fecSymbolSize,
|
|
29632
29681
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -30168,24 +30217,24 @@ var MediaRouter = class {
|
|
|
30168
30217
|
if (!this.#multicastVideoKeyed) return;
|
|
30169
30218
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
30170
30219
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
30171
|
-
const
|
|
30220
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
30172
30221
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
30173
30222
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
30174
|
-
this.#commitMulticastActivation(
|
|
30223
|
+
this.#commitMulticastActivation(token2);
|
|
30175
30224
|
return;
|
|
30176
30225
|
}
|
|
30177
30226
|
const ready = Promise.resolve(maybeReady);
|
|
30178
|
-
this.#pendingMulticastToken =
|
|
30227
|
+
this.#pendingMulticastToken = token2;
|
|
30179
30228
|
this.#sourceSwitchReady = ready;
|
|
30180
30229
|
void ready.then(
|
|
30181
|
-
() => this.#commitMulticastActivation(
|
|
30182
|
-
(err) => this.#rejectMulticastActivation(
|
|
30230
|
+
() => this.#commitMulticastActivation(token2),
|
|
30231
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
30183
30232
|
).finally(() => {
|
|
30184
30233
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
30185
30234
|
});
|
|
30186
30235
|
}
|
|
30187
|
-
#commitMulticastActivation(
|
|
30188
|
-
if (
|
|
30236
|
+
#commitMulticastActivation(token2) {
|
|
30237
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30189
30238
|
this.#active = "multicast";
|
|
30190
30239
|
this.#suppressed = true;
|
|
30191
30240
|
this.#pendingMulticastToken = null;
|
|
@@ -30196,9 +30245,9 @@ var MediaRouter = class {
|
|
|
30196
30245
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
30197
30246
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
30198
30247
|
}
|
|
30199
|
-
#rejectMulticastActivation(
|
|
30200
|
-
if (
|
|
30201
|
-
if (this.#pendingMulticastToken ===
|
|
30248
|
+
#rejectMulticastActivation(token2, err) {
|
|
30249
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30250
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
30202
30251
|
this.#pendingMulticastVideo = null;
|
|
30203
30252
|
this.#pendingMulticastAudio = null;
|
|
30204
30253
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -31167,63 +31216,65 @@ var FecStarvationDetector = class {
|
|
|
31167
31216
|
|
|
31168
31217
|
// src/cmcd-producer.ts
|
|
31169
31218
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
31170
|
-
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31178
|
-
|
|
31179
|
-
|
|
31180
|
-
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
}
|
|
31189
|
-
if (snap.multicastSource != null) {
|
|
31190
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
31191
|
-
}
|
|
31192
|
-
if (snap.amtRelay != null) {
|
|
31193
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
31194
|
-
}
|
|
31195
|
-
return params;
|
|
31196
|
-
}
|
|
31197
|
-
function formatQueryString(params) {
|
|
31198
|
-
return Object.keys(params).sort().map((key) => {
|
|
31199
|
-
const v = params[key];
|
|
31200
|
-
if (typeof v === "string") return `${key}=${sanitize(v)}`;
|
|
31201
|
-
return `${key}=${v}`;
|
|
31219
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
31220
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
31221
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
31222
|
+
var TRANSPORT_TOKEN = {
|
|
31223
|
+
native: "ssm",
|
|
31224
|
+
amt: "amt",
|
|
31225
|
+
moq: "moq"
|
|
31226
|
+
};
|
|
31227
|
+
function sfString(value) {
|
|
31228
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
31229
|
+
}
|
|
31230
|
+
function encodeCmcdRecord(record) {
|
|
31231
|
+
return Object.keys(record).sort().map((key) => {
|
|
31232
|
+
const value = record[key];
|
|
31233
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
31234
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
31235
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
31236
|
+
return `${key}=${value.token}`;
|
|
31202
31237
|
}).join(",");
|
|
31203
31238
|
}
|
|
31239
|
+
var token = (value) => ({ token: value });
|
|
31240
|
+
function putCustom(record, name, value) {
|
|
31241
|
+
if (value === void 0) return;
|
|
31242
|
+
if (typeof value === "number") value = String(value);
|
|
31243
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
31244
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
31245
|
+
}
|
|
31204
31246
|
var CmcdProducer = class {
|
|
31205
|
-
#onCmcdParams;
|
|
31206
31247
|
#onCmcdPost;
|
|
31248
|
+
#intervalMs;
|
|
31249
|
+
#sessionId;
|
|
31250
|
+
#now;
|
|
31251
|
+
#sn = 0;
|
|
31252
|
+
#lastHeartbeat = null;
|
|
31207
31253
|
#transport;
|
|
31208
31254
|
#lastStats;
|
|
31209
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
31210
31255
|
#playMode = null;
|
|
31211
31256
|
#deltaFailRate = null;
|
|
31212
31257
|
#switchMiscount = 0;
|
|
31213
31258
|
#manifestStats = null;
|
|
31214
31259
|
#fecFailClosed = null;
|
|
31260
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
31261
|
+
#lifecycle = null;
|
|
31215
31262
|
constructor(options) {
|
|
31216
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
31217
31263
|
this.#onCmcdPost = options.onCmcdPost;
|
|
31264
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
31265
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
31266
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
31267
|
+
}
|
|
31268
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
31269
|
+
this.#now = options.now ?? Date.now;
|
|
31218
31270
|
}
|
|
31219
31271
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
31220
31272
|
updateTransport(snapshot) {
|
|
31221
31273
|
this.#transport = snapshot;
|
|
31222
31274
|
}
|
|
31223
31275
|
/**
|
|
31224
|
-
* Set the current play-mode
|
|
31225
|
-
*
|
|
31226
|
-
* (CLAUDE.md: error on bad init).
|
|
31276
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
31277
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
31227
31278
|
*/
|
|
31228
31279
|
updatePlayMode(mode) {
|
|
31229
31280
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -31234,9 +31285,8 @@ var CmcdProducer = class {
|
|
|
31234
31285
|
this.#playMode = mode;
|
|
31235
31286
|
}
|
|
31236
31287
|
/**
|
|
31237
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
31238
|
-
* `
|
|
31239
|
-
* Omitted on single-track streams.
|
|
31288
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
31289
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
31240
31290
|
*/
|
|
31241
31291
|
updateDeltaFailRate(rate) {
|
|
31242
31292
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -31246,112 +31296,139 @@ var CmcdProducer = class {
|
|
|
31246
31296
|
}
|
|
31247
31297
|
this.#deltaFailRate = rate;
|
|
31248
31298
|
}
|
|
31249
|
-
/**
|
|
31250
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
31251
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
31252
|
-
*/
|
|
31299
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
31253
31300
|
incrementSwitchMiscount() {
|
|
31254
31301
|
this.#switchMiscount += 1;
|
|
31255
31302
|
}
|
|
31256
31303
|
/**
|
|
31257
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
31258
|
-
*
|
|
31259
|
-
* `
|
|
31260
|
-
* stream playing without FEC is told apart from one
|
|
31304
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
31305
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
31306
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
31307
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
31308
|
+
* with no loss.
|
|
31261
31309
|
*/
|
|
31262
31310
|
updateFecFailClosed(reason) {
|
|
31311
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
31263
31312
|
this.#fecFailClosed = reason;
|
|
31313
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
31264
31314
|
}
|
|
31265
31315
|
updateManifestVerification(stats) {
|
|
31266
31316
|
this.#manifestStats = stats;
|
|
31267
31317
|
}
|
|
31268
31318
|
/**
|
|
31269
|
-
*
|
|
31270
|
-
*
|
|
31271
|
-
*
|
|
31272
|
-
*
|
|
31319
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
31320
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
31321
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
31322
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
31273
31323
|
*/
|
|
31324
|
+
onRepairLifecycle(event) {
|
|
31325
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
31326
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
31327
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
31328
|
+
else this.#lifecycle.fetch++;
|
|
31329
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
31330
|
+
this.#lifecycle.failedKeyframe++;
|
|
31331
|
+
}
|
|
31332
|
+
}
|
|
31333
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
31274
31334
|
emitSwitchDrift(driftUs) {
|
|
31275
31335
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
31276
31336
|
throw new Error(
|
|
31277
31337
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
31278
31338
|
);
|
|
31279
31339
|
}
|
|
31280
|
-
const
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31284
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
31285
|
-
this.#emit(params);
|
|
31340
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
31341
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
31342
|
+
this.#post([record]);
|
|
31286
31343
|
}
|
|
31287
|
-
/**
|
|
31288
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31289
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31290
|
-
*/
|
|
31344
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31291
31345
|
onSnapshot(data) {
|
|
31292
31346
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31293
31347
|
if (!videoTrack) return;
|
|
31294
31348
|
this.#lastStats = videoTrack;
|
|
31295
|
-
|
|
31296
|
-
...buildFecParams(videoTrack),
|
|
31297
|
-
...buildTransportParams(this.#transport)
|
|
31298
|
-
};
|
|
31299
|
-
if (this.#playMode !== null) {
|
|
31300
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
31301
|
-
}
|
|
31302
|
-
if (this.#fecFailClosed) {
|
|
31303
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
31304
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
31305
|
-
}
|
|
31306
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31307
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
31308
|
-
}
|
|
31309
|
-
if (this.#switchMiscount > 0) {
|
|
31310
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31311
|
-
}
|
|
31312
|
-
if (this.#manifestStats) {
|
|
31313
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
31314
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
31315
|
-
}
|
|
31316
|
-
this.#emit(params);
|
|
31349
|
+
this.#heartbeat();
|
|
31317
31350
|
}
|
|
31318
31351
|
/**
|
|
31319
|
-
*
|
|
31320
|
-
*
|
|
31352
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
31353
|
+
* heartbeat when due.
|
|
31321
31354
|
*/
|
|
31322
31355
|
onTransportSnapshot(data) {
|
|
31323
31356
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31324
31357
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31325
31358
|
}
|
|
31326
31359
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31327
|
-
if (data.switchMiscount !== void 0
|
|
31328
|
-
|
|
31360
|
+
if (data.switchMiscount !== void 0) {
|
|
31361
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
31362
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
31363
|
+
}
|
|
31364
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31329
31365
|
}
|
|
31330
31366
|
this.#transport = data.transport;
|
|
31331
|
-
|
|
31332
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
31333
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
31334
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
31335
|
-
this.#emit(params);
|
|
31367
|
+
this.#heartbeat();
|
|
31336
31368
|
}
|
|
31337
|
-
/**
|
|
31338
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
31339
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
31340
|
-
* x-bc-ct (one-shot).
|
|
31341
|
-
*/
|
|
31369
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31342
31370
|
onTransition(event) {
|
|
31343
|
-
const
|
|
31344
|
-
|
|
31345
|
-
|
|
31371
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
31372
|
+
putCustom(record, "ct", token(event));
|
|
31373
|
+
this.#putTransport(record);
|
|
31374
|
+
this.#post([record]);
|
|
31375
|
+
}
|
|
31376
|
+
#base(event) {
|
|
31377
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
31378
|
+
}
|
|
31379
|
+
#heartbeat() {
|
|
31380
|
+
const now = this.#now();
|
|
31381
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
31382
|
+
this.#lastHeartbeat = now;
|
|
31383
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
31384
|
+
}
|
|
31385
|
+
#putTransport(record) {
|
|
31386
|
+
const snap = this.#transport;
|
|
31387
|
+
if (!snap) return;
|
|
31388
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
31389
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
31390
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
31391
|
+
}
|
|
31392
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
31393
|
+
}
|
|
31394
|
+
/** The `e=ce` counter report. */
|
|
31395
|
+
#counters() {
|
|
31396
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
31397
|
+
const stats = this.#lastStats;
|
|
31398
|
+
if (stats) {
|
|
31399
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
31400
|
+
putCustom(record, "fk", stats.config.k);
|
|
31401
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
31402
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
31403
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
31404
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
31405
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
31406
|
+
}
|
|
31407
|
+
if (this.#lifecycle) {
|
|
31408
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
31409
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
31410
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
31411
|
+
}
|
|
31412
|
+
this.#putTransport(record);
|
|
31413
|
+
if (this.#playMode !== null) {
|
|
31414
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
31415
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31416
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
31417
|
+
}
|
|
31418
|
+
}
|
|
31419
|
+
if (this.#fecFailClosed) {
|
|
31420
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
31421
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
31422
|
+
}
|
|
31423
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
31424
|
+
if (this.#manifestStats) {
|
|
31425
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
31426
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
31346
31427
|
}
|
|
31347
|
-
|
|
31348
|
-
params["x-bc-ct"] = event;
|
|
31349
|
-
this.#emit(params);
|
|
31428
|
+
return record;
|
|
31350
31429
|
}
|
|
31351
|
-
|
|
31352
|
-
|
|
31353
|
-
this.#onCmcdParams?.(params);
|
|
31354
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
31430
|
+
#post(records) {
|
|
31431
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31355
31432
|
}
|
|
31356
31433
|
};
|
|
31357
31434
|
|
|
@@ -31447,18 +31524,31 @@ function resolveRepairLayers(catalog, source) {
|
|
|
31447
31524
|
totalBaseRepairSymbols: binding.totalBaseRepairSymbols
|
|
31448
31525
|
};
|
|
31449
31526
|
}
|
|
31527
|
+
function valueAt(catalog, pointer) {
|
|
31528
|
+
let node = catalog;
|
|
31529
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
31530
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
31531
|
+
node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
31532
|
+
}
|
|
31533
|
+
return node;
|
|
31534
|
+
}
|
|
31535
|
+
function catalogTrackDeliveryMode(catalog, source) {
|
|
31536
|
+
const binding = bindingOf(catalog, source);
|
|
31537
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
|
|
31538
|
+
return entry?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
31539
|
+
}
|
|
31450
31540
|
function moqtPriority2(value) {
|
|
31451
31541
|
return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
|
|
31452
31542
|
}
|
|
31453
31543
|
function catalogRepairTarget(catalog, source) {
|
|
31454
31544
|
const binding = bindingOf(catalog, source);
|
|
31455
31545
|
if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
|
|
31456
|
-
const
|
|
31546
|
+
const layerZero2 = binding.layers[0];
|
|
31457
31547
|
const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
|
|
31458
31548
|
const tracks = catalog.tracks;
|
|
31459
31549
|
const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
|
|
31460
31550
|
const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
|
|
31461
|
-
return { repairTrack:
|
|
31551
|
+
return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
|
|
31462
31552
|
}
|
|
31463
31553
|
function numericField(value) {
|
|
31464
31554
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
@@ -31490,6 +31580,7 @@ function fecParams(catalog, track, fps, sampleRate) {
|
|
|
31490
31580
|
repairTrackName: base.repairTrack,
|
|
31491
31581
|
repairLayers: repair.baseLayers,
|
|
31492
31582
|
repairOverlays: repair.overlays,
|
|
31583
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track),
|
|
31493
31584
|
...mediaRate
|
|
31494
31585
|
};
|
|
31495
31586
|
}
|
|
@@ -31540,7 +31631,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
|
|
|
31540
31631
|
var PATH_BITS = { moq: 1, multicast: 2 };
|
|
31541
31632
|
var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
|
|
31542
31633
|
var MmtpFecSession = class {
|
|
31634
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
31635
|
+
get repairSymbols() {
|
|
31636
|
+
return this.#repairSymbols;
|
|
31637
|
+
}
|
|
31543
31638
|
#decoder;
|
|
31639
|
+
#repairSymbols;
|
|
31640
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
31641
|
+
#message;
|
|
31544
31642
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
31545
31643
|
#blocks = /* @__PURE__ */ new Map();
|
|
31546
31644
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -31578,6 +31676,24 @@ var MmtpFecSession = class {
|
|
|
31578
31676
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
31579
31677
|
*/
|
|
31580
31678
|
constructor(config) {
|
|
31679
|
+
if (config && "alFecFlow" in config) {
|
|
31680
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow);
|
|
31681
|
+
if (!message) {
|
|
31682
|
+
throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
|
|
31683
|
+
}
|
|
31684
|
+
this.#message = message;
|
|
31685
|
+
this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
|
|
31686
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
|
|
31687
|
+
this.sourceSymbols = message.maximumK;
|
|
31688
|
+
this.#repairSymbols = message.maximumP;
|
|
31689
|
+
this.symbolSize = message.repairSymbolSize;
|
|
31690
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
|
|
31691
|
+
this.#repairPacketId = message.repairFlowId;
|
|
31692
|
+
this.#heldLayers = layerZero(message.maximumP);
|
|
31693
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
31694
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
31695
|
+
return;
|
|
31696
|
+
}
|
|
31581
31697
|
const fec = config?.fec;
|
|
31582
31698
|
if (!fec || typeof fec !== "object") {
|
|
31583
31699
|
throw new Error("MmtpFecSession: the catalog fec instance is required");
|
|
@@ -31588,7 +31704,7 @@ var MmtpFecSession = class {
|
|
|
31588
31704
|
}
|
|
31589
31705
|
this.algorithm = fec.scheme;
|
|
31590
31706
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
|
|
31591
|
-
this
|
|
31707
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
|
|
31592
31708
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
|
|
31593
31709
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
|
|
31594
31710
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
|
|
@@ -31608,17 +31724,30 @@ var MmtpFecSession = class {
|
|
|
31608
31724
|
config.heldLayers
|
|
31609
31725
|
);
|
|
31610
31726
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
|
|
31611
|
-
|
|
31612
|
-
|
|
31613
|
-
|
|
31614
|
-
|
|
31615
|
-
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
31620
|
-
|
|
31621
|
-
|
|
31727
|
+
this.#decoder = buildDecoder(config.Decoder, this);
|
|
31728
|
+
}
|
|
31729
|
+
/**
|
|
31730
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
31731
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
31732
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
31733
|
+
* previous one stays valid before the first block carrying the new total
|
|
31734
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
31735
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
31736
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
31737
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
31738
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
31739
|
+
*/
|
|
31740
|
+
updateAlFecFlow(flow) {
|
|
31741
|
+
const held = this.#message;
|
|
31742
|
+
if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
|
|
31743
|
+
const next = alFecRaptorQFlowConfig(flow);
|
|
31744
|
+
if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
|
|
31745
|
+
return "refused";
|
|
31746
|
+
}
|
|
31747
|
+
if (next.maximumP <= this.#repairSymbols) return "unchanged";
|
|
31748
|
+
this.#repairSymbols = next.maximumP;
|
|
31749
|
+
this.#heldLayers = layerZero(next.maximumP);
|
|
31750
|
+
return "raised";
|
|
31622
31751
|
}
|
|
31623
31752
|
/** True once release() ran. */
|
|
31624
31753
|
get released() {
|
|
@@ -31713,7 +31842,7 @@ var MmtpFecSession = class {
|
|
|
31713
31842
|
}
|
|
31714
31843
|
if (admission.reject) {
|
|
31715
31844
|
const verdict2 = this.#heldLayers.failsClosed(
|
|
31716
|
-
repairTrackName,
|
|
31845
|
+
this.#trackFor(repairTrackName),
|
|
31717
31846
|
frame.ssStart,
|
|
31718
31847
|
frame.rsbLength,
|
|
31719
31848
|
this.#anchorHorizon,
|
|
@@ -31728,7 +31857,7 @@ var MmtpFecSession = class {
|
|
|
31728
31857
|
this.#repairPacketId ??= frame.packetId;
|
|
31729
31858
|
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
31730
31859
|
const verdict = this.#heldLayers.admit(
|
|
31731
|
-
repairTrackName,
|
|
31860
|
+
this.#trackFor(repairTrackName),
|
|
31732
31861
|
frame.ssStart,
|
|
31733
31862
|
frame.rsbLength,
|
|
31734
31863
|
this.#anchorHorizon,
|
|
@@ -31787,6 +31916,10 @@ var MmtpFecSession = class {
|
|
|
31787
31916
|
decoder.free();
|
|
31788
31917
|
return true;
|
|
31789
31918
|
}
|
|
31919
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
31920
|
+
#trackFor(repairTrackName) {
|
|
31921
|
+
return this.#message ? void 0 : repairTrackName;
|
|
31922
|
+
}
|
|
31790
31923
|
#drop(reason) {
|
|
31791
31924
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
|
|
31792
31925
|
return [];
|
|
@@ -31908,6 +32041,22 @@ var MmtpFecSession = class {
|
|
|
31908
32041
|
}
|
|
31909
32042
|
}
|
|
31910
32043
|
};
|
|
32044
|
+
function layerZero(total) {
|
|
32045
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
|
|
32046
|
+
}
|
|
32047
|
+
function buildDecoder(Decoder, session) {
|
|
32048
|
+
if (typeof Decoder !== "function") {
|
|
32049
|
+
throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
|
|
32050
|
+
}
|
|
32051
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
|
|
32052
|
+
try {
|
|
32053
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize);
|
|
32054
|
+
} catch (error) {
|
|
32055
|
+
decoder.free();
|
|
32056
|
+
throw error;
|
|
32057
|
+
}
|
|
32058
|
+
return decoder;
|
|
32059
|
+
}
|
|
31911
32060
|
function requirePositiveInteger(value, path) {
|
|
31912
32061
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
31913
32062
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
|
|
@@ -31921,6 +32070,168 @@ function requireNumber(value, path) {
|
|
|
31921
32070
|
return value;
|
|
31922
32071
|
}
|
|
31923
32072
|
|
|
32073
|
+
// src/repair-layer-subscriptions.ts
|
|
32074
|
+
var SERIAL32_HALF2 = 2147483648;
|
|
32075
|
+
var RepairLayerSubscriptions = class {
|
|
32076
|
+
#opts;
|
|
32077
|
+
#windowPackets;
|
|
32078
|
+
#margin;
|
|
32079
|
+
#dropAfterWindows;
|
|
32080
|
+
#source;
|
|
32081
|
+
#layerCount = 0;
|
|
32082
|
+
#subs = /* @__PURE__ */ new Map();
|
|
32083
|
+
#disposed = false;
|
|
32084
|
+
#lastSeq;
|
|
32085
|
+
#expected = 0;
|
|
32086
|
+
#lost = 0;
|
|
32087
|
+
#wantFewer = 0;
|
|
32088
|
+
#lossRate = null;
|
|
32089
|
+
#repairPackets = 0;
|
|
32090
|
+
#repairBytes = 0;
|
|
32091
|
+
#staleDropped = 0;
|
|
32092
|
+
constructor(opts) {
|
|
32093
|
+
this.#opts = opts;
|
|
32094
|
+
this.#source = opts.source;
|
|
32095
|
+
this.#windowPackets = opts.windowPackets ?? 500;
|
|
32096
|
+
this.#margin = opts.margin ?? 2;
|
|
32097
|
+
this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
|
|
32098
|
+
}
|
|
32099
|
+
get layerCount() {
|
|
32100
|
+
return this.#layerCount;
|
|
32101
|
+
}
|
|
32102
|
+
/**
|
|
32103
|
+
* One source packet's MMTP `packet_sequence_number`, in arrival order.
|
|
32104
|
+
* The sequence is per `packet_id`: pass the packets of ONE packet_id only,
|
|
32105
|
+
* or interleaved sequences read as loss.
|
|
32106
|
+
*/
|
|
32107
|
+
noteSourceSequence(seq) {
|
|
32108
|
+
if (this.#disposed || this.#delivery() === "stream") return;
|
|
32109
|
+
seq >>>= 0;
|
|
32110
|
+
if (this.#lastSeq === void 0) {
|
|
32111
|
+
this.#lastSeq = seq;
|
|
32112
|
+
return;
|
|
32113
|
+
}
|
|
32114
|
+
const d = serial32Distance(this.#lastSeq, seq);
|
|
32115
|
+
if (d === 0) return;
|
|
32116
|
+
const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
|
|
32117
|
+
if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
|
|
32118
|
+
this.#lastSeq = seq;
|
|
32119
|
+
this.#expected = 0;
|
|
32120
|
+
this.#lost = 0;
|
|
32121
|
+
return;
|
|
32122
|
+
}
|
|
32123
|
+
if (back === 0) {
|
|
32124
|
+
this.#expected += d;
|
|
32125
|
+
this.#lost += d - 1;
|
|
32126
|
+
this.#lastSeq = seq;
|
|
32127
|
+
} else if (this.#lost > 0) {
|
|
32128
|
+
this.#lost -= 1;
|
|
32129
|
+
}
|
|
32130
|
+
if (this.#expected >= this.#windowPackets) this.#closeWindow();
|
|
32131
|
+
}
|
|
32132
|
+
/** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
|
|
32133
|
+
switchSource(next) {
|
|
32134
|
+
if (this.#disposed) return;
|
|
32135
|
+
this.#closeAll();
|
|
32136
|
+
this.#source = next;
|
|
32137
|
+
this.#lastSeq = void 0;
|
|
32138
|
+
this.#expected = 0;
|
|
32139
|
+
this.#lost = 0;
|
|
32140
|
+
this.#wantFewer = 0;
|
|
32141
|
+
this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
|
|
32142
|
+
this.#reconcile();
|
|
32143
|
+
}
|
|
32144
|
+
snapshot() {
|
|
32145
|
+
return {
|
|
32146
|
+
delivery: this.#delivery(),
|
|
32147
|
+
layerCount: this.#layerCount,
|
|
32148
|
+
subscribedTracks: [...this.#subs.keys()],
|
|
32149
|
+
repairPackets: this.#repairPackets,
|
|
32150
|
+
repairBytes: this.#repairBytes,
|
|
32151
|
+
staleDropped: this.#staleDropped,
|
|
32152
|
+
lossRate: this.#lossRate
|
|
32153
|
+
};
|
|
32154
|
+
}
|
|
32155
|
+
dispose() {
|
|
32156
|
+
if (this.#disposed) return;
|
|
32157
|
+
this.#closeAll();
|
|
32158
|
+
this.#disposed = true;
|
|
32159
|
+
}
|
|
32160
|
+
#delivery() {
|
|
32161
|
+
return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32162
|
+
}
|
|
32163
|
+
#layers() {
|
|
32164
|
+
return this.#source.repairLayers ?? [];
|
|
32165
|
+
}
|
|
32166
|
+
#closeWindow() {
|
|
32167
|
+
const rate = this.#lost / this.#expected;
|
|
32168
|
+
this.#lossRate = rate;
|
|
32169
|
+
this.#expected = 0;
|
|
32170
|
+
this.#lost = 0;
|
|
32171
|
+
const target = this.#targetLayers(rate);
|
|
32172
|
+
if (target > this.#layerCount) {
|
|
32173
|
+
this.#wantFewer = 0;
|
|
32174
|
+
this.#layerCount = target;
|
|
32175
|
+
} else if (target < this.#layerCount) {
|
|
32176
|
+
this.#wantFewer += 1;
|
|
32177
|
+
if (this.#wantFewer >= this.#dropAfterWindows) {
|
|
32178
|
+
this.#wantFewer = 0;
|
|
32179
|
+
this.#layerCount -= 1;
|
|
32180
|
+
}
|
|
32181
|
+
} else {
|
|
32182
|
+
this.#wantFewer = 0;
|
|
32183
|
+
}
|
|
32184
|
+
this.#reconcile();
|
|
32185
|
+
}
|
|
32186
|
+
/** Fewest base layers whose summed P covers margin x the measured loss of K. */
|
|
32187
|
+
#targetLayers(rate) {
|
|
32188
|
+
if (rate <= 0) return 0;
|
|
32189
|
+
const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
|
|
32190
|
+
const layers = this.#layers();
|
|
32191
|
+
let held = 0;
|
|
32192
|
+
for (let n = 0; n < layers.length; n++) {
|
|
32193
|
+
held += layers[n].repairSymbols;
|
|
32194
|
+
if (held >= needed) return n + 1;
|
|
32195
|
+
}
|
|
32196
|
+
return layers.length;
|
|
32197
|
+
}
|
|
32198
|
+
/** Stream delivery never leaves layer count 0, so it never subscribes. */
|
|
32199
|
+
#reconcile() {
|
|
32200
|
+
const want = /* @__PURE__ */ new Map();
|
|
32201
|
+
for (const layer of this.#layers().slice(0, this.#layerCount)) {
|
|
32202
|
+
want.set(layer.repairTrackName, layer.priority);
|
|
32203
|
+
}
|
|
32204
|
+
const overlay = this.#source.repairOverlays?.[0];
|
|
32205
|
+
if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
|
|
32206
|
+
for (const [name, entry] of this.#subs) {
|
|
32207
|
+
if (!want.has(name)) {
|
|
32208
|
+
entry.sub.close();
|
|
32209
|
+
this.#subs.delete(name);
|
|
32210
|
+
}
|
|
32211
|
+
}
|
|
32212
|
+
for (const [name, priority] of want) {
|
|
32213
|
+
if (this.#subs.has(name)) continue;
|
|
32214
|
+
const entry = {};
|
|
32215
|
+
entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
|
|
32216
|
+
this.#subs.set(name, entry);
|
|
32217
|
+
}
|
|
32218
|
+
}
|
|
32219
|
+
/** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
|
|
32220
|
+
#onPacket(name, entry, packet) {
|
|
32221
|
+
if (this.#disposed || this.#subs.get(name) !== entry) {
|
|
32222
|
+
this.#staleDropped += 1;
|
|
32223
|
+
return;
|
|
32224
|
+
}
|
|
32225
|
+
this.#repairPackets += 1;
|
|
32226
|
+
this.#repairBytes += packet.byteLength;
|
|
32227
|
+
this.#opts.feed(packet, name);
|
|
32228
|
+
}
|
|
32229
|
+
#closeAll() {
|
|
32230
|
+
for (const entry of this.#subs.values()) entry.sub.close();
|
|
32231
|
+
this.#subs.clear();
|
|
32232
|
+
}
|
|
32233
|
+
};
|
|
32234
|
+
|
|
31924
32235
|
// src/repair-fetch.ts
|
|
31925
32236
|
var MOQT_PRIORITY_MAX = 255;
|
|
31926
32237
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
@@ -32566,6 +32877,7 @@ export {
|
|
|
32566
32877
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
32567
32878
|
REPAIR_PRIORITY_MAX,
|
|
32568
32879
|
REPAIR_PRIORITY_MIN,
|
|
32880
|
+
RepairLayerSubscriptions,
|
|
32569
32881
|
SOURCE_MEDIA_PRIORITY_MAX,
|
|
32570
32882
|
SampleCombineError,
|
|
32571
32883
|
SampleCombiner,
|
|
@@ -32590,6 +32902,7 @@ export {
|
|
|
32590
32902
|
catalogGroupDurationMs,
|
|
32591
32903
|
catalogKeyframeCadenceMs,
|
|
32592
32904
|
catalogRepairTarget,
|
|
32905
|
+
catalogTrackDeliveryMode,
|
|
32593
32906
|
catalogTrackFecUnavailable,
|
|
32594
32907
|
classifyH264Keyframe,
|
|
32595
32908
|
classifyHevcKeyframe,
|
|
@@ -32610,6 +32923,7 @@ export {
|
|
|
32610
32923
|
detectH264Keyframe,
|
|
32611
32924
|
detectHevcKeyframe,
|
|
32612
32925
|
driftCheck,
|
|
32926
|
+
encodeCmcdRecord,
|
|
32613
32927
|
encodeDataSegment,
|
|
32614
32928
|
encodeFecSourceInfo,
|
|
32615
32929
|
encodeLocTimestamp,
|