← back to Consulting Rentv Com
data/audit-video/capture.mjs
122 lines
// RENTV portal walkthrough capture harness.
// node capture.mjs --shots -> per-tab screenshots into ./shots/
// node capture.mjs --record -> timed screen recording into ./rec/, driven by timing.json
// Playwright resolved from the global install (symlinked into node_modules); drives system Chrome.
import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
const PORTAL_URL = 'http://localhost:9701/portal';
const CREDS = { username: 'admin', password: 'DW2024!' };
const HERE = path.dirname(new URL(import.meta.url).pathname);
const NAR = JSON.parse(fs.readFileSync(path.join(HERE, 'narration.json'), 'utf8'));
const SEGS = NAR.segments;
const mode = process.argv[2] || '--shots';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function openOnly(page, id) {
await page.evaluate((id) => {
if (typeof window.setAll === 'function') { try { window.setAll(false); } catch (e) {} }
document.querySelectorAll('section.open').forEach((s) => {
if (s.id !== id) { const h = s.querySelector('.acchead'); if (h) h.click(); }
});
const s = document.getElementById(id);
if (s) {
const head = s.querySelector('.acchead');
if (head && !s.classList.contains('open')) head.click();
const anchor = s.querySelector('.eyebrow') || s;
anchor.scrollIntoView({ behavior: 'instant', block: 'start' });
window.scrollBy(0, -70);
// force Leaflet/charts that init inside a just-revealed accordion to lay out
window.dispatchEvent(new Event('resize'));
setTimeout(() => window.dispatchEvent(new Event('resize')), 400);
}
}, id);
}
// slowly scroll through a tall panel until absolute time `until`, so the whole tab is read
async function readScroll(page, id, until) {
const info = await page.evaluate((id) => {
const s = document.getElementById(id);
const r = s.getBoundingClientRect();
return { top: window.scrollY + r.top - 70, height: s.scrollHeight };
}, id);
const overflow = Math.max(0, info.height - 1080);
const segStart = Date.now();
const segLen = Math.max(1, until - segStart);
while (Date.now() < until) {
if (overflow > 0) {
const frac = Math.min(1, (Date.now() - segStart) / segLen);
const e = frac < 0.15 ? 0 : frac > 0.9 ? 1 : (frac - 0.15) / 0.75; // linger top & bottom
await page.evaluate((y) => window.scrollTo(0, y), info.top + overflow * e);
}
await sleep(180);
}
}
async function main() {
const record = mode === '--record';
const recDir = path.join(HERE, 'rec');
const shotsDir = path.join(HERE, 'shots');
fs.mkdirSync(record ? recDir : shotsDir, { recursive: true });
// bundled Chromium (isolated from system/OpenClaw Chrome); headless is stable for recordVideo
const browser = await chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'] });
const ctx = await browser.newContext({
viewport: { width: 1920, height: 1080 },
deviceScaleFactor: 1,
httpCredentials: CREDS,
...(record ? { recordVideo: { dir: recDir, size: { width: 1920, height: 1080 } } } : {}),
});
const page = await ctx.newPage();
await page.goto(PORTAL_URL, { waitUntil: 'load', timeout: 45000 });
await sleep(1200);
if (!record) {
for (let i = 0; i < SEGS.length; i++) {
await openOnly(page, SEGS[i].id);
await sleep(1100);
const f = path.join(shotsDir, String(i + 1).padStart(2, '0') + '-' + SEGS[i].id + '.png');
await page.screenshot({ path: f });
console.log('shot', f);
}
await ctx.close();
await browser.close();
return;
}
// ---- record mode: absolute timeline from timing.json (zero drift) ----
const timing = JSON.parse(fs.readFileSync(path.join(HERE, 'timing.json'), 'utf8'));
const starts = timing.starts; // {id: narration start seconds}
const audioEnd = timing.audio_end; // total seconds
await openOnly(page, SEGS[0].id); // summary already on screen at t=0
await sleep(300);
const T0 = Date.now();
const video = page.video();
try {
for (let i = 0; i < SEGS.length; i++) {
const id = SEGS[i].id;
const offset = (i === 0 ? 0 : starts[id]) * 1000;
const nextOffset = (i + 1 < SEGS.length ? starts[SEGS[i + 1].id] * 1000 : audioEnd * 1000);
while (Date.now() - T0 < offset) await sleep(50);
try {
if (i !== 0) await openOnly(page, id);
await readScroll(page, id, T0 + nextOffset);
} catch (e) {
console.log(`tab ${id}: recovered from error (${e.message.slice(0, 40)}) — holding`);
while (Date.now() - T0 < nextOffset) await sleep(100);
}
console.log(`tab ${id}: ${(offset / 1000).toFixed(1)}s -> ${(nextOffset / 1000).toFixed(1)}s`);
}
} finally {
await ctx.close();
const vp = await video.path();
const dest = path.join(recDir, 'portal-walk.webm');
fs.renameSync(vp, dest);
console.log('VIDEO', dest);
await browser.close();
}
}
main().catch((e) => { console.error(e); process.exit(1); });