← 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 };