video_monitor/app/utils/Config.py
2026-09-04 18:16:14 +08:00

334 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Monitor 启动配置 — 所有可配置项均来自 config.json后台「启动配置」页可编辑。"""
import json
import os
from urllib.parse import quote
from framework.settings import BASE_DIR
from monitor_runtime.paths import RESOURCE_ROOT, DESKTOP
from app.utils.Secrets import get_runtime_secret
def _resolve_path(path, base=None):
"""相对路径基于项目根目录 BASE_DIR 解析为绝对路径。"""
base = base or str(BASE_DIR)
if not path:
return ""
p = str(path).strip()
if not p:
return ""
if os.path.isabs(p):
return os.path.normpath(p)
return os.path.normpath(os.path.join(base, p.replace("\\", "/")))
def _bool(v, default=False):
if v is None:
return default
if isinstance(v, bool):
return v
if isinstance(v, (int, float)):
return v != 0
return str(v).strip().lower() in ("1", "true", "yes", "on")
def _int(v, default=0):
try:
return int(v)
except (TypeError, ValueError):
return default
def _float(v, default=0.0):
try:
return float(v)
except (TypeError, ValueError):
return default
class Config:
def __init__(self, filepath):
self.__filepath = filepath
config_data = None
for encoding in ["utf-8", "gbk"]:
try:
with open(filepath, "r", encoding=encoding) as f:
config_data = json.loads(f.read())
break
except Exception as e:
print("Config.__init__() error:%s,encoding=%s|%s" % (str(e), encoding, str(filepath)))
if not config_data:
raise Exception("Config.__init__() read %s error" % str(filepath))
self.__config_data = config_data
self._apply(config_data)
def _apply(self, config_data):
self.__config_data_str = str(config_data)
base = str(BASE_DIR)
# —— 基础 ——
# Internal credentials are never sourced from the web-editable config.
self.internalApiSecret = get_runtime_secret("internal_api_secret")
self.internalHost = "127.0.0.1"
self.externalHost = config_data.get("host", "127.0.0.1")
self.adminPort = _int(config_data.get("adminPort"), 10001)
self.logDebug = _bool(config_data.get("logDebug"), False)
self.isEnableLoginCaptcha = _bool(config_data.get("isEnableLoginCaptcha"), True)
self.isEnableUpdatePopup = _bool(config_data.get("isEnableUpdatePopup"), False)
# Both flags require an explicit local opt-in. Endpoints are environment-only.
self.telemetryEnabled = _bool(config_data.get("telemetryEnabled"), False)
self.updateCheckEnabled = _bool(config_data.get("updateCheckEnabled"), False)
self.autoAddStreamProxy = _bool(config_data.get("autoAddStreamProxy"), True)
self.autoAddStreamProxySleep = _int(config_data.get("autoAddStreamProxySleep"), 15)
# —— 路径与工具 ——
self.ffmpeg = str(os.environ.get("MONITOR_FFMPEG") or config_data.get("ffmpeg", "ffmpeg") or "ffmpeg").strip()
if DESKTOP:
self.ffmpeg = str(RESOURCE_ROOT / "tools" / "ffmpeg.exe")
self.fontPath = _resolve_path(config_data.get("fontPath", ""), str(RESOURCE_ROOT))
self.uploadDir = _resolve_path(config_data.get("uploadDir", "static/upload"), base)
self.storageDir = _resolve_path(config_data.get("storageDir", "static/storage"), base)
# —— 流媒体 ZLM ——
self.mediaHttpPort = _int(config_data.get("mediaHttpPort"), 10002)
self.mediaRtspPort = _int(config_data.get("mediaRtspPort"), 10554)
self.mediaRtmpPort = _int(config_data.get("mediaRtmpPort"), 10935)
self.isEnableMediaProxyRtmp = _bool(config_data.get("isEnableMediaProxyRtmp"), False)
self.mediaSecret = get_runtime_secret("media_secret")
self.mediaStartPath = _resolve_path(config_data.get("mediaStartPath", ""), str(RESOURCE_ROOT))
if DESKTOP:
self.mediaStartPath = str(RESOURCE_ROOT / "zlm" / "monitor_zlm.exe")
self.mediaStartConfigPath = _resolve_path(config_data.get("mediaStartConfigPath", ""), base)
self.autoStartMedia = _bool(config_data.get("autoStartMedia"), False)
if DESKTOP:
self.autoStartMedia = True
self.adminHost = "http://" + self.internalHost + ":" + str(self.adminPort)
self.mediaHttpHost = "http://" + self.internalHost + ":" + str(self.mediaHttpPort)
# —— 视频分析 ——
self.analysisTargetFps = _int(config_data.get("analysisTargetFps"), 5)
self.analysisConfThreshold = _float(config_data.get("analysisConfThreshold"), 0.4)
self.analysisProcessMode = _int(config_data.get("analysisProcessMode"), 1)
self.analysisSharedInference = _bool(config_data.get("analysisSharedInference"), True)
self.analysisInferenceWorkers = max(1, _int(config_data.get("analysisInferenceWorkers"), 1))
# —— 录像 ——
self.recordingEnabled = _bool(config_data.get("recordingEnabled"), False)
self.recordingSegmentSeconds = max(60, _int(config_data.get("recordingSegmentSeconds"), 600))
self.recordingRetainDays = max(0, _int(config_data.get("recordingRetainDays"), 7))
self.recordingRetainGb = max(0.0, _float(config_data.get("recordingRetainGb"), 0.0))
# —— GB28181 SIP ——
__sip = config_data.get("sipServer") or {}
self.sipServer = {
"sipServerIp": str(__sip.get("sipServerIp", "127.0.0.1")).strip(),
"sipServerPort": _int(__sip.get("sipServerPort"), 15060),
"sipServerNonce": get_runtime_secret("sip_server_nonce"),
"sipTransferMode": _int(__sip.get("sipTransferMode"), 0),
"sipServerId": str(__sip.get("sipServerId", "34020000002000000001")).strip(),
"sipServerRealm": str(__sip.get("sipServerRealm", "3402000000")).strip(),
"sipServerPass": get_runtime_secret("sip_server_password"),
"sipServerTimeout": _int(__sip.get("sipServerTimeout"), 1800),
"sipServerExpiry": _int(__sip.get("sipServerExpiry"), 3600),
"rtpTransferMode": _int(__sip.get("rtpTransferMode"), 0),
"rtpTransferAudioType": _int(__sip.get("rtpTransferAudioType"), 0),
"autoInviteAfterRecCateLog": _bool(__sip.get("autoInviteAfterRecCateLog"), True),
}
self._ensure_storage_dirs()
self._sync_media_server_config()
def _sync_media_server_config(self):
"""Inject runtime-only secrets into the selected ZLM config.
ZLM supports a static hook URL but cannot produce our per-request HMAC,
so its two hooks carry the high-entropy token and are additionally
restricted to a direct loopback peer by Django middleware.
"""
source_path = self.mediaStartConfigPath
if not source_path or not os.path.isfile(source_path):
return
try:
lines = None
source_encoding = "utf-8"
for encoding in ("utf-8", "gbk"):
try:
with open(source_path, "r", encoding=encoding) as f:
lines = f.readlines()
source_encoding = encoding
break
except UnicodeDecodeError:
continue
if lines is None:
raise RuntimeError("unsupported config encoding")
section = ""
token = quote(self.internalApiSecret, safe="")
hook_urls = {
"on_publish": "http://127.0.0.1:%d/inner/on_publish?token=%s" % (self.adminPort, token),
"on_stream_not_found": "http://127.0.0.1:%d/inner/on_stream_not_found?token=%s" % (self.adminPort, token),
}
output = []
for line in lines:
stripped = line.strip()
if stripped.startswith("[") and stripped.endswith("]"):
section = stripped[1:-1].strip().lower()
if "=" in line and not stripped.startswith(";"):
key = line.split("=", 1)[0].strip()
if section == "api" and key == "secret":
newline = "secret=%s\n" % self.mediaSecret
line = newline
elif section == "hook" and key in hook_urls:
newline = "%s=%s\n" % (key, hook_urls[key])
line = newline
output.append(line)
runtime_dir = os.path.join(str(BASE_DIR), ".runtime")
os.makedirs(runtime_dir, exist_ok=True)
runtime_path = os.path.join(runtime_dir, "zlm-config.ini")
temp = runtime_path + ".tmp"
with open(temp, "w", encoding=source_encoding, newline="") as f:
f.writelines(output)
os.replace(temp, runtime_path)
self.mediaStartConfigPath = runtime_path
except Exception as e:
raise RuntimeError("failed to secure ZLM config %s: %s" % (source_path, e))
def _ensure_storage_dirs(self):
for d in (self.uploadDir, self.storageDir):
if d and not os.path.exists(d):
os.makedirs(d, exist_ok=True)
self.uploadAlgorithmWeightDir = os.path.join(self.uploadDir, "weight")
self.uploadAudioDir = os.path.join(self.uploadDir, "audio")
self.uploadAudioDir_www = "/upload/audio/"
self.storageTempDir = os.path.join(self.storageDir, "temp")
self.storageAlarmDir = os.path.join(self.storageDir, "alarm")
self.storageSnapshotsDir = os.path.join(self.storageDir, "snapshots")
self.storageRecordDir = os.path.join(self.storageDir, "record")
for d in (self.storageTempDir, self.storageAlarmDir, self.storageSnapshotsDir, self.storageRecordDir):
if not os.path.exists(d):
os.makedirs(d, exist_ok=True)
self.storageDir_www = "/storage/openAccess?filename="
def getStr(self):
return json.dumps(self.to_dict(), ensure_ascii=False)
def to_dict(self, include_secrets=False):
"""供启动配置页 / API 使用的完整配置快照。"""
d = dict(self.__config_data)
d.setdefault("sipServer", {})
d.pop("safe", None)
d.pop("internalApiSecret", None)
d.pop("mediaSecret", None)
# 布尔/int 与内存态保持一致
d["host"] = self.externalHost
d["autoAddStreamProxy"] = self.autoAddStreamProxy
d["autoAddStreamProxySleep"] = self.autoAddStreamProxySleep
d["isEnableLoginCaptcha"] = self.isEnableLoginCaptcha
d["logDebug"] = self.logDebug
d["isEnableUpdatePopup"] = self.isEnableUpdatePopup
d["telemetryEnabled"] = self.telemetryEnabled
d["updateCheckEnabled"] = self.updateCheckEnabled
d["isEnableMediaProxyRtmp"] = self.isEnableMediaProxyRtmp
d["autoStartMedia"] = self.autoStartMedia
d["analysisSharedInference"] = self.analysisSharedInference
d["recordingEnabled"] = self.recordingEnabled
d["sipServer"] = dict(self.sipServer)
d["sipServer"].pop("sipServerPass", None)
d["sipServer"].pop("sipServerNonce", None)
if include_secrets:
d["internalApiSecret"] = self.internalApiSecret
d["mediaSecret"] = self.mediaSecret
d["sipServer"]["sipServerPass"] = self.sipServer["sipServerPass"]
d["sipServer"]["sipServerNonce"] = self.sipServer["sipServerNonce"]
return d
def save_from_web(self, params):
"""合并 Web 表单并写回 config.json保留未在表单中的键"""
data = dict(self.__config_data)
p = params or {}
def _set(key, val):
data[key] = val
for key in ("ffmpeg",):
if key in p:
_set(key, str(p.get(key) or "").strip())
if "host" in p:
_set("host", str(p.get("host") or "").strip())
if "uploadDir" in p:
_set("uploadDir", str(p.get("uploadDir") or "").strip())
if "storageDir" in p:
_set("storageDir", str(p.get("storageDir") or "").strip())
if "fontPath" in p:
_set("fontPath", str(p.get("fontPath") or "").strip())
if "mediaStartPath" in p:
_set("mediaStartPath", str(p.get("mediaStartPath") or "").strip())
if "mediaStartConfigPath" in p:
_set("mediaStartConfigPath", str(p.get("mediaStartConfigPath") or "").strip())
int_keys = (
"adminPort", "mediaHttpPort", "mediaRtspPort", "mediaRtmpPort",
"autoAddStreamProxySleep",
"analysisTargetFps", "analysisProcessMode", "analysisInferenceWorkers",
"recordingSegmentSeconds", "recordingRetainDays",
)
for key in int_keys:
if key in p:
_set(key, _int(p.get(key), data.get(key)))
float_keys = ("analysisConfThreshold", "recordingRetainGb")
for key in float_keys:
if key in p:
_set(key, _float(p.get(key), data.get(key)))
bool_keys = (
"autoAddStreamProxy", "isEnableLoginCaptcha", "logDebug", "isEnableUpdatePopup",
"telemetryEnabled", "updateCheckEnabled",
"isEnableMediaProxyRtmp", "autoStartMedia",
"analysisSharedInference", "recordingEnabled",
)
for key in bool_keys:
if key in p:
_set(key, _bool(p.get(key), _bool(data.get(key))))
sip = dict(data.get("sipServer") or {})
sip_map = {
"sipServerIp": "sipServerIp",
"sipServerPort": ("sipServerPort", _int),
"sipTransferMode": ("sipTransferMode", _int),
"sipServerId": "sipServerId",
"sipServerRealm": "sipServerRealm",
"sipServerTimeout": ("sipServerTimeout", _int),
"sipServerExpiry": ("sipServerExpiry", _int),
"rtpTransferMode": ("rtpTransferMode", _int),
"rtpTransferAudioType": ("rtpTransferAudioType", _int),
"autoInviteAfterRecCateLog": ("autoInviteAfterRecCateLog", _bool),
}
for param_key, spec in sip_map.items():
if param_key not in p:
continue
if isinstance(spec, tuple):
sip_key, conv = spec
sip[sip_key] = conv(p.get(param_key), sip.get(sip_key))
else:
sip[spec] = str(p.get(param_key) or "").strip()
data["sipServer"] = sip
for _k in (
"install", "code", "name", "describe", "safe", "internalApiSecret",
"mediaSecret",
):
data.pop(_k, None)
for _k in ("sipServerPass", "sipServerNonce"):
data["sipServer"].pop(_k, None)
with open(self.__filepath, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
self.__config_data = data
self._apply(data)