Skip to content

feat: take camera snapshots from the camera's RTSP stream, without Connect - #3

Merged
trevin-lee merged 2 commits into
trevin-lee:mainfrom
ynezz:pr/rtsp-camera
Oct 10, 2026
Merged

trevin-lee merged 2 commits into
trevin-lee:mainfrom
ynezz:pr/rtsp-camera

Conversation

@ynezz

@ynezz ynezz commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

hi, my clankers found this nice project during research, I liked it so decided to make you aware about the changes I've made for my use cases, so everyone can profit from that, but feel free to close it or implement it differently, happy to help with testing, cheers, Petr


get_camera_snapshot and prusactl camera needed Prusa Connect. The PrusaLink of Buddy firmware has no camera endpoint, and the Buddy3D camera is a separate Wi-Fi device. The Buddy3D serves RTSP at rtsp://<camera-ip>/live. This PR adds that stream as a local source.

  • prusactl setup --camera URL saves the address next to the printer. PRUSACTL_CAMERA_URL overrides it. --no-camera forgets only the camera. --forget also removes it.

  • A snapshot comes from the first source that works:

    1. the /api/v1/cameras/snap endpoint of the printer
    2. the RTSP stream, grabbed with ffmpeg (one frame, a timeout, no shell)
    3. Prusa Connect

    If the camera fails, prusactl falls back to Connect.

  • The address can carry a password. prusactl masks it wherever it shows the address, including in errors for addresses that do not parse, and in internal/redact.

  • Only rtsp:// is accepted. With ffmpeg 7.1.5, the RTSP demuxer does not pass -tls_verify or -ca_file to TLS. A self-signed impostor received the RTSP OPTIONS request even with -tls_verify 1. Therefore rtsps:// is refused. The README states that plain RTSP sends the password and the video unencrypted.

  • Two more checks: a picture that was cut short is rejected, and a scheme in capitals is lowercased, because ffmpeg needs lowercase.

  • Two refactors are included. The config file is now read in one place (readFile), so saving the printer keeps the camera and saving the camera keeps the printer. Both snapshot routes also share one image reader.

Testing

  • Unit tests with a fake ffmpeg: the source order, the fallback, the password masking, and that ffmpeg never starts for rtsps://.
  • Live: snapshots over RTSP from a Buddy3D camera on a CORE One L, with ffmpeg 7.1.5.

@trevin-lee

Copy link
Copy Markdown
Owner

The architecture fits, and it still fits after 5dc071d on main (Prusa moved Connect snapshots to a new camera service, and the handler you refactored is the one I rewrote). I'll do the rebase since I wrote that side. The auto-merge also breaks connect_tools_test.go (fc becomes an http.Handler and main does fc.base = srv.URL), so it needs a hand.

I found three credential paths that need closing before this merges:

  • camera.go:88 passes the full URL to ffmpeg as argv, so for up to 30s per snapshot any local user can read the password with ps.
  • The URL goes into plaintext config.json (config.go:41) while the printer password goes to the keychain.
  • tools_status.go:52 masks with camera.Mask, which only matches rtsp://, and the env value never goes through Validate. PRUSACTL_CAMERA_URL=http://user:pw@... (a one-character typo) prints the password in connection_status, the first tool an agent calls.

Since the Buddy3D use case is rtsp://<ip>/live with no credentials, I'm going to refuse userinfo in the URL outright. That removes all three paths. If you have a camera that needs a password, say so and I'll feed the URL to ffmpeg off argv instead (the concat demuxer takes it on stdin).

Also pushing: the fallback to Connect says why RTSP was skipped (the likely case is ffmpeg on the terminal PATH but not the MCP server's, so the local camera never works and nobody knows), a non-404 from /api/v1/cameras/snap falls through to RTSP/Connect instead of aborting, and setup refuses the flag combinations it currently ignores (--camera with --forget, --api-key on a camera-only run).

After that lands, could you re-test a grab on your camera? I'll test on my Buddy3D as well.

ynezz and others added 2 commits October 10, 2026 01:13
…nnect

`get_camera_snapshot` and `prusactl camera` needed Prusa Connect. The
PrusaLink of Buddy firmware has no camera endpoint. The Buddy3D camera
is a separate Wi-Fi device that serves RTSP at `rtsp://<camera-ip>/live`.
A printer that was reached only directly therefore had no picture.

`prusactl setup --camera URL` (or `PRUSACTL_CAMERA_URL`) saves the
address beside the printer. ffmpeg grabs one frame. A snapshot comes
from the first source that works:

1. the `/api/v1/cameras/snap` endpoint of the printer
2. the RTSP stream
3. Prusa Connect

If the camera fails, the Connect fallback is used. A printer without any
camera still says so. `setup --no-camera` forgets only the camera.
`--forget` removes it with the printer, also when it was saved without
one. `status` shows the camera.

The address can carry a password. prusactl masks it wherever it shows
the address, including in the error for an address that does not parse,
and in `internal/redact`. Only plain `rtsp://` is accepted. With ffmpeg
7.1.5, a self-signed certificate completes the TLS handshake and ffmpeg
sends the credentials, even with `-tls_verify 1`. Therefore `rtsps://`
is refused. A picture that was cut short is refused. A scheme in
capitals is lowercased, because ffmpeg needs this.

The config file is now read in one place (`readFile`). Saving the
printer keeps the camera, and saving the camera keeps the printer. The
Connect snapshot moved into `connectSnapshot`. Both routes share one
reader for the image they get back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Petr Štetiar <ynezz@true.cz>
…a was skipped

A password in the RTSP address went two places it shouldn't: onto
ffmpeg's command line, where any local user can read it with ps for the
length of the grab, and into config.json in plain text while the
printer's own password goes to the keychain. The Buddy3D camera takes no
credentials, so rather than carry a password carefully the address is
refused, with the plain address to use instead. The saved address is
checked again before every grab, so PRUSACTL_CAMERA_URL is checked too.

connection_status printed the raw value when the scheme was wrong: the
mask only knew rtsp://, and a one-character typo in the environment
variable showed the password in the first tool an agent calls. It
validates first and never prints the raw value.

A camera that couldn't be grabbed fell back to Connect's picture with
nothing said, so the likely cause, ffmpeg on the terminal's PATH but not
the MCP server's, would never be noticed. The note says the camera was
skipped and why.

A printer that errored on its camera endpoint ended the call, though the
camera is its own device; the RTSP source is tried unless there is
nothing else local to try. setup refuses --camera with --forget and the
printer-only flags on a camera-only run instead of ignoring them, and
--forget says when it took the camera address too. The bundle gains the
camera setting its own advice pointed at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@trevin-lee
trevin-lee merged commit 3bec180 into trevin-lee:main Oct 10, 2026
@trevin-lee

Copy link
Copy Markdown
Owner

Merged in v0.3.0, with one change from what I said: instead of feeding the URL to ffmpeg off argv, credentials in the address are refused outright, since the Buddy3D takes none. setup --camera, PRUSACTL_CAMERA_URL and the saved address all go through the same check, and the refusal says which plain address to use. If you have a camera that needs a password, open an issue and I'll do the stdin route.

The rebase onto main's camera-service change kept your handler and moved the GraphQL lookup into connectSnapshot; the Connect tests from both sides pass. Could you re-test a grab on your camera with 0.3.0? I haven't run one against mine yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants