mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(camera): P2S RTSP stream dropped every frame after the first (#1395)
A fresh P2S support bundle showed ffmpeg's reason for the stall: `frame=1 time=00:00:00.06 dup=0 drop=526 speed=0.0037x`. ffmpeg connects and frames arrive (drop counter climbs ~15/s), but it emits one output frame and the output clock freezes. The streaming command ends with `-r 15`, putting ffmpeg in CFR mode: it drops/dupes input frames to hit 15 fps based on the source's timestamps. P2S firmware 01.02.00.00 sends an RTSP stream whose RTP timestamps don't advance, so CFR treats every frame after the first as a same-timestamp duplicate and drops it. Snapshot capture works on the same printer because that path has no `-r` (no CFR conversion); X1/H2 are unaffected because their firmware timestamps are correct. The earlier probesize fix was masking this second bug. Add `-use_wallclock_as_timestamps 1` to the P2S camera profile via the existing extra_ffmpeg_input_args hook. ffmpeg rebuilds each packet's PTS from arrival wall-clock time, the output clock advances, and CFR conversion works. No dataclass change, no other model touched. Tests: 2 new in test_camera_profiles.py (P2S splices the flag+value pair; default profile keeps extra_ffmpeg_input_args empty so the override never leaks to X1/H2).
This commit is contained in:
@@ -31,6 +31,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Library files now display the filename, not the embedded 3MF Title (#1489, reported by @needo37)** — File Manager cards, search and sort keyed off `file_metadata.print_name`, which `ThreeMFParser` lifts from the 3MF's `<metadata name="Title">`. That title is the in-app project title — generic `"Exported 3D Model"` for any Bambu Studio "Save As", a marketing title for a MakerWorld download — and almost never the filename the user actually saved. So a card for `Whatever.3mf` showed `Exported 3D Model`, and the only way to correct it was a rename round-trip (the Rename dialog's Save button is disabled while the name is unchanged, so the user had to rename to a different name and back). The slicer-output write path already dropped `print_name` for exactly this reason; the **four** other write paths that store parsed 3MF metadata onto a `LibraryFile` did not — external-folder scan, managed multipart upload, the multi-file ZIP-upload branch, and MakerWorld import. **Fix**: a shared `_without_print_name()` helper strips `print_name` from library-file metadata, applied at all four import paths (and the slicer path switched to it, so there is one rule). A `LibraryFile`'s display name is its filename; only `PrintArchive` carries a real `print_name`, and that is untouched. The now-redundant filename→`print_name` mirroring in the rename route is removed. A one-time data migration (`_migrate_drop_library_print_name`, idempotent, SQLite `json_remove` / PostgreSQL `jsonb` key-removal branched on `is_sqlite()`) clears `print_name` from rows imported before the fix, so existing libraries correct themselves without the rename workaround. No frontend change — `print_name || filename` naturally yields the filename once `print_name` is gone. **Tests**: 6 new in `test_library_print_name.py` — `_without_print_name` (strips, keeps siblings, `None` pass-through, no-op identity return, no input mutation, print-name-only → `{}`) and the migration (clears `print_name`, leaves siblings and metadata-free rows alone, idempotent). 109 library + dialect tests green; the migration's PostgreSQL branch additionally ran live against real Postgres during the integration-test app boot. Backend ruff clean.
|
||||
- **Camera: ffmpeg's stderr is now captured when an RTSP stream stalls instead of only when ffmpeg crashes (#1395, reported by @Tschipel)** — A P2S support bundle taken on 0.2.5b1 (the per-model probesize fix already applied) showed the camera still failing: ffmpeg connects, stays alive 30+ seconds, emits zero JPEG bytes, the stream's 30 s `stdout.read` times out, reconnect loop repeats — but with **no ffmpeg stderr anywhere in the log** to say why. Root cause was a diagnostic bug, not the camera path: `_read_ffmpeg_stderr` called `process.stderr.read()` (read-to-EOF). A stalled-but-still-alive ffmpeg — exactly the P2S RTSP failure mode — never closes stderr, so the read blocked until the 2 s `wait_for` timeout and returned `None`, discarding the banner + stream-analysis lines ffmpeg had already printed. ffmpeg's stderr was therefore captured *only* when it fully exited; the earlier "not enough frames to estimate rate" smoking gun was available only because ffmpeg crashed back then, and once the probesize bump turned the crash into a hang the diagnostic went dark. **Fix**: `_read_ffmpeg_stderr` now drains stderr incrementally in bounded 8 KB chunks (64 KB cap), returning whatever ffmpeg has printed so far whether or not it has exited — so a hung stream is self-describing in the next support bundle. Additionally, `generate_rtsp_mjpeg_stream` now logs the resolved per-model `probesize` / `analyzeduration` on the info-level "Starting RTSP camera stream" line (verifiable without debug logging), and the debug-level ffmpeg-command line logs the full argv with only the credential-bearing camera URL redacted, instead of hiding the entire command. No behaviour change to streaming itself — this makes the still-unresolved P2S RTSP stall diagnosable. **Tests**: 4 new in `test_camera_stderr_summary.py` cover `_read_ffmpeg_stderr` capturing output from a *running* (un-exited, no-EOF) ffmpeg — the regression — as well as the exited case, the no-stderr-pipe case, and banner-only output summarizing to `None`. 9 camera-stderr tests green; backend ruff clean.
|
||||
- **Camera diagnostic (stethoscope) was missing from the pop-out camera window (#1395, reported by @Tschipel)** — The #1395 camera-diagnostic follow-up — stethoscope icon in the control bar, **Diagnose** button in the stream-error state, `CameraDiagnoseModal` — shipped wired into `EmbeddedCameraViewer.tsx` only, the *embedded* camera mode. It was never added to `CameraPage.tsx`, the standalone window that opens at `/camera/{id}` when `camera_view_mode` is `window` (the default). The reporter's support bundle had `"camera_view_mode": "window"`, so they were on `CameraPage` the whole time and genuinely could not see the stethoscope no matter how many container rebuilds or cache clears they tried — the JS bundle did contain the `camera.diagnose` strings (they come from `EmbeddedCameraViewer`), but that component never renders in window mode. Switching to overlay mode made it appear instantly, exactly as the reporter found. **Fix**: ported the diagnostic into `CameraPage.tsx` — the `Stethoscope` control-bar button (between **Refresh** and **Fullscreen**, matching the embedded viewer), a **Diagnose** button next to **Retry** in the `streamError` block, and the `CameraDiagnoseModal` render. No new i18n keys — `camera.diagnose.*` already exist in all 9 locales. The backend per-model camera-profile fix from the same issue is view-mode-agnostic and already applied; this only makes the diagnostic reachable in the default window mode. Frontend build clean.
|
||||
- **Camera: P2S RTSP stream no longer drops every frame after the first (#1395, reported by @Tschipel)** — With the stderr-capture diagnostic fix in place, a fresh P2S support bundle finally showed ffmpeg's reason for the stall: `frame=1 time=00:00:00.06 dup=0 drop=526 speed=0.0037x`. ffmpeg connects fine and frames *are* arriving (the `drop` counter climbs steadily, ~15/s) — but it emits exactly one output frame and the output clock freezes at 0.06 s. **Root cause**: the streaming ffmpeg command ends with `-r 15`, which puts ffmpeg in CFR (constant-frame-rate) mode — it drops/dupes input frames to hit 15 fps *based on the source's timestamps*. P2S firmware 01.02.00.00 sends an RTSP stream whose RTP timestamps don't advance (every frame is stamped ~0.06 s), so CFR sees every frame after the first as a same-timestamp duplicate and drops it. The browser gets one frame, then nothing → "connection lost", reconnect, repeat. This is why snapshot capture works on the same printer (that path has no `-r`, so no CFR conversion — timestamps are irrelevant) and why X1/H2 are unaffected (their firmware sends correct, advancing timestamps). The earlier "increase probesize" fix was real but had been masking this second bug — once ffmpeg got past the probe, the timestamp bug surfaced. **Fix**: the P2S camera profile gains `-use_wallclock_as_timestamps 1` as an ffmpeg input arg (via the existing `extra_ffmpeg_input_args` hook — no dataclass change, no other model touched). ffmpeg then rebuilds each packet's PTS from arrival wall-clock time, the output clock advances normally, and `-r 15` CFR conversion works as intended. **Tests**: 2 new in `test_camera_profiles.py` — the P2S profile splices the flag and value as an adjacent pair, and the default profile keeps an empty `extra_ffmpeg_input_args` so the override never leaks to X1/H2. 13 camera-profile tests green; backend ruff clean.
|
||||
- **A backend restart mid-print no longer duplicates the job in the archive (#1485, reported by @pwostran)** — When the server running Bambuddy restarted during an active print, the running job was duplicated in the archive — and deleting the duplicate didn't help: every subsequent restart while the print was still running spawned a fresh one. Both support bundles confirmed it: `WARNING Found stale 'printing' archive 3 (age: 9:46:23), marking as cancelled and creating new archive` → `Created archive 4`. On reconnect `on_print_start` fires (Bambuddy sees the printer running) and tries to re-attach to the existing archive in `main.py`. The reliable match is by `subtask_id`; the fallback is a name match plus — and this was the bug — a **4-hour staleness heuristic**: a name-matched `printing` archive older than 4h was assumed dead, marked `cancelled`, and a new archive created. Bambu prints routinely run far longer than 4h, so a genuine long print's *live* archive was destroyed and duplicated on every restart. **Two root causes, both fixed.** **(1) Queue/scheduled archives never persisted a restart-stable `subtask_id`.** Bambuddy mints a per-job id (`project_id`/`subtask_id`/`task_id`) inside `start_print` when it sends the `project_file` command, and the printer echoes it back — but often not within the ~10s before `on_print_start` first fires, so the expected-print branch's `if subtask_id and not archive.subtask_id` write got an empty value and the archive was left with no id. A later restart then had nothing to match on and fell through to the fragile name path. Fix: `BambuMQTTClient.start_print` now records the minted id on `last_dispatch_subtask_id`, and `on_print_start` falls back to it when the printer hasn't echoed `subtask_id` yet — so every dispatched archive persists a stable id and a restart resumes it by id, age-independent. **(2) The 4-hour cutoff itself.** Replaced with a progress-aware check: when a name-matched `printing` archive is found on restart, the printer's *current* reported progress decides resume-vs-stale, not wall-clock age. Real progress (or unknown progress — printer offline) always resumes the existing archive. It is only treated as a stale leftover when the printer clearly shows a *different, freshly-started* print — under 1% progress on an archive more than 2h old, a state a real in-progress print is never in. The arbitrary 4h constant is gone. **Net effect**: a restart mid-print resumes the existing archive (`started_at`, energy, timelapse intact) instead of ever cancelling it and creating a duplicate. **Tests**: 2 new in `test_bambu_mqtt.py` (`start_print` records `last_dispatch_subtask_id`, and updates it per submission); new `TestStaleVsResume` in `test_subtask_archive_resume.py` — 6 cases pinning the progress-aware decision (long print mid-run resumes; barely-started long print resumes; ~0% + old archive is stale; ~0% + young archive resumes; unknown progress never cancels; the sub-1%/2h boundary). 472 print-start / MQTT / scheduler / dispatch tests green; backend ruff clean.
|
||||
- **File Manager no longer polls the printer over FTPS every 30 seconds while open (#1480, reported by @OscarsWorldTech)** — The reporter's P1S churned through MQTT disconnect/reconnect cycles and timelapse downloads silently failed. The support bundle showed the real picture: during the churn windows, MQTT (`Connection stale - no message for 60.2s`), FTPS (`_ssl.c:1015: The handshake operation timed out`) and the camera all timed out *together* and recovered together — the P1S's embedded controller saturating, not a network fault (wifi -44 dBm, Docker host networking). A visible contributor on Bambuddy's side: `FileManagerModal.tsx` ran its `getPrinterFiles` query with `refetchInterval: 30000`, so every 30 s while the File Manager modal sat open it opened a *fresh* FTPS connection — full TLS handshake — to re-list the current directory. A printer's file list doesn't change on its own; it only changes on upload / delete (the modal's mutations already `invalidateQueries`) or when a print finishes. The blind 30 s poll was pure load, and on a fragile controller like the P1S it was enough to tip MQTT and FTP into the timeouts above. **Fix**: the `refetchInterval` is removed. The listing still refreshes on modal open, on directory / tab change (the path is in the query key), after every upload / delete, and via the existing manual Refresh button — so nothing stops updating, the printer just isn't hammered. Reduces steady-state FTPS connection load while the modal is open from one handshake every 30 s to zero. 19 FileManagerModal tests green; frontend build clean.
|
||||
- **STL thumbnail generation failures now log a full traceback** — Surfaced by the #1480 support bundle: every STL in the reporter's library failed thumbnail generation with `unsupported operand type(s) for /: 'str' and 'str'`, but `generate_stl_thumbnail`'s `except` handler logged only the bare exception message — no traceback, no line number. The fault could not be reproduced from a clean STL across path shapes (`#` and spaces in the path), `str` vs `Path` arguments, or large meshes that exercise `simplify_quadric_decimation`, so it is data- or environment-specific and the message alone is not enough to locate it. `stl_thumbnail.py` now passes `exc_info=True` on that warning, so the next support bundle carries the traceback and the exact failing line. No behaviour change to thumbnail generation itself.
|
||||
|
||||
@@ -77,13 +77,24 @@ DEFAULT_PROFILE = CameraProfile()
|
||||
# AFTER alias normalisation, so internal SSDP codes ("N7") resolve via
|
||||
# ``_MODEL_ALIASES`` below.
|
||||
_PROFILES: dict[str, CameraProfile] = {
|
||||
# P2S firmware 01.02.00.00 RTSP keyframe pacing is slow enough that
|
||||
# ffmpeg's "32-byte probe + zero analyze" combo can't estimate the
|
||||
# frame rate. ffmpeg's own stderr literally says "consider increasing
|
||||
# probesize" (#1395 follow-up).
|
||||
# P2S firmware 01.02.00.00 has two RTSP quirks, both surfaced by #1395:
|
||||
#
|
||||
# 1. Slow keyframe pacing — ffmpeg's "32-byte probe + zero analyze"
|
||||
# combo can't estimate the frame rate ("consider increasing
|
||||
# probesize"). Fixed by the relaxed probesize/analyzeduration below.
|
||||
#
|
||||
# 2. Non-advancing RTP timestamps — every frame is stamped at ~t=0.06s.
|
||||
# With ffmpeg's default CFR rate conversion (`-r 15`), this freezes
|
||||
# the output clock after the first frame and drops every subsequent
|
||||
# frame as a same-timestamp duplicate (ffmpeg stderr: `frame=1
|
||||
# time=00:00:00.06 dup=0 drop=526`). `-use_wallclock_as_timestamps 1`
|
||||
# regenerates each packet's PTS from arrival wall-clock time, so the
|
||||
# output clock advances and CFR conversion works. X1/H2 send correct
|
||||
# timestamps and need no override.
|
||||
"P2S": CameraProfile(
|
||||
probesize=1_000_000,
|
||||
analyzeduration=500_000,
|
||||
extra_ffmpeg_input_args=("-use_wallclock_as_timestamps", "1"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,24 @@ class TestGetCameraProfile:
|
||||
assert profile.probesize >= 1_000_000
|
||||
assert profile.analyzeduration >= 500_000
|
||||
|
||||
def test_p2s_regenerates_timestamps_from_wallclock(self):
|
||||
"""P2S firmware 01.02.00.00 sends non-advancing RTP timestamps;
|
||||
ffmpeg's default CFR conversion (`-r 15`) then freezes the output
|
||||
clock after frame 1 and drops everything else (#1395). The profile
|
||||
must splice `-use_wallclock_as_timestamps 1` into the input args so
|
||||
ffmpeg rebuilds PTS from arrival time. Order matters — the flag and
|
||||
its value must be adjacent so they reach ffmpeg as a pair."""
|
||||
args = get_camera_profile("P2S").extra_ffmpeg_input_args
|
||||
assert "-use_wallclock_as_timestamps" in args
|
||||
idx = args.index("-use_wallclock_as_timestamps")
|
||||
assert args[idx + 1] == "1"
|
||||
|
||||
def test_default_profile_has_no_timestamp_override(self):
|
||||
"""X1/H2 send correct, advancing RTP timestamps — they must NOT get
|
||||
the wallclock override, which would needlessly re-stamp a healthy
|
||||
stream. Guards against the P2S fix leaking into the default."""
|
||||
assert DEFAULT_PROFILE.extra_ffmpeg_input_args == ()
|
||||
|
||||
def test_p2s_internal_code_resolves_to_p2s_profile(self):
|
||||
"""SSDP internal codes (e.g. `N7` for P2S) must resolve to the
|
||||
same profile as their display name. Otherwise printers freshly
|
||||
|
||||
Reference in New Issue
Block a user