#!/usr/bin/env python3 """ Enhanced webcam server that provides both RTSP streaming and HTTP snapshot endpoints Compatible with CMS UI requirements for camera configuration """ import cv2 import threading import time import logging import socket from http.server import BaseHTTPRequestHandler, HTTPServer import subprocess import sys import os # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("webcam_rtsp_server") # Global webcam capture object webcam_cap = None rtsp_process = None class WebcamHTTPHandler(BaseHTTPRequestHandler): """HTTP handler for snapshot requests""" def do_GET(self): if self.path == '/snapshot' or self.path == '/snapshot.jpg': try: # Capture fresh frame from webcam for each request ret, frame = webcam_cap.read() if ret and frame is not None: # Encode as JPEG success, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) if success: self.send_response(200) self.send_header('Content-Type', 'image/jpeg') self.send_header('Content-Length', str(len(buffer))) self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') self.send_header('Pragma', 'no-cache') self.send_header('Expires', '0') self.end_headers() self.wfile.write(buffer.tobytes()) logger.debug(f"Served webcam snapshot, size: {len(buffer)} bytes") return else: logger.error("Failed to encode frame as JPEG") else: logger.error("Failed to capture frame from webcam") # Send error response self.send_response(500) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'Failed to capture webcam frame') except Exception as e: logger.error(f"Error serving snapshot: {e}") self.send_response(500) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(f'Error: {str(e)}'.encode()) elif self.path == '/status': # Status endpoint for health checking self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() width = int(webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = webcam_cap.get(cv2.CAP_PROP_FPS) status = f'{{"status": "online", "width": {width}, "height": {height}, "fps": {fps}}}' self.wfile.write(status.encode()) else: # 404 for other paths self.send_response(404) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'Not Found - Available endpoints: /snapshot, /snapshot.jpg, /status') def log_message(self, format, *args): # Suppress default HTTP server logging to avoid spam pass def check_ffmpeg(): """Check if FFmpeg is available for RTSP streaming""" try: result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5) if result.returncode == 0: logger.info("FFmpeg found and working") return True except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): pass logger.warning("FFmpeg not found. RTSP streaming will not be available.") logger.info("To enable RTSP streaming, install FFmpeg:") logger.info(" Windows: Download from https://ffmpeg.org/download.html") logger.info(" Linux: sudo apt install ffmpeg") logger.info(" macOS: brew install ffmpeg") return False def get_windows_camera_name(): """Get the actual camera device name on Windows""" try: # List video devices using FFmpeg with proper encoding handling result = subprocess.run(['ffmpeg', '-f', 'dshow', '-list_devices', 'true', '-i', 'dummy'], capture_output=True, text=True, timeout=10, encoding='utf-8', errors='ignore') output = result.stderr # FFmpeg outputs device list to stderr # Look for video devices in the output lines = output.split('\n') video_devices = [] # Parse the output - look for lines with (video) that contain device names in quotes for line in lines: if '[dshow @' in line and '(video)' in line and '"' in line: # Extract device name between first pair of quotes start = line.find('"') + 1 end = line.find('"', start) if start > 0 and end > start: device_name = line[start:end] video_devices.append(device_name) logger.info(f"Found Windows video devices: {video_devices}") if video_devices: # Force use the first device (index 0) which is the Logitech HD webcam return video_devices[0] # This will be "罗技高清网络摄像机 C930c" else: logger.info("No devices found via FFmpeg detection, using fallback") # Fall through to fallback names except Exception as e: logger.debug(f"Failed to get Windows camera name: {e}") # Try common camera device names as fallback # Prioritize Integrated Camera since that's what's working now common_names = [ "Integrated Camera", # This is working for the current setup "USB Video Device", # Common name for USB cameras "USB2.0 Camera", "C930c", # Direct model name "HD Pro Webcam C930c", # Full Logitech name "Logitech", # Brand name "USB Camera", "Webcam" ] logger.info(f"Using fallback camera names: {common_names}") return common_names[0] # Return "Integrated Camera" first def start_rtsp_stream(webcam_index=0, rtsp_port=8554): """Start RTSP streaming using FFmpeg""" global rtsp_process if not check_ffmpeg(): return None try: # Get the actual camera device name for Windows if sys.platform.startswith('win'): camera_name = get_windows_camera_name() logger.info(f"Using Windows camera device: {camera_name}") # FFmpeg command to stream webcam via RTSP if sys.platform.startswith('win'): cmd = [ 'ffmpeg', '-f', 'dshow', '-i', f'video={camera_name}', # Use detected camera name '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', '-r', '30', '-s', '1280x720', '-f', 'rtsp', f'rtsp://localhost:{rtsp_port}/stream' ] elif sys.platform.startswith('linux'): cmd = [ 'ffmpeg', '-f', 'v4l2', '-i', f'/dev/video{webcam_index}', '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', '-r', '30', '-s', '1280x720', '-f', 'rtsp', f'rtsp://localhost:{rtsp_port}/stream' ] else: # macOS cmd = [ 'ffmpeg', '-f', 'avfoundation', '-i', f'{webcam_index}:', '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', '-r', '30', '-s', '1280x720', '-f', 'rtsp', f'rtsp://localhost:{rtsp_port}/stream' ] logger.info(f"Starting RTSP stream on rtsp://localhost:{rtsp_port}/stream") logger.info(f"FFmpeg command: {' '.join(cmd)}") rtsp_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Give FFmpeg a moment to start time.sleep(2) # Check if process is still running if rtsp_process.poll() is None: logger.info("RTSP streaming started successfully") return rtsp_process else: # Get error output if process failed stdout, stderr = rtsp_process.communicate(timeout=2) logger.error("RTSP streaming failed to start") logger.error(f"FFmpeg stdout: {stdout}") logger.error(f"FFmpeg stderr: {stderr}") return None except Exception as e: logger.error(f"Failed to start RTSP stream: {e}") return None def get_local_ip(): """Get the Wireguard IP address for external access""" # Use Wireguard IP for external access return "10.101.1.4" def main(): global webcam_cap, rtsp_process # Configuration - Force use index 0 for Logitech HD webcam webcam_index = 0 # Logitech HD webcam C930c (1920x1080@30fps) http_port = 8080 rtsp_port = 8554 logger.info("=== Webcam RTSP & HTTP Server ===") # Initialize webcam logger.info("Initializing webcam...") webcam_cap = cv2.VideoCapture(webcam_index) if not webcam_cap.isOpened(): logger.error(f"Failed to open webcam at index {webcam_index}") logger.info("Try different webcam indices (0, 1, 2, etc.)") return # Set webcam properties - Use high resolution for Logitech HD webcam webcam_cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) webcam_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) webcam_cap.set(cv2.CAP_PROP_FPS, 30) width = int(webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = webcam_cap.get(cv2.CAP_PROP_FPS) logger.info(f"Webcam initialized: {width}x{height} @ {fps}fps") # Get local IP for CMS configuration local_ip = get_local_ip() # Start RTSP streaming (optional, requires FFmpeg) rtsp_process = start_rtsp_stream(webcam_index, rtsp_port) # Start HTTP server for snapshots server_address = ('0.0.0.0', http_port) # Bind to all interfaces http_server = HTTPServer(server_address, WebcamHTTPHandler) logger.info("\n=== Server URLs for CMS Configuration ===") logger.info(f"HTTP Snapshot URL: http://{local_ip}:{http_port}/snapshot") if rtsp_process: logger.info(f"RTSP Stream URL: rtsp://{local_ip}:{rtsp_port}/stream") else: logger.info("RTSP Stream: Not available (FFmpeg not found)") logger.info("HTTP-only mode: Use Snapshot URL for camera input") logger.info(f"Status URL: http://{local_ip}:{http_port}/status") logger.info("\n=== CMS Configuration Suggestions ===") logger.info(f"Camera Identifier: webcam-local-01") logger.info(f"RTSP Stream URL: rtsp://{local_ip}:{rtsp_port}/stream") logger.info(f"Snapshot URL: http://{local_ip}:{http_port}/snapshot") logger.info(f"Snapshot Interval: 2000 (ms)") logger.info("\nPress Ctrl+C to stop all servers") try: # Start HTTP server http_server.serve_forever() except KeyboardInterrupt: logger.info("Shutting down servers...") finally: # Clean up if webcam_cap: webcam_cap.release() if rtsp_process: logger.info("Stopping RTSP stream...") rtsp_process.terminate() try: rtsp_process.wait(timeout=5) except subprocess.TimeoutExpired: rtsp_process.kill() http_server.server_close() logger.info("All servers stopped") if __name__ == "__main__": main()