About Multi-camera viewer optimized for RTSP streams
0

Configure Feed

Select the types of activity you want to include in your feed.

at master 7.1 kB View raw
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