Files
continuum-proxy/src/go2rtc.rs
T
octoturgeandClaude Opus 5 bda5e95b66 Generate go2rtc's config from what the printers report
go2rtc has been supervised since before this commit, with nothing to
serve: `CONTINUUM_GO2RTC_CONFIG` pointed at `./go2rtc.yaml`, and that
file did not exist in the repository or anywhere else. A watchdog was
restarting a process that had no cameras configured.

`camera.rs` writes it, from the `rtsp_url` a printer publishes in its own
`print.push_status` `ipcam` block rather than from `printers.toml`. The
URL is already something the machine tells us, so asking an operator to
keep a second copy of it - and to update it by hand when a printer's
address changes - would be asking for drift.

Two tasks, not one: the builder decides what should be served, the
watchdog keeps the thing serving it alive. They meet at one `Notify`,
because go2rtc reads its configuration only at startup, so a changed
config needs a restart. The watchdog now selects the child's exit against
that notification and skips its backoff on a deliberate change.

What this does *not* cover is most printers, and that is the honest
limit. Only the X1/H2 class publishes an RTSP URL - confirmed against two
real machines: the H2C reports
`rtsps://192.168.0.21:322/streaming/live/1`, the P1P reports an `ipcam`
block with `ipcam_dev: "1"` and no URL at all. "A camera exists" and "a
camera you can connect to" are different facts. A P1/A1 needs Bambu's
proprietary port-6000 JPEG stream, which this crate does not speak, so
those printers report no `camera_stream` and a UI should offer a player
only when one is present.

Details worth keeping:

- The config is rendered sorted so an unchanged fleet is byte-identical,
  and on the first pass it is compared against the file on disk - without
  that, every daemon restart rewrites an identical file and drops every
  live viewer.
- Written through a `.tmp` and renamed. The watchdog can restart go2rtc at
  any moment, and a half-written config fails to parse, which takes every
  working stream down rather than one.
- Never logged, and gitignored: the credentials go in the URL because
  that is the only place go2rtc takes them for an RTSP source, so the file
  holds printer access codes.
- A reported URL with no scheme is refused rather than repaired. One bad
  entry makes go2rtc fail to load the whole file.
- No `rtsp:` or `webrtc:` block. The API listener is what serves WebRTC to
  a browser, which is the only consumer here; opening go2rtc's own RTSP
  server would publish every printer's camera on the LAN unauthenticated.
- `stream_base_url` is reported in `hello`, not derived from the listen
  address: `:1984` is a bind address and not something a browser can open.
  Unset means "advertise nothing", which is the right default, because a
  wrong URL makes the UI offer a player that cannot connect. It is a LAN
  address either way - there is no relay, so this works for an operator in
  the workshop and not for anyone outside it.

63 tests, clippy unchanged at the two deliberate variant-size warnings,
`cargo deny check licenses` ok. go2rtc itself stays operator-installed and
is not distributed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 23:03:02 +00:00

64 lines
2.7 KiB
Rust

use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use tokio::process::Command;
use tokio::sync::Notify;
use tracing::{error, info, warn};
/// Supervises the go2rtc subprocess used for camera restreaming
/// (RTSP -> WebRTC). Restarts it with a fixed backoff whenever it exits, and
/// immediately when `restart` fires.
///
/// `restart` is what `camera::run` rings after rewriting the config: go2rtc
/// reads its configuration once at startup, so a config that changed while it
/// was running would otherwise be ignored until the process happened to die.
/// Before that existed this watchdog supervised a process whose config file
/// nothing wrote - it restarted go2rtc forever over a file that did not exist.
pub async fn watchdog(bin: String, config_path: String, restart: Arc<Notify>) {
loop {
info!(%bin, %config_path, "starting go2rtc");
let spawned = Command::new(&bin)
.arg("-config")
.arg(&config_path)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.kill_on_drop(true)
.spawn();
match spawned {
Ok(mut child) => {
tokio::select! {
// Whichever happens first. `kill_on_drop` means the
// child dies when `child` goes out of scope at the end
// of this arm, so the restart path needs no explicit
// kill - and cannot leave an orphan serving the old
// config.
waited = child.wait() => match waited {
Ok(status) => warn!(%status, "go2rtc exited, restarting after backoff"),
Err(err) => error!(?err, "failed to wait on go2rtc process"),
},
_ = restart.notified() => {
info!("go2rtc config changed, restarting it");
// Explicitly, rather than relying on the drop: this
// awaits the kill so the next iteration cannot race
// the old process for the API port.
if let Err(err) = child.kill().await {
warn!(?err, "could not stop go2rtc for a config change");
}
// No backoff on this path - a config change is a
// deliberate act, not a crash loop.
continue;
}
}
}
Err(err) => {
error!(?err, "failed to spawn go2rtc, is it installed and on PATH?");
}
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
}