From 7622d2ae5c5a6e1016b6d8c26851da2fde101e1e Mon Sep 17 00:00:00 2001 From: maziggy Date: Wed, 24 Dec 2025 08:41:23 +0100 Subject: [PATCH] - Fix total print hours calculation in set_total_hours to include all prints (not just completed), matching get_printer_total_hours behavior - Add option to keep or delete archives when deleting a printer - Custom maintenance types no longer auto-assign to all printers - Add UI to manually assign/remove custom maintenance types per printer - Add backend endpoints for assigning types to printers and removing items - Exclude static/assets from large file pre-commit check - Fix A1/P1 camera streaming with extended timeouts and lower FPS cap --- backend/app/api/routes/camera.py | 58 +++++++++++++++++++++++++------- backend/app/services/camera.py | 44 +++++++++++++++--------- 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/backend/app/api/routes/camera.py b/backend/app/api/routes/camera.py index 0812835bf..2d368fa82 100644 --- a/backend/app/api/routes/camera.py +++ b/backend/app/api/routes/camera.py @@ -15,6 +15,7 @@ from backend.app.services.camera import ( capture_camera_frame, get_camera_port, get_ffmpeg_path, + is_low_fps_model, test_camera_connection, ) @@ -66,6 +67,11 @@ async def generate_mjpeg_stream( port = get_camera_port(model) camera_url = f"rtsps://bblp:{access_code}@{ip_address}:{port}/streaming/live/1" + low_fps = is_low_fps_model(model) + + # For A1/P1 models, use lower FPS and longer timeouts + # These models have more limited camera streaming capability + effective_fps = min(fps, 5) if low_fps else fps # ffmpeg command to output MJPEG stream to stdout # -rtsp_transport tcp: Use TCP for reliability @@ -79,19 +85,41 @@ async def generate_mjpeg_stream( "tcp", "-rtsp_flags", "prefer_tcp", - "-i", - camera_url, - "-f", - "mjpeg", - "-q:v", - "5", - "-r", - str(fps), - "-an", # No audio - "-", # Output to stdout ] - logger.info(f"Starting camera stream for {ip_address} (stream_id={stream_id})") + # Add longer timeouts for A1/P1 models which may be slower to respond + if low_fps: + cmd.extend( + [ + "-timeout", + "10000000", # 10 seconds in microseconds + "-stimeout", + "10000000", # Socket timeout + "-analyzeduration", + "10000000", # Longer analysis time + "-probesize", + "5000000", # Larger probe size + ] + ) + + cmd.extend( + [ + "-i", + camera_url, + "-f", + "mjpeg", + "-q:v", + "5", + "-r", + str(effective_fps), + "-an", # No audio + "-", # Output to stdout + ] + ) + + logger.info(f"Starting camera stream for {ip_address} (stream_id={stream_id}, model={model}, fps={effective_fps})") + if low_fps: + logger.info(f"Using extended timeouts for {model} camera") logger.debug(f"ffmpeg command: {ffmpeg} ... (url hidden)") process = None @@ -107,7 +135,9 @@ async def generate_mjpeg_stream( _active_streams[stream_id] = process # Give ffmpeg a moment to start and check for immediate failures - await asyncio.sleep(0.5) + # A1/P1 models may need longer to establish connection + startup_wait = 2.0 if low_fps else 0.5 + await asyncio.sleep(startup_wait) if process.returncode is not None: stderr = await process.stderr.read() logger.error(f"ffmpeg failed immediately: {stderr.decode()}") @@ -132,7 +162,9 @@ async def generate_mjpeg_stream( try: # Read chunk from ffmpeg - chunk = await asyncio.wait_for(process.stdout.read(8192), timeout=10.0) + # A1/P1 models may have longer gaps between frames + read_timeout = 30.0 if low_fps else 10.0 + chunk = await asyncio.wait_for(process.stdout.read(8192), timeout=read_timeout) if not chunk: logger.warning("Camera stream ended (no more data)") diff --git a/backend/app/services/camera.py b/backend/app/services/camera.py index d2d454044..341e8f848 100644 --- a/backend/app/services/camera.py +++ b/backend/app/services/camera.py @@ -6,11 +6,9 @@ Captures images from the printer's RTSPS camera stream using ffmpeg. import asyncio import logging import shutil -from pathlib import Path -from datetime import datetime import uuid - -from backend.app.core.config import settings +from datetime import datetime +from pathlib import Path logger = logging.getLogger(__name__) @@ -69,6 +67,19 @@ def get_camera_port(model: str | None) -> int: return 6000 +def is_low_fps_model(model: str | None) -> bool: + """Check if printer model has limited camera FPS capability. + + A1 and P1 series have more limited camera streaming compared to X1. + They may need lower FPS and longer timeouts. + """ + if model: + model_upper = model.upper() + if model_upper.startswith(("A1", "P1")): + return True + return False + + def build_camera_url(ip_address: str, access_code: str, model: str | None) -> str: """Build the RTSPS URL for the printer camera.""" port = get_camera_port(model) @@ -114,12 +125,18 @@ async def capture_camera_frame( cmd = [ ffmpeg, "-y", # Overwrite output - "-rtsp_transport", "tcp", - "-rtsp_flags", "prefer_tcp", - "-i", camera_url, - "-frames:v", "1", - "-update", "1", - "-q:v", "2", + "-rtsp_transport", + "tcp", + "-rtsp_flags", + "prefer_tcp", + "-i", + camera_url, + "-frames:v", + "1", + "-update", + "1", + "-q:v", + "2", str(output_path), ] @@ -134,11 +151,8 @@ async def capture_camera_frame( ) try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), - timeout=timeout - ) - except asyncio.TimeoutError: + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) + except TimeoutError: process.kill() await process.wait() logger.error(f"Camera capture timed out after {timeout}s")