Refactor API route setup and enhance modularity
- Consolidated API route definitions by registering routes from separate modules for better organization and maintainability. - Removed redundant route definitions from the APIServer class, improving code clarity. - Updated camera monitoring and recording modules to utilize a shared context manager for suppressing camera SDK errors, enhancing error handling. - Adjusted timeout settings in camera operations for improved reliability during frame capture. - Enhanced logging and error handling across camera operations to facilitate better debugging and monitoring.
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
Camera-related API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from ...core.config import Config
|
||||
from ...core.state_manager import StateManager
|
||||
from ...camera.manager import CameraManager
|
||||
from ..models import (
|
||||
CameraStatusResponse,
|
||||
CameraTestResponse,
|
||||
CameraConfigResponse,
|
||||
CameraConfigRequest,
|
||||
CameraRecoveryResponse,
|
||||
MachineStatusResponse,
|
||||
)
|
||||
|
||||
|
||||
def register_camera_routes(
|
||||
app: FastAPI,
|
||||
config: Config,
|
||||
state_manager: StateManager,
|
||||
camera_manager: CameraManager,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register camera-related routes"""
|
||||
|
||||
@app.get("/machines", response_model=Dict[str, MachineStatusResponse])
|
||||
async def get_machines():
|
||||
"""Get all machine statuses"""
|
||||
try:
|
||||
machines = state_manager.get_all_machines()
|
||||
return {
|
||||
name: MachineStatusResponse(
|
||||
name=machine.name,
|
||||
state=machine.state.value,
|
||||
last_updated=machine.last_updated.isoformat(),
|
||||
last_message=machine.last_message,
|
||||
mqtt_topic=machine.mqtt_topic
|
||||
)
|
||||
for name, machine in machines.items()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting machines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras", response_model=Dict[str, CameraStatusResponse])
|
||||
async def get_cameras():
|
||||
"""Get all camera statuses"""
|
||||
try:
|
||||
cameras = state_manager.get_all_cameras()
|
||||
return {
|
||||
name: CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None,
|
||||
auto_recording_enabled=camera.auto_recording_enabled,
|
||||
auto_recording_active=camera.auto_recording_active,
|
||||
auto_recording_failure_count=camera.auto_recording_failure_count,
|
||||
auto_recording_last_attempt=camera.auto_recording_last_attempt.isoformat() if camera.auto_recording_last_attempt else None,
|
||||
auto_recording_last_error=camera.auto_recording_last_error,
|
||||
)
|
||||
for name, camera in cameras.items()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cameras: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse)
|
||||
async def get_camera_status(camera_name: str):
|
||||
"""Get specific camera status"""
|
||||
try:
|
||||
camera = state_manager.get_camera_status(camera_name)
|
||||
if not camera:
|
||||
raise HTTPException(status_code=404, detail=f"Camera not found: {camera_name}")
|
||||
|
||||
return CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting camera status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/test-connection", response_model=CameraTestResponse)
|
||||
async def test_camera_connection(camera_name: str):
|
||||
"""Test camera connection"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.test_camera_connection(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraTestResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} connection test passed",
|
||||
camera_name=camera_name
|
||||
)
|
||||
else:
|
||||
return CameraTestResponse(
|
||||
success=False,
|
||||
message=f"Camera {camera_name} connection test failed",
|
||||
camera_name=camera_name
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing camera connection: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras/{camera_name}/stream")
|
||||
async def camera_stream(camera_name: str):
|
||||
"""Get live MJPEG stream from camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
# Get camera streamer
|
||||
streamer = camera_manager.get_camera_streamer(camera_name)
|
||||
if not streamer:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
# Start streaming if not already active
|
||||
if not streamer.is_streaming():
|
||||
success = streamer.start_streaming()
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start streaming for camera {camera_name}")
|
||||
|
||||
# Return MJPEG stream
|
||||
return StreamingResponse(streamer.get_frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/start-stream")
|
||||
async def start_camera_stream(camera_name: str):
|
||||
"""Start streaming for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.start_camera_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Started streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to start streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/stop-stream")
|
||||
async def stop_camera_stream(camera_name: str):
|
||||
"""Stop streaming for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.stop_camera_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Stopped streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to stop streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/start-rtsp")
|
||||
async def start_camera_rtsp_stream(camera_name: str):
|
||||
"""Start RTSP streaming for a camera to MediaMTX"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.start_camera_rtsp_streaming(camera_name)
|
||||
if success:
|
||||
rtsp_url = f"rtsp://{os.getenv('MEDIAMTX_HOST', 'localhost')}:{os.getenv('MEDIAMTX_RTSP_PORT', '8554')}/{camera_name}"
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Started RTSP streaming for camera {camera_name}",
|
||||
"rtsp_url": rtsp_url
|
||||
}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to start RTSP streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting RTSP stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/stop-rtsp")
|
||||
async def stop_camera_rtsp_stream(camera_name: str):
|
||||
"""Stop RTSP streaming for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.stop_camera_rtsp_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Stopped RTSP streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to stop RTSP streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping RTSP stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras/{camera_name}/config", response_model=CameraConfigResponse)
|
||||
async def get_camera_config(camera_name: str):
|
||||
"""Get camera configuration"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
camera_config = config.get_camera_by_name(camera_name)
|
||||
if not camera_config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
return CameraConfigResponse(
|
||||
name=camera_config.name,
|
||||
machine_topic=camera_config.machine_topic,
|
||||
storage_path=camera_config.storage_path,
|
||||
enabled=camera_config.enabled,
|
||||
# Auto-recording settings
|
||||
auto_start_recording_enabled=camera_config.auto_start_recording_enabled,
|
||||
auto_recording_max_retries=camera_config.auto_recording_max_retries,
|
||||
auto_recording_retry_delay_seconds=camera_config.auto_recording_retry_delay_seconds,
|
||||
# Basic settings
|
||||
exposure_ms=camera_config.exposure_ms,
|
||||
gain=camera_config.gain,
|
||||
target_fps=camera_config.target_fps,
|
||||
# Video recording settings
|
||||
video_format=camera_config.video_format,
|
||||
video_codec=camera_config.video_codec,
|
||||
video_quality=camera_config.video_quality,
|
||||
# Image Quality Settings
|
||||
sharpness=camera_config.sharpness,
|
||||
contrast=camera_config.contrast,
|
||||
saturation=camera_config.saturation,
|
||||
gamma=camera_config.gamma,
|
||||
# Noise Reduction
|
||||
noise_filter_enabled=camera_config.noise_filter_enabled,
|
||||
denoise_3d_enabled=camera_config.denoise_3d_enabled,
|
||||
# Color Settings
|
||||
auto_white_balance=camera_config.auto_white_balance,
|
||||
color_temperature_preset=camera_config.color_temperature_preset,
|
||||
# Manual White Balance RGB Gains
|
||||
wb_red_gain=camera_config.wb_red_gain,
|
||||
wb_green_gain=camera_config.wb_green_gain,
|
||||
wb_blue_gain=camera_config.wb_blue_gain,
|
||||
# Advanced Settings
|
||||
anti_flicker_enabled=camera_config.anti_flicker_enabled,
|
||||
light_frequency=camera_config.light_frequency,
|
||||
bit_depth=camera_config.bit_depth,
|
||||
# HDR Settings
|
||||
hdr_enabled=camera_config.hdr_enabled,
|
||||
hdr_gain_mode=camera_config.hdr_gain_mode,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.put("/cameras/{camera_name}/config")
|
||||
async def update_camera_config(camera_name: str, request: CameraConfigRequest):
|
||||
"""Update camera configuration"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
# Convert request to dict, excluding None values
|
||||
config_updates = {k: v for k, v in request.dict().items() if v is not None}
|
||||
|
||||
if not config_updates:
|
||||
raise HTTPException(status_code=400, detail="No configuration updates provided")
|
||||
|
||||
success = camera_manager.update_camera_config(camera_name, **config_updates)
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Camera {camera_name} configuration updated",
|
||||
"updated_settings": list(config_updates.keys())
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found or update failed")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/apply-config")
|
||||
async def apply_camera_config(camera_name: str):
|
||||
"""Apply current configuration to active camera (requires camera restart)"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.apply_camera_config(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Configuration applied to camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to apply configuration to camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse)
|
||||
async def reconnect_camera(camera_name: str):
|
||||
"""Reconnect to a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.reconnect_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} reconnected successfully",
|
||||
camera_name=camera_name,
|
||||
operation="reconnect"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to reconnect camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="reconnect"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error reconnecting camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/restart-grab", response_model=CameraRecoveryResponse)
|
||||
async def restart_camera_grab(camera_name: str):
|
||||
"""Restart camera grab process"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.restart_camera_grab(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} grab process restarted successfully",
|
||||
camera_name=camera_name,
|
||||
operation="restart-grab"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to restart grab process for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="restart-grab"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error restarting camera grab: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/reset-timestamp", response_model=CameraRecoveryResponse)
|
||||
async def reset_camera_timestamp(camera_name: str):
|
||||
"""Reset camera timestamp"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.reset_camera_timestamp(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} timestamp reset successfully",
|
||||
camera_name=camera_name,
|
||||
operation="reset-timestamp"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to reset timestamp for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="reset-timestamp"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting camera timestamp: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/full-reset", response_model=CameraRecoveryResponse)
|
||||
async def full_reset_camera(camera_name: str):
|
||||
"""Perform full camera reset (uninitialize and reinitialize)"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.full_reset_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} full reset completed successfully",
|
||||
camera_name=camera_name,
|
||||
operation="full-reset"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to perform full reset for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="full-reset"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error performing full camera reset: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/reinitialize", response_model=CameraRecoveryResponse)
|
||||
async def reinitialize_camera(camera_name: str):
|
||||
"""Reinitialize a failed camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.reinitialize_failed_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} reinitialized successfully",
|
||||
camera_name=camera_name,
|
||||
operation="reinitialize"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to reinitialize camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="reinitialize"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error reinitializing camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/debug/camera-manager")
|
||||
async def debug_camera_manager():
|
||||
"""Debug endpoint to check camera manager state"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
return {"error": "Camera manager not available"}
|
||||
|
||||
return {
|
||||
"available_cameras": len(camera_manager.available_cameras),
|
||||
"camera_recorders": list(camera_manager.camera_recorders.keys()),
|
||||
"camera_streamers": list(camera_manager.camera_streamers.keys()),
|
||||
"streamer_states": {
|
||||
name: {
|
||||
"exists": streamer is not None,
|
||||
"is_streaming": streamer.is_streaming() if streamer else False,
|
||||
"streaming": getattr(streamer, 'streaming', False) if streamer else False
|
||||
}
|
||||
for name, streamer in camera_manager.camera_streamers.items()
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
Reference in New Issue
Block a user