Answer relayed WebRTC offers from the gateway's own go2rtc
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.
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
//! buffering.
|
||||
|
||||
mod download;
|
||||
mod webrtc;
|
||||
mod protocol;
|
||||
|
||||
pub use protocol::{ClientMessage, PrinterCommand, PrinterTelemetry, ServerMessage};
|
||||
@@ -274,6 +275,21 @@ async fn serve(
|
||||
tracing::error!(%message, "cloud rejected this gateway's handshake - check CONTINUUM_GATEWAY_TOKEN");
|
||||
return Ok(());
|
||||
}
|
||||
// Spawned, never awaited inline: go2rtc gathers
|
||||
// ICE candidates before answering, which
|
||||
// includes a STUN round trip, so this takes
|
||||
// seconds. Awaiting it here would stall
|
||||
// telemetry and heartbeats for the whole farm
|
||||
// while one person opened one camera.
|
||||
Ok(ServerMessage::WebrtcOffer { session_id, camera_stream, sdp }) => {
|
||||
spawn_webrtc_negotiation(
|
||||
config.go2rtc_api_listen.clone(),
|
||||
session_id,
|
||||
camera_stream,
|
||||
sdp,
|
||||
outbound.clone(),
|
||||
);
|
||||
}
|
||||
Ok(other) => info!(?other, "received from cloud"),
|
||||
// Loudly. This used to be an `if let Ok(..)` with
|
||||
// no else, which is the same silent-drop bug that
|
||||
@@ -424,6 +440,35 @@ fn spawn_long_command(
|
||||
});
|
||||
}
|
||||
|
||||
/// Answers one WebRTC offer, off this connection's task.
|
||||
///
|
||||
/// Always answers, success or failure: the cloud is holding a browser's
|
||||
/// request open waiting for this, and a silent failure there is a viewer
|
||||
/// watching a video element that never loads until something times out.
|
||||
fn spawn_webrtc_negotiation(
|
||||
api_listen: String,
|
||||
session_id: String,
|
||||
camera_stream: String,
|
||||
offer_sdp: String,
|
||||
outbound: tokio::sync::mpsc::Sender<ClientMessage>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let reply = match webrtc::negotiate(&api_listen, &camera_stream, &offer_sdp).await {
|
||||
Ok(sdp) => ClientMessage::WebrtcAnswer { session_id, sdp },
|
||||
Err(err) => {
|
||||
// Logged as well as returned. The cloud gets the reason, but
|
||||
// this is the copy that survives a gateway whose uplink is
|
||||
// about to drop.
|
||||
warn!(%camera_stream, %err, "could not negotiate a camera stream");
|
||||
ClientMessage::WebrtcError { session_id, message: err.to_string() }
|
||||
}
|
||||
};
|
||||
if outbound.send(reply).await.is_err() {
|
||||
warn!(%camera_stream, "uplink is shutting down; the viewer gets no answer");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Finds the printer by our own id and runs the command. The error is a
|
||||
/// `String` rather than a typed error because its only destination is a
|
||||
/// `CommandResult` going back to the cloud as text.
|
||||
|
||||
Reference in New Issue
Block a user