python-detector-worker/webcam_rtsp_server.py
2025-08-20 21:26:54 +07:00

325 lines
No EOL
12 KiB
Python

#!/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()