fix: camera api endpoint

This commit is contained in:
ziesorx 2025-09-26 13:05:58 +07:00
parent 83aaf95f59
commit 519e073f7f
5 changed files with 69 additions and 68 deletions

56
app.py
View file

@ -6,8 +6,9 @@ import json
import logging import logging
import os import os
import time import time
import cv2
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket, HTTPException, Request from fastapi import FastAPI, WebSocket, HTTPException
from fastapi.responses import Response from fastapi.responses import Response
# Import new modular communication system # Import new modular communication system
@ -27,8 +28,8 @@ logging.basicConfig(
logger = logging.getLogger("detector_worker") logger = logging.getLogger("detector_worker")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
# Store cached frames for REST API access (temporary storage) # Frames are now stored in the shared cache buffer from core.streaming.buffers
latest_frames = {} # latest_frames = {} # Deprecated - using shared_cache_buffer instead
# Lifespan event handler (modern FastAPI approach) # Lifespan event handler (modern FastAPI approach)
@asynccontextmanager @asynccontextmanager
@ -49,7 +50,7 @@ async def lifespan(app: FastAPI):
worker_state.set_subscriptions([]) worker_state.set_subscriptions([])
worker_state.session_ids.clear() worker_state.session_ids.clear()
worker_state.progression_stages.clear() worker_state.progression_stages.clear()
latest_frames.clear() # latest_frames.clear() # No longer needed - frames are in shared_cache_buffer
logger.info("Detector Worker shutdown complete") logger.info("Detector Worker shutdown complete")
# Create FastAPI application with detailed WebSocket logging # Create FastAPI application with detailed WebSocket logging
@ -90,8 +91,8 @@ from core.streaming import initialize_stream_manager
initialize_stream_manager(max_streams=config.get('max_streams', 10)) initialize_stream_manager(max_streams=config.get('max_streams', 10))
logger.info(f"Initialized stream manager with max_streams={config.get('max_streams', 10)}") logger.info(f"Initialized stream manager with max_streams={config.get('max_streams', 10)}")
# Store cached frames for REST API access (temporary storage) # Frames are now stored in the shared cache buffer from core.streaming.buffers
latest_frames = {} # latest_frames = {} # Deprecated - using shared_cache_buffer instead
logger.info("Starting detector worker application (refactored)") logger.info("Starting detector worker application (refactored)")
logger.info(f"Configuration: Target FPS: {config.get('target_fps', 10)}, " logger.info(f"Configuration: Target FPS: {config.get('target_fps', 10)}, "
@ -150,31 +151,36 @@ async def get_camera_image(camera_id: str):
detail=f"Camera {camera_id} not found or not active" detail=f"Camera {camera_id} not found or not active"
) )
# Check if we have a cached frame for this camera # Extract actual camera_id from subscription identifier (displayId;cameraId)
if camera_id not in latest_frames: # Frames are stored using just the camera_id part
logger.warning(f"No cached frame available for camera '{camera_id}'") actual_camera_id = camera_id.split(';')[-1] if ';' in camera_id else camera_id
# Get frame from the shared cache buffer
from core.streaming.buffers import shared_cache_buffer
# Debug: Log available cameras in buffer
available_cameras = shared_cache_buffer.frame_buffer.get_camera_list()
logger.debug(f"Available cameras in buffer: {available_cameras}")
logger.debug(f"Looking for camera: '{actual_camera_id}'")
frame = shared_cache_buffer.get_frame(actual_camera_id)
if frame is None:
logger.warning(f"No cached frame available for camera '{actual_camera_id}' (from subscription '{camera_id}')")
logger.warning(f"Available cameras in buffer: {available_cameras}")
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail=f"No frame available for camera {camera_id}" detail=f"No frame available for camera {actual_camera_id}"
) )
frame = latest_frames[camera_id] logger.debug(f"Retrieved cached frame for camera '{actual_camera_id}' (from subscription '{camera_id}'), shape: {frame.shape}")
logger.debug(f"Retrieved cached frame for camera '{camera_id}', shape: {frame.shape}")
# TODO: This import will be replaced in Phase 3 (Streaming System) # Encode frame as JPEG
# For now, we need to handle the case where OpenCV is not available success, buffer_img = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
try: if not success:
import cv2 raise HTTPException(status_code=500, detail="Failed to encode image as JPEG")
# Encode frame as JPEG
success, buffer_img = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
if not success:
raise HTTPException(status_code=500, detail="Failed to encode image as JPEG")
# Return image as binary response # Return image as binary response
return Response(content=buffer_img.tobytes(), media_type="image/jpeg") return Response(content=buffer_img.tobytes(), media_type="image/jpeg")
except ImportError:
logger.error("OpenCV not available for image encoding")
raise HTTPException(status_code=500, detail="Image processing not available")
except HTTPException: except HTTPException:
raise raise

View file

@ -377,6 +377,8 @@ class WebSocketHandler:
camera_id = subscription_id.split(';')[-1] camera_id = subscription_id.split(';')[-1]
model_id = payload['modelId'] model_id = payload['modelId']
logger.info(f"[SUBSCRIPTION_MAPPING] subscription_id='{subscription_id}' → camera_id='{camera_id}'")
# Get tracking integration for this model # Get tracking integration for this model
tracking_integration = tracking_integrations.get(model_id) tracking_integration = tracking_integrations.get(model_id)

View file

@ -46,13 +46,7 @@ class FrameBuffer:
frame_data = self._frames[camera_id] frame_data = self._frames[camera_id]
# Check if frame is too old # Return frame regardless of age - frames persist until replaced
age = time.time() - frame_data['timestamp']
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]
return None
return frame_data['frame'].copy() return frame_data['frame'].copy()
def get_frame_info(self, camera_id: str) -> Optional[Dict[str, Any]]: def get_frame_info(self, camera_id: str) -> Optional[Dict[str, Any]]:
@ -64,10 +58,7 @@ class FrameBuffer:
frame_data = self._frames[camera_id] frame_data = self._frames[camera_id]
age = time.time() - frame_data['timestamp'] age = time.time() - frame_data['timestamp']
if age > self.max_age_seconds: # Return frame info regardless of age - frames persist until replaced
del self._frames[camera_id]
return None
return { return {
'timestamp': frame_data['timestamp'], 'timestamp': frame_data['timestamp'],
'age': age, 'age': age,
@ -95,24 +86,10 @@ class FrameBuffer:
logger.debug(f"Cleared all frames ({count} cameras)") logger.debug(f"Cleared all frames ({count} cameras)")
def get_camera_list(self) -> list: def get_camera_list(self) -> list:
"""Get list of cameras with valid frames.""" """Get list of cameras with frames - all frames persist until replaced."""
with self._lock: with self._lock:
current_time = time.time() # Return all cameras that have frames - no age-based filtering
valid_cameras = [] return list(self._frames.keys())
expired_cameras = []
for camera_id, frame_data in self._frames.items():
age = current_time - frame_data['timestamp']
if age <= self.max_age_seconds:
valid_cameras.append(camera_id)
else:
expired_cameras.append(camera_id)
# Clean up expired frames
for camera_id in expired_cameras:
del self._frames[camera_id]
return valid_cameras
def get_stats(self) -> Dict[str, Any]: def get_stats(self) -> Dict[str, Any]:
"""Get buffer statistics.""" """Get buffer statistics."""
@ -120,8 +97,8 @@ class FrameBuffer:
current_time = time.time() current_time = time.time()
stats = { stats = {
'total_cameras': len(self._frames), 'total_cameras': len(self._frames),
'valid_cameras': 0, 'recent_cameras': 0,
'expired_cameras': 0, 'stale_cameras': 0,
'total_memory_mb': 0, 'total_memory_mb': 0,
'cameras': {} 'cameras': {}
} }
@ -130,16 +107,17 @@ class FrameBuffer:
age = current_time - frame_data['timestamp'] age = current_time - frame_data['timestamp']
size_mb = frame_data.get('size_mb', 0) size_mb = frame_data.get('size_mb', 0)
# All frames are valid/available, but categorize by freshness for monitoring
if age <= self.max_age_seconds: if age <= self.max_age_seconds:
stats['valid_cameras'] += 1 stats['recent_cameras'] += 1
else: else:
stats['expired_cameras'] += 1 stats['stale_cameras'] += 1
stats['total_memory_mb'] += size_mb stats['total_memory_mb'] += size_mb
stats['cameras'][camera_id] = { stats['cameras'][camera_id] = {
'age': age, 'age': age,
'valid': age <= self.max_age_seconds, 'recent': age <= self.max_age_seconds, # Recent but all frames available
'shape': frame_data['shape'], 'shape': frame_data['shape'],
'dtype': frame_data['dtype'], 'dtype': frame_data['dtype'],
'size_mb': size_mb 'size_mb': size_mb

View file

@ -130,6 +130,7 @@ class StreamManager:
try: try:
if stream_config.rtsp_url: if stream_config.rtsp_url:
# RTSP stream using FFmpeg subprocess with CUDA acceleration # RTSP stream using FFmpeg subprocess with CUDA acceleration
logger.info(f"[STREAM_START] Starting FFmpeg RTSP stream for camera_id='{camera_id}' URL={stream_config.rtsp_url}")
reader = FFmpegRTSPReader( reader = FFmpegRTSPReader(
camera_id=camera_id, camera_id=camera_id,
rtsp_url=stream_config.rtsp_url, rtsp_url=stream_config.rtsp_url,
@ -138,10 +139,11 @@ class StreamManager:
reader.set_frame_callback(self._frame_callback) reader.set_frame_callback(self._frame_callback)
reader.start() reader.start()
self._streams[camera_id] = reader self._streams[camera_id] = reader
logger.info(f"Started FFmpeg RTSP stream for camera {camera_id}") logger.info(f"[STREAM_START] ✅ Started FFmpeg RTSP stream for camera_id='{camera_id}'")
elif stream_config.snapshot_url: elif stream_config.snapshot_url:
# HTTP snapshot stream # HTTP snapshot stream
logger.info(f"[STREAM_START] Starting HTTP snapshot stream for camera_id='{camera_id}' URL={stream_config.snapshot_url}")
reader = HTTPSnapshotReader( reader = HTTPSnapshotReader(
camera_id=camera_id, camera_id=camera_id,
snapshot_url=stream_config.snapshot_url, snapshot_url=stream_config.snapshot_url,
@ -151,7 +153,7 @@ class StreamManager:
reader.set_frame_callback(self._frame_callback) reader.set_frame_callback(self._frame_callback)
reader.start() reader.start()
self._streams[camera_id] = reader self._streams[camera_id] = reader
logger.info(f"Started HTTP snapshot stream for camera {camera_id}") logger.info(f"[STREAM_START] ✅ Started HTTP snapshot stream for camera_id='{camera_id}'")
else: else:
logger.error(f"No valid URL provided for camera {camera_id}") logger.error(f"No valid URL provided for camera {camera_id}")
@ -169,8 +171,9 @@ class StreamManager:
try: try:
self._streams[camera_id].stop() self._streams[camera_id].stop()
del self._streams[camera_id] del self._streams[camera_id]
shared_cache_buffer.clear_camera(camera_id) # DON'T clear frames - they should persist until replaced
logger.info(f"Stopped stream for camera {camera_id}") # shared_cache_buffer.clear_camera(camera_id) # REMOVED - frames should persist
logger.info(f"Stopped stream for camera {camera_id} (frames preserved in buffer)")
except Exception as e: except Exception as e:
logger.error(f"Error stopping stream for camera {camera_id}: {e}") logger.error(f"Error stopping stream for camera {camera_id}: {e}")
@ -179,6 +182,11 @@ class StreamManager:
try: try:
# Store frame in shared buffer # Store frame in shared buffer
shared_cache_buffer.put_frame(camera_id, frame) shared_cache_buffer.put_frame(camera_id, frame)
logger.info(f"[FRAME_CALLBACK] Stored frame for camera_id='{camera_id}' in shared_cache_buffer, shape={frame.shape}")
# Log current buffer state
available_cameras = shared_cache_buffer.frame_buffer.get_camera_list()
logger.info(f"[FRAME_CALLBACK] Buffer now contains {len(available_cameras)} cameras: {available_cameras}")
# Process tracking for subscriptions with tracking integration # Process tracking for subscriptions with tracking integration
self._process_tracking_for_camera(camera_id, frame) self._process_tracking_for_camera(camera_id, frame)

View file

@ -101,14 +101,14 @@ class FFmpegRTSPReader:
# This ensures each file is complete when written # This ensures each file is complete when written
camera_id_safe = self.camera_id.replace(' ', '_') camera_id_safe = self.camera_id.replace(' ', '_')
self.frame_prefix = f"camera_{camera_id_safe}" self.frame_prefix = f"camera_{camera_id_safe}"
# Using strftime pattern with microseconds for unique filenames # Using strftime pattern with seconds for unique filenames (avoid %f which may not work)
self.frame_pattern = f"{self.frame_dir}/{self.frame_prefix}_%Y%m%d_%H%M%S_%f.ppm" self.frame_pattern = f"{self.frame_dir}/{self.frame_prefix}_%Y%m%d_%H%M%S.ppm"
cmd = [ cmd = [
'ffmpeg', 'ffmpeg',
# DO NOT REMOVE # DO NOT REMOVE
'-hwaccel', 'cuda', # '-hwaccel', 'cuda',
'-hwaccel_device', '0', # '-hwaccel_device', '0',
'-rtsp_transport', 'tcp', '-rtsp_transport', 'tcp',
'-i', self.rtsp_url, '-i', self.rtsp_url,
'-f', 'image2', '-f', 'image2',
@ -201,14 +201,17 @@ class FFmpegRTSPReader:
# Sort by filename (which includes timestamp) and get the latest # Sort by filename (which includes timestamp) and get the latest
frame_files.sort() frame_files.sort()
latest_frame = frame_files[-1] latest_frame = frame_files[-1]
logger.debug(f"Camera {self.camera_id}: Found {len(frame_files)} frames, processing latest: {latest_frame}")
# Read the latest frame (it's complete since FFmpeg wrote it atomically) # Read the latest frame (it's complete since FFmpeg wrote it atomically)
frame = cv2.imread(latest_frame) frame = cv2.imread(latest_frame)
if frame is not None and frame.shape == (self.height, self.width, 3): if frame is not None:
# Call frame callback directly logger.debug(f"Camera {self.camera_id}: Successfully read frame {frame.shape} from {latest_frame}")
# Accept any frame dimensions initially for debugging
if self.frame_callback: if self.frame_callback:
self.frame_callback(self.camera_id, frame) self.frame_callback(self.camera_id, frame)
logger.debug(f"Camera {self.camera_id}: Called frame callback")
frame_count += 1 frame_count += 1
@ -217,6 +220,8 @@ class FFmpegRTSPReader:
if current_time - last_log_time >= 30: if current_time - last_log_time >= 30:
logger.info(f"Camera {self.camera_id}: {frame_count} frames processed") logger.info(f"Camera {self.camera_id}: {frame_count} frames processed")
last_log_time = current_time last_log_time = current_time
else:
logger.warning(f"Camera {self.camera_id}: Failed to read frame from {latest_frame}")
# Clean up old frame files to prevent disk filling # Clean up old frame files to prevent disk filling
# Keep only the latest 5 frames # Keep only the latest 5 frames
@ -226,6 +231,8 @@ class FFmpegRTSPReader:
os.remove(old_file) os.remove(old_file)
except: except:
pass pass
else:
logger.warning(f"Camera {self.camera_id}: No frame files found in {self.frame_dir} with pattern {self.frame_prefix}*.ppm")
except Exception as e: except Exception as e:
logger.debug(f"Camera {self.camera_id}: Error reading frames: {e}") logger.debug(f"Camera {self.camera_id}: Error reading frames: {e}")