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 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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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}")