Merge pull request 'dev' (#17) from dev into main
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build Worker Base and Application Images / check-base-changes (push) Successful in 8s
				
			
		
			
				
	
				Build Worker Base and Application Images / build-base (push) Successful in 9m7s
				
			
		
			
				
	
				Build Worker Base and Application Images / build-docker (push) Successful in 4m3s
				
			
		
			
				
	
				Build Worker Base and Application Images / deploy-stack (push) Successful in 29s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build Worker Base and Application Images / check-base-changes (push) Successful in 8s
				
			Build Worker Base and Application Images / build-base (push) Successful in 9m7s
				
			Build Worker Base and Application Images / build-docker (push) Successful in 4m3s
				
			Build Worker Base and Application Images / deploy-stack (push) Successful in 29s
				
			Reviewed-on: #17
This commit is contained in:
		
						commit
						d663aaa446
					
				
					 11 changed files with 871 additions and 218 deletions
				
			
		
							
								
								
									
										10
									
								
								.claude/settings.local.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.claude/settings.local.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
{
 | 
			
		||||
  "permissions": {
 | 
			
		||||
    "allow": [
 | 
			
		||||
      "Bash(dir:*)",
 | 
			
		||||
      "WebSearch"
 | 
			
		||||
    ],
 | 
			
		||||
    "deny": [],
 | 
			
		||||
    "ask": []
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								Dockerfile.base
									
										
									
									
									
								
							
							
						
						
									
										124
									
								
								Dockerfile.base
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,21 +1,123 @@
 | 
			
		|||
# Base image with all ML dependencies
 | 
			
		||||
# Base image with complete ML and hardware acceleration stack
 | 
			
		||||
FROM pytorch/pytorch:2.8.0-cuda12.6-cudnn9-runtime
 | 
			
		||||
 | 
			
		||||
# Install system dependencies
 | 
			
		||||
RUN apt update && apt install -y \
 | 
			
		||||
    libgl1 \
 | 
			
		||||
# Install build dependencies and system libraries
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
    # Build tools
 | 
			
		||||
    build-essential \
 | 
			
		||||
    cmake \
 | 
			
		||||
    git \
 | 
			
		||||
    pkg-config \
 | 
			
		||||
    wget \
 | 
			
		||||
    unzip \
 | 
			
		||||
    yasm \
 | 
			
		||||
    nasm \
 | 
			
		||||
    # Additional dependencies for FFmpeg/NVIDIA build
 | 
			
		||||
    libtool \
 | 
			
		||||
    libc6 \
 | 
			
		||||
    libc6-dev \
 | 
			
		||||
    libnuma1 \
 | 
			
		||||
    libnuma-dev \
 | 
			
		||||
    # Essential compilation libraries
 | 
			
		||||
    gcc \
 | 
			
		||||
    g++ \
 | 
			
		||||
    libc6-dev \
 | 
			
		||||
    linux-libc-dev \
 | 
			
		||||
    # System libraries
 | 
			
		||||
    libgl1-mesa-glx \
 | 
			
		||||
    libglib2.0-0 \
 | 
			
		||||
    libgstreamer1.0-0 \
 | 
			
		||||
    libgtk-3-0 \
 | 
			
		||||
    libavcodec58 \
 | 
			
		||||
    libavformat58 \
 | 
			
		||||
    libswscale5 \
 | 
			
		||||
    libgomp1 \
 | 
			
		||||
    # Core media libraries (essential ones only)
 | 
			
		||||
    libjpeg-dev \
 | 
			
		||||
    libpng-dev \
 | 
			
		||||
    libx264-dev \
 | 
			
		||||
    libx265-dev \
 | 
			
		||||
    libvpx-dev \
 | 
			
		||||
    libmp3lame-dev \
 | 
			
		||||
    libv4l-dev \
 | 
			
		||||
    # TurboJPEG for fast JPEG encoding
 | 
			
		||||
    libturbojpeg0-dev \
 | 
			
		||||
    # Python development
 | 
			
		||||
    python3-dev \
 | 
			
		||||
    python3-numpy \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Copy and install base requirements (ML dependencies that rarely change)
 | 
			
		||||
# Add NVIDIA CUDA repository and install minimal development tools
 | 
			
		||||
RUN apt-get update && apt-get install -y wget gnupg && \
 | 
			
		||||
    wget -O - https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/3bf863cc.pub | apt-key add - && \
 | 
			
		||||
    echo "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \
 | 
			
		||||
    apt-get update && \
 | 
			
		||||
    apt-get install -y \
 | 
			
		||||
    cuda-nvcc-12-6 \
 | 
			
		||||
    cuda-cudart-dev-12-6 \
 | 
			
		||||
    libnpp-dev-12-6 \
 | 
			
		||||
    && apt-get remove -y wget gnupg && \
 | 
			
		||||
    apt-get autoremove -y && \
 | 
			
		||||
    rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Ensure CUDA paths are available
 | 
			
		||||
ENV PATH="/usr/local/cuda/bin:${PATH}"
 | 
			
		||||
ENV LD_LIBRARY_PATH="/usr/local/cuda/lib64:${LD_LIBRARY_PATH}"
 | 
			
		||||
 | 
			
		||||
# Install NVIDIA Video Codec SDK headers (official method)
 | 
			
		||||
RUN cd /tmp && \
 | 
			
		||||
    git clone https://git.videolan.org/git/ffmpeg/nv-codec-headers.git && \
 | 
			
		||||
    cd nv-codec-headers && \
 | 
			
		||||
    make install && \
 | 
			
		||||
    cd / && rm -rf /tmp/*
 | 
			
		||||
 | 
			
		||||
# Build FFmpeg from source with NVIDIA CUDA support
 | 
			
		||||
RUN cd /tmp && \
 | 
			
		||||
    echo "Building FFmpeg with NVIDIA CUDA support..." && \
 | 
			
		||||
    # Download FFmpeg source (official method)
 | 
			
		||||
    git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg/ && \
 | 
			
		||||
    cd ffmpeg && \
 | 
			
		||||
    # Configure with NVIDIA support (simplified to avoid configure issues)
 | 
			
		||||
    ./configure \
 | 
			
		||||
        --prefix=/usr/local \
 | 
			
		||||
        --enable-shared \
 | 
			
		||||
        --disable-static \
 | 
			
		||||
        --enable-nonfree \
 | 
			
		||||
        --enable-gpl \
 | 
			
		||||
        --enable-cuda-nvcc \
 | 
			
		||||
        --enable-cuvid \
 | 
			
		||||
        --enable-nvdec \
 | 
			
		||||
        --enable-nvenc \
 | 
			
		||||
        --enable-libnpp \
 | 
			
		||||
        --extra-cflags=-I/usr/local/cuda/include \
 | 
			
		||||
        --extra-ldflags=-L/usr/local/cuda/lib64 \
 | 
			
		||||
        --enable-libx264 \
 | 
			
		||||
        --enable-libx265 \
 | 
			
		||||
        --enable-libvpx \
 | 
			
		||||
        --enable-libmp3lame && \
 | 
			
		||||
    # Build and install
 | 
			
		||||
    make -j$(nproc) && \
 | 
			
		||||
    make install && \
 | 
			
		||||
    ldconfig && \
 | 
			
		||||
    # Verify CUVID decoders are available
 | 
			
		||||
    echo "=== Verifying FFmpeg CUVID Support ===" && \
 | 
			
		||||
    (ffmpeg -hide_banner -decoders 2>/dev/null | grep cuvid || echo "No CUVID decoders found") && \
 | 
			
		||||
    echo "=== Verifying FFmpeg NVENC Support ===" && \
 | 
			
		||||
    (ffmpeg -hide_banner -encoders 2>/dev/null | grep nvenc || echo "No NVENC encoders found") && \
 | 
			
		||||
    cd / && rm -rf /tmp/*
 | 
			
		||||
 | 
			
		||||
# Set environment variables for maximum hardware acceleration
 | 
			
		||||
ENV LD_LIBRARY_PATH="/usr/local/cuda/lib64:/usr/local/lib:${LD_LIBRARY_PATH}"
 | 
			
		||||
ENV PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH}"
 | 
			
		||||
ENV PYTHONPATH="/usr/local/lib/python3.10/dist-packages:${PYTHONPATH}"
 | 
			
		||||
 | 
			
		||||
# Optimized environment variables for hardware acceleration
 | 
			
		||||
ENV OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp|hwaccel;cuda|hwaccel_device;0|video_codec;h264_cuvid|hwaccel_output_format;cuda"
 | 
			
		||||
ENV OPENCV_FFMPEG_WRITER_OPTIONS="video_codec;h264_nvenc|preset;fast|tune;zerolatency|gpu;0"
 | 
			
		||||
ENV CUDA_VISIBLE_DEVICES=0
 | 
			
		||||
ENV NVIDIA_VISIBLE_DEVICES=all
 | 
			
		||||
ENV NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
 | 
			
		||||
 | 
			
		||||
# Copy and install base requirements (exclude opencv-python since we built from source)
 | 
			
		||||
COPY requirements.base.txt .
 | 
			
		||||
RUN pip install --no-cache-dir -r requirements.base.txt
 | 
			
		||||
RUN grep -v opencv-python requirements.base.txt > requirements.tmp && \
 | 
			
		||||
    mv requirements.tmp requirements.base.txt && \
 | 
			
		||||
    pip install --no-cache-dir -r requirements.base.txt
 | 
			
		||||
 | 
			
		||||
# Set working directory
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import json
 | 
			
		|||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import cv2
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from datetime import datetime, timezone, timedelta
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Optional
 | 
			
		||||
from fastapi import WebSocket, WebSocketDisconnect
 | 
			
		||||
| 
						 | 
				
			
			@ -483,8 +483,8 @@ class WebSocketHandler:
 | 
			
		|||
            images_dir.mkdir(exist_ok=True)
 | 
			
		||||
 | 
			
		||||
            # Generate filename with timestamp and session ID
 | 
			
		||||
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 | 
			
		||||
            filename = f"{display_identifier}_{session_id}_{timestamp}.jpg"
 | 
			
		||||
            timestamp = datetime.now(tz=timezone(timedelta(hours=7))).strftime("%Y%m%d_%H%M%S")
 | 
			
		||||
            filename = f"{session_id}_{display_identifier}_{timestamp}.jpg"
 | 
			
		||||
            filepath = images_dir / filename
 | 
			
		||||
 | 
			
		||||
            # Use existing HTTPSnapshotReader to fetch snapshot
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
Streaming system for RTSP and HTTP camera feeds.
 | 
			
		||||
Provides modular frame readers, buffers, and stream management.
 | 
			
		||||
"""
 | 
			
		||||
from .readers import RTSPReader, HTTPSnapshotReader
 | 
			
		||||
from .readers import RTSPReader, HTTPSnapshotReader, FFmpegRTSPReader
 | 
			
		||||
from .buffers import FrameBuffer, CacheBuffer, shared_frame_buffer, shared_cache_buffer
 | 
			
		||||
from .manager import StreamManager, StreamConfig, SubscriptionInfo, shared_stream_manager, initialize_stream_manager
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ __all__ = [
 | 
			
		|||
    # Readers
 | 
			
		||||
    'RTSPReader',
 | 
			
		||||
    'HTTPSnapshotReader',
 | 
			
		||||
    'FFmpegRTSPReader',
 | 
			
		||||
 | 
			
		||||
    # Buffers
 | 
			
		||||
    'FrameBuffer',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,53 +9,25 @@ import logging
 | 
			
		|||
import numpy as np
 | 
			
		||||
from typing import Optional, Dict, Any, Tuple
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from enum import Enum
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StreamType(Enum):
 | 
			
		||||
    """Stream type enumeration."""
 | 
			
		||||
    RTSP = "rtsp"  # 1280x720 @ 6fps
 | 
			
		||||
    HTTP = "http"  # 2560x1440 high quality
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FrameBuffer:
 | 
			
		||||
    """Thread-safe frame buffer optimized for different stream types."""
 | 
			
		||||
    """Thread-safe frame buffer for all camera streams."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, max_age_seconds: int = 5):
 | 
			
		||||
        self.max_age_seconds = max_age_seconds
 | 
			
		||||
        self._frames: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
        self._stream_types: Dict[str, StreamType] = {}
 | 
			
		||||
        self._lock = threading.RLock()
 | 
			
		||||
 | 
			
		||||
        # Stream-specific settings
 | 
			
		||||
        self.rtsp_config = {
 | 
			
		||||
            'width': 1280,
 | 
			
		||||
            'height': 720,
 | 
			
		||||
            'fps': 6,
 | 
			
		||||
            'max_size_mb': 3  # 1280x720x3 bytes = ~2.6MB
 | 
			
		||||
        }
 | 
			
		||||
        self.http_config = {
 | 
			
		||||
            'width': 2560,
 | 
			
		||||
            'height': 1440,
 | 
			
		||||
            'max_size_mb': 10
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def put_frame(self, camera_id: str, frame: np.ndarray, stream_type: Optional[StreamType] = None):
 | 
			
		||||
        """Store a frame for the given camera ID with type-specific validation."""
 | 
			
		||||
    def put_frame(self, camera_id: str, frame: np.ndarray):
 | 
			
		||||
        """Store a frame for the given camera ID."""
 | 
			
		||||
        with self._lock:
 | 
			
		||||
            # Detect stream type if not provided
 | 
			
		||||
            if stream_type is None:
 | 
			
		||||
                stream_type = self._detect_stream_type(frame)
 | 
			
		||||
 | 
			
		||||
            # Store stream type
 | 
			
		||||
            self._stream_types[camera_id] = stream_type
 | 
			
		||||
 | 
			
		||||
            # Validate frame based on stream type
 | 
			
		||||
            if not self._validate_frame(frame, stream_type):
 | 
			
		||||
                logger.warning(f"Frame validation failed for camera {camera_id} ({stream_type.value})")
 | 
			
		||||
            # Validate frame
 | 
			
		||||
            if not self._validate_frame(frame):
 | 
			
		||||
                logger.warning(f"Frame validation failed for camera {camera_id}")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            self._frames[camera_id] = {
 | 
			
		||||
| 
						 | 
				
			
			@ -63,14 +35,9 @@ class FrameBuffer:
 | 
			
		|||
                'timestamp': time.time(),
 | 
			
		||||
                'shape': frame.shape,
 | 
			
		||||
                'dtype': str(frame.dtype),
 | 
			
		||||
                'stream_type': stream_type.value,
 | 
			
		||||
                'size_mb': frame.nbytes / (1024 * 1024)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            # Commented out verbose frame storage logging
 | 
			
		||||
            # logger.debug(f"Stored {stream_type.value} frame for camera {camera_id}: "
 | 
			
		||||
            #              f"{frame.shape[1]}x{frame.shape[0]}, {frame.nbytes / (1024 * 1024):.2f}MB")
 | 
			
		||||
 | 
			
		||||
    def get_frame(self, camera_id: str) -> Optional[np.ndarray]:
 | 
			
		||||
        """Get the latest frame for the given camera ID."""
 | 
			
		||||
        with self._lock:
 | 
			
		||||
| 
						 | 
				
			
			@ -84,8 +51,6 @@ class FrameBuffer:
 | 
			
		|||
            if age > self.max_age_seconds:
 | 
			
		||||
                logger.debug(f"Frame for camera {camera_id} is {age:.1f}s old, discarding")
 | 
			
		||||
                del self._frames[camera_id]
 | 
			
		||||
                if camera_id in self._stream_types:
 | 
			
		||||
                    del self._stream_types[camera_id]
 | 
			
		||||
                return None
 | 
			
		||||
 | 
			
		||||
            return frame_data['frame'].copy()
 | 
			
		||||
| 
						 | 
				
			
			@ -101,8 +66,6 @@ class FrameBuffer:
 | 
			
		|||
 | 
			
		||||
            if age > self.max_age_seconds:
 | 
			
		||||
                del self._frames[camera_id]
 | 
			
		||||
                if camera_id in self._stream_types:
 | 
			
		||||
                    del self._stream_types[camera_id]
 | 
			
		||||
                return None
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +73,6 @@ class FrameBuffer:
 | 
			
		|||
                'age': age,
 | 
			
		||||
                'shape': frame_data['shape'],
 | 
			
		||||
                'dtype': frame_data['dtype'],
 | 
			
		||||
                'stream_type': frame_data.get('stream_type', 'unknown'),
 | 
			
		||||
                'size_mb': frame_data.get('size_mb', 0)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,8 +85,6 @@ class FrameBuffer:
 | 
			
		|||
        with self._lock:
 | 
			
		||||
            if camera_id in self._frames:
 | 
			
		||||
                del self._frames[camera_id]
 | 
			
		||||
            if camera_id in self._stream_types:
 | 
			
		||||
                del self._stream_types[camera_id]
 | 
			
		||||
            logger.debug(f"Cleared frames for camera {camera_id}")
 | 
			
		||||
 | 
			
		||||
    def clear_all(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +92,6 @@ class FrameBuffer:
 | 
			
		|||
        with self._lock:
 | 
			
		||||
            count = len(self._frames)
 | 
			
		||||
            self._frames.clear()
 | 
			
		||||
            self._stream_types.clear()
 | 
			
		||||
            logger.debug(f"Cleared all frames ({count} cameras)")
 | 
			
		||||
 | 
			
		||||
    def get_camera_list(self) -> list:
 | 
			
		||||
| 
						 | 
				
			
			@ -152,8 +111,6 @@ class FrameBuffer:
 | 
			
		|||
            # Clean up expired frames
 | 
			
		||||
            for camera_id in expired_cameras:
 | 
			
		||||
                del self._frames[camera_id]
 | 
			
		||||
                if camera_id in self._stream_types:
 | 
			
		||||
                    del self._stream_types[camera_id]
 | 
			
		||||
 | 
			
		||||
            return valid_cameras
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -165,15 +122,12 @@ class FrameBuffer:
 | 
			
		|||
                'total_cameras': len(self._frames),
 | 
			
		||||
                'valid_cameras': 0,
 | 
			
		||||
                'expired_cameras': 0,
 | 
			
		||||
                'rtsp_cameras': 0,
 | 
			
		||||
                'http_cameras': 0,
 | 
			
		||||
                'total_memory_mb': 0,
 | 
			
		||||
                'cameras': {}
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for camera_id, frame_data in self._frames.items():
 | 
			
		||||
                age = current_time - frame_data['timestamp']
 | 
			
		||||
                stream_type = frame_data.get('stream_type', 'unknown')
 | 
			
		||||
                size_mb = frame_data.get('size_mb', 0)
 | 
			
		||||
 | 
			
		||||
                if age <= self.max_age_seconds:
 | 
			
		||||
| 
						 | 
				
			
			@ -181,11 +135,6 @@ class FrameBuffer:
 | 
			
		|||
                else:
 | 
			
		||||
                    stats['expired_cameras'] += 1
 | 
			
		||||
 | 
			
		||||
                if stream_type == StreamType.RTSP.value:
 | 
			
		||||
                    stats['rtsp_cameras'] += 1
 | 
			
		||||
                elif stream_type == StreamType.HTTP.value:
 | 
			
		||||
                    stats['http_cameras'] += 1
 | 
			
		||||
 | 
			
		||||
                stats['total_memory_mb'] += size_mb
 | 
			
		||||
 | 
			
		||||
                stats['cameras'][camera_id] = {
 | 
			
		||||
| 
						 | 
				
			
			@ -193,74 +142,45 @@ class FrameBuffer:
 | 
			
		|||
                    'valid': age <= self.max_age_seconds,
 | 
			
		||||
                    'shape': frame_data['shape'],
 | 
			
		||||
                    'dtype': frame_data['dtype'],
 | 
			
		||||
                    'stream_type': stream_type,
 | 
			
		||||
                    'size_mb': size_mb
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            return stats
 | 
			
		||||
 | 
			
		||||
    def _detect_stream_type(self, frame: np.ndarray) -> StreamType:
 | 
			
		||||
        """Detect stream type based on frame dimensions."""
 | 
			
		||||
        h, w = frame.shape[:2]
 | 
			
		||||
 | 
			
		||||
        # Check if it matches RTSP dimensions (1280x720)
 | 
			
		||||
        if w == self.rtsp_config['width'] and h == self.rtsp_config['height']:
 | 
			
		||||
            return StreamType.RTSP
 | 
			
		||||
 | 
			
		||||
        # Check if it matches HTTP dimensions (2560x1440) or close to it
 | 
			
		||||
        if w >= 2000 and h >= 1000:
 | 
			
		||||
            return StreamType.HTTP
 | 
			
		||||
 | 
			
		||||
        # Default based on size
 | 
			
		||||
        if w <= 1920 and h <= 1080:
 | 
			
		||||
            return StreamType.RTSP
 | 
			
		||||
        else:
 | 
			
		||||
            return StreamType.HTTP
 | 
			
		||||
 | 
			
		||||
    def _validate_frame(self, frame: np.ndarray, stream_type: StreamType) -> bool:
 | 
			
		||||
        """Validate frame based on stream type."""
 | 
			
		||||
    def _validate_frame(self, frame: np.ndarray) -> bool:
 | 
			
		||||
        """Validate frame - basic validation for any stream type."""
 | 
			
		||||
        if frame is None or frame.size == 0:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        h, w = frame.shape[:2]
 | 
			
		||||
        size_mb = frame.nbytes / (1024 * 1024)
 | 
			
		||||
 | 
			
		||||
        if stream_type == StreamType.RTSP:
 | 
			
		||||
            config = self.rtsp_config
 | 
			
		||||
            # Allow some tolerance for RTSP streams
 | 
			
		||||
            if abs(w - config['width']) > 100 or abs(h - config['height']) > 100:
 | 
			
		||||
                logger.warning(f"RTSP frame size mismatch: {w}x{h} (expected {config['width']}x{config['height']})")
 | 
			
		||||
            if size_mb > config['max_size_mb']:
 | 
			
		||||
                logger.warning(f"RTSP frame too large: {size_mb:.2f}MB (max {config['max_size_mb']}MB)")
 | 
			
		||||
                return False
 | 
			
		||||
        # Basic size validation - reject extremely large frames regardless of type
 | 
			
		||||
        max_size_mb = 50  # Generous limit for any frame type
 | 
			
		||||
        if size_mb > max_size_mb:
 | 
			
		||||
            logger.warning(f"Frame too large: {size_mb:.2f}MB (max {max_size_mb}MB) for {w}x{h}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        elif stream_type == StreamType.HTTP:
 | 
			
		||||
            config = self.http_config
 | 
			
		||||
            # More flexible for HTTP snapshots
 | 
			
		||||
            if size_mb > config['max_size_mb']:
 | 
			
		||||
                logger.warning(f"HTTP snapshot too large: {size_mb:.2f}MB (max {config['max_size_mb']}MB)")
 | 
			
		||||
                return False
 | 
			
		||||
        # Basic dimension validation
 | 
			
		||||
        if w < 100 or h < 100:
 | 
			
		||||
            logger.warning(f"Frame too small: {w}x{h}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CacheBuffer:
 | 
			
		||||
    """Enhanced frame cache with support for cropping and optimized for different formats."""
 | 
			
		||||
    """Enhanced frame cache with support for cropping."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, max_age_seconds: int = 10):
 | 
			
		||||
        self.frame_buffer = FrameBuffer(max_age_seconds)
 | 
			
		||||
        self._crop_cache: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
        self._cache_lock = threading.RLock()
 | 
			
		||||
        self.jpeg_quality = 95  # High quality for all frames
 | 
			
		||||
 | 
			
		||||
        # Quality settings for different stream types
 | 
			
		||||
        self.jpeg_quality = {
 | 
			
		||||
            StreamType.RTSP: 90,  # Good quality for 720p
 | 
			
		||||
            StreamType.HTTP: 95   # High quality for 2K
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def put_frame(self, camera_id: str, frame: np.ndarray, stream_type: Optional[StreamType] = None):
 | 
			
		||||
    def put_frame(self, camera_id: str, frame: np.ndarray):
 | 
			
		||||
        """Store a frame and clear any associated crop cache."""
 | 
			
		||||
        self.frame_buffer.put_frame(camera_id, frame, stream_type)
 | 
			
		||||
        self.frame_buffer.put_frame(camera_id, frame)
 | 
			
		||||
 | 
			
		||||
        # Clear crop cache for this camera since we have a new frame
 | 
			
		||||
        with self._cache_lock:
 | 
			
		||||
| 
						 | 
				
			
			@ -325,21 +245,15 @@ class CacheBuffer:
 | 
			
		|||
 | 
			
		||||
    def get_frame_as_jpeg(self, camera_id: str, crop_coords: Optional[Tuple[int, int, int, int]] = None,
 | 
			
		||||
                          quality: Optional[int] = None) -> Optional[bytes]:
 | 
			
		||||
        """Get frame as JPEG bytes with format-specific quality settings."""
 | 
			
		||||
        """Get frame as JPEG bytes."""
 | 
			
		||||
        frame = self.get_frame(camera_id, crop_coords)
 | 
			
		||||
        if frame is None:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # Determine quality based on stream type if not specified
 | 
			
		||||
            # Use specified quality or default
 | 
			
		||||
            if quality is None:
 | 
			
		||||
                frame_info = self.frame_buffer.get_frame_info(camera_id)
 | 
			
		||||
                if frame_info:
 | 
			
		||||
                    stream_type_str = frame_info.get('stream_type', StreamType.RTSP.value)
 | 
			
		||||
                    stream_type = StreamType.RTSP if stream_type_str == StreamType.RTSP.value else StreamType.HTTP
 | 
			
		||||
                    quality = self.jpeg_quality[stream_type]
 | 
			
		||||
                else:
 | 
			
		||||
                    quality = 90  # Default
 | 
			
		||||
                quality = self.jpeg_quality
 | 
			
		||||
 | 
			
		||||
            # Encode as JPEG with specified quality
 | 
			
		||||
            encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,8 +9,8 @@ from typing import Dict, Set, Optional, List, Any
 | 
			
		|||
from dataclasses import dataclass
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
 | 
			
		||||
from .readers import RTSPReader, HTTPSnapshotReader
 | 
			
		||||
from .buffers import shared_cache_buffer, StreamType
 | 
			
		||||
from .readers import RTSPReader, HTTPSnapshotReader, FFmpegRTSPReader
 | 
			
		||||
from .buffers import shared_cache_buffer
 | 
			
		||||
from ..tracking.integration import TrackingPipelineIntegration
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -129,8 +129,8 @@ class StreamManager:
 | 
			
		|||
        """Start a stream for the given camera."""
 | 
			
		||||
        try:
 | 
			
		||||
            if stream_config.rtsp_url:
 | 
			
		||||
                # RTSP stream
 | 
			
		||||
                reader = RTSPReader(
 | 
			
		||||
                # RTSP stream using FFmpeg subprocess with CUDA acceleration
 | 
			
		||||
                reader = FFmpegRTSPReader(
 | 
			
		||||
                    camera_id=camera_id,
 | 
			
		||||
                    rtsp_url=stream_config.rtsp_url,
 | 
			
		||||
                    max_retries=stream_config.max_retries
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +138,7 @@ class StreamManager:
 | 
			
		|||
                reader.set_frame_callback(self._frame_callback)
 | 
			
		||||
                reader.start()
 | 
			
		||||
                self._streams[camera_id] = reader
 | 
			
		||||
                logger.info(f"Started RTSP stream for camera {camera_id}")
 | 
			
		||||
                logger.info(f"Started FFmpeg RTSP stream for camera {camera_id}")
 | 
			
		||||
 | 
			
		||||
            elif stream_config.snapshot_url:
 | 
			
		||||
                # HTTP snapshot stream
 | 
			
		||||
| 
						 | 
				
			
			@ -177,12 +177,8 @@ class StreamManager:
 | 
			
		|||
    def _frame_callback(self, camera_id: str, frame):
 | 
			
		||||
        """Callback for when a new frame is available."""
 | 
			
		||||
        try:
 | 
			
		||||
            # Detect stream type based on frame dimensions
 | 
			
		||||
            stream_type = self._detect_stream_type(frame)
 | 
			
		||||
 | 
			
		||||
            # Store frame in shared buffer with stream type
 | 
			
		||||
            shared_cache_buffer.put_frame(camera_id, frame, stream_type)
 | 
			
		||||
 | 
			
		||||
            # Store frame in shared buffer
 | 
			
		||||
            shared_cache_buffer.put_frame(camera_id, frame)
 | 
			
		||||
 | 
			
		||||
            # Process tracking for subscriptions with tracking integration
 | 
			
		||||
            self._process_tracking_for_camera(camera_id, frame)
 | 
			
		||||
| 
						 | 
				
			
			@ -404,26 +400,6 @@ class StreamManager:
 | 
			
		|||
                    stats[subscription_id] = subscription_info.tracking_integration.get_statistics()
 | 
			
		||||
        return stats
 | 
			
		||||
 | 
			
		||||
    def _detect_stream_type(self, frame) -> StreamType:
 | 
			
		||||
        """Detect stream type based on frame dimensions."""
 | 
			
		||||
        if frame is None:
 | 
			
		||||
            return StreamType.RTSP  # Default
 | 
			
		||||
 | 
			
		||||
        h, w = frame.shape[:2]
 | 
			
		||||
 | 
			
		||||
        # RTSP: 1280x720
 | 
			
		||||
        if w == 1280 and h == 720:
 | 
			
		||||
            return StreamType.RTSP
 | 
			
		||||
 | 
			
		||||
        # HTTP: 2560x1440 or larger
 | 
			
		||||
        if w >= 2000 and h >= 1000:
 | 
			
		||||
            return StreamType.HTTP
 | 
			
		||||
 | 
			
		||||
        # Default based on size
 | 
			
		||||
        if w <= 1920 and h <= 1080:
 | 
			
		||||
            return StreamType.RTSP
 | 
			
		||||
        else:
 | 
			
		||||
            return StreamType.HTTP
 | 
			
		||||
 | 
			
		||||
    def get_stats(self) -> Dict[str, Any]:
 | 
			
		||||
        """Get comprehensive streaming statistics."""
 | 
			
		||||
| 
						 | 
				
			
			@ -431,22 +407,11 @@ class StreamManager:
 | 
			
		|||
            buffer_stats = shared_cache_buffer.get_stats()
 | 
			
		||||
            tracking_stats = self.get_tracking_stats()
 | 
			
		||||
 | 
			
		||||
            # Add stream type information
 | 
			
		||||
            stream_types = {}
 | 
			
		||||
            for camera_id in self._streams.keys():
 | 
			
		||||
                if isinstance(self._streams[camera_id], RTSPReader):
 | 
			
		||||
                    stream_types[camera_id] = 'rtsp'
 | 
			
		||||
                elif isinstance(self._streams[camera_id], HTTPSnapshotReader):
 | 
			
		||||
                    stream_types[camera_id] = 'http'
 | 
			
		||||
                else:
 | 
			
		||||
                    stream_types[camera_id] = 'unknown'
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                'active_subscriptions': len(self._subscriptions),
 | 
			
		||||
                'active_streams': len(self._streams),
 | 
			
		||||
                'cameras_with_subscribers': len(self._camera_subscribers),
 | 
			
		||||
                'max_streams': self.max_streams,
 | 
			
		||||
                'stream_types': stream_types,
 | 
			
		||||
                'subscriptions_by_camera': {
 | 
			
		||||
                    camera_id: len(subscribers)
 | 
			
		||||
                    for camera_id, subscribers in self._camera_subscribers.items()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,10 @@ import threading
 | 
			
		|||
import requests
 | 
			
		||||
import numpy as np
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
from typing import Optional, Callable
 | 
			
		||||
from watchdog.observers import Observer
 | 
			
		||||
from watchdog.events import FileSystemEventHandler
 | 
			
		||||
 | 
			
		||||
# Suppress FFMPEG/H.264 error messages if needed
 | 
			
		||||
# Set this environment variable to reduce noise from decoder errors
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +21,208 @@ os.environ["OPENCV_FFMPEG_LOGLEVEL"] = "-8"  # Suppress FFMPEG warnings
 | 
			
		|||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# Suppress noisy watchdog debug logs
 | 
			
		||||
logging.getLogger('watchdog.observers.inotify_buffer').setLevel(logging.CRITICAL)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FrameFileHandler(FileSystemEventHandler):
 | 
			
		||||
    """File system event handler for frame file changes."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, callback):
 | 
			
		||||
        self.callback = callback
 | 
			
		||||
        self.last_modified = 0
 | 
			
		||||
 | 
			
		||||
    def on_modified(self, event):
 | 
			
		||||
        if event.is_directory:
 | 
			
		||||
            return
 | 
			
		||||
        # Debounce rapid file changes
 | 
			
		||||
        current_time = time.time()
 | 
			
		||||
        if current_time - self.last_modified > 0.01:  # 10ms debounce
 | 
			
		||||
            self.last_modified = current_time
 | 
			
		||||
            self.callback()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FFmpegRTSPReader:
 | 
			
		||||
    """RTSP stream reader using subprocess FFmpeg with CUDA hardware acceleration and file watching."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, camera_id: str, rtsp_url: str, max_retries: int = 3):
 | 
			
		||||
        self.camera_id = camera_id
 | 
			
		||||
        self.rtsp_url = rtsp_url
 | 
			
		||||
        self.max_retries = max_retries
 | 
			
		||||
        self.process = None
 | 
			
		||||
        self.stop_event = threading.Event()
 | 
			
		||||
        self.thread = None
 | 
			
		||||
        self.frame_callback: Optional[Callable] = None
 | 
			
		||||
        self.observer = None
 | 
			
		||||
        self.frame_ready_event = threading.Event()
 | 
			
		||||
 | 
			
		||||
        # Stream specs
 | 
			
		||||
        self.width = 1280
 | 
			
		||||
        self.height = 720
 | 
			
		||||
 | 
			
		||||
    def set_frame_callback(self, callback: Callable[[str, np.ndarray], None]):
 | 
			
		||||
        """Set callback function to handle captured frames."""
 | 
			
		||||
        self.frame_callback = callback
 | 
			
		||||
 | 
			
		||||
    def start(self):
 | 
			
		||||
        """Start the FFmpeg subprocess reader."""
 | 
			
		||||
        if self.thread and self.thread.is_alive():
 | 
			
		||||
            logger.warning(f"FFmpeg reader for {self.camera_id} already running")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.stop_event.clear()
 | 
			
		||||
        self.thread = threading.Thread(target=self._read_frames, daemon=True)
 | 
			
		||||
        self.thread.start()
 | 
			
		||||
        logger.info(f"Started FFmpeg reader for camera {self.camera_id}")
 | 
			
		||||
 | 
			
		||||
    def stop(self):
 | 
			
		||||
        """Stop the FFmpeg subprocess reader."""
 | 
			
		||||
        self.stop_event.set()
 | 
			
		||||
        if self.process:
 | 
			
		||||
            self.process.terminate()
 | 
			
		||||
            try:
 | 
			
		||||
                self.process.wait(timeout=5)
 | 
			
		||||
            except subprocess.TimeoutExpired:
 | 
			
		||||
                self.process.kill()
 | 
			
		||||
        if self.thread:
 | 
			
		||||
            self.thread.join(timeout=5.0)
 | 
			
		||||
        logger.info(f"Stopped FFmpeg reader for camera {self.camera_id}")
 | 
			
		||||
 | 
			
		||||
    def _start_ffmpeg_process(self):
 | 
			
		||||
        """Start FFmpeg subprocess with CUDA hardware acceleration writing to temp file."""
 | 
			
		||||
        # Create temp file path for this camera
 | 
			
		||||
        self.temp_file = f"/tmp/claude/camera_{self.camera_id.replace(' ', '_')}.raw"
 | 
			
		||||
        os.makedirs("/tmp/claude", exist_ok=True)
 | 
			
		||||
 | 
			
		||||
        # Use PPM format - uncompressed with header, supports -update 1
 | 
			
		||||
        self.temp_file = f"/tmp/claude/camera_{self.camera_id.replace(' ', '_')}.ppm"
 | 
			
		||||
 | 
			
		||||
        cmd = [
 | 
			
		||||
            'ffmpeg',
 | 
			
		||||
            '-hwaccel', 'cuda',
 | 
			
		||||
            '-hwaccel_device', '0',
 | 
			
		||||
            '-rtsp_transport', 'tcp',
 | 
			
		||||
            '-i', self.rtsp_url,
 | 
			
		||||
            '-f', 'image2',
 | 
			
		||||
            '-update', '1',  # Works with image2 format
 | 
			
		||||
            '-pix_fmt', 'rgb24',  # PPM uses RGB not BGR
 | 
			
		||||
            '-an',  # No audio
 | 
			
		||||
            '-y',  # Overwrite output file
 | 
			
		||||
            self.temp_file
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # Start FFmpeg detached - we don't need to communicate with it
 | 
			
		||||
            self.process = subprocess.Popen(
 | 
			
		||||
                cmd,
 | 
			
		||||
                stdout=subprocess.DEVNULL,
 | 
			
		||||
                stderr=subprocess.DEVNULL
 | 
			
		||||
            )
 | 
			
		||||
            logger.info(f"Started FFmpeg process PID {self.process.pid} for camera {self.camera_id} -> {self.temp_file}")
 | 
			
		||||
            return True
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Failed to start FFmpeg for camera {self.camera_id}: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def _setup_file_watcher(self):
 | 
			
		||||
        """Setup file system watcher for temp file."""
 | 
			
		||||
        if not os.path.exists(self.temp_file):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Setup file watcher
 | 
			
		||||
        handler = FrameFileHandler(self._on_file_changed)
 | 
			
		||||
        self.observer = Observer()
 | 
			
		||||
        self.observer.schedule(handler, os.path.dirname(self.temp_file), recursive=False)
 | 
			
		||||
        self.observer.start()
 | 
			
		||||
        logger.info(f"Started file watcher for {self.temp_file}")
 | 
			
		||||
 | 
			
		||||
    def _on_file_changed(self):
 | 
			
		||||
        """Called when temp file is modified."""
 | 
			
		||||
        if os.path.basename(self.temp_file) in str(self.temp_file):
 | 
			
		||||
            self.frame_ready_event.set()
 | 
			
		||||
 | 
			
		||||
    def _read_frames(self):
 | 
			
		||||
        """Reactively read frames when file changes."""
 | 
			
		||||
        frame_count = 0
 | 
			
		||||
        last_log_time = time.time()
 | 
			
		||||
        bytes_per_frame = self.width * self.height * 3
 | 
			
		||||
        restart_check_interval = 10  # Check FFmpeg status every 10 seconds
 | 
			
		||||
 | 
			
		||||
        while not self.stop_event.is_set():
 | 
			
		||||
            try:
 | 
			
		||||
                # Start FFmpeg if not running
 | 
			
		||||
                if not self.process or self.process.poll() is not None:
 | 
			
		||||
                    if self.process and self.process.poll() is not None:
 | 
			
		||||
                        logger.warning(f"FFmpeg process died for camera {self.camera_id}, restarting...")
 | 
			
		||||
 | 
			
		||||
                    if not self._start_ffmpeg_process():
 | 
			
		||||
                        time.sleep(5.0)
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    # Wait for temp file to be created
 | 
			
		||||
                    wait_count = 0
 | 
			
		||||
                    while not os.path.exists(self.temp_file) and wait_count < 30:
 | 
			
		||||
                        time.sleep(1.0)
 | 
			
		||||
                        wait_count += 1
 | 
			
		||||
 | 
			
		||||
                    if not os.path.exists(self.temp_file):
 | 
			
		||||
                        logger.error(f"Temp file not created after 30s for {self.camera_id}")
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    # Setup file watcher
 | 
			
		||||
                    self._setup_file_watcher()
 | 
			
		||||
 | 
			
		||||
                # Wait for file change event (or timeout for health check)
 | 
			
		||||
                if self.frame_ready_event.wait(timeout=restart_check_interval):
 | 
			
		||||
                    self.frame_ready_event.clear()
 | 
			
		||||
 | 
			
		||||
                    # Read PPM frame (uncompressed with header)
 | 
			
		||||
                    try:
 | 
			
		||||
                        if os.path.exists(self.temp_file):
 | 
			
		||||
                            # Read PPM with OpenCV (handles RGB->BGR conversion automatically)
 | 
			
		||||
                            frame = cv2.imread(self.temp_file)
 | 
			
		||||
 | 
			
		||||
                            if frame is not None and frame.shape == (self.height, self.width, 3):
 | 
			
		||||
                                # Call frame callback directly
 | 
			
		||||
                                if self.frame_callback:
 | 
			
		||||
                                    self.frame_callback(self.camera_id, frame)
 | 
			
		||||
 | 
			
		||||
                                frame_count += 1
 | 
			
		||||
 | 
			
		||||
                                # Log progress
 | 
			
		||||
                                current_time = time.time()
 | 
			
		||||
                                if current_time - last_log_time >= 30:
 | 
			
		||||
                                    logger.info(f"Camera {self.camera_id}: {frame_count} PPM frames processed reactively")
 | 
			
		||||
                                    last_log_time = current_time
 | 
			
		||||
                            else:
 | 
			
		||||
                                logger.debug(f"Camera {self.camera_id}: Invalid PPM frame")
 | 
			
		||||
                        else:
 | 
			
		||||
                            logger.debug(f"Camera {self.camera_id}: PPM file not found yet")
 | 
			
		||||
 | 
			
		||||
                    except (IOError, OSError) as e:
 | 
			
		||||
                        logger.debug(f"Camera {self.camera_id}: File read error: {e}")
 | 
			
		||||
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"Camera {self.camera_id}: Error in reactive frame reading: {e}")
 | 
			
		||||
                time.sleep(1.0)
 | 
			
		||||
 | 
			
		||||
        # Cleanup
 | 
			
		||||
        if self.observer:
 | 
			
		||||
            self.observer.stop()
 | 
			
		||||
            self.observer.join()
 | 
			
		||||
        if self.process:
 | 
			
		||||
            self.process.terminate()
 | 
			
		||||
        # Clean up temp file
 | 
			
		||||
        try:
 | 
			
		||||
            if hasattr(self, 'temp_file') and os.path.exists(self.temp_file):
 | 
			
		||||
                os.remove(self.temp_file)
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
        logger.info(f"Reactive FFmpeg reader ended for camera {self.camera_id}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RTSPReader:
 | 
			
		||||
    """RTSP stream frame reader optimized for 1280x720 @ 6fps streams."""
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +242,6 @@ class RTSPReader:
 | 
			
		|||
        self.expected_fps = 6
 | 
			
		||||
 | 
			
		||||
        # Frame processing parameters
 | 
			
		||||
        self.frame_interval = 1.0 / self.expected_fps  # ~167ms for 6fps
 | 
			
		||||
        self.error_recovery_delay = 5.0  # Increased from 2.0 for stability
 | 
			
		||||
        self.max_consecutive_errors = 30  # Increased from 10 to handle network jitter
 | 
			
		||||
        self.stream_timeout = 30.0
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +276,6 @@ class RTSPReader:
 | 
			
		|||
        frame_count = 0
 | 
			
		||||
        last_log_time = time.time()
 | 
			
		||||
        last_successful_frame_time = time.time()
 | 
			
		||||
        last_frame_time = 0
 | 
			
		||||
 | 
			
		||||
        while not self.stop_event.is_set():
 | 
			
		||||
            try:
 | 
			
		||||
| 
						 | 
				
			
			@ -90,19 +293,35 @@ class RTSPReader:
 | 
			
		|||
                    last_successful_frame_time = time.time()
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Rate limiting for 6fps
 | 
			
		||||
                current_time = time.time()
 | 
			
		||||
                if current_time - last_frame_time < self.frame_interval:
 | 
			
		||||
                    time.sleep(0.01)  # Small sleep to avoid busy waiting
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                ret, frame = self.cap.read()
 | 
			
		||||
                # Read frame immediately without rate limiting for minimum latency
 | 
			
		||||
                try:
 | 
			
		||||
                    ret, frame = self.cap.read()
 | 
			
		||||
                    if ret and frame is None:
 | 
			
		||||
                        # Grab succeeded but retrieve failed - decoder issue
 | 
			
		||||
                        logger.error(f"Camera {self.camera_id}: Frame grab OK but decode failed")
 | 
			
		||||
                except Exception as read_error:
 | 
			
		||||
                    logger.error(f"Camera {self.camera_id}: cap.read() threw exception: {type(read_error).__name__}: {read_error}")
 | 
			
		||||
                    ret, frame = False, None
 | 
			
		||||
 | 
			
		||||
                if not ret or frame is None:
 | 
			
		||||
                    consecutive_errors += 1
 | 
			
		||||
 | 
			
		||||
                    # Enhanced logging to diagnose the issue
 | 
			
		||||
                    logger.error(f"Camera {self.camera_id}: cap.read() failed - ret={ret}, frame={frame is not None}")
 | 
			
		||||
 | 
			
		||||
                    # Try to get more info from the capture
 | 
			
		||||
                    try:
 | 
			
		||||
                        if self.cap and self.cap.isOpened():
 | 
			
		||||
                            backend = self.cap.getBackendName()
 | 
			
		||||
                            pos_frames = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
 | 
			
		||||
                            logger.error(f"Camera {self.camera_id}: Capture open, backend: {backend}, pos_frames: {pos_frames}")
 | 
			
		||||
                        else:
 | 
			
		||||
                            logger.error(f"Camera {self.camera_id}: Capture is closed or None!")
 | 
			
		||||
                    except Exception as info_error:
 | 
			
		||||
                        logger.error(f"Camera {self.camera_id}: Error getting capture info: {type(info_error).__name__}: {info_error}")
 | 
			
		||||
 | 
			
		||||
                    if consecutive_errors >= self.max_consecutive_errors:
 | 
			
		||||
                        logger.error(f"Camera {self.camera_id}: Too many consecutive errors, reinitializing")
 | 
			
		||||
                        logger.error(f"Camera {self.camera_id}: Too many consecutive errors ({consecutive_errors}), reinitializing")
 | 
			
		||||
                        self._reinitialize_capture()
 | 
			
		||||
                        consecutive_errors = 0
 | 
			
		||||
                        time.sleep(self.error_recovery_delay)
 | 
			
		||||
| 
						 | 
				
			
			@ -118,15 +337,10 @@ class RTSPReader:
 | 
			
		|||
                        time.sleep(sleep_time)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Validate frame dimensions
 | 
			
		||||
                if frame.shape[1] != self.expected_width or frame.shape[0] != self.expected_height:
 | 
			
		||||
                    logger.warning(f"Camera {self.camera_id}: Unexpected frame dimensions {frame.shape[1]}x{frame.shape[0]}")
 | 
			
		||||
                    # Try to resize if dimensions are wrong
 | 
			
		||||
                    if frame.shape[1] > 0 and frame.shape[0] > 0:
 | 
			
		||||
                        frame = cv2.resize(frame, (self.expected_width, self.expected_height))
 | 
			
		||||
                    else:
 | 
			
		||||
                        consecutive_errors += 1
 | 
			
		||||
                        continue
 | 
			
		||||
                # Accept any valid frame dimensions - don't force specific resolution
 | 
			
		||||
                if frame.shape[1] <= 0 or frame.shape[0] <= 0:
 | 
			
		||||
                    consecutive_errors += 1
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Check for corrupted frames (all black, all white, excessive noise)
 | 
			
		||||
                if self._is_frame_corrupted(frame):
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +352,6 @@ class RTSPReader:
 | 
			
		|||
                consecutive_errors = 0
 | 
			
		||||
                frame_count += 1
 | 
			
		||||
                last_successful_frame_time = time.time()
 | 
			
		||||
                last_frame_time = current_time
 | 
			
		||||
 | 
			
		||||
                # Call frame callback
 | 
			
		||||
                if self.frame_callback:
 | 
			
		||||
| 
						 | 
				
			
			@ -148,6 +361,7 @@ class RTSPReader:
 | 
			
		|||
                        logger.error(f"Camera {self.camera_id}: Frame callback error: {e}")
 | 
			
		||||
 | 
			
		||||
                # Log progress every 30 seconds
 | 
			
		||||
                current_time = time.time()
 | 
			
		||||
                if current_time - last_log_time >= 30:
 | 
			
		||||
                    logger.info(f"Camera {self.camera_id}: {frame_count} frames processed")
 | 
			
		||||
                    last_log_time = current_time
 | 
			
		||||
| 
						 | 
				
			
			@ -166,41 +380,104 @@ class RTSPReader:
 | 
			
		|||
        logger.info(f"RTSP reader thread ended for camera {self.camera_id}")
 | 
			
		||||
 | 
			
		||||
    def _initialize_capture(self) -> bool:
 | 
			
		||||
        """Initialize video capture with optimized settings for 1280x720@6fps."""
 | 
			
		||||
        """Initialize video capture with FFmpeg hardware acceleration (CUVID/NVDEC) for 1280x720@6fps."""
 | 
			
		||||
        try:
 | 
			
		||||
            # Release previous capture if exists
 | 
			
		||||
            if self.cap:
 | 
			
		||||
                self.cap.release()
 | 
			
		||||
                time.sleep(0.5)
 | 
			
		||||
 | 
			
		||||
            logger.info(f"Initializing capture for camera {self.camera_id}")
 | 
			
		||||
            logger.info(f"Initializing capture for camera {self.camera_id} with FFmpeg hardware acceleration")
 | 
			
		||||
            hw_accel_success = False
 | 
			
		||||
 | 
			
		||||
            # Create capture with FFMPEG backend and TCP transport for reliability
 | 
			
		||||
            # Use TCP instead of UDP to prevent packet loss
 | 
			
		||||
            rtsp_url_tcp = self.rtsp_url.replace('rtsp://', 'rtsp://')
 | 
			
		||||
            if '?' in rtsp_url_tcp:
 | 
			
		||||
                rtsp_url_tcp += '&tcp'
 | 
			
		||||
            else:
 | 
			
		||||
                rtsp_url_tcp += '?tcp'
 | 
			
		||||
            # Method 1: Try OpenCV CUDA VideoReader (if built with CUVID support)
 | 
			
		||||
            if not hw_accel_success:
 | 
			
		||||
                try:
 | 
			
		||||
                    # Check if OpenCV was built with CUDA codec support
 | 
			
		||||
                    build_info = cv2.getBuildInformation()
 | 
			
		||||
                    if 'cudacodec' in build_info or 'CUVID' in build_info:
 | 
			
		||||
                        logger.info(f"Attempting OpenCV CUDA VideoReader for camera {self.camera_id}")
 | 
			
		||||
 | 
			
		||||
            # Alternative: Set environment variable for RTSP transport
 | 
			
		||||
            import os
 | 
			
		||||
            os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp'
 | 
			
		||||
                        # Use OpenCV's CUDA backend
 | 
			
		||||
                        self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG, [
 | 
			
		||||
                            cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY
 | 
			
		||||
                        ])
 | 
			
		||||
 | 
			
		||||
            self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
 | 
			
		||||
                        if self.cap.isOpened():
 | 
			
		||||
                            hw_accel_success = True
 | 
			
		||||
                            logger.info(f"Camera {self.camera_id}: Using OpenCV CUDA hardware acceleration")
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.debug(f"Camera {self.camera_id}: OpenCV not built with CUDA codec support")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.debug(f"Camera {self.camera_id}: OpenCV CUDA not available: {e}")
 | 
			
		||||
 | 
			
		||||
            # Method 2: Try FFmpeg with optimal hardware acceleration (CUVID/NVDEC)
 | 
			
		||||
            if not hw_accel_success:
 | 
			
		||||
                try:
 | 
			
		||||
                    from core.utils.ffmpeg_detector import get_optimal_rtsp_options
 | 
			
		||||
                    import os
 | 
			
		||||
 | 
			
		||||
                    # Get optimal FFmpeg options based on detected capabilities
 | 
			
		||||
                    optimal_options = get_optimal_rtsp_options(self.rtsp_url)
 | 
			
		||||
                    os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = optimal_options
 | 
			
		||||
 | 
			
		||||
                    logger.info(f"Attempting FFmpeg with detected hardware acceleration for camera {self.camera_id}")
 | 
			
		||||
                    logger.debug(f"Camera {self.camera_id}: Using FFmpeg options: {optimal_options}")
 | 
			
		||||
 | 
			
		||||
                    self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
 | 
			
		||||
 | 
			
		||||
                    if self.cap.isOpened():
 | 
			
		||||
                        hw_accel_success = True
 | 
			
		||||
                        # Try to get backend info to confirm hardware acceleration
 | 
			
		||||
                        backend = self.cap.getBackendName()
 | 
			
		||||
                        logger.info(f"Camera {self.camera_id}: Using FFmpeg hardware acceleration (backend: {backend})")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.debug(f"Camera {self.camera_id}: FFmpeg optimal hardware acceleration not available: {e}")
 | 
			
		||||
 | 
			
		||||
            # Method 3: Try FFmpeg with NVIDIA NVDEC (better for RTX 3060)
 | 
			
		||||
            if not hw_accel_success:
 | 
			
		||||
                try:
 | 
			
		||||
                    import os
 | 
			
		||||
                    os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'hwaccel;cuda|hwaccel_device;0|rtsp_transport;tcp'
 | 
			
		||||
 | 
			
		||||
                    logger.info(f"Attempting FFmpeg with NVDEC hardware acceleration for camera {self.camera_id}")
 | 
			
		||||
                    self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
 | 
			
		||||
 | 
			
		||||
                    if self.cap.isOpened():
 | 
			
		||||
                        hw_accel_success = True
 | 
			
		||||
                        logger.info(f"Camera {self.camera_id}: Using FFmpeg NVDEC hardware acceleration")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.debug(f"Camera {self.camera_id}: FFmpeg NVDEC not available: {e}")
 | 
			
		||||
 | 
			
		||||
            # Method 4: Try FFmpeg with VAAPI (Intel/AMD GPUs)
 | 
			
		||||
            if not hw_accel_success:
 | 
			
		||||
                try:
 | 
			
		||||
                    import os
 | 
			
		||||
                    os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'hwaccel;vaapi|hwaccel_device;/dev/dri/renderD128|video_codec;h264|rtsp_transport;tcp'
 | 
			
		||||
 | 
			
		||||
                    logger.info(f"Attempting FFmpeg with VAAPI for camera {self.camera_id}")
 | 
			
		||||
                    self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
 | 
			
		||||
 | 
			
		||||
                    if self.cap.isOpened():
 | 
			
		||||
                        hw_accel_success = True
 | 
			
		||||
                        logger.info(f"Camera {self.camera_id}: Using FFmpeg VAAPI hardware acceleration")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.debug(f"Camera {self.camera_id}: FFmpeg VAAPI not available: {e}")
 | 
			
		||||
 | 
			
		||||
            # Fallback: Standard FFmpeg with software decoding
 | 
			
		||||
            if not hw_accel_success:
 | 
			
		||||
                logger.warning(f"Camera {self.camera_id}: Hardware acceleration not available, falling back to software decoding")
 | 
			
		||||
                import os
 | 
			
		||||
                os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp'
 | 
			
		||||
                self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
 | 
			
		||||
 | 
			
		||||
            if not self.cap.isOpened():
 | 
			
		||||
                logger.error(f"Failed to open stream for camera {self.camera_id}")
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
            # Set capture properties for 1280x720@6fps
 | 
			
		||||
            self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.expected_width)
 | 
			
		||||
            self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.expected_height)
 | 
			
		||||
            self.cap.set(cv2.CAP_PROP_FPS, self.expected_fps)
 | 
			
		||||
            # Don't force resolution/fps - let the stream determine its natural specs
 | 
			
		||||
            # The camera will provide whatever resolution/fps it supports
 | 
			
		||||
 | 
			
		||||
            # Set moderate buffer to handle network jitter while avoiding excessive latency
 | 
			
		||||
            # Buffer of 3 frames provides resilience without major delay
 | 
			
		||||
            self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 3)
 | 
			
		||||
 | 
			
		||||
            # Set FFMPEG options for better H.264 handling
 | 
			
		||||
            self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264'))
 | 
			
		||||
| 
						 | 
				
			
			@ -337,15 +614,10 @@ class HTTPSnapshotReader:
 | 
			
		|||
                    time.sleep(min(2.0, interval_seconds))
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Validate image dimensions
 | 
			
		||||
                if frame.shape[1] != self.expected_width or frame.shape[0] != self.expected_height:
 | 
			
		||||
                    logger.info(f"Camera {self.camera_id}: Snapshot dimensions {frame.shape[1]}x{frame.shape[0]} "
 | 
			
		||||
                               f"(expected {self.expected_width}x{self.expected_height})")
 | 
			
		||||
                    # Resize if needed (maintaining aspect ratio for high quality)
 | 
			
		||||
                    if frame.shape[1] > 0 and frame.shape[0] > 0:
 | 
			
		||||
                        # Only resize if significantly different
 | 
			
		||||
                        if abs(frame.shape[1] - self.expected_width) > 100:
 | 
			
		||||
                            frame = self._resize_maintain_aspect(frame, self.expected_width, self.expected_height)
 | 
			
		||||
                # Accept any valid image dimensions - don't force specific resolution
 | 
			
		||||
                if frame.shape[1] <= 0 or frame.shape[0] <= 0:
 | 
			
		||||
                    logger.warning(f"Camera {self.camera_id}: Invalid frame dimensions {frame.shape[1]}x{frame.shape[0]}")
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Reset retry counter on successful fetch
 | 
			
		||||
                retries = 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										214
									
								
								core/utils/ffmpeg_detector.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								core/utils/ffmpeg_detector.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,214 @@
 | 
			
		|||
"""
 | 
			
		||||
FFmpeg hardware acceleration detection and configuration
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import subprocess
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from typing import Dict, List, Optional
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("detector_worker")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FFmpegCapabilities:
 | 
			
		||||
    """Detect and configure FFmpeg hardware acceleration capabilities."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        """Initialize FFmpeg capabilities detector."""
 | 
			
		||||
        self.hwaccels = []
 | 
			
		||||
        self.codecs = {}
 | 
			
		||||
        self.nvidia_support = False
 | 
			
		||||
        self.vaapi_support = False
 | 
			
		||||
        self.qsv_support = False
 | 
			
		||||
 | 
			
		||||
        self._detect_capabilities()
 | 
			
		||||
 | 
			
		||||
    def _detect_capabilities(self):
 | 
			
		||||
        """Detect available hardware acceleration methods."""
 | 
			
		||||
        try:
 | 
			
		||||
            # Get hardware accelerators
 | 
			
		||||
            result = subprocess.run(
 | 
			
		||||
                ['ffmpeg', '-hide_banner', '-hwaccels'],
 | 
			
		||||
                capture_output=True, text=True, timeout=10
 | 
			
		||||
            )
 | 
			
		||||
            if result.returncode == 0:
 | 
			
		||||
                self.hwaccels = [line.strip() for line in result.stdout.strip().split('\n')[1:] if line.strip()]
 | 
			
		||||
                logger.info(f"Available FFmpeg hardware accelerators: {', '.join(self.hwaccels)}")
 | 
			
		||||
 | 
			
		||||
            # Check for NVIDIA support
 | 
			
		||||
            self.nvidia_support = any(hw in self.hwaccels for hw in ['cuda', 'cuvid', 'nvdec'])
 | 
			
		||||
            self.vaapi_support = 'vaapi' in self.hwaccels
 | 
			
		||||
            self.qsv_support = 'qsv' in self.hwaccels
 | 
			
		||||
 | 
			
		||||
            # Get decoder information
 | 
			
		||||
            self._detect_decoders()
 | 
			
		||||
 | 
			
		||||
            # Log capabilities
 | 
			
		||||
            if self.nvidia_support:
 | 
			
		||||
                logger.info("NVIDIA hardware acceleration available (CUDA/CUVID/NVDEC)")
 | 
			
		||||
                logger.info(f"Detected hardware codecs: {self.codecs}")
 | 
			
		||||
            if self.vaapi_support:
 | 
			
		||||
                logger.info("VAAPI hardware acceleration available")
 | 
			
		||||
            if self.qsv_support:
 | 
			
		||||
                logger.info("Intel QuickSync hardware acceleration available")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.warning(f"Failed to detect FFmpeg capabilities: {e}")
 | 
			
		||||
 | 
			
		||||
    def _detect_decoders(self):
 | 
			
		||||
        """Detect available hardware decoders."""
 | 
			
		||||
        try:
 | 
			
		||||
            result = subprocess.run(
 | 
			
		||||
                ['ffmpeg', '-hide_banner', '-decoders'],
 | 
			
		||||
                capture_output=True, text=True, timeout=10
 | 
			
		||||
            )
 | 
			
		||||
            if result.returncode == 0:
 | 
			
		||||
                # Parse decoder output to find hardware decoders
 | 
			
		||||
                for line in result.stdout.split('\n'):
 | 
			
		||||
                    if 'cuvid' in line or 'nvdec' in line:
 | 
			
		||||
                        match = re.search(r'(\w+)\s+.*?(\w+(?:_cuvid|_nvdec))', line)
 | 
			
		||||
                        if match:
 | 
			
		||||
                            codec_type, decoder = match.groups()
 | 
			
		||||
                            if 'h264' in decoder:
 | 
			
		||||
                                self.codecs['h264_hw'] = decoder
 | 
			
		||||
                            elif 'hevc' in decoder or 'h265' in decoder:
 | 
			
		||||
                                self.codecs['h265_hw'] = decoder
 | 
			
		||||
                    elif 'vaapi' in line:
 | 
			
		||||
                        match = re.search(r'(\w+)\s+.*?(\w+_vaapi)', line)
 | 
			
		||||
                        if match:
 | 
			
		||||
                            codec_type, decoder = match.groups()
 | 
			
		||||
                            if 'h264' in decoder:
 | 
			
		||||
                                self.codecs['h264_vaapi'] = decoder
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.debug(f"Failed to detect decoders: {e}")
 | 
			
		||||
 | 
			
		||||
    def get_optimal_capture_options(self, codec: str = 'h264') -> Dict[str, str]:
 | 
			
		||||
        """
 | 
			
		||||
        Get optimal FFmpeg capture options for the given codec.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            codec: Video codec (h264, h265, etc.)
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dictionary of FFmpeg options
 | 
			
		||||
        """
 | 
			
		||||
        options = {
 | 
			
		||||
            'rtsp_transport': 'tcp',
 | 
			
		||||
            'buffer_size': '1024k',
 | 
			
		||||
            'max_delay': '500000',  # 500ms
 | 
			
		||||
            'fflags': '+genpts',
 | 
			
		||||
            'flags': '+low_delay',
 | 
			
		||||
            'probesize': '32',
 | 
			
		||||
            'analyzeduration': '0'
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # Add hardware acceleration if available
 | 
			
		||||
        if self.nvidia_support:
 | 
			
		||||
            # Force enable CUDA hardware acceleration for H.264 if CUDA is available
 | 
			
		||||
            if codec == 'h264':
 | 
			
		||||
                options.update({
 | 
			
		||||
                    'hwaccel': 'cuda',
 | 
			
		||||
                    'hwaccel_device': '0'
 | 
			
		||||
                })
 | 
			
		||||
                logger.info("Using NVIDIA NVDEC hardware acceleration for H.264")
 | 
			
		||||
            elif codec == 'h265':
 | 
			
		||||
                options.update({
 | 
			
		||||
                    'hwaccel': 'cuda',
 | 
			
		||||
                    'hwaccel_device': '0',
 | 
			
		||||
                    'video_codec': 'hevc_cuvid',
 | 
			
		||||
                    'hwaccel_output_format': 'cuda'
 | 
			
		||||
                })
 | 
			
		||||
                logger.info("Using NVIDIA CUVID hardware acceleration for H.265")
 | 
			
		||||
 | 
			
		||||
        elif self.vaapi_support:
 | 
			
		||||
            if codec == 'h264':
 | 
			
		||||
                options.update({
 | 
			
		||||
                    'hwaccel': 'vaapi',
 | 
			
		||||
                    'hwaccel_device': '/dev/dri/renderD128',
 | 
			
		||||
                    'video_codec': 'h264_vaapi'
 | 
			
		||||
                })
 | 
			
		||||
                logger.debug("Using VAAPI hardware acceleration")
 | 
			
		||||
 | 
			
		||||
        return options
 | 
			
		||||
 | 
			
		||||
    def format_opencv_options(self, options: Dict[str, str]) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Format options for OpenCV FFmpeg backend.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            options: Dictionary of FFmpeg options
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Formatted options string for OpenCV
 | 
			
		||||
        """
 | 
			
		||||
        return '|'.join(f"{key};{value}" for key, value in options.items())
 | 
			
		||||
 | 
			
		||||
    def get_hardware_encoder_options(self, codec: str = 'h264', quality: str = 'fast') -> Dict[str, str]:
 | 
			
		||||
        """
 | 
			
		||||
        Get optimal hardware encoding options.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            codec: Video codec for encoding
 | 
			
		||||
            quality: Quality preset (fast, medium, slow)
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dictionary of encoding options
 | 
			
		||||
        """
 | 
			
		||||
        options = {}
 | 
			
		||||
 | 
			
		||||
        if self.nvidia_support:
 | 
			
		||||
            if codec == 'h264':
 | 
			
		||||
                options.update({
 | 
			
		||||
                    'video_codec': 'h264_nvenc',
 | 
			
		||||
                    'preset': quality,
 | 
			
		||||
                    'tune': 'zerolatency',
 | 
			
		||||
                    'gpu': '0',
 | 
			
		||||
                    'rc': 'cbr_hq',
 | 
			
		||||
                    'surfaces': '64'
 | 
			
		||||
                })
 | 
			
		||||
            elif codec == 'h265':
 | 
			
		||||
                options.update({
 | 
			
		||||
                    'video_codec': 'hevc_nvenc',
 | 
			
		||||
                    'preset': quality,
 | 
			
		||||
                    'tune': 'zerolatency',
 | 
			
		||||
                    'gpu': '0'
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        elif self.vaapi_support:
 | 
			
		||||
            if codec == 'h264':
 | 
			
		||||
                options.update({
 | 
			
		||||
                    'video_codec': 'h264_vaapi',
 | 
			
		||||
                    'vaapi_device': '/dev/dri/renderD128'
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        return options
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Global instance
 | 
			
		||||
_ffmpeg_caps = None
 | 
			
		||||
 | 
			
		||||
def get_ffmpeg_capabilities() -> FFmpegCapabilities:
 | 
			
		||||
    """Get or create the global FFmpeg capabilities instance."""
 | 
			
		||||
    global _ffmpeg_caps
 | 
			
		||||
    if _ffmpeg_caps is None:
 | 
			
		||||
        _ffmpeg_caps = FFmpegCapabilities()
 | 
			
		||||
    return _ffmpeg_caps
 | 
			
		||||
 | 
			
		||||
def get_optimal_rtsp_options(rtsp_url: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Get optimal OpenCV FFmpeg options for RTSP streaming.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        rtsp_url: RTSP stream URL
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Formatted options string for cv2.VideoCapture
 | 
			
		||||
    """
 | 
			
		||||
    caps = get_ffmpeg_capabilities()
 | 
			
		||||
 | 
			
		||||
    # Detect codec from URL or assume H.264
 | 
			
		||||
    codec = 'h265' if any(x in rtsp_url.lower() for x in ['h265', 'hevc']) else 'h264'
 | 
			
		||||
 | 
			
		||||
    options = caps.get_optimal_capture_options(codec)
 | 
			
		||||
    return caps.format_opencv_options(options)
 | 
			
		||||
							
								
								
									
										173
									
								
								core/utils/hardware_encoder.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								core/utils/hardware_encoder.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,173 @@
 | 
			
		|||
"""
 | 
			
		||||
Hardware-accelerated image encoding using NVIDIA NVENC or Intel QuickSync
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import cv2
 | 
			
		||||
import numpy as np
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Optional, Tuple
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("detector_worker")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HardwareEncoder:
 | 
			
		||||
    """Hardware-accelerated JPEG encoder using GPU."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        """Initialize hardware encoder."""
 | 
			
		||||
        self.nvenc_available = False
 | 
			
		||||
        self.vaapi_available = False
 | 
			
		||||
        self.turbojpeg_available = False
 | 
			
		||||
 | 
			
		||||
        # Check for TurboJPEG (fastest CPU-based option)
 | 
			
		||||
        try:
 | 
			
		||||
            from turbojpeg import TurboJPEG
 | 
			
		||||
            self.turbojpeg = TurboJPEG()
 | 
			
		||||
            self.turbojpeg_available = True
 | 
			
		||||
            logger.info("TurboJPEG accelerated encoding available")
 | 
			
		||||
        except ImportError:
 | 
			
		||||
            logger.debug("TurboJPEG not available")
 | 
			
		||||
 | 
			
		||||
        # Check for NVIDIA NVENC support
 | 
			
		||||
        try:
 | 
			
		||||
            # Test if we can create an NVENC encoder
 | 
			
		||||
            test_frame = np.zeros((720, 1280, 3), dtype=np.uint8)
 | 
			
		||||
            fourcc = cv2.VideoWriter_fourcc(*'H264')
 | 
			
		||||
            test_writer = cv2.VideoWriter(
 | 
			
		||||
                "test.mp4",
 | 
			
		||||
                fourcc,
 | 
			
		||||
                30,
 | 
			
		||||
                (1280, 720),
 | 
			
		||||
                [cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY]
 | 
			
		||||
            )
 | 
			
		||||
            if test_writer.isOpened():
 | 
			
		||||
                self.nvenc_available = True
 | 
			
		||||
                logger.info("NVENC hardware encoding available")
 | 
			
		||||
            test_writer.release()
 | 
			
		||||
            if os.path.exists("test.mp4"):
 | 
			
		||||
                os.remove("test.mp4")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.debug(f"NVENC not available: {e}")
 | 
			
		||||
 | 
			
		||||
    def encode_jpeg(self, frame: np.ndarray, quality: int = 85) -> Optional[bytes]:
 | 
			
		||||
        """
 | 
			
		||||
        Encode frame to JPEG using the fastest available method.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            frame: BGR image frame
 | 
			
		||||
            quality: JPEG quality (1-100)
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Encoded JPEG bytes or None on failure
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            # Method 1: TurboJPEG (3-5x faster than cv2.imencode)
 | 
			
		||||
            if self.turbojpeg_available:
 | 
			
		||||
                # Convert BGR to RGB for TurboJPEG
 | 
			
		||||
                rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
 | 
			
		||||
                encoded = self.turbojpeg.encode(rgb_frame, quality=quality)
 | 
			
		||||
                return encoded
 | 
			
		||||
 | 
			
		||||
            # Method 2: Hardware-accelerated encoding via GStreamer (if available)
 | 
			
		||||
            if self.nvenc_available:
 | 
			
		||||
                return self._encode_with_nvenc(frame, quality)
 | 
			
		||||
 | 
			
		||||
            # Fallback: Standard OpenCV encoding
 | 
			
		||||
            encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
 | 
			
		||||
            success, encoded = cv2.imencode('.jpg', frame, encode_params)
 | 
			
		||||
            if success:
 | 
			
		||||
                return encoded.tobytes()
 | 
			
		||||
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Failed to encode frame: {e}")
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def _encode_with_nvenc(self, frame: np.ndarray, quality: int) -> Optional[bytes]:
 | 
			
		||||
        """
 | 
			
		||||
        Encode using NVIDIA NVENC hardware encoder.
 | 
			
		||||
 | 
			
		||||
        This is complex to implement directly, so we'll use a GStreamer pipeline
 | 
			
		||||
        if available.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            # Create a GStreamer pipeline for hardware encoding
 | 
			
		||||
            height, width = frame.shape[:2]
 | 
			
		||||
            gst_pipeline = (
 | 
			
		||||
                f"appsrc ! "
 | 
			
		||||
                f"video/x-raw,format=BGR,width={width},height={height},framerate=30/1 ! "
 | 
			
		||||
                f"videoconvert ! "
 | 
			
		||||
                f"nvvideoconvert ! "  # GPU color conversion
 | 
			
		||||
                f"nvjpegenc quality={quality} ! "  # Hardware JPEG encoder
 | 
			
		||||
                f"appsink"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # This would require GStreamer Python bindings
 | 
			
		||||
            # For now, fall back to TurboJPEG or standard encoding
 | 
			
		||||
            logger.debug("NVENC JPEG encoding not fully implemented, using fallback")
 | 
			
		||||
            encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
 | 
			
		||||
            success, encoded = cv2.imencode('.jpg', frame, encode_params)
 | 
			
		||||
            if success:
 | 
			
		||||
                return encoded.tobytes()
 | 
			
		||||
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"NVENC encoding failed: {e}")
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def encode_batch(self, frames: list, quality: int = 85) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Batch encode multiple frames for better GPU utilization.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            frames: List of BGR frames
 | 
			
		||||
            quality: JPEG quality
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            List of encoded JPEG bytes
 | 
			
		||||
        """
 | 
			
		||||
        encoded_frames = []
 | 
			
		||||
 | 
			
		||||
        if self.turbojpeg_available:
 | 
			
		||||
            # TurboJPEG can handle batch encoding efficiently
 | 
			
		||||
            for frame in frames:
 | 
			
		||||
                rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
 | 
			
		||||
                encoded = self.turbojpeg.encode(rgb_frame, quality=quality)
 | 
			
		||||
                encoded_frames.append(encoded)
 | 
			
		||||
        else:
 | 
			
		||||
            # Fallback to sequential encoding
 | 
			
		||||
            for frame in frames:
 | 
			
		||||
                encoded = self.encode_jpeg(frame, quality)
 | 
			
		||||
                encoded_frames.append(encoded)
 | 
			
		||||
 | 
			
		||||
        return encoded_frames
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Global encoder instance
 | 
			
		||||
_hardware_encoder = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_hardware_encoder() -> HardwareEncoder:
 | 
			
		||||
    """Get or create the global hardware encoder instance."""
 | 
			
		||||
    global _hardware_encoder
 | 
			
		||||
    if _hardware_encoder is None:
 | 
			
		||||
        _hardware_encoder = HardwareEncoder()
 | 
			
		||||
    return _hardware_encoder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def encode_frame_hardware(frame: np.ndarray, quality: int = 85) -> Optional[bytes]:
 | 
			
		||||
    """
 | 
			
		||||
    Convenience function to encode a frame using hardware acceleration.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        frame: BGR image frame
 | 
			
		||||
        quality: JPEG quality (1-100)
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Encoded JPEG bytes or None on failure
 | 
			
		||||
    """
 | 
			
		||||
    encoder = get_hardware_encoder()
 | 
			
		||||
    return encoder.encode_jpeg(frame, quality)
 | 
			
		||||
| 
						 | 
				
			
			@ -6,4 +6,5 @@ scipy
 | 
			
		|||
filterpy
 | 
			
		||||
psycopg2-binary
 | 
			
		||||
lap>=0.5.12
 | 
			
		||||
pynvml
 | 
			
		||||
pynvml
 | 
			
		||||
PyTurboJPEG
 | 
			
		||||
| 
						 | 
				
			
			@ -5,4 +5,5 @@ fastapi[standard]
 | 
			
		|||
redis
 | 
			
		||||
urllib3<2.0.0
 | 
			
		||||
numpy
 | 
			
		||||
requests
 | 
			
		||||
requests
 | 
			
		||||
watchdog
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue