-
-
Save bencooling/f49251d0afcee72f28097a10ab0bbc70 to your computer and use it in GitHub Desktop.
Record an HTML file to a MP4 video
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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