Skip to content

Instantly share code, notes, and snippets.

@bencooling
Forked from peterc/recorder.cjs
Created February 12, 2025 05:31
Show Gist options
  • Save bencooling/f49251d0afcee72f28097a10ab0bbc70 to your computer and use it in GitHub Desktop.
Save bencooling/f49251d0afcee72f28097a10ab0bbc70 to your computer and use it in GitHub Desktop.
Record an HTML file to a MP4 video
// Open a supplied HTML file and record whatever's going on to an MP4.
//
// Usage: node recorder.cjs <path_to_html_file>
// Dependencies: npm install puppeteer fluent-ffmpeg
// (and yes, you need ffmpeg installed)
//
// It expects a <canvas> element to be on the page as it waits for
// that to load in first, but you can edit the code below if you
// don't want that.
//
// This code is 'do whatever you want'. Let's just say public domain.
// It's scrappy and just an example of an idea that I needed for a
// quick script.
const puppeteer = require('puppeteer');
const ffmpeg = require('fluent-ffmpeg');
const fs = require('fs');
const path = require('path');
const TEMP_DIR = path.join(__dirname, 'temp_frames');
const DURATION = 5; // <<<<< Put your desired recording time here!
// Needs a temporary folder to store the frames we catch
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR);
async function captureVideo() {
const browser = await puppeteer.launch({
headless: "new",
args: ['--window-size=1280,820']
});
// ^^^ I had to increase the window size to 820 vertical as I think some
// browser chrome is getting in the way?
const page = await browser.newPage();
// Change the viewport if you really want to
await page.setViewport({ width: 1280, height: 720 });
await page.goto(`file:${path.join(__dirname, process.argv[2] || 'animation.html')}`);
// Change this if you have no canvas
await page.waitForSelector('canvas');
const client = await page.target().createCDPSession();
await client.send('Page.startScreencast', { format: 'png', quality: 100, everyNthFrame: 1 });
let frameCount = 0;
client.on('Page.screencastFrame', async (frame) => {
const filename = path.join(TEMP_DIR, `frame_${String(frameCount).padStart(5, '0')}.png`);
fs.writeFileSync(filename, frame.data, 'base64');
frameCount++;
await client.send('Page.screencastFrameAck', { sessionId: frame.sessionId });
});
// This feels like a silly way to do it but hey it works
await new Promise(resolve => setTimeout(resolve, DURATION * 1000));
await client.send('Page.stopScreencast');
await browser.close();
const effectiveFPS = frameCount / DURATION;
return new Promise((resolve, reject) => {
ffmpeg()
.input(path.join(TEMP_DIR, 'frame_%05d.png'))
.inputFPS(effectiveFPS)
.output('output.mp4')
.videoCodec('libx264')
.outputOptions([
'-pix_fmt yuv420p',
'-profile:v high',
'-level 4.0',
'-preset veryslow',
'-crf 17',
'-movflags +faststart'
])
.on('end', resolve)
.on('error', reject)
.run();
});
}
// Get rid of that temporary folder the frames are in
function cleanup() {
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
}
(async function main() {
try {
await captureVideo();
cleanup();
console.log('Video generated: output.mp4');
} catch (error) {
console.error(error);
cleanup();
process.exit(1);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment