About Multi-camera viewer optimized for RTSP streams
1import ipaddress
2import socket
3
4import requests
5from PyQt6.QtCore import QThread, pyqtSignal
6from requests.auth import HTTPDigestAuth
7
8from camera_utils import _ssdp_discovery, _udp_reolink_probe, _ws_discovery
9from i18n import tr
10
11
12class CameraDiscoveryThread(QThread):
13 """Thread für automatische Kamera-Suche im Netzwerk"""
14 camera_found = pyqtSignal(dict) # {ip, name, model, ports, uid}
15 progress_update = pyqtSignal(int, str)
16 scan_complete = pyqtSignal(int)
17
18 def __init__(self, network_range, ports=None, username="admin", password=""):
19 super().__init__()
20 self.network_range = network_range
21 self.ports = ports or [554, 8000, 80, 8554] # Typische Reolink/RTSP Ports
22 self.username = username
23 self.password = password
24 self.running = False
25 self.found_cameras = []
26
27 def run(self):
28 """Netzwerk nach Kameras durchsuchen"""
29 self.running = True
30 self.found_cameras = []
31
32 try:
33 # 1. Multi-Discovery (UDP Broadcasts)
34 self.progress_update.emit(5, "Starte Netzwerk-Suche (UDP/WS/SSDP)...")
35
36 discovery_ips = set()
37
38 # Reolink BC Discovery
39 broadcast_results = _udp_reolink_probe("255.255.255.255", timeout=1.5)
40 if broadcast_results and isinstance(broadcast_results, list):
41 for info in broadcast_results:
42 discovery_ips.add(info['remote_ip'])
43 camera_info = {
44 'ip': info.get('remote_ip', ''),
45 'ports': [554, 8000, 9000],
46 'name': info.get('name', 'Reolink Camera'),
47 'model': info.get('model', 'Unknown'),
48 'manufacturer': "Reolink",
49 'uid': info.get('devNo', '') or info.get('serial', '')
50 }
51 if camera_info['ip'] not in [c['ip'] for c in self.found_cameras]:
52 self.found_cameras.append(camera_info)
53 self.camera_found.emit(camera_info)
54
55 # ONVIF Discovery
56 onvif_ips = _ws_discovery(timeout=1.0)
57 discovery_ips.update(onvif_ips)
58
59 # SSDP Discovery
60 ssdp_ips = _ssdp_discovery(timeout=1.0)
61 discovery_ips.update(ssdp_ips)
62
63 # Wenn wir Kameras über Broadcast gefunden haben, prüfen wir diese zuerst
64 for dip in discovery_ips:
65 if dip not in [c['ip'] for c in self.found_cameras]:
66 # Hole Details für diese IP
67 c_info = self._get_camera_info(dip, [80, 8000, 554, 9000])
68 if c_info:
69 self.found_cameras.append(c_info)
70 self.camera_found.emit(c_info)
71
72 network = ipaddress.ip_network(self.network_range, strict=False)
73 total_hosts = network.num_addresses - 2 # Ohne Netzwerk- und Broadcast-Adresse
74 checked = 0
75
76 for ip in network.hosts():
77 if not self.running:
78 break
79
80 ip_str = str(ip)
81 checked += 1
82 self.progress_update.emit(int((checked / total_hosts) * 100), tr("scan.checking", ip=ip_str))
83
84 # Schneller Port-Scan
85 open_ports = self._scan_ports(ip_str)
86
87 if open_ports:
88 # Versuche Kamera-Info abzurufen
89 camera_info = self._get_camera_info(ip_str, open_ports)
90 if camera_info:
91 self.found_cameras.append(camera_info)
92 self.camera_found.emit(camera_info)
93
94 self.scan_complete.emit(len(self.found_cameras))
95
96 except Exception as e:
97 self.progress_update.emit(100, tr("scan.error", error=str(e)))
98
99 def _scan_ports(self, ip, timeout=0.5):
100 """Schneller Port-Scan für bestimmte IP"""
101 open_ports = []
102
103 for port in self.ports:
104 if not self.running:
105 break
106
107 try:
108 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109 sock.settimeout(timeout)
110 result = sock.connect_ex((ip, port))
111 sock.close()
112
113 if result == 0:
114 open_ports.append(port)
115 except Exception:
116 pass
117
118 return open_ports
119
120 def _get_camera_info(self, ip, ports):
121 """Versuche Kamera-Informationen abzurufen"""
122 camera_info = {
123 'ip': ip,
124 'ports': ports,
125 'name': tr("camera.default_name.ip", ip=ip),
126 'model': tr("camera.meta.unknown"),
127 'manufacturer': tr("camera.meta.unknown"),
128 'uid': '',
129 }
130
131 # 1. Versuche UDP Reolink Probe (Port 9000) - am besten für UID & Standby
132 udp_info = _udp_reolink_probe(ip)
133 if udp_info:
134 camera_info['name'] = udp_info.get('name', camera_info['name'])
135 camera_info['model'] = udp_info.get('model', camera_info['model'])
136 camera_info['manufacturer'] = "Reolink"
137 camera_info['uid'] = udp_info.get('devNo', '') or udp_info.get('serial', '')
138 return camera_info
139
140 # 2. Versuche ONVIF/HTTP Zugriff
141 if 80 in ports or 8000 in ports:
142 for port in [80, 8000]:
143 if port in ports:
144 try:
145 # Reolink API Versuch
146 url = f"http://{ip}:{port}/api.cgi?cmd=GetDevInfo"
147 response = requests.get(
148 url,
149 auth=HTTPDigestAuth(self.username, self.password),
150 timeout=2
151 )
152
153 if response.status_code == 200:
154 data = response.json()
155 if isinstance(data, list) and len(data) > 0:
156 info = data[0].get('value', {}).get('DevInfo', {})
157 camera_info['name'] = info.get('name', camera_info['name'])
158 camera_info['model'] = info.get('model', camera_info['model'])
159 camera_info['manufacturer'] = "Reolink"
160 camera_info['uid'] = info.get('devNo', '') or info.get('serial', '')
161 return camera_info
162 except Exception:
163 pass
164
165 # Wenn HTTP nicht funktioniert, aber RTSP Port offen ist
166 if 554 in ports or 8554 in ports:
167 camera_info['manufacturer'] = tr("camera.meta.rtsp_camera")
168 return camera_info
169
170 return None
171
172 def stop(self):
173 """Scan stoppen"""
174 self.running = False