Add WebRTC signalling to the edge protocol
A browser cannot signal to a gateway's go2rtc directly. Measured against go2rtc 1.9.14: it sends no Access-Control-Allow-Origin on any response, the SDP POST needs a preflight anyway because application/sdp is not a CORS-safelisted content type, and it checks its API password before CORS - so the preflight, which a browser always sends without credentials, gets a 401. A gateway is behind a workshop NAT besides, so nothing can dial in. So the cloud relays the SDP exchange over the socket the gateway already holds open: webrtc_offer down, webrtc_answer or webrtc_error back up. Signalling only - no video on this socket, and that is the design rather than a limitation of it. Media goes browser-to-gateway through a TURN relay, DTLS-SRTP encrypted, so neither the backend nor the relay can read it. The alternative was tunnelling fMP4 up this socket and streaming it from the API, which would have meant plaintext video through the cloud and - since <video> cannot send an Authorization header - a credential in a URL, the pattern /ws/telemetry rejected. webrtc_error is its own variant rather than an empty answer because the cloud holds a browser's request open on this, and a silent failure there is a viewer watching a video element that never loads.
This commit is contained in:
@@ -209,6 +209,23 @@ export const EdgeClientMessageSchema = Type.Union([
|
||||
/** Only when `ok` is false. A message for an operator, not a code. */
|
||||
error: Maybe(Type.String()),
|
||||
}),
|
||||
/** go2rtc's answer SDP for a `webrtc_offer`, passed through verbatim. */
|
||||
Type.Object({
|
||||
type: Type.Literal("webrtc_answer"),
|
||||
session_id: Type.String(),
|
||||
sdp: Type.String(),
|
||||
}),
|
||||
/**
|
||||
* No answer was obtainable: no such camera, go2rtc not running, or it
|
||||
* refused the offer. A variant of its own rather than an empty answer, so
|
||||
* a viewer is told something instead of watching a `<video>` that never
|
||||
* loads.
|
||||
*/
|
||||
Type.Object({
|
||||
type: Type.Literal("webrtc_error"),
|
||||
session_id: Type.String(),
|
||||
message: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
type: Type.Literal("frame_batch"),
|
||||
printer_id: Type.String(),
|
||||
@@ -227,6 +244,39 @@ export const EdgeServerMessageSchema = Type.Union([
|
||||
Type.Object({ type: Type.Literal("hello_ack"), session_id: Type.String() }),
|
||||
Type.Object({ type: Type.Literal("hello_error"), message: Type.String() }),
|
||||
Type.Object({ type: Type.Literal("heartbeat_ack"), seq: Type.Integer({ minimum: 0 }) }),
|
||||
/**
|
||||
* Relay a browser's WebRTC offer to this gateway's go2rtc and send the
|
||||
* answer back. **Signalling only - no video crosses this socket.** The
|
||||
* gateway POSTs the offer to its own go2rtc over loopback, which go2rtc
|
||||
* exempts from its API password, so no credential is needed or sent;
|
||||
* browser and go2rtc then negotiate media directly over ICE.
|
||||
*
|
||||
* It exists because a browser cannot signal to a gateway itself. Measured
|
||||
* against go2rtc 1.9.14: it sends no `Access-Control-Allow-Origin` on
|
||||
* anything, and checks its password before CORS, so the unauthenticated
|
||||
* preflight a browser always sends gets a 401.
|
||||
*
|
||||
* Relaying only the signalling is the point. The media stays
|
||||
* peer-to-peer - on a workshop LAN it never leaves the building and costs
|
||||
* no cloud egress - and go2rtc's answer carries a server-reflexive
|
||||
* candidate as well as its host ones, so a remote viewer can often
|
||||
* hole-punch straight to the gateway. A TURN server in go2rtc's
|
||||
* `ice_servers` covers the NATs where that fails, and that is
|
||||
* configuration rather than a change to this protocol.
|
||||
*/
|
||||
Type.Object({
|
||||
type: Type.Literal("webrtc_offer"),
|
||||
/** Correlates the answer with this offer. One negotiation, not a subscription. */
|
||||
session_id: Type.String(),
|
||||
/**
|
||||
* The go2rtc stream name, which is what `PrinterTelemetry.camera_stream`
|
||||
* reported. Deliberately not called a printer id: the two are equal
|
||||
* today, but one is the gateway's own naming and the other is ours.
|
||||
*/
|
||||
camera_stream: Type.String(),
|
||||
/** The browser's offer SDP, passed through verbatim. */
|
||||
sdp: Type.String(),
|
||||
}, { additionalProperties: false }),
|
||||
Type.Object({
|
||||
type: Type.Literal("command"),
|
||||
command_id: Type.String(),
|
||||
|
||||
Reference in New Issue
Block a user