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>
66 lines
2.0 KiB
JavaScript
66 lines
2.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const { copyFileSync, chmodSync, existsSync, mkdirSync, statSync } = require('node:fs')
|
|
const path = require('node:path')
|
|
const { spawnSync } = require('node:child_process')
|
|
|
|
const args = process.argv.slice(2)
|
|
const optional = args.includes('--optional')
|
|
const sourceIndex = args.indexOf('--source')
|
|
const explicitSource = sourceIndex >= 0 ? args[sourceIndex + 1] : ''
|
|
|
|
const root = path.resolve(__dirname, '..')
|
|
const exe = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'
|
|
const targetDir = path.join(root, 'bin', process.platform)
|
|
const target = path.join(targetDir, exe)
|
|
|
|
try {
|
|
mkdirSync(targetDir, { recursive: true })
|
|
|
|
if (existsSync(target) && statSync(target).isFile()) {
|
|
chmodIfNeeded(target)
|
|
console.log(`ffmpeg already staged at ${path.relative(root, target)}`)
|
|
process.exit(0)
|
|
}
|
|
|
|
const source = explicitSource || process.env.FFMPEG_PATH || findOnPath()
|
|
if (!source) {
|
|
finishWithoutBinary('ffmpeg was not found. Set FFMPEG_PATH or pass --source /path/to/ffmpeg.')
|
|
}
|
|
|
|
const resolvedSource = path.resolve(source)
|
|
if (!existsSync(resolvedSource) || !statSync(resolvedSource).isFile()) {
|
|
finishWithoutBinary(`ffmpeg source is not a file: ${source}`)
|
|
}
|
|
|
|
copyFileSync(resolvedSource, target)
|
|
chmodIfNeeded(target)
|
|
console.log(`staged ffmpeg from ${resolvedSource} to ${path.relative(root, target)}`)
|
|
} catch (err) {
|
|
if (optional) {
|
|
console.warn(`warning: could not stage ffmpeg: ${err.message}`)
|
|
process.exit(0)
|
|
}
|
|
throw err
|
|
}
|
|
|
|
function findOnPath() {
|
|
const command = process.platform === 'win32' ? 'where' : 'which'
|
|
const result = spawnSync(command, [exe], { encoding: 'utf8' })
|
|
if (result.status !== 0) return ''
|
|
return result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || ''
|
|
}
|
|
|
|
function chmodIfNeeded(file) {
|
|
if (process.platform !== 'win32') chmodSync(file, 0o755)
|
|
}
|
|
|
|
function finishWithoutBinary(message) {
|
|
if (optional) {
|
|
console.warn(`warning: ${message}`)
|
|
process.exit(0)
|
|
}
|
|
console.error(message)
|
|
process.exit(1)
|
|
}
|