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]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue