Files
RUWIO/scripts/verify-resolution.cjs
T
thandClaude Opus 4.8 518d43855b Add desktop/server MP4 export pipeline, hardening, and render tests (v1.3.0)
Desktop MP4 export renders frames from a hidden BrowserWindow via
capturePage, pipes PNGs to a bundled ffmpeg (libx264/yuv420p/+faststart),
and writes the file through an atomic temp-file swap. It shares scene
validation (shared/renderValidation.cjs) and the render.html renderer with
the standalone Puppeteer server, which is hardened in parallel.

Render worker & main (electron/):
- abortable render loop with an inactivity watchdog and cancel plumbing
- atomic temp-file + rename so a cancel/failure never truncates a
  pre-existing target (#7); stdin EPIPE / early-exit / backpressure-hang
  handled (#8)
- HiDPI frames normalized to canvas size (#10); odd dimensions padded to
  even (#13, also server); backgroundThrottling disabled (#12)
- reject canvases larger than the display (#11); single-render guard;
  isolated render session that blocks network requests (#5)

Tests:
- Electron harnesses (scripts/verify-*.cjs) for capture, export, cancel,
  timeout, assets, background, capture-fidelity, resolution, odd-dims,
  aggregated by verify-desktop-render.cjs
- server smoke + e2e and shared validation unit tests
- headless hook (src/devtools/headless.ts) to run the in-app suite under
  browser/electron hosts; pixel-diff util + capture fixtures (test/)
- dev_docs plans: integration, fixes (#7-#13), and the e2e/capture strategy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:41:25 +02:00

59 lines
1.8 KiB
JavaScript

// Regression for capture resolution normalization (#10). On scale-factor-1
// displays this is a no-op, while HiDPI displays catch capturePage inflation.
const { app } = require('electron')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { execFileSync } = require('node:child_process')
const { renderMp4Job } = require('../dist-electron/renderWorker.cjs')
const scene = {
canvas: { width: 320, height: 240, background: '#ffffff' },
duration: 0.25,
assets: [{ id: 'rect', kind: 'rect', width: 80, height: 60, fill: '#2563eb' }],
layers: [
{
id: 'layer-rect',
assetId: 'rect',
visible: true,
base: { x: 160, y: 120, scaleX: 1, scaleY: 1, rotation: 0, opacity: 1 },
keyframes: [{ time: 0, props: {} }],
},
],
}
async function main() {
const outPath = path.join(os.tmpdir(), 'ruwio-verify-resolution.mp4')
fs.rmSync(outPath, { force: true })
await renderMp4Job({
jobId: 'verify-resolution',
scene,
options: { fps: 2, duration: 0.25, totalFrames: 1, crf: '28', preset: 'ultrafast' },
outputPath: outPath,
signal: new AbortController().signal,
onProgress: () => {},
})
const probe = JSON.parse(execFileSync('ffprobe', [
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=width,height',
'-of', 'json',
outPath,
]).toString())
const stream = probe.streams[0]
const pass = stream.width === scene.canvas.width && stream.height === scene.canvas.height
console.log(`ffprobe dimensions: ${stream.width}x${stream.height}`)
console.log(pass ? 'RESULT: PASS' : 'RESULT: FAIL')
app.exit(pass ? 0 : 1)
}
app.on('window-all-closed', () => {})
app.whenReady().then(() =>
main().catch((err) => {
console.error('HARNESS ERROR:', err)
app.exit(2)
}),
)