← back to Goodquestion Ai
video-html/capture.js
146 lines
#!/usr/bin/env node
/**
* capture.js — Puppeteer frame capture
*
* Opens an HTML slide in headless Chrome and screenshots at the specified FPS.
*
* Usage:
* node capture.js <slide.html> <output-dir> [--duration 5] [--fps 30] [--width 1920] [--height 1080]
*
* Output: frame-0001.png, frame-0002.png, etc.
*/
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
function parseArgs(args) {
const opts = {
input: null,
outputDir: null,
duration: 5,
fps: 30,
width: 1920,
height: 1080,
};
const positional = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--duration' && args[i + 1]) {
opts.duration = parseFloat(args[++i]);
} else if (args[i] === '--fps' && args[i + 1]) {
opts.fps = parseInt(args[++i], 10);
} else if (args[i] === '--width' && args[i + 1]) {
opts.width = parseInt(args[++i], 10);
} else if (args[i] === '--height' && args[i + 1]) {
opts.height = parseInt(args[++i], 10);
} else {
positional.push(args[i]);
}
}
if (positional.length < 2) {
console.error('Usage: node capture.js <slide.html> <output-dir> [--duration 5] [--fps 30] [--width 1920] [--height 1080]');
process.exit(1);
}
opts.input = path.resolve(positional[0]);
opts.outputDir = path.resolve(positional[1]);
return opts;
}
async function captureFrames(opts) {
const { input, outputDir, duration, fps, width, height } = opts;
// Validate input exists
if (!fs.existsSync(input)) {
console.error(`Input file not found: ${input}`);
process.exit(1);
}
// Create output directory
fs.mkdirSync(outputDir, { recursive: true });
const totalFrames = Math.ceil(duration * fps);
const frameInterval = 1000 / fps; // ms between frames
console.log(`Capture config:`);
console.log(` Input: ${input}`);
console.log(` Output: ${outputDir}`);
console.log(` Resolution: ${width}x${height}`);
console.log(` Duration: ${duration}s`);
console.log(` FPS: ${fps}`);
console.log(` Frames: ${totalFrames}`);
console.log('');
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
`--window-size=${width},${height}`,
],
});
const page = await browser.newPage();
await page.setViewport({ width, height, deviceScaleFactor: 1 });
// Load the HTML file
const fileUrl = `file://${input}`;
await page.goto(fileUrl, { waitUntil: 'load' });
// Wait for CSS animations to initialize
await new Promise(r => setTimeout(r, 200));
console.log('Capturing frames...');
const startTime = Date.now();
for (let i = 0; i < totalFrames; i++) {
const frameNum = String(i + 1).padStart(4, '0');
const framePath = path.join(outputDir, `frame-${frameNum}.png`);
await page.screenshot({
path: framePath,
type: 'png',
clip: { x: 0, y: 0, width, height },
});
// Wait for the next frame interval
// We account for the time spent screenshotting
const elapsed = Date.now() - startTime;
const expectedTime = (i + 1) * frameInterval;
const waitTime = Math.max(0, expectedTime - elapsed);
if (waitTime > 0) {
await new Promise(r => setTimeout(r, waitTime));
}
// Progress every 10%
if ((i + 1) % Math.max(1, Math.floor(totalFrames / 10)) === 0) {
const pct = Math.round(((i + 1) / totalFrames) * 100);
console.log(` ${pct}% (${i + 1}/${totalFrames} frames)`);
}
}
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\nDone! ${totalFrames} frames captured in ${totalTime}s`);
console.log(`Output: ${outputDir}/frame-NNNN.png`);
await browser.close();
return totalFrames;
}
// CLI entry point
if (require.main === module) {
const opts = parseArgs(process.argv.slice(2));
captureFrames(opts).catch(err => {
console.error('Capture failed:', err);
process.exit(1);
});
}
module.exports = { captureFrames };