camera_profiles.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """Per-printer-model camera tuning knobs.
  2. Bambuddy talks to multiple Bambu Lab printer models that all expose a
  3. camera but in subtly different ways:
  4. - **Chamber image** (port 6000, proprietary binary protocol) — A1, A1
  5. Mini, P1P, P1S. Frame pacing and TLS quirks are firmware-driven and
  6. don't go through ffmpeg.
  7. - **RTSPS** (port 322) — X1 series, X2D, H2 series, P2S. Wrapped by a
  8. local TLS proxy + ffmpeg to MJPEG.
  9. The RTSPS path used to live with hard-coded module constants in
  10. ``camera.py``: a single ``-probesize 32 -analyzeduration 0`` tuned for
  11. X1/H2 fast startup. That breaks the P2S on firmware 01.02.00.00, whose
  12. RTSP keyframe pacing is slow enough that ffmpeg can't lock onto the
  13. stream within 32 bytes and gives up with "not enough frames to estimate
  14. rate" (#1395 follow-up — Tschipel's reproduction).
  15. This module replaces those module constants with per-model
  16. :class:`CameraProfile` entries. Defaults match the historical pre-fix
  17. behaviour, so existing models (X1, H2, X2D, X1E) keep their fast-
  18. startup tuning unchanged. Quirky models override the relevant fields
  19. only — the P2S entry below is the first example.
  20. Adding a new model's quirk is a config edit (an entry in ``_PROFILES``
  21. plus the alias for its internal SSDP code if needed), not another
  22. hard-coded global constant.
  23. """
  24. from __future__ import annotations
  25. from dataclasses import dataclass, field
  26. @dataclass(frozen=True)
  27. class CameraProfile:
  28. """Tuning knobs for one printer model's camera path.
  29. All defaults reflect the historical X1/H2 behaviour (fast startup,
  30. minimal probing). Models with quirky firmware override individual
  31. fields rather than re-defining the whole profile.
  32. """
  33. # --- RTSPS / ffmpeg path -------------------------------------------------
  34. # ffmpeg's `-probesize` (bytes). Smaller = lower startup latency but
  35. # less margin to lock onto a stream whose first keyframe is delayed
  36. # or whose container metadata is incomplete. P2S 01.02.00.00 needs a
  37. # full MB to lock; X1/H2 lock within ~32 bytes.
  38. probesize: int = 32
  39. # ffmpeg's `-analyzeduration` (microseconds). 0 = skip format
  40. # analysis entirely. Same trade-off as probesize.
  41. analyzeduration: int = 0
  42. # Max consecutive ffmpeg respawns when the printer drops the RTSP
  43. # session mid-stream. Some firmwares cut the stream after a few
  44. # seconds (originally noted on P2S), so we transparently respawn
  45. # to keep the MJPEG client alive. Consecutive means failed sessions
  46. # in a row: a session that delivered frames resets the count. A stock
  47. # X1C ends every session after about a minute, so a lifetime count
  48. # stopped a live view for good after half an hour.
  49. rtsp_reconnect_max: int = 30
  50. # Seconds before respawning after a session that delivered frames --
  51. # the routine drop, reconnected fast enough that viewers don't notice.
  52. rtsp_reconnect_delay: float = 0.2
  53. # Each further failed attempt in a row doubles the delay up to this cap,
  54. # so a printer that is refusing sessions for a while (another client on
  55. # its camera, a busy camera service) gets minutes rather than 30 dials in
  56. # nine seconds before the stream is given up.
  57. rtsp_reconnect_backoff_max: float = 5.0
  58. # --- Extra ffmpeg input args ---------------------------------------------
  59. # Hook for future per-model knobs (e.g. `-fflags` overrides) without
  60. # changing the dataclass shape. Tuple, not list, so the dataclass
  61. # stays hashable / frozen-friendly.
  62. extra_ffmpeg_input_args: tuple[str, ...] = field(default_factory=tuple)
  63. # ---------------------------------------------------------------------------
  64. # Profile registry
  65. # ---------------------------------------------------------------------------
  66. # Default profile = historical X1/H2 fast-startup behaviour. Used for
  67. # every RTSP-capable model that doesn't have an entry in ``_PROFILES``.
  68. DEFAULT_PROFILE = CameraProfile()
  69. # Per-model overrides. Keys are uppercase display names (e.g. "P2S")
  70. # AFTER alias normalisation, so internal SSDP codes ("N7") resolve via
  71. # ``_MODEL_ALIASES`` below.
  72. _PROFILES: dict[str, CameraProfile] = {
  73. # P2S firmware 01.02.00.00 has two RTSP quirks, both surfaced by #1395:
  74. #
  75. # 1. Slow keyframe pacing — ffmpeg's "32-byte probe + zero analyze"
  76. # combo can't estimate the frame rate ("consider increasing
  77. # probesize"). Fixed by the relaxed probesize/analyzeduration below.
  78. #
  79. # 2. Non-advancing RTP timestamps — every frame is stamped at ~t=0.06s.
  80. # With ffmpeg's default CFR rate conversion (`-r 15`), this freezes
  81. # the output clock after the first frame and drops every subsequent
  82. # frame as a same-timestamp duplicate (ffmpeg stderr: `frame=1
  83. # time=00:00:00.06 dup=0 drop=526`). `-use_wallclock_as_timestamps 1`
  84. # regenerates each packet's PTS from arrival wall-clock time, so the
  85. # output clock advances and CFR conversion works. X1/H2 send correct
  86. # timestamps and need no override.
  87. "P2S": CameraProfile(
  88. probesize=1_000_000,
  89. analyzeduration=500_000,
  90. extra_ffmpeg_input_args=("-use_wallclock_as_timestamps", "1"),
  91. ),
  92. }
  93. # SSDP internal codes that should resolve to a display-name profile.
  94. # Display-name lookup is the canonical path; this just lets the camera
  95. # code pass through whatever ``Printer.model`` carries without each
  96. # call site needing to know the code→name map.
  97. _MODEL_ALIASES: dict[str, str] = {
  98. "N7": "P2S", # P2S internal SSDP code
  99. }
  100. def get_camera_profile(model: str | None) -> CameraProfile:
  101. """Return the :class:`CameraProfile` for *model*, or the default.
  102. ``model`` can be either a display name (e.g. ``"P2S"``) or an
  103. internal SSDP code (e.g. ``"N7"``). Unknown models fall back to
  104. :data:`DEFAULT_PROFILE` so the camera path is never blocked on a
  105. missing entry.
  106. """
  107. if not model:
  108. return DEFAULT_PROFILE
  109. key = model.upper().strip()
  110. key = _MODEL_ALIASES.get(key, key)
  111. return _PROFILES.get(key, DEFAULT_PROFILE)