refactor: simplify frame handling by removing stream type management and enhancing validation
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 7s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m55s
Build Worker Base and Application Images / deploy-stack (push) Successful in 12s
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 7s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m55s
Build Worker Base and Application Images / deploy-stack (push) Successful in 12s
This commit is contained in:
parent
dc1db635d0
commit
719d16ae4d
4 changed files with 51 additions and 182 deletions
|
@ -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]
|
||||
|
|
|
@ -10,7 +10,7 @@ from dataclasses import dataclass
|
|||
from collections import defaultdict
|
||||
|
||||
from .readers import RTSPReader, HTTPSnapshotReader
|
||||
from .buffers import shared_cache_buffer, StreamType
|
||||
from .buffers import shared_cache_buffer
|
||||
from ..tracking.integration import TrackingPipelineIntegration
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
|
|
@ -37,7 +37,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 +71,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,12 +88,7 @@ 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
|
||||
|
||||
# Read frame immediately without rate limiting for minimum latency
|
||||
ret, frame = self.cap.read()
|
||||
|
||||
if not ret or frame is None:
|
||||
|
@ -118,15 +111,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 +126,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 +135,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
|
||||
|
@ -261,14 +249,12 @@ class RTSPReader:
|
|||
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 minimal buffer for lowest latency - single frame buffer
|
||||
# This ensures we always get the most recent frame
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
# Set FFMPEG options for better H.264 handling
|
||||
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264'))
|
||||
|
@ -405,15 +391,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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue