Prove the TURN-relayed camera path, the one the browser actually uses

The first e2e-camera.sh connected over direct host candidates. The real
browser is pinned to iceTransportPolicy "relay", and PLAN.md recorded
go2rtc's known trap for that configuration: a relay candidate on one
side only can leave ICE stuck in `checking`.

The script now runs the peer a second time pinned the same way, with the
iceServers the backend hands out. The peer asserts its offer contains
only `typ relay` candidates, so a pass cannot mean it connected directly,
and never prints the credentials. Measured against the H2C: 937 packets
(~943 KB) arrive through Cloudflare, and the trap did not occur.

The phase is skipped with an explicit message when TURN credentials are
absent, so a run without them cannot be mistaken for one that verified
the relay. Both limits are written into the script header: viewer and
gateway share a host, so this is not two distinct NATs, and no real
browser has rendered a frame.
This commit is contained in:
2026-10-24 08:06:58 +00:00
parent ac484a99ba
commit e4940f9404
2 changed files with 81 additions and 20 deletions
+32 -1
View File
@@ -19,6 +19,13 @@
*
* Environment: BASE, PRINTER_UUID, ACCESS_TOKEN. The token never reaches a
* log line.
*
* ICE_MODE=relay runs it the way the real browser is configured: pinned to
* `iceTransportPolicy: "relay"` with the short-lived TURN servers that
* `GET /printers/:id/stream` hands out, so media can only travel through
* Cloudflare. That is the product's actual path, and the one go2rtc's issue
* tracker warns can strand ICE in `checking` when only one side holds a relay.
* The TURN credentials are used and never printed.
*/
import { RTCPeerConnection, RTCRtpCodecParameters } from "werift";
@@ -39,8 +46,22 @@ const say = (good: boolean, label: string, detail = "") => {
// matched: video:H264 => video:VP8" - correctly. Every browser offers H.264,
// so offering it here is what a real viewer does, not a concession to the test.
// The parameters are Chrome's: packetization-mode=1, constrained baseline.
const relayOnly = process.env.ICE_MODE === "relay";
let iceServers: { urls: string | string[]; username?: string; credential?: string }[] = [];
if (relayOnly) {
const info = await fetch(`${base}/printers/${printer}/stream`, {
headers: { authorization: `Bearer ${token}` },
});
const parsed = (await info.json()) as { iceServers?: typeof iceServers };
iceServers = parsed.iceServers ?? [];
say(info.ok && iceServers.length > 0, "the backend hands out ICE servers for a relay-only viewer",
`${iceServers.length} entr(ies)`);
if (iceServers.length === 0) process.exit(1);
}
const pc = new RTCPeerConnection({
iceServers: [],
iceServers,
iceTransportPolicy: relayOnly ? "relay" : "all",
codecs: {
video: [
new RTCRtpCodecParameters({
@@ -69,6 +90,16 @@ const offerSdp = pc.localDescription!.sdp;
say(/^m=video /m.test(offerSdp) && /^a=ice-ufrag:/m.test(offerSdp),
"the offer has a video section and an ice-ufrag, unlike the stub it replaces");
if (relayOnly) {
// Asserted rather than assumed: if the viewer also offered host or srflx
// candidates, a pass below could mean "connected directly" and prove nothing
// about TURN.
const kinds = [...offerSdp.matchAll(/^a=candidate:.* typ (\w+)/gm)].map((m) => m[1]);
say(kinds.length > 0 && kinds.every((k) => k === "relay"),
"the viewer offers only relay candidates, as the browser does",
`candidates: ${kinds.length ? [...new Set(kinds)].join(",") : "none - the TURN allocation failed"}`);
}
// A player that gets "stream not found" just asks again, and so does this: the
// gateway advertises a camera as soon as the printer reports its URL, a moment
// before go2rtc has been restarted with the stream in its config. Bounded, and