About Multi-camera viewer optimized for RTSP streams
1#!/usr/bin/env python3
2"""
3ReolinkProxy Manager fuer WildCam.
4
5Generiert reolinkproxy.env ausschliesslich aus camera_config.json.
6"""
7import json
8import sys
9from pathlib import Path
10
11from camera_utils import (
12 _parse_rtsp_url,
13 _reolinkproxy_camera_name,
14 _reolinkproxy_proxy_config,
15 _reolinkproxy_rtsp_url,
16)
17
18
19def parse_rtsp_url(rtsp_url):
20 host, port, username, password = _parse_rtsp_url(rtsp_url or "")
21 if not host:
22 return None
23 return {
24 "host": host,
25 "port": port,
26 "username": username or "",
27 "password": password or "",
28 "scheme": "rtsp",
29 }
30
31
32def camera_name(name):
33 return _reolinkproxy_camera_name(name)
34
35
36def is_proxy_url(rtsp_url):
37 info = parse_rtsp_url(rtsp_url or "")
38 return bool(info and info["host"] in ("localhost", "127.0.0.1") and info["port"] == 8554)
39
40
41def is_reolinkproxy_camera(camera):
42 proxy = camera.get("proxy") or {}
43 if proxy.get("type") == "reolinkproxy":
44 return True
45
46 url_info = parse_rtsp_url(camera.get("url", ""))
47 if url_info and url_info["port"] == 9000:
48 return True
49 return False
50
51
52def env_value(value):
53 value = "" if value is None else str(value)
54 value = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
55 return f'"{value}"'
56
57
58def make_proxy_from_rtsp(camera):
59 return _reolinkproxy_proxy_config(
60 rtsp_url=camera.get("url", ""),
61 name=camera.get("name", ""),
62 username="",
63 password="",
64 uid=camera.get("uid", ""),
65 model=camera.get("model", ""),
66 manufacturer=camera.get("manufacturer", ""),
67 )
68
69
70def write_env(cameras, output_path):
71 lines = [
72 "# Auto-generated by WildCam reolinkproxy_manager.py",
73 "# Existing WildCam RTSP paths are preserved with REOLINK_CAMERA_N_RTSP_PATH.",
74 "REOLINK_HEALTHCHECK_RTSP_ONLY=true",
75 "",
76 ]
77
78 proxy_cameras = [cam for cam in cameras if is_reolinkproxy_camera(cam)]
79 incomplete_proxy_cameras = [
80 cam for cam in cameras
81 if is_proxy_url(cam.get("url", "")) and (cam.get("proxy") or {}).get("type") != "reolinkproxy"
82 ]
83 if incomplete_proxy_cameras:
84 names = ", ".join(camera_name(cam.get("name", "")) for cam in incomplete_proxy_cameras)
85 print(f"Unvollstaendige Proxy-Konfiguration fuer: {names}")
86 print("Bitte in camera_config.json je Kamera einen proxy-Block mit host/username/password ergaenzen.")
87 return False
88 if not proxy_cameras:
89 print("Keine ReolinkProxy-Kameras gefunden.")
90 return False
91
92 print(f"{len(proxy_cameras)} ReolinkProxy-Kamera(s) gefunden:")
93
94 for index, cam in enumerate(proxy_cameras):
95 name = camera_name(cam.get("name", f"Camera_{index}"))
96 proxy = cam.get("proxy") or make_proxy_from_rtsp(cam) or {}
97
98 use_uid_only = bool(proxy.get("uid_only", False))
99 host = "" if use_uid_only else proxy.get("host", "")
100 port = int(proxy.get("port") or 9000)
101 username = proxy.get("username", "")
102 password = proxy.get("password", "")
103 stream = proxy.get("stream", "main")
104 uid = proxy.get("uid") or cam.get("uid", "")
105 battery = bool(proxy.get("battery", True))
106 pause_on_client = bool(proxy.get("pause_on_client", True))
107 idle_disconnect = bool(proxy.get("idle_disconnect", True))
108 idle_timeout = proxy.get("idle_timeout", "30s")
109
110 print(f" - {name}: host={host or 'UID-only'}, uid={uid or '-'}")
111
112 lines.extend(
113 [
114 f"REOLINK_CAMERA_{index}_NAME={env_value(name)}",
115 f"REOLINK_CAMERA_{index}_PORT={port}",
116 f"REOLINK_CAMERA_{index}_USERNAME={env_value(username)}",
117 f"REOLINK_CAMERA_{index}_PASSWORD={env_value(password)}",
118 f"REOLINK_CAMERA_{index}_STREAM={env_value(stream)}",
119 f"REOLINK_CAMERA_{index}_RTSP_PATH={env_value(f'{name}/mainStream')}",
120 f"REOLINK_CAMERA_{index}_BATTERY_CAMERA={str(battery).lower()}",
121 f"REOLINK_CAMERA_{index}_PAUSE_ON_CLIENT={str(pause_on_client).lower()}",
122 f"REOLINK_CAMERA_{index}_IDLE_DISCONNECT={str(idle_disconnect).lower()}",
123 f"REOLINK_CAMERA_{index}_IDLE_TIMEOUT={env_value(idle_timeout)}",
124 ]
125 )
126 if host:
127 lines.append(f"REOLINK_CAMERA_{index}_HOST={env_value(host)}")
128 if uid:
129 lines.append(f"REOLINK_CAMERA_{index}_UID={env_value(uid)}")
130 lines.append("")
131
132 output_path.write_text("\n".join(lines), encoding="utf-8")
133 print(f"{output_path} geschrieben.")
134 return True
135
136
137def update_camera_config(camera_config_path):
138 try:
139 config = json.loads(camera_config_path.read_text(encoding="utf-8"))
140 except Exception as e:
141 print(f"Fehler beim Laden der Config: {e}")
142 return False
143
144 updated = False
145 for cam in config.get("cameras", []):
146 if not is_reolinkproxy_camera(cam):
147 continue
148 name = camera_name(cam.get("name", ""))
149 if "proxy" not in cam:
150 proxy = make_proxy_from_rtsp(cam)
151 if proxy:
152 cam["proxy"] = proxy
153 updated = True
154 new_url = _reolinkproxy_rtsp_url(name)
155 if cam.get("url") != new_url:
156 print(f"{cam.get('name', name)}: {cam.get('url')} -> {new_url}")
157 cam["url"] = new_url
158 updated = True
159
160 if not updated:
161 return False
162
163 backup_path = camera_config_path.with_suffix(camera_config_path.suffix + ".backup")
164 backup_path.write_text(camera_config_path.read_text(encoding="utf-8"), encoding="utf-8")
165 camera_config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
166 print(f"{camera_config_path} aktualisiert, Backup: {backup_path}")
167 return True
168
169
170def main():
171 script_dir = Path(__file__).parent
172 config_path = script_dir / "camera_config.json"
173 env_path = script_dir / "reolinkproxy.env"
174
175 print("WildCam ReolinkProxy Manager")
176 print("=" * 50)
177
178 try:
179 config = json.loads(config_path.read_text(encoding="utf-8"))
180 except FileNotFoundError:
181 print(f"Fehler: {config_path} nicht gefunden.")
182 sys.exit(0)
183 except json.JSONDecodeError as e:
184 print(f"Fehler beim Parsen der Config: {e}")
185 sys.exit(1)
186
187 success = write_env(config.get("cameras", []), env_path)
188 if not success:
189 sys.exit(0)
190
191 if "--auto-update" in sys.argv or "--yes" in sys.argv:
192 update_camera_config(config_path)
193 else:
194 print("camera_config.json bleibt unveraendert. Nutze --auto-update zum Umschreiben alter Port-9000-URLs.")
195
196
197if __name__ == "__main__":
198 main()