Просмотр исходного кода

- 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

maziggy 8 месяцев назад
Родитель
Сommit
7622d2ae5c
2 измененных файлов с 74 добавлено и 28 удалено
  1. 45 13
      backend/app/api/routes/camera.py
  2. 29 15
      backend/app/services/camera.py

+ 45 - 13
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)")

+ 29 - 15
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")