#!/usr/bin/env python3 """ Multi-Camera Simulator Creates 4 virtual cameras using the same webcam with different ports Run this in 4 separate terminals to simulate real-world scenario with 4 cameras """ import cv2 import threading import time import logging import socket import sys import os import argparse import subprocess from http.server import BaseHTTPRequestHandler, HTTPServer # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] Camera%(camera_id)s: %(message)s" ) class MultiCameraHandler(BaseHTTPRequestHandler): """HTTP handler for camera snapshot requests with camera-specific branding""" def __init__(self, camera_id, webcam_cap, *args, **kwargs): self.camera_id = camera_id self.webcam_cap = webcam_cap super().__init__(*args, **kwargs) def do_GET(self): if self.path == '/snapshot' or self.path == '/snapshot.jpg': try: # Capture fresh frame from webcam ret, frame = self.webcam_cap.read() if ret and frame is not None: # Add camera branding overlay frame = self.add_camera_branding(frame) # 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()) return # 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: logging.error(f"Camera{self.camera_id}: 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(self.webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(self.webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = self.webcam_cap.get(cv2.CAP_PROP_FPS) status = f'{{"status": "online", "camera_id": "{self.camera_id}", "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 add_camera_branding(self, frame): """Add visual branding to differentiate cameras""" # Clone frame to avoid modifying original branded_frame = frame.copy() # Camera-specific colors and positions colors = { 1: (0, 255, 255), # Yellow 2: (255, 0, 255), # Magenta 3: (0, 255, 0), # Green 4: (255, 0, 0) # Blue } # Add colored border color = colors.get(self.camera_id, (255, 255, 255)) cv2.rectangle(branded_frame, (0, 0), (branded_frame.shape[1]-1, branded_frame.shape[0]-1), color, 10) # Add camera ID text font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 2 thickness = 3 # Camera ID text text = f"CAMERA {self.camera_id}" text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] text_x = 30 text_y = 60 # Add background rectangle for text cv2.rectangle(branded_frame, (text_x - 10, text_y - text_size[1] - 10), (text_x + text_size[0] + 10, text_y + 10), (0, 0, 0), -1) # Add text cv2.putText(branded_frame, text, (text_x, text_y), font, font_scale, color, thickness) # Add timestamp timestamp = time.strftime("%Y-%m-%d %H:%M:%S") ts_text = f"CAM{self.camera_id}: {timestamp}" cv2.putText(branded_frame, ts_text, (30, branded_frame.shape[0] - 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) return branded_frame def log_message(self, format, *args): # Suppress default HTTP server logging pass def create_handler_class(camera_id, webcam_cap): """Create a handler class with bound camera_id and webcam_cap""" def handler(*args, **kwargs): return MultiCameraHandler(camera_id, webcam_cap, *args, **kwargs) return handler def check_port_available(port): """Check if a port is available""" try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('localhost', port)) return True except OSError: return False def start_rtsp_stream(camera_id, webcam_index, rtsp_port): """Start RTSP streaming for a specific camera""" try: # Check if FFmpeg is available result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5) if result.returncode != 0: logging.warning(f"Camera{camera_id}: FFmpeg not available, RTSP disabled") return None except Exception: logging.warning(f"Camera{camera_id}: FFmpeg not available, RTSP disabled") return None try: # Get camera device name for Windows if sys.platform.startswith('win'): # Use the integrated camera camera_name = "Integrated Camera" # FFmpeg command to stream webcam via RTSP if sys.platform.startswith('win'): cmd = [ 'ffmpeg', '-f', 'dshow', '-i', f'video={camera_name}', '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', '-r', '15', # Lower FPS for multiple streams '-s', '1280x720', '-f', 'rtsp', f'rtsp://localhost:{rtsp_port}/stream' ] else: cmd = [ 'ffmpeg', '-f', 'v4l2', '-i', f'/dev/video{webcam_index}', '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', '-r', '15', '-s', '1280x720', '-f', 'rtsp', f'rtsp://localhost:{rtsp_port}/stream' ] logging.info(f"Camera{camera_id}: Starting RTSP stream on port {rtsp_port}") rtsp_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Give FFmpeg a moment to start time.sleep(2) if rtsp_process.poll() is None: logging.info(f"Camera{camera_id}: RTSP streaming started successfully") return rtsp_process else: logging.error(f"Camera{camera_id}: RTSP streaming failed to start") return None except Exception as e: logging.error(f"Camera{camera_id}: Failed to start RTSP stream: {e}") return None def main(): parser = argparse.ArgumentParser(description='Multi-Camera Simulator') parser.add_argument('camera_id', type=int, choices=[1, 2, 3, 4], help='Camera ID (1-4)') parser.add_argument('--webcam-index', type=int, default=0, help='Webcam device index (default: 0)') parser.add_argument('--base-port', type=int, default=8080, help='Base port for HTTP servers (default: 8080)') parser.add_argument('--rtsp-base-port', type=int, default=8550, help='Base port for RTSP servers (default: 8550)') parser.add_argument('--no-rtsp', action='store_true', help='Disable RTSP streaming (HTTP only)') args = parser.parse_args() camera_id = args.camera_id webcam_index = args.webcam_index # Calculate ports based on camera ID http_port = args.base_port + camera_id - 1 # 8080, 8081, 8082, 8083 rtsp_port = args.rtsp_base_port + camera_id - 1 # 8550, 8551, 8552, 8553 # Check if ports are available if not check_port_available(http_port): logging.error(f"Camera{camera_id}: HTTP port {http_port} is already in use") sys.exit(1) if not args.no_rtsp and not check_port_available(rtsp_port): logging.error(f"Camera{camera_id}: RTSP port {rtsp_port} is already in use") sys.exit(1) logging.info(f"=== Starting Camera {camera_id} Simulator ===") # Initialize webcam logging.info(f"Camera{camera_id}: Initializing webcam at index {webcam_index}...") webcam_cap = cv2.VideoCapture(webcam_index) if not webcam_cap.isOpened(): logging.error(f"Camera{camera_id}: Failed to open webcam at index {webcam_index}") sys.exit(1) # Set webcam properties webcam_cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) webcam_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) 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) logging.info(f"Camera{camera_id}: Webcam initialized: {width}x{height} @ {fps}fps") # Start RTSP streaming (if enabled) rtsp_process = None if not args.no_rtsp: rtsp_process = start_rtsp_stream(camera_id, webcam_index, rtsp_port) # Start HTTP server server_address = ('0.0.0.0', http_port) handler_class = create_handler_class(camera_id, webcam_cap) http_server = HTTPServer(server_address, handler_class) # Get local IP local_ip = "10.101.1.4" # Wireguard IP logging.info(f"\n=== Camera {camera_id} URLs ===") logging.info(f"HTTP Snapshot: http://{local_ip}:{http_port}/snapshot") logging.info(f"Status: http://{local_ip}:{http_port}/status") if rtsp_process: logging.info(f"RTSP Stream: rtsp://{local_ip}:{rtsp_port}/stream") else: logging.info("RTSP Stream: Disabled") logging.info(f"\n=== CMS Configuration for Camera {camera_id} ===") logging.info(f"Camera Identifier: webcam-camera-0{camera_id}") logging.info(f"Snapshot URL: http://{local_ip}:{http_port}/snapshot") logging.info(f"Snapshot Interval: 2000") if rtsp_process: logging.info(f"RTSP URL: rtsp://{local_ip}:{rtsp_port}/stream") logging.info(f"\nCamera {camera_id} is ready! Press Ctrl+C to stop") try: # Start HTTP server http_server.serve_forever() except KeyboardInterrupt: logging.info(f"Camera{camera_id}: Shutting down...") finally: # Clean up if webcam_cap: webcam_cap.release() if rtsp_process: logging.info(f"Camera{camera_id}: Stopping RTSP stream...") rtsp_process.terminate() try: rtsp_process.wait(timeout=5) except subprocess.TimeoutExpired: rtsp_process.kill() http_server.server_close() logging.info(f"Camera{camera_id}: Stopped") if __name__ == "__main__": main()