Files
gods-eye-view/server/vcam/plugin.test.mjs
Claude 56586a234b feat(vcam): use a phone on the network as a virtual camera
SCENES > VCAM pairs a phone through a QR code and turns it into a
handheld camera, after Unreal's Live Link VCAM and Blender's VirtuCamera:

- Android Chrome tracks 6DoF through WebXR immersive-ar; iPhone Safari,
  which has no WebXR AR, tracks rotation through DeviceOrientation, and
  thumb sticks dolly, truck, crane and pan.
- A motion scale (1:1 walk to 1:10000 orbit), smoothing, height and
  horizon locks, hold/clutch and recenter, none of which make the camera
  jump. Focal length and scale-aware depth of field with tap-to-focus.
- The phone shows the app's render as a monitor (newest-frame JPEG pull).
- REC records a take as a version-7 track shot in the selected scene.
- The rig claims the camera through the navigation seam (Cockpit refuses
  it) and holds on any other navigation, scene run or globe input.

Pairing never exposes the app: a loopback-only session request starts a
separate HTTPS listener (VCAM_PORT, default 4174) that serves only the
static phone page and a token-paired relay, and stops after the last
session. Its certificate comes from a local CA built with node:crypto, which
an iPhone can trust once; without it the phone falls back from WebSocket to
HTTPS requests, which iOS allows past a certificate warning.

scripts/qa-vcam.mjs drives it end to end: the real phone page over the
HTTPS fallback, a scripted 6DoF phone, a recorded and replayed take, depth
of field and camera handoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015H1iLusTzWpVKitGic2AW8
2026-10-19 04:03:02 +00:00

275 lines
11 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import os from 'node:os';
import path from 'node:path';
import WebSocket from 'ws';
import { createVcamService, lanAddresses, PHONE_HEADERS } from './plugin.js';
/** The slice of a Vite server the service mounts on. */
function appServer() {
const stack = [];
const httpServer = http.createServer((req, res) => {
const layer = stack.find(({ prefix }) => req.url.startsWith(prefix));
if (!layer) {
res.statusCode = 404;
return res.end();
}
req.url = req.url.slice(layer.prefix.length) || '/';
layer.handle(req, res, () => {
res.statusCode = 404;
res.end();
});
});
return {
httpServer,
middlewares: { use: (prefix, handle) => stack.push({ prefix, handle }) },
};
}
const listen = (server) =>
new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
function request(url, { method = 'GET', headers = {}, body, ca, checkServerIdentity } = {}) {
const target = new URL(url);
const client = target.protocol === 'https:' ? https : http;
return new Promise((resolve, reject) => {
const req = client.request(
target,
{ method, headers, ca, ...(checkServerIdentity ? { checkServerIdentity } : {}) },
(res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () =>
resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }),
);
},
);
req.on('error', reject);
req.end(body);
});
}
/** Buffer a socket's messages from creation so back-to-back sends are never missed. */
const inboxes = new WeakMap();
function connect(url, options) {
const ws = new WebSocket(url, options);
const box = { queued: [], waiting: [] };
inboxes.set(ws, box);
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
const waiter = box.waiting.shift();
if (waiter) waiter(message);
else box.queued.push(message);
});
return ws;
}
function nextMessage(ws) {
const box = inboxes.get(ws);
if (box.queued.length) return Promise.resolve(box.queued.shift());
return new Promise((resolve) => box.waiting.push(resolve));
}
const opened = (ws) =>
new Promise((resolve, reject) => {
ws.once('open', resolve);
ws.once('error', reject);
ws.once('unexpected-response', (_req, res) => reject(new Error(`HTTP ${res.statusCode}`)));
});
async function harness(t) {
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-vcam-'));
const service = createVcamService({
env: {},
cacheDir,
port: 0,
host: '127.0.0.1',
// The phone reaches the listener on loopback in this test.
addresses: () => ['127.0.0.1'],
logger: {},
});
const app = appServer();
service.install(app);
await listen(app.httpServer);
const appOrigin = `http://127.0.0.1:${app.httpServer.address().port}`;
t.after(() => {
service.destroy();
app.httpServer.close();
fs.rmSync(cacheDir, { recursive: true, force: true });
});
const session = async () => {
const response = await request(`${appOrigin}/api/vcam/session`, {
method: 'POST',
headers: { Origin: appOrigin },
});
assert.equal(response.status, 200);
return JSON.parse(response.body);
};
const ca = () => fs.readFileSync(path.join(cacheDir, 'ca.crt'));
return { service, appOrigin, session, ca };
}
test('the desktop pairs a phone over a trusted local CA and both legs relay', async (t) => {
const { appOrigin, session, ca } = await harness(t);
const created = await session();
assert.equal(created.code.length, 6);
assert.deepEqual(created.addresses, ['127.0.0.1']);
const phoneOrigin = `https://127.0.0.1:${created.port}`;
const page = await request(`${phoneOrigin}/`, { ca: ca() });
assert.equal(page.status, 200);
assert.match(page.body.toString(), /VIRTUAL CAMERA/);
for (const [name, value] of Object.entries(PHONE_HEADERS))
assert.equal(page.headers[name.toLowerCase()], value);
assert.equal((await request(`${phoneOrigin}/api/vcam/session`, { method: 'POST', ca: ca() })).status, 404,
'the phone listener exposes none of the desktop routes');
const certificate = await request(`${phoneOrigin}/ca.crt`, { ca: ca() });
assert.equal(certificate.headers['content-type'], 'application/x-x509-ca-cert');
assert.equal(certificate.body.toString(), ca().toString());
const host = connect(
`ws://127.0.0.1:${new URL(appOrigin).port}/api/vcam/host?key=${created.hostKey}`,
{ origin: appOrigin },
);
const hostCode = nextMessage(host);
await opened(host);
assert.deepEqual(await hostCode, { type: 'code', code: created.code });
await nextMessage(host); // phone not yet connected
const join = await request(`${phoneOrigin}/api/join`, {
method: 'POST', ca: ca(), body: JSON.stringify({ code: created.code }),
});
assert.equal(join.status, 200);
const { phoneKey } = JSON.parse(join.body);
const phone = connect(`wss://127.0.0.1:${created.port}/ws?key=${phoneKey}`, {
ca: ca(), origin: phoneOrigin,
});
const hostSeesPhone = nextMessage(host);
await opened(phone);
assert.deepEqual(await hostSeesPhone, { type: 'peer', role: 'phone', connected: true });
const pose = { type: 'pose', t: 1, q: [0, 0, 0, 1], p: [0, 1.5, 0] };
const received = nextMessage(host);
phone.send(JSON.stringify(pose));
assert.deepEqual(await received, pose);
await nextMessage(phone); // host presence
const state = nextMessage(phone);
host.send(JSON.stringify({ type: 'state', state: { recording: true } }));
assert.deepEqual(await state, { type: 'state', state: { recording: true } });
// The host's binary messages are preview frames the phone pulls over HTTPS.
host.send(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 7]));
const frame = await request(`${phoneOrigin}/api/frame?after=0`, { ca: ca(), headers: { 'X-Vcam-Key': phoneKey } });
assert.equal(frame.status, 200);
assert.equal(frame.headers['content-type'], 'image/jpeg');
assert.equal(frame.headers['x-frame-seq'], '1');
assert.equal(frame.body.at(-1), 7);
phone.close();
host.close();
});
test('the HTTPS fallback relays poses by POST and state by server-sent events', async (t) => {
const { appOrigin, session, ca } = await harness(t);
const created = await session();
const phoneOrigin = `https://127.0.0.1:${created.port}`;
const host = connect(
`ws://127.0.0.1:${new URL(appOrigin).port}/api/vcam/host?key=${created.hostKey}`,
{ origin: appOrigin },
);
await opened(host);
const { phoneKey } = JSON.parse(
(await request(`${phoneOrigin}/api/join`, { method: 'POST', ca: ca(), body: JSON.stringify({ code: created.code }) })).body,
);
const events = await new Promise((resolve) => {
https.get(`${phoneOrigin}/api/events?key=${phoneKey}`, { ca: ca() }, resolve);
});
assert.equal(events.headers['content-type'], 'text/event-stream; charset=utf-8');
const firstEvent = new Promise((resolve) => events.once('data', (chunk) => resolve(chunk.toString())));
assert.match(await firstEvent, /^data: \{"type":"peer","role":"host","connected":true\}/);
const relayed = [];
const collect = async () => {
for (;;) relayed.push(await nextMessage(host));
};
collect();
const sent = await request(`${phoneOrigin}/api/send`, {
method: 'POST', ca: ca(), headers: { 'X-Vcam-Key': phoneKey },
body: JSON.stringify([{ type: 'cmd', cmd: 'record' }, { type: 'pose', t: 2 }, 'not an object']),
});
assert.deepEqual(JSON.parse(sent.body), { accepted: 2 });
await new Promise((resolve) => setTimeout(resolve, 50));
assert.deepEqual(relayed.filter(({ type }) => !['peer', 'code'].includes(type)), [{ type: 'cmd', cmd: 'record' }, { type: 'pose', t: 2 }]);
const downstream = new Promise((resolve) => events.once('data', (chunk) => resolve(chunk.toString())));
host.send(JSON.stringify({ type: 'toast', text: 'hi' }));
assert.equal(await downstream, 'data: {"type":"toast","text":"hi"}\n\n');
const ended = new Promise((resolve) => events.once('end', resolve));
await request(`${appOrigin}/api/vcam/session`, { method: 'DELETE', headers: { Origin: appOrigin, 'X-Vcam-Host-Key': created.hostKey } });
await ended;
const after = await request(`${phoneOrigin}/api/send`, {
method: 'POST', ca: ca(), headers: { 'X-Vcam-Key': phoneKey }, body: '[]',
});
assert.equal(after.status, 403, 'an ended session revokes the phone');
host.close();
});
test('pairing refuses foreign origins, bad codes, bad keys and cross-site sockets', async (t) => {
const { appOrigin, session, ca } = await harness(t);
const noOrigin = await request(`${appOrigin}/api/vcam/session`, { method: 'POST' });
assert.equal(noOrigin.status, 403, 'a session needs the page origin');
const foreign = await request(`${appOrigin}/api/vcam/session`, { method: 'POST', headers: { Origin: 'https://evil.example' } });
assert.equal(foreign.status, 403);
const proxied = await request(`${appOrigin}/api/vcam/session`, { method: 'POST', headers: { Origin: appOrigin, 'X-Forwarded-For': '1.2.3.4' } });
assert.equal(proxied.status, 403);
const created = await session();
const phoneOrigin = `https://127.0.0.1:${created.port}`;
const badJoin = await request(`${phoneOrigin}/api/join`, { method: 'POST', ca: ca(), body: '{"code":"222222"}' });
assert.equal(badJoin.status, 403);
const hugeJoin = await request(`${phoneOrigin}/api/join`, { method: 'POST', ca: ca(), body: 'x'.repeat(5000) }).catch(() => ({ status: 413 }));
assert.equal(hugeJoin.status, 413);
const badHost = await request(`${phoneOrigin}/`, {
ca: ca(), headers: { Host: 'rebind.example' }, checkServerIdentity: () => undefined,
});
assert.equal(badHost.status, 421, 'the phone listener answers IP addresses and allowed hosts only');
await assert.rejects(
opened(new WebSocket(`ws://127.0.0.1:${new URL(appOrigin).port}/api/vcam/host?key=${created.hostKey}`, { origin: 'https://evil.example' })),
/403/,
);
await assert.rejects(
opened(new WebSocket(`ws://127.0.0.1:${new URL(appOrigin).port}/api/vcam/host?key=wrong`, { origin: appOrigin })),
/403/,
);
const { phoneKey } = JSON.parse(
(await request(`${phoneOrigin}/api/join`, { method: 'POST', ca: ca(), body: JSON.stringify({ code: created.code }) })).body,
);
await assert.rejects(
opened(new WebSocket(`wss://127.0.0.1:${created.port}/ws?key=${phoneKey}`, { ca: ca(), origin: 'https://evil.example' })),
/403/,
);
await assert.rejects(
opened(new WebSocket(`wss://127.0.0.1:${created.port}/ws?key=wrong`, { ca: ca(), origin: phoneOrigin })),
/403/,
);
});
test('LAN addresses prefer private ranges and skip loopback and link-local', () => {
assert.deepEqual(
lanAddresses({
lo: [{ family: 'IPv4', address: '127.0.0.1', internal: true }],
docker0: [{ family: 'IPv4', address: '172.17.0.1', internal: false }],
en0: [
{ family: 'IPv6', address: 'fe80::1', internal: false },
{ family: 4, address: '192.168.1.23', internal: false },
],
tun0: [{ family: 'IPv4', address: '100.64.0.2', internal: false }],
bad: [{ family: 'IPv4', address: '169.254.3.3', internal: false }],
}),
['192.168.1.23', '172.17.0.1', '100.64.0.2'],
);
});