Relay camera signalling and mint TURN credentials
Makes the printer cameras playable from a hosted console, which they were not: GET /printers/:id/stream handed out the gateway's own LAN address, and a browser cannot use it. go2rtc sends no CORS headers and checks its API password before CORS, so a browser's unauthenticated preflight gets a 401 (measured against 1.9.14), and a gateway is behind a workshop NAT anyway. That endpoint could only ever have worked for an operator standing on the same network, and offered everyone else a player that silently never loaded. So /:id/stream now returns a camera name and short-lived ICE servers, and POST /:id/webrtc relays the offer to the gateway over the socket it already holds open and returns go2rtc's answer. Media travels browser-to-gateway through the TURN relay, DTLS-SRTP encrypted, so neither this process nor Cloudflare can read it - which is why this is signalling rather than proxying fMP4 through the API. That alternative also needed a credential in the URL, because <video> cannot send an Authorization header. Only the browser gets TURN credentials. It is pinned to relay-only, so it allocates the relay and go2rtc sends to it; one side is enough. That keeps rotation on a per-viewer request instead of in go2rtc's config, which is read once at startup - rotating there would restart it and drop every viewer on a timer. Unconfigured TURN is supported rather than fatal, unlike R2: a viewer on the gateway's LAN connects on host candidates. turnConfigured is returned alongside, because empty iceServers means "not set up" in one case and "minting failed" in the other, and those need different answers. Camera negotiations are failed immediately when a gateway disconnects, rather than waiting out the 12s timeout - a browser is holding a request open on each one. getStreamBaseUrl is kept but no longer served to anyone: gateways still report the field and it is the only record of where one thinks it is reachable, which is useful when diagnosing one. e2e-edge.sh asserts the new shape, that no streamUrl comes back, and drives a real offer through the relay - asserting the refusal came from go2rtc rather than from a timeout, since that is what proves the round trip happened.
This commit is contained in:
+19
-11
@@ -1,29 +1,37 @@
|
||||
/**
|
||||
* Camera stream resolution, on its own with no imports - the same reason as
|
||||
* `dispatch/job_status.ts`: `../db` opens a connection at import time, and
|
||||
* the two distinct failure messages here are worth testing without one.
|
||||
* the distinct failure messages here are worth testing without one.
|
||||
*
|
||||
* This used to hand back the gateway's own LAN address for the browser to
|
||||
* play from. It cannot: go2rtc sends no CORS headers and checks its API
|
||||
* password before CORS, so a browser's unauthenticated preflight gets a 401 -
|
||||
* measured against 1.9.14 - and a gateway is behind a workshop NAT anyway.
|
||||
* So there is no URL to give out. What a viewer needs is a camera name to ask
|
||||
* for and a gateway that is connected to relay the SDP exchange.
|
||||
*/
|
||||
|
||||
export const NO_CAMERA_MESSAGE = "this printer reports no camera that can be streamed";
|
||||
export const NO_GATEWAY_STREAM_MESSAGE =
|
||||
"this farm's gateway is not connected or advertises no stream base URL";
|
||||
export const NO_GATEWAY_MESSAGE =
|
||||
"this farm's gateway is not connected, so its camera cannot be negotiated";
|
||||
|
||||
export type StreamResolution =
|
||||
| { ok: true; stream: { streamUrl: string; cameraId: string } }
|
||||
| { ok: false; reason: "no_camera" | "no_stream_base"; message: string };
|
||||
| { ok: true; stream: { cameraId: string } }
|
||||
| { ok: false; reason: "no_camera" | "gateway_offline"; message: string };
|
||||
|
||||
/**
|
||||
* Both halves or an error naming the missing one. The printer is checked
|
||||
* A camera name, or an error naming what is missing. The printer is checked
|
||||
* first: if it can never stream, saying the gateway is offline would send
|
||||
* someone off to fix the wrong thing.
|
||||
* someone off to fix the wrong thing - and a P1/A1 can never stream, so this
|
||||
* is the common case rather than the unlucky one.
|
||||
*/
|
||||
export function resolveStream(
|
||||
cameraStream: string | null | undefined,
|
||||
streamBaseUrl: string | undefined,
|
||||
gatewayOnline: boolean,
|
||||
): StreamResolution {
|
||||
if (!cameraStream) return { ok: false, reason: "no_camera", message: NO_CAMERA_MESSAGE };
|
||||
if (!streamBaseUrl) {
|
||||
return { ok: false, reason: "no_stream_base", message: NO_GATEWAY_STREAM_MESSAGE };
|
||||
if (!gatewayOnline) {
|
||||
return { ok: false, reason: "gateway_offline", message: NO_GATEWAY_MESSAGE };
|
||||
}
|
||||
return { ok: true, stream: { streamUrl: streamBaseUrl, cameraId: cameraStream } };
|
||||
return { ok: true, stream: { cameraId: cameraStream } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user