fix: camera api endpoint
This commit is contained in:
parent
83aaf95f59
commit
519e073f7f
5 changed files with 69 additions and 68 deletions
56
app.py
56
app.py
|
@ -6,8 +6,9 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import time
|
||||
import cv2
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, WebSocket, HTTPException, Request
|
||||
from fastapi import FastAPI, WebSocket, HTTPException
|
||||
from fastapi.responses import Response
|
||||
|
||||
# Import new modular communication system
|
||||
|
@ -27,8 +28,8 @@ logging.basicConfig(
|
|||
logger = logging.getLogger("detector_worker")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Store cached frames for REST API access (temporary storage)
|
||||
latest_frames = {}
|
||||
# Frames are now stored in the shared cache buffer from core.streaming.buffers
|
||||
# latest_frames = {} # Deprecated - using shared_cache_buffer instead
|
||||
|
||||
# Lifespan event handler (modern FastAPI approach)
|
||||
@asynccontextmanager
|
||||
|
@ -49,7 +50,7 @@ async def lifespan(app: FastAPI):
|
|||
worker_state.set_subscriptions([])
|
||||
worker_state.session_ids.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")
|
||||
|
||||
# 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))
|
||||
logger.info(f"Initialized stream manager with max_streams={config.get('max_streams', 10)}")
|
||||
|
||||
# Store cached frames for REST API access (temporary storage)
|
||||
latest_frames = {}
|
||||
# Frames are now stored in the shared cache buffer from core.streaming.buffers
|
||||
# latest_frames = {} # Deprecated - using shared_cache_buffer instead
|
||||
|
||||
logger.info("Starting detector worker application (refactored)")
|
||||
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"
|
||||
)
|
||||
|
||||
# Check if we have a cached frame for this camera
|
||||
if camera_id not in latest_frames:
|
||||
logger.warning(f"No cached frame available for camera '{camera_id}'")
|
||||
# Extract actual camera_id from subscription identifier (displayId;cameraId)
|
||||
# Frames are stored using just the camera_id part
|
||||
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(
|
||||
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 '{camera_id}', shape: {frame.shape}")
|
||||
logger.debug(f"Retrieved cached frame for camera '{actual_camera_id}' (from subscription '{camera_id}'), shape: {frame.shape}")
|
||||
|
||||
# TODO: This import will be replaced in Phase 3 (Streaming System)
|
||||
# For now, we need to handle the case where OpenCV is not available
|
||||
try:
|
||||
import cv2
|
||||
# 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")
|
||||
# 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 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")
|
||||
# Return image as binary response
|
||||
return Response(content=buffer_img.tobytes(), media_type="image/jpeg")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
|
@ -377,6 +377,8 @@ class WebSocketHandler:
|
|||
camera_id = subscription_id.split(';')[-1]
|
||||
model_id = payload['modelId']
|
||||
|
||||
logger.info(f"[SUBSCRIPTION_MAPPING] subscription_id='{subscription_id}' → camera_id='{camera_id}'")
|
||||
|
||||
# Get tracking integration for this model
|
||||
tracking_integration = tracking_integrations.get(model_id)
|
||||
|
||||
|
|
|
@ -46,13 +46,7 @@ class FrameBuffer:
|
|||
|
||||
frame_data = self._frames[camera_id]
|
||||
|
||||
# Check if frame is too old
|
||||
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 regardless of age - frames persist until replaced
|
||||
return frame_data['frame'].copy()
|
||||
|
||||
def get_frame_info(self, camera_id: str) -> Optional[Dict[str, Any]]:
|
||||
|
@ -64,10 +58,7 @@ class FrameBuffer:
|
|||
frame_data = self._frames[camera_id]
|
||||
age = time.time() - frame_data['timestamp']
|
||||
|
||||
if age > self.max_age_seconds:
|
||||
del self._frames[camera_id]
|
||||
return None
|
||||
|
||||
# Return frame info regardless of age - frames persist until replaced
|
||||
return {
|
||||
'timestamp': frame_data['timestamp'],
|
||||
'age': age,
|
||||
|
@ -95,24 +86,10 @@ class FrameBuffer:
|
|||
logger.debug(f"Cleared all frames ({count} cameras)")
|
||||
|
||||
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:
|
||||
current_time = time.time()
|
||||
valid_cameras = []
|
||||
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
|
||||
# Return all cameras that have frames - no age-based filtering
|
||||
return list(self._frames.keys())
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get buffer statistics."""
|
||||
|
@ -120,8 +97,8 @@ class FrameBuffer:
|
|||
current_time = time.time()
|
||||
stats = {
|
||||
'total_cameras': len(self._frames),
|
||||
'valid_cameras': 0,
|
||||
'expired_cameras': 0,
|
||||
'recent_cameras': 0,
|
||||
'stale_cameras': 0,
|
||||
'total_memory_mb': 0,
|
||||
'cameras': {}
|
||||
}
|
||||
|
@ -130,16 +107,17 @@ class FrameBuffer:
|
|||
age = current_time - frame_data['timestamp']
|
||||
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:
|
||||
stats['valid_cameras'] += 1
|
||||
stats['recent_cameras'] += 1
|
||||
else:
|
||||
stats['expired_cameras'] += 1
|
||||
stats['stale_cameras'] += 1
|
||||
|
||||
stats['total_memory_mb'] += size_mb
|
||||
|
||||
stats['cameras'][camera_id] = {
|
||||
'age': age,
|
||||
'valid': age <= self.max_age_seconds,
|
||||
'recent': age <= self.max_age_seconds, # Recent but all frames available
|
||||
'shape': frame_data['shape'],
|
||||
'dtype': frame_data['dtype'],
|
||||
'size_mb': size_mb
|
||||
|
|
|
@ -130,6 +130,7 @@ class StreamManager:
|
|||
try:
|
||||
if stream_config.rtsp_url:
|
||||
# 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(
|
||||
camera_id=camera_id,
|
||||
rtsp_url=stream_config.rtsp_url,
|
||||
|
@ -138,10 +139,11 @@ class StreamManager:
|
|||
reader.set_frame_callback(self._frame_callback)
|
||||
reader.start()
|
||||
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:
|
||||
# HTTP snapshot stream
|
||||
logger.info(f"[STREAM_START] Starting HTTP snapshot stream for camera_id='{camera_id}' URL={stream_config.snapshot_url}")
|
||||
reader = HTTPSnapshotReader(
|
||||
camera_id=camera_id,
|
||||
snapshot_url=stream_config.snapshot_url,
|
||||
|
@ -151,7 +153,7 @@ class StreamManager:
|
|||
reader.set_frame_callback(self._frame_callback)
|
||||
reader.start()
|
||||
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:
|
||||
logger.error(f"No valid URL provided for camera {camera_id}")
|
||||
|
@ -169,8 +171,9 @@ class StreamManager:
|
|||
try:
|
||||
self._streams[camera_id].stop()
|
||||
del self._streams[camera_id]
|
||||
shared_cache_buffer.clear_camera(camera_id)
|
||||
logger.info(f"Stopped stream for camera {camera_id}")
|
||||
# DON'T clear frames - they should persist until replaced
|
||||
# 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:
|
||||
logger.error(f"Error stopping stream for camera {camera_id}: {e}")
|
||||
|
||||
|
@ -179,6 +182,11 @@ class StreamManager:
|
|||
try:
|
||||
# Store frame in shared buffer
|
||||
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
|
||||
self._process_tracking_for_camera(camera_id, frame)
|
||||
|
|
|
@ -101,14 +101,14 @@ class FFmpegRTSPReader:
|
|||
# This ensures each file is complete when written
|
||||
camera_id_safe = self.camera_id.replace(' ', '_')
|
||||
self.frame_prefix = f"camera_{camera_id_safe}"
|
||||
# Using strftime pattern with microseconds for unique filenames
|
||||
self.frame_pattern = f"{self.frame_dir}/{self.frame_prefix}_%Y%m%d_%H%M%S_%f.ppm"
|
||||
# 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.ppm"
|
||||
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
# DO NOT REMOVE
|
||||
'-hwaccel', 'cuda',
|
||||
'-hwaccel_device', '0',
|
||||
# '-hwaccel', 'cuda',
|
||||
# '-hwaccel_device', '0',
|
||||
'-rtsp_transport', 'tcp',
|
||||
'-i', self.rtsp_url,
|
||||
'-f', 'image2',
|
||||
|
@ -201,14 +201,17 @@ class FFmpegRTSPReader:
|
|||
# Sort by filename (which includes timestamp) and get the latest
|
||||
frame_files.sort()
|
||||
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)
|
||||
frame = cv2.imread(latest_frame)
|
||||
|
||||
if frame is not None and frame.shape == (self.height, self.width, 3):
|
||||
# Call frame callback directly
|
||||
if frame is not None:
|
||||
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:
|
||||
self.frame_callback(self.camera_id, frame)
|
||||
logger.debug(f"Camera {self.camera_id}: Called frame callback")
|
||||
|
||||
frame_count += 1
|
||||
|
||||
|
@ -217,6 +220,8 @@ class FFmpegRTSPReader:
|
|||
if current_time - last_log_time >= 30:
|
||||
logger.info(f"Camera {self.camera_id}: {frame_count} frames processed")
|
||||
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
|
||||
# Keep only the latest 5 frames
|
||||
|
@ -226,6 +231,8 @@ class FFmpegRTSPReader:
|
|||
os.remove(old_file)
|
||||
except:
|
||||
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:
|
||||
logger.debug(f"Camera {self.camera_id}: Error reading frames: {e}")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue