systemctl status printer-streamer

3D Printer Streamer

Unattended live streaming and timelapses for a Prusa CORE One, driven by print state.

It watches the printer. When a print starts, it opens the camera, composites a live telemetry panel over the video, and pushes the result to YouTube. When the print ends, it stops on its own and uploads a timelapse. Nobody has to be at the machine for any of it.

How it fits together

The camera feeds ffmpeg over RTSP while a SkiaSharp overlay is piped in over stdin. ffmpeg outputs to YouTube over RTMP and writes a preview JPEG for the web UI. Separately, PrusaLink is polled every five seconds into a session state machine, which starts and stops ffmpeg. Buddy3D camera RTSP / TCP SkiaSharp overlay stdin (RGBA) PrusaLink poll 5s ffmpeg state machine start / stop YouTube RTMP · H.264 / AAC preview.jpg web UI · timelapse
The overlay is piped in as raw frames rather than burned in by a filter, so the panel can change every frame without restarting the encoder.

The control panel

The control panel mid-print, showing the live camera preview, a frame-rate readout and the print's telemetry
Live mid-print. The frame-rate line is there because of the bug below; the preview refreshes from the same JPEG the timelapse samples.
A frame from an actual broadcast, with the telemetry panel composited over the print in progress
A real broadcast frame — the whole pipeline in one image.

The camera was never degrading

For five weeks the stream would collapse from a healthy 22 fps to somewhere between 2 and 8, in bursts split by multi-second freezes. The camera was suspected, then the wifi, then the bitrate. All three were wrong. The pipeline was strangling its own input.

ffmpeg's split=2 only advances when both branches accept a frame. One branch fed the encoder; the other wrote preview.jpg to disk every three seconds. Every slow write stalled the split — and a stalled graph stops draining the RTSP socket, so TCP backpressures the camera, and the camera drops the frames it can no longer buffer. The corrupt frames in the logs were holes we were punching ourselves.

Bisected against the live camera, five minutes per arm, one variable at a time:

Armfpsgaps >2sstderr
-c copy, no processing at all22.7500
+ full decode and re-encode at 5000k22.3001
+ overlay input and filter graph22.6807
+ split, fps=1/3, MJPEG encode to null22.2904
+ write the JPEG to disk17.5812163
write via atomic_writing20.10661
write via the fifo muxer (the fix)22.33111

Writing the JPEG through ffmpeg's fifo muxer decouples the branch from the graph and the symptom disappears. A 7h29m soak afterwards held 22.73 fps with zero gaps. It has run at 1080p and 5000 kbps ever since.

That result also decided the timelapse design. The obvious approach — branch the filter graph for timelapse frames — is the same construct that broke the stream twice. So the timelapse samples the preview.jpg that already exists on disk, which costs the pipeline nothing.

Other decisions worth the time

What it does when things go wrong

SituationWhat happens
Print finishesStreams 120s more, then stops
Print cancelled or errorsStreams 30s more, then stops
Print pauses or needs attentionKeeps streaming with a red badge. After 15 minutes, stops.
Printer unreachableKeeps streaming. The overlay greys out and shows stale.
Stream dropsReconnects with backoff capped at 30s, so YouTube continues the same video
Bad stream keyFaults immediately rather than retrying forever

Settings

The settings page, showing encoder, timelapse and session-behaviour options
Credentials are write-only in the UI and encrypted at rest; the ffmpeg command line is redacted before it reaches a log.

How it is built

ProjectResponsibility
PrinterStreamer.CoreSession state machine and domain models. Pure logic, references nothing.
PrinterStreamer.PrusaLinkStatus polling and response mapping
PrinterStreamer.OverlaySkiaSharp panel rendering
PrinterStreamer.Streamingffmpeg argument building, process supervision, broadcast targets
PrinterStreamer.HostBlazor UI, settings, secrets, composition root

.NET 9 with a Blazor front end, SkiaSharp for the overlay, and ffmpeg doing the video work. It publishes self-contained, so the always-on box it runs on needs no .NET installed.

Watch a print

Every finished print is also uploaded as a timelapse, sped up 180× — an eight-hour print becomes about two and a half minutes. The channel is past a hundred of them now, every one posted without anybody being asked to do anything.

Watch a print timelapse on YouTube