fix one machine only

This commit is contained in:
2026-04-30 21:29:11 +07:00
parent 5e1f529ed7
commit 176e6bdb8f
8 changed files with 185 additions and 149 deletions
+37 -70
View File
@@ -5,7 +5,6 @@ import logging
import subprocess
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
import httpx
import yaml
@@ -17,6 +16,7 @@ from .config_store import default_store
from .mediamtx_client import MediaMTXClient
from .models import (
AppConfig,
AppConfigUpdate,
Camera,
MediaMTXAddCameraRequest,
MediaMTXCamera,
@@ -50,51 +50,11 @@ app.add_middleware(
store = default_store()
def _extract_port(address: str, fallback: int) -> int:
if not address:
return fallback
text = str(address).strip()
if text.startswith(":"):
text = text[1:]
if ":" in text:
text = text.rsplit(":", 1)[-1]
try:
value = int(text)
return value if 1 <= value <= 65535 else fallback
except ValueError:
return fallback
def _host_from_url(url: str, fallback: str) -> str:
try:
parsed = urlparse(url)
host = (parsed.hostname or "").strip()
if host and host not in {"0.0.0.0", "::"}:
return host
except ValueError:
pass
return fallback
def _extract_host(address: str, fallback: str) -> str:
text = str(address or "").strip()
if not text:
return fallback
if text.startswith(":"):
return fallback
if "://" in text:
return _host_from_url(text, fallback)
if text.startswith("[") and "]" in text:
host = text[1 : text.index("]")]
return host or fallback
if ":" in text:
host = text.rsplit(":", 1)[0].strip()
if host and host not in {"0.0.0.0", "::"}:
return host
return fallback
if text in {"0.0.0.0", "::"}:
return fallback
return text
def _clean_text(value: Optional[str]) -> Optional[str]:
if value is None:
return None
cleaned = str(value).strip().strip("`'\"")
return cleaned or None
def _load_mediamtx_yml() -> dict:
@@ -111,33 +71,24 @@ def _save_mediamtx_yml(data: dict) -> None:
)
def _build_mediamtx_view(data: dict, current_cfg: Optional[AppConfig] = None) -> MediaMTXConfigView:
api_port = _extract_port(str(data.get("apiAddress", ":9997")), 9997)
webrtc_port = _extract_port(str(data.get("webrtcAddress", ":8889")), 8889)
current_api_host = _host_from_url((current_cfg.mediamtx_api_url if current_cfg else ""), "127.0.0.1")
current_webrtc_host = _host_from_url((current_cfg.mediamtx_webrtc_url if current_cfg else ""), "127.0.0.1")
api_host = _extract_host(str(data.get("apiAddress", ":9997")), current_api_host)
hosts = data.get("webrtcAdditionalHosts") or []
host = hosts[0] if isinstance(hosts, list) and hosts else _extract_host(str(data.get("webrtcAddress", ":8889")), current_webrtc_host)
def _build_mediamtx_view(data: dict, cfg: AppConfig) -> MediaMTXConfigView:
path_defaults = data.get("pathDefaults") or {}
record_enabled = bool(path_defaults.get("record", False))
cameras: list[MediaMTXCamera] = []
paths = data.get("paths") or {}
if isinstance(paths, dict):
for name, cfg in paths.items():
if not isinstance(cfg, dict):
for name, path_cfg in paths.items():
if not isinstance(path_cfg, dict):
continue
source = cfg.get("source")
source = path_cfg.get("source")
if isinstance(source, str) and source.strip():
cameras.append(MediaMTXCamera(name=str(name), rtsp_url=source))
cameras.sort(key=lambda x: x.name)
return MediaMTXConfigView(
api_url=f"http://{api_host}:{api_port}",
webrtc_url=f"http://{host}:{webrtc_port}",
api_url=cfg.mediamtx_api_url,
webrtc_url=cfg.mediamtx_webrtc_url,
record_enabled=record_enabled,
cameras=cameras,
)
@@ -146,11 +97,10 @@ def _build_mediamtx_view(data: dict, current_cfg: Optional[AppConfig] = None) ->
async def _sync_app_config_from_mediamtx() -> AppConfig:
cfg = await store.load()
data = _load_mediamtx_yml()
view = _build_mediamtx_view(data, cfg)
cfg.mediamtx_api_url = view.api_url
cfg.mediamtx_webrtc_url = view.webrtc_url
cfg.cameras = [Camera(name=c.name, rtsp_url=c.rtsp_url) for c in view.cameras]
cfg.cameras = [
Camera(name=c.name, rtsp_url=c.rtsp_url)
for c in _build_mediamtx_view(data, cfg).cameras
]
await store.save(cfg)
return cfg
@@ -274,14 +224,29 @@ async def get_config() -> AppConfig:
@app.get("/api/mediamtx/config")
async def get_mediamtx_config() -> MediaMTXConfigView:
cfg = await store.load()
data = _load_mediamtx_yml()
view = _build_mediamtx_view(data, await store.load())
view = _build_mediamtx_view(data, cfg)
await _sync_app_config_from_mediamtx()
return view
@app.post("/api/config/basic")
async def update_basic_config(payload: AppConfigUpdate) -> AppConfig:
cfg = await store.load()
cfg.mediamtx_api_url = _clean_text(payload.mediamtx_api_url) or cfg.mediamtx_api_url
cfg.mediamtx_webrtc_url = _clean_text(payload.mediamtx_webrtc_url) or cfg.mediamtx_webrtc_url
cfg.mediamtx_api_user = _clean_text(payload.mediamtx_api_user)
cfg.mediamtx_api_pass = _clean_text(payload.mediamtx_api_pass)
cfg.recordings_dir = _clean_text(payload.recordings_dir) or cfg.recordings_dir
cfg.api_port = payload.api_port
await store.save(cfg)
return await _sync_app_config_from_mediamtx()
@app.post("/api/mediamtx/cameras")
async def add_mediamtx_camera(payload: MediaMTXAddCameraRequest) -> MediaMTXConfigView:
cfg = await store.load()
data = _load_mediamtx_yml()
paths = data.setdefault("paths", {})
if not isinstance(paths, dict):
@@ -295,11 +260,12 @@ async def add_mediamtx_camera(payload: MediaMTXAddCameraRequest) -> MediaMTXConf
paths[name] = {"source": payload.rtsp_url.strip()}
_save_mediamtx_yml(data)
await _sync_app_config_from_mediamtx()
return _build_mediamtx_view(data, await store.load())
return _build_mediamtx_view(data, cfg)
@app.delete("/api/mediamtx/cameras/{name}")
async def delete_mediamtx_camera(name: str) -> MediaMTXConfigView:
cfg = await store.load()
data = _load_mediamtx_yml()
paths = data.get("paths") or {}
if not isinstance(paths, dict) or name not in paths:
@@ -308,11 +274,12 @@ async def delete_mediamtx_camera(name: str) -> MediaMTXConfigView:
data["paths"] = paths
_save_mediamtx_yml(data)
await _sync_app_config_from_mediamtx()
return _build_mediamtx_view(data, await store.load())
return _build_mediamtx_view(data, cfg)
@app.post("/api/mediamtx/recording")
async def set_mediamtx_recording(data: RecordingToggle) -> MediaMTXConfigView:
cfg = await store.load()
payload = _load_mediamtx_yml()
path_defaults = payload.setdefault("pathDefaults", {})
if not isinstance(path_defaults, dict):
@@ -320,7 +287,7 @@ async def set_mediamtx_recording(data: RecordingToggle) -> MediaMTXConfigView:
path_defaults["record"] = bool(data.enabled)
payload["pathDefaults"] = path_defaults
_save_mediamtx_yml(payload)
return _build_mediamtx_view(payload, await store.load())
return _build_mediamtx_view(payload, cfg)
@app.post("/api/mediamtx/restart")