Browse Source

Revert " Camera P2S + snapshot fix (#661):"

This reverts commit 7ea78a2969bbac4b3956263a24312b50358c92d6.
maziggy 2 months ago
parent
commit
9f93c04f6f
4 changed files with 16 additions and 40 deletions
  1. 1 3
      CHANGELOG.md
  2. 1 3
      Dockerfile
  3. 14 23
      backend/app/api/routes/camera.py
  4. 0 11
      backend/app/services/camera.py

+ 1 - 3
CHANGELOG.md

@@ -14,9 +14,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Cloud Profiles Shared Across All Users** ([#665](https://github.com/maziggy/bambuddy/issues/665)) — When authentication was enabled, Bambu Cloud credentials were stored globally — one account per Bambuddy instance. If User A logged into Cloud, every other user saw User A's account and profiles. User B logging in would overwrite User A's credentials. Cloud credentials are now stored per-user: each user logs into their own Bambu Cloud account independently. When auth is disabled (single-user mode), behavior is unchanged. Also fixed cloud data endpoints (`/cloud/settings`, `/cloud/fields`, preset CRUD) requiring `settings:read` / `settings:update` permissions instead of `cloud:auth` — users who had "Cloud Auth" enabled but "Settings" disabled couldn't load profiles after logging in. Reported by @cadtoolbox.
 - **Cloud Profiles Shared Across All Users** ([#665](https://github.com/maziggy/bambuddy/issues/665)) — When authentication was enabled, Bambu Cloud credentials were stored globally — one account per Bambuddy instance. If User A logged into Cloud, every other user saw User A's account and profiles. User B logging in would overwrite User A's credentials. Cloud credentials are now stored per-user: each user logs into their own Bambu Cloud account independently. When auth is disabled (single-user mode), behavior is unchanged. Also fixed cloud data endpoints (`/cloud/settings`, `/cloud/fields`, preset CRUD) requiring `settings:read` / `settings:update` permissions instead of `cloud:auth` — users who had "Cloud Auth" enabled but "Settings" disabled couldn't load profiles after logging in. Reported by @cadtoolbox.
 - **Local Profiles Not Shown in AMS Slot Configuration** — Imported local filament profiles were hidden in the AMS slot configure modal when a printer model was set. The `compatible_printers` filter parsed the stored JSON array as a semicolon-delimited string, so the matching always failed and every local preset was silently skipped. Removed the filter entirely — user-imported profiles should be available on any printer.
 - **Local Profiles Not Shown in AMS Slot Configuration** — Imported local filament profiles were hidden in the AMS slot configure modal when a printer model was set. The `compatible_printers` filter parsed the stored JSON array as a semicolon-delimited string, so the matching always failed and every local preset was silently skipped. Removed the filter entirely — user-imported profiles should be available on any printer.
 - **Interface Aliases Not Shown in Virtual Printer Interface Select** — Interface aliases (e.g. `eth0:1`) added for multi-virtual-printer setups were invisible in the bind IP dropdown. The Docker image didn't include `iproute2`, so the `ip` command wasn't available and the code fell back to ioctl-based enumeration which can only return one IP per interface. Added `iproute2` to the Docker image.
 - **Interface Aliases Not Shown in Virtual Printer Interface Select** — Interface aliases (e.g. `eth0:1`) added for multi-virtual-printer setups were invisible in the bind IP dropdown. The Docker image didn't include `iproute2`, so the `ip` command wasn't available and the code fell back to ioctl-based enumeration which can only return one IP per interface. Added `iproute2` to the Docker image.
-- **P2S Camera Stream Disconnects After a Few Seconds** ([#661](https://github.com/maziggy/bambuddy/issues/661)) — The P2S firmware drops RTSP sessions after a few seconds with an I/O error due to TLS renegotiation rejected by Debian's hardened GnuTLS defaults. The backend treated this as a fatal failure. Three fixes: (1) Added a GnuTLS config in the Docker image allowing unsafe renegotiation and legacy ciphers for P2S compatibility. (2) Added ffmpeg fast-start flags (`-probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay`) and reduced reconnect delay from 1.0s to 0.2s so even transient drops barely interrupt the stream. (3) Removed double rate-limiting on external camera streams (go2rtc, etc.) that caused choppiness — `generate_mjpeg_stream()` already throttles, so the wrapper no longer throttles again. Also added GnuTLS debug logging when debug mode is active for TLS diagnostics. Reported by @ddetton, confirmed by @DMoenning.
-- **Camera Snapshot Capture Killed by Orphan Cleanup** — The periodic orphan ffmpeg cleanup scanned `/proc` for Bambu RTSP processes and killed any not tracked in `_active_streams`. Snapshot captures (`capture_camera_frame_bytes`) spawn short-lived ffmpeg processes that weren't registered, so they got SIGKILL'd (exit code -9) if the cleanup ran during capture. Now registers snapshot PIDs in the spawn tracker and excludes all tracked PIDs from the orphan scan.
-- **Energy Costs Not Updating Per Timeframe** ([#695](https://github.com/maziggy/bambuddy/issues/695)) — In "total" energy tracking mode, the statistics endpoint queried smart plug lifetime counters which can't be filtered by date range, so energy costs and kWh values stayed the same regardless of the selected timeframe (Today, This Week, This Month). Now falls back to per-print archive data when date filters are active. Reported by @Bademeister89.
+- **P2S Camera Stream Disconnects After a Few Seconds** ([#661](https://github.com/maziggy/bambuddy/issues/661)) — The P2S firmware drops RTSP sessions after a few seconds with an I/O error. The backend treated this as a fatal failure, ending the MJPEG stream and forcing the frontend through a full reconnection cycle (stop → start → brief connection → fail → repeat). Added transparent auto-reconnection: when ffmpeg's RTSP connection dies, it respawns immediately and continues streaming MJPEG frames to the browser without interruption. Reported by @ddetton, confirmed by @DMoenning.
 - **iOS/iPadOS Cannot Reposition Floating Camera** ([#687](https://github.com/maziggy/bambuddy/issues/687)) — The floating camera viewer (embedded camera window on the dashboard) could not be dragged or resized on iOS/iPadOS because it only handled mouse events. Touch input scrolled the page underneath instead of moving the camera window. Added touch event support (`touchstart`/`touchmove`/`touchend`) to both the header drag handle and the resize handle, with `preventDefault` to stop page scrolling during drag. Reported by @dsmitty166.
 - **iOS/iPadOS Cannot Reposition Floating Camera** ([#687](https://github.com/maziggy/bambuddy/issues/687)) — The floating camera viewer (embedded camera window on the dashboard) could not be dragged or resized on iOS/iPadOS because it only handled mouse events. Touch input scrolled the page underneath instead of moving the camera window. Added touch event support (`touchstart`/`touchmove`/`touchend`) to both the header drag handle and the resize handle, with `preventDefault` to stop page scrolling during drag. Reported by @dsmitty166.
 - **PA-CF / PA12-CF / PAHT-CF Not Treated as Compatible** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable, but the print scheduler and filament override UI used exact string matching. If a 3MF required PA-CF but the AMS had PA12-CF loaded, the scheduler wouldn't assign the job and the filament override dropdown was empty/disabled. Added a filament type equivalence system so these PA variants are treated as compatible in scheduler assignment, AMS slot matching, force color match validation, and the filament override dropdown. Reported by @aneopsy.
 - **PA-CF / PA12-CF / PAHT-CF Not Treated as Compatible** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable, but the print scheduler and filament override UI used exact string matching. If a 3MF required PA-CF but the AMS had PA12-CF loaded, the scheduler wouldn't assign the job and the filament override dropdown was empty/disabled. Added a filament type equivalence system so these PA variants are treated as compatible in scheduler assignment, AMS slot matching, force color match validation, and the filament override dropdown. Reported by @aneopsy.
 - **Force Color Match Toggle Click Target Too Large** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — In the Schedule Print modal, clicking anywhere on the "Force color match" row toggled the checkbox, not just the checkbox and its label. The click target now covers only the checkbox, icon, and label text. Reported by @aneopsy.
 - **Force Color Match Toggle Click Target Too Large** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — In the Schedule Print modal, clicking anywhere on the "Force color match" row toggled the checkbox, not just the checkbox and its label. The click target now covers only the checkbox, icon, and label text. Reported by @aneopsy.

+ 1 - 3
Dockerfile

@@ -25,9 +25,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
     ffmpeg \
     ffmpeg \
     iproute2 \
     iproute2 \
     libcap2-bin \
     libcap2-bin \
-    && rm -rf /var/lib/apt/lists/* \
-    && mkdir -p /etc/gnutls \
-    && printf '[overrides]\ninsecure-hash = SHA1\n[priorities]\nSYSTEM = NORMAL:%%UNSAFE_RENEGOTIATION:%%COMPAT\n' > /etc/gnutls/config
+    && rm -rf /var/lib/apt/lists/*
 
 
 # Allow binding to privileged ports (e.g. 990/FTPS) as non-root user.
 # Allow binding to privileged ports (e.g. 990/FTPS) as non-root user.
 # File capabilities are more reliable than Docker cap_add with user: directive,
 # File capabilities are more reliable than Docker cap_add with user: directive,

+ 14 - 23
backend/app/api/routes/camera.py

@@ -197,7 +197,7 @@ async def _read_ffmpeg_stderr(process: asyncio.subprocess.Process) -> str | None
 # Some printer firmwares (notably P2S) drop RTSP sessions after a few seconds,
 # Some printer firmwares (notably P2S) drop RTSP sessions after a few seconds,
 # so we transparently respawn ffmpeg to keep the MJPEG stream alive.
 # so we transparently respawn ffmpeg to keep the MJPEG stream alive.
 _RTSP_MAX_RECONNECTS = 30
 _RTSP_MAX_RECONNECTS = 30
-_RTSP_RECONNECT_DELAY = 0.2  # seconds between respawns
+_RTSP_RECONNECT_DELAY = 1.0  # seconds between respawns
 
 
 
 
 async def generate_rtsp_mjpeg_stream(
 async def generate_rtsp_mjpeg_stream(
@@ -244,14 +244,6 @@ async def generate_rtsp_mjpeg_stream(
         "1024000",  # 1MB buffer
         "1024000",  # 1MB buffer
         "-max_delay",
         "-max_delay",
         "500000",  # 0.5 seconds max delay
         "500000",  # 0.5 seconds max delay
-        "-probesize",
-        "32",  # Minimal probing for fast start
-        "-analyzeduration",
-        "0",  # Skip format analysis
-        "-fflags",
-        "nobuffer",  # Reduce internal buffering
-        "-flags",
-        "low_delay",  # Minimize decode latency
         "-i",
         "-i",
         camera_url,
         camera_url,
         "-f",
         "-f",
@@ -299,18 +291,11 @@ async def generate_rtsp_mjpeg_stream(
                 if disconnect_event and disconnect_event.is_set():
                 if disconnect_event and disconnect_event.is_set():
                     break
                     break
 
 
-            # Spawn ffmpeg — enable GnuTLS debug output in debug mode
-            env = None
-            if logger.isEnabledFor(logging.DEBUG):
-                import os
-
-                env = {**os.environ, "GNUTLS_DEBUG_LEVEL": "2"}
-
+            # Spawn ffmpeg
             process = await asyncio.create_subprocess_exec(
             process = await asyncio.create_subprocess_exec(
                 *cmd,
                 *cmd,
                 stdout=asyncio.subprocess.PIPE,
                 stdout=asyncio.subprocess.PIPE,
                 stderr=asyncio.subprocess.PIPE,
                 stderr=asyncio.subprocess.PIPE,
-                env=env,
                 **spawn_kwargs,
                 **spawn_kwargs,
             )
             )
 
 
@@ -321,7 +306,7 @@ async def generate_rtsp_mjpeg_stream(
             _spawned_ffmpeg_pids[process.pid] = _time.time()
             _spawned_ffmpeg_pids[process.pid] = _time.time()
 
 
             # Give ffmpeg a moment to start and check for immediate failures
             # Give ffmpeg a moment to start and check for immediate failures
-            await asyncio.sleep(0.1)
+            await asyncio.sleep(0.5)
             if process.returncode is not None:
             if process.returncode is not None:
                 stderr = await process.stderr.read()
                 stderr = await process.stderr.read()
                 stderr_text = stderr.decode(errors="replace")
                 stderr_text = stderr.decode(errors="replace")
@@ -501,12 +486,19 @@ async def camera_stream(
 
 
         async def external_stream_wrapper():
         async def external_stream_wrapper():
             """Wrap external stream to track start/stop and update frame times."""
             """Wrap external stream to track start/stop and update frame times."""
+            frame_interval = 1.0 / fps
+            last_yield_time = 0.0
             try:
             try:
                 async for frame in generate_mjpeg_stream(
                 async for frame in generate_mjpeg_stream(
                     printer.external_camera_url, printer.external_camera_type, fps
                     printer.external_camera_url, printer.external_camera_type, fps
                 ):
                 ):
-                    # generate_mjpeg_stream already rate-limits; just track frame times
-                    _last_frame_times[printer_id] = time.time()
+                    # Rate limit to prevent overwhelming browser
+                    current_time = time.time()
+                    elapsed = current_time - last_yield_time
+                    if elapsed < frame_interval:
+                        await asyncio.sleep(frame_interval - elapsed)
+                    last_yield_time = time.time()
+                    _last_frame_times[printer_id] = last_yield_time
                     yield frame
                     yield frame
             finally:
             finally:
                 _active_external_streams.discard(printer_id)
                 _active_external_streams.discard(printer_id)
@@ -1282,12 +1274,11 @@ async def cleanup_orphaned_streams():
     cleaned = 0
     cleaned = 0
     now = time.time()
     now = time.time()
 
 
-    # Collect PIDs that are legitimately in-use (active streams + any tracked spawn)
+    # Collect PIDs that are legitimately in-use (active stream, process alive)
     active_pids = {proc.pid for proc in _active_streams.values() if proc.returncode is None}
     active_pids = {proc.pid for proc in _active_streams.values() if proc.returncode is None}
-    active_pids.update(_spawned_ffmpeg_pids.keys())
 
 
     # 1. /proc scan — catch ALL orphaned Bambu ffmpeg processes on the system.
     # 1. /proc scan — catch ALL orphaned Bambu ffmpeg processes on the system.
-    #    Any ffmpeg with rtsps://bblp: that is NOT tracked by us is orphaned.
+    #    Any ffmpeg with rtsps://bblp: that is NOT in an active stream is orphaned.
     for pid in _scan_bambu_ffmpeg_pids():
     for pid in _scan_bambu_ffmpeg_pids():
         if pid in active_pids:
         if pid in active_pids:
             continue
             continue

+ 0 - 11
backend/app/services/camera.py

@@ -386,10 +386,6 @@ async def capture_camera_frame_bytes(
 
 
     logger.info("Capturing camera frame bytes from %s using RTSP (model: %s)", ip_address, model)
     logger.info("Capturing camera frame bytes from %s using RTSP (model: %s)", ip_address, model)
 
 
-    # Register PID with the orphan cleanup tracker so the periodic /proc scan
-    # doesn't SIGKILL our short-lived snapshot process (see camera routes).
-    from backend.app.api.routes.camera import _spawned_ffmpeg_pids
-
     try:
     try:
         process = await asyncio.create_subprocess_exec(
         process = await asyncio.create_subprocess_exec(
             *cmd,
             *cmd,
@@ -397,21 +393,14 @@ async def capture_camera_frame_bytes(
             stderr=asyncio.subprocess.PIPE,
             stderr=asyncio.subprocess.PIPE,
         )
         )
 
 
-        import time as _time
-
-        _spawned_ffmpeg_pids[process.pid] = _time.time()
-
         try:
         try:
             stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
             stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
         except TimeoutError:
         except TimeoutError:
             process.kill()
             process.kill()
             await process.wait()
             await process.wait()
-            _spawned_ffmpeg_pids.pop(process.pid, None)
             logger.error("Camera frame bytes capture timed out after %ss", timeout)
             logger.error("Camera frame bytes capture timed out after %ss", timeout)
             return None
             return None
 
 
-        _spawned_ffmpeg_pids.pop(process.pid, None)
-
         if process.returncode == 0 and stdout and len(stdout) >= 100:
         if process.returncode == 0 and stdout and len(stdout) >= 100:
             logger.info("Successfully captured camera frame bytes: %s bytes", len(stdout))
             logger.info("Successfully captured camera frame bytes: %s bytes", len(stdout))
             return stdout
             return stdout