mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
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
275 lines
11 KiB
JavaScript
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'],
|
|
);
|
|
});
|