reqwest's Display is only "error sending request for url (...)" - the
reason is one or two sources down, and "connection refused" versus
"timed out" is the entire content of the message. This error is relayed
to the cloud and then to a viewer, so it is the only explanation anyone
gets for a camera that will not play.
It now reads "could not reach go2rtc at http://127.0.0.1:1984: ...",
naming go2rtc explicitly, because by far the likeliest cause is that it
is not running and an error mentioning only a port sends whoever reads
it looking at the network instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds src/uplink/webrtc.rs: on a webrtc_offer from the cloud, POST the
offer to go2rtc on 127.0.0.1 and send the answer back as webrtc_answer,
or the reason as webrtc_error.
No credential is involved, because the request goes to loopback and
go2rtc exempts loopback from its API password. That is measured, and it
is also why a probe from the gateway cannot be used to check the
password is working - test from another host for that.
Spawned rather than awaited in the select! loop: go2rtc gathers ICE
candidates before answering, which includes a STUN round trip, so
awaiting inline would stall telemetry and heartbeats for a whole farm
while one person opened one camera. It rides the outbound queue added
earlier for exactly this kind of out-of-band reply.
The bind address is turned into a loopback URL by taking its port and
refusing anything it cannot read a port out of - guessing 1984 would
produce a gateway that silently signals to the wrong place when someone
has moved it. Stream names are validated rather than percent-encoded,
since a real one is a printer id and anything else is a bug or an
attempt at one.
camera.rs now states why there are no ice_servers and no TURN
credentials on this side: the viewer's browser is pinned to relay-only,
so it allocates the relay and go2rtc just sends to it. Only one side of
a session needs one. That asymmetry is load-bearing, not tidy - go2rtc
reads its config once at startup, so rotating short-lived credentials
here would mean restarting it and dropping every viewer on a timer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>