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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user