| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- """Per-printer-model camera tuning knobs.
- Bambuddy talks to multiple Bambu Lab printer models that all expose a
- camera but in subtly different ways:
- - **Chamber image** (port 6000, proprietary binary protocol) — A1, A1
- Mini, P1P, P1S. Frame pacing and TLS quirks are firmware-driven and
- don't go through ffmpeg.
- - **RTSPS** (port 322) — X1 series, X2D, H2 series, P2S. Wrapped by a
- local TLS proxy + ffmpeg to MJPEG.
- The RTSPS path used to live with hard-coded module constants in
- ``camera.py``: a single ``-probesize 32 -analyzeduration 0`` tuned for
- X1/H2 fast startup. That breaks the P2S on firmware 01.02.00.00, whose
- RTSP keyframe pacing is slow enough that ffmpeg can't lock onto the
- stream within 32 bytes and gives up with "not enough frames to estimate
- rate" (#1395 follow-up — Tschipel's reproduction).
- This module replaces those module constants with per-model
- :class:`CameraProfile` entries. Defaults match the historical pre-fix
- behaviour, so existing models (X1, H2, X2D, X1E) keep their fast-
- startup tuning unchanged. Quirky models override the relevant fields
- only — the P2S entry below is the first example.
- Adding a new model's quirk is a config edit (an entry in ``_PROFILES``
- plus the alias for its internal SSDP code if needed), not another
- hard-coded global constant.
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- @dataclass(frozen=True)
- class CameraProfile:
- """Tuning knobs for one printer model's camera path.
- All defaults reflect the historical X1/H2 behaviour (fast startup,
- minimal probing). Models with quirky firmware override individual
- fields rather than re-defining the whole profile.
- """
- # --- RTSPS / ffmpeg path -------------------------------------------------
- # ffmpeg's `-probesize` (bytes). Smaller = lower startup latency but
- # less margin to lock onto a stream whose first keyframe is delayed
- # or whose container metadata is incomplete. P2S 01.02.00.00 needs a
- # full MB to lock; X1/H2 lock within ~32 bytes.
- probesize: int = 32
- # ffmpeg's `-analyzeduration` (microseconds). 0 = skip format
- # analysis entirely. Same trade-off as probesize.
- analyzeduration: int = 0
- # Max consecutive ffmpeg respawns when the printer drops the RTSP
- # session mid-stream. Some firmwares cut the stream after a few
- # seconds (originally noted on P2S), so we transparently respawn
- # to keep the MJPEG client alive.
- rtsp_reconnect_max: int = 30
- # Seconds between ffmpeg respawn attempts.
- rtsp_reconnect_delay: float = 0.2
- # --- Extra ffmpeg input args ---------------------------------------------
- # Hook for future per-model knobs (e.g. `-fflags` overrides) without
- # changing the dataclass shape. Tuple, not list, so the dataclass
- # stays hashable / frozen-friendly.
- extra_ffmpeg_input_args: tuple[str, ...] = field(default_factory=tuple)
- # ---------------------------------------------------------------------------
- # Profile registry
- # ---------------------------------------------------------------------------
- # Default profile = historical X1/H2 fast-startup behaviour. Used for
- # every RTSP-capable model that doesn't have an entry in ``_PROFILES``.
- DEFAULT_PROFILE = CameraProfile()
- # Per-model overrides. Keys are uppercase display names (e.g. "P2S")
- # AFTER alias normalisation, so internal SSDP codes ("N7") resolve via
- # ``_MODEL_ALIASES`` below.
- _PROFILES: dict[str, CameraProfile] = {
- # 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"),
- ),
- }
- # SSDP internal codes that should resolve to a display-name profile.
- # Display-name lookup is the canonical path; this just lets the camera
- # code pass through whatever ``Printer.model`` carries without each
- # call site needing to know the code→name map.
- _MODEL_ALIASES: dict[str, str] = {
- "N7": "P2S", # P2S internal SSDP code
- }
- def get_camera_profile(model: str | None) -> CameraProfile:
- """Return the :class:`CameraProfile` for *model*, or the default.
- ``model`` can be either a display name (e.g. ``"P2S"``) or an
- internal SSDP code (e.g. ``"N7"``). Unknown models fall back to
- :data:`DEFAULT_PROFILE` so the camera path is never blocked on a
- missing entry.
- """
- if not model:
- return DEFAULT_PROFILE
- key = model.upper().strip()
- key = _MODEL_ALIASES.get(key, key)
- return _PROFILES.get(key, DEFAULT_PROFILE)
|