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
|
||||
|
||||
+45
-15
@@ -9,13 +9,21 @@
|
||||
# more than it was because a stray go2rtc left over from an earlier run
|
||||
# answered the stub with an HTTP 500 and the test still printed "ok".
|
||||
#
|
||||
# This one runs the real thing end to end:
|
||||
# This one runs the real thing end to end, twice - once over direct candidates
|
||||
# and once pinned to relay-only through Cloudflare TURN, which is how the real
|
||||
# browser is configured:
|
||||
#
|
||||
# werift peer (stands in for the browser)
|
||||
# -> POST /printers/:id/webrtc on the real backend
|
||||
# -> /ws/edge/v1 -> the real continuum-proxy -> its real go2rtc
|
||||
# -> RTSPS to the printer's camera
|
||||
# and the RTP that comes back is counted.
|
||||
# and the RTP that comes back is counted. The relay phase needs
|
||||
# CLOUDFLARE_TURN_KEY_ID / CLOUDFLARE_TURN_API_TOKEN and is skipped, with a
|
||||
# message saying the TURN path went unverified, when they are absent.
|
||||
#
|
||||
# Not covered: the viewer and the gateway share this host, so what is proven is
|
||||
# that ICE, DTLS and media work via TURN, not that they survive two distinct
|
||||
# NATs; and a real browser has still not rendered a frame.
|
||||
#
|
||||
# SAFETY: read-only against the printers. It sends no printer command of any
|
||||
# kind; the proxy's MQTT subscription is passive and the camera is a pull of
|
||||
@@ -205,25 +213,47 @@ ok "the generated go2rtc config is owner-only (mode $mode)"
|
||||
|
||||
# --- 4. the viewer ---------------------------------------------------------
|
||||
|
||||
echo "==> a real WebRTC peer plays the camera through the relay"
|
||||
RESULTS="$(BASE="$BASE" PRINTER_UUID="$CAM_UUID" ACCESS_TOKEN="$USER_TOKEN" bun scripts/e2e-camera-peer.ts 2>&1 || true)"
|
||||
SEEN=0; BAD=0
|
||||
# Runs the peer once and reads its `ok<TAB>label` / `FAILED<TAB>label` lines.
|
||||
# $1 names the phase, $2 is how many results a complete run produces: fewer
|
||||
# means the peer died before finishing, and a silent peer must not read as a
|
||||
# pass.
|
||||
run_viewer() {
|
||||
local phase="$1" expected="$2" results seen=0 bad=0 verdict label
|
||||
shift 2
|
||||
results="$(env BASE="$BASE" PRINTER_UUID="$CAM_UUID" ACCESS_TOKEN="$USER_TOKEN" "$@" \
|
||||
bun scripts/e2e-camera-peer.ts 2>&1 || true)"
|
||||
while IFS=$'\t' read -r verdict label; do
|
||||
case "$verdict" in
|
||||
ok) ok "$label"; SEEN=$((SEEN + 1)) ;;
|
||||
FAILED) echo " FAILED: $label" >&2; SEEN=$((SEEN + 1)); BAD=$((BAD + 1)) ;;
|
||||
ok) ok "$label"; seen=$((seen + 1)) ;;
|
||||
FAILED) echo " FAILED: $label" >&2; seen=$((seen + 1)); bad=$((bad + 1)) ;;
|
||||
esac
|
||||
done < <(printf '%s\n' "$RESULTS" | grep -E "^(ok|FAILED)$(printf '\t')")
|
||||
# Fewer results than the peer's five checks means it died before finishing,
|
||||
# and a silent peer must not read as a pass.
|
||||
if [ "$SEEN" -lt 5 ] && [ "$BAD" -eq 0 ]; then
|
||||
BAD=1; echo " FAILED: the peer produced only $SEEN of 5 results" >&2
|
||||
done < <(printf '%s\n' "$results" | grep -E "^(ok|FAILED)$(printf '\t')")
|
||||
if [ "$seen" -lt "$expected" ] && [ "$bad" -eq 0 ]; then
|
||||
bad=1; echo " FAILED: the $phase peer produced only $seen of $expected results" >&2
|
||||
fi
|
||||
if [ "$BAD" -ne 0 ]; then
|
||||
printf '%s\n' "$RESULTS" | sed 's/^/ | /' >&2
|
||||
fail "the camera did not deliver video"
|
||||
if [ "$bad" -ne 0 ]; then
|
||||
printf '%s\n' "$results" | sed 's/^/ | /' >&2
|
||||
fail "the camera did not deliver video ($phase)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> a real WebRTC peer plays the camera through the relay (direct candidates)"
|
||||
run_viewer "direct" 5
|
||||
|
||||
# The path the product actually uses: the browser is pinned to relay-only, so
|
||||
# media goes through Cloudflare TURN. Needs real credentials, which come from
|
||||
# the environment, so it is skipped - loudly - rather than failed without them.
|
||||
if [ -n "${CLOUDFLARE_TURN_KEY_ID:-}" ] && [ -n "${CLOUDFLARE_TURN_API_TOKEN:-}" ]; then
|
||||
echo "==> the same camera, viewer pinned to relay-only through Cloudflare TURN"
|
||||
run_viewer "relay-only" 7 ICE_MODE=relay
|
||||
else
|
||||
echo " SKIPPED: relay-only phase - CLOUDFLARE_TURN_KEY_ID / CLOUDFLARE_TURN_API_TOKEN not set."
|
||||
echo " The TURN-relayed path, which the browser is pinned to, is NOT verified by this run."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "PASS - a real WebRTC peer received video from the printer's camera"
|
||||
echo " through the backend relay, the uplink, the proxy and go2rtc."
|
||||
if [ -n "${CLOUDFLARE_TURN_KEY_ID:-}" ] && [ -n "${CLOUDFLARE_TURN_API_TOKEN:-}" ]; then
|
||||
echo " Both directly and pinned to relay-only through Cloudflare TURN."
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user