Video Export from p5.js Sketch
Save mp4 videos from within a browser. No server needed.
The p5.js library itself does not have a way to export a video out of a sketch but because everything you do is simply drawing on an HTML5 Canvas, if you can find a library to export what’s on the Canvas, you should be able to use it. I recently stumbled upon a really good and simple library that exports to H.264 mp4 video straight out of a browser (no server needed) and made a simple example to make it work with a p5js sketch.
If you would rather check out the original library, here is the link, and I will also link to a few other resources at the end of this post.
https://github.com/TrevorSundberg/h264-mp4-encoder
I only spent a little time with the library, but by far, this has been the easiest and most reliable way to export a video out of a p5js sketch. I have tried a few other libraries that will export to either image sequences or .webm videos, but this library directly exports to .mp4 format, which I think is still better supported than .webm, and also supports the current version of p5js (v1.x)
The example included in the original repo explains everything for general use, but I made two quick examples if you just want to take it and use it in your p5js sketch.
Fixed-length Animation Loop
The first one is when you have a fixed-length animation. You can first define numFrames
and play and record the animation. The only difference between a regular p5js sketch and this one is that I created the anim()
function to replace draw()
so I can loop it when I want to. You can just treat it like the draw function and do everything you need to inside. The UI is very minimal. If you only want to preview the animation, check the box, preview only. Here is the code:
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Document</title> | |
<script src="https://unpkg.com/h264-mp4-encoder/embuild/dist/h264-mp4-encoder.web.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js"></script> | |
<style> | |
html { font-family: sans-serif; font-size: 12px; } | |
</style> | |
</head> | |
<body> | |
<script> | |
/* | |
library used: https://github.com/TrevorSundberg/h264-mp4-encoder | |
a simple example exporting mp4 with p5js. | |
custom animation loop, not using draw(), fixed-length animation. | |
*/ | |
let cwidth = 640 | |
let cheight = 360 | |
let button | |
let checkbox | |
const frate = 30 // frame rate | |
const numFrames = 100 // num of frames to record | |
let recording = true | |
function setup() { | |
createCanvas(cwidth, cheight) | |
frameRate(frate) | |
button = createButton('record') | |
button.mousePressed(record) | |
checkbox = createCheckbox(' preview only') | |
checkbox.style('display', 'inline') | |
checkbox.changed(()=> { | |
recording = !recording | |
}) | |
anim(0) // display first frame | |
} | |
function anim(count) { | |
background(220) | |
textSize(128) | |
textAlign(CENTER, CENTER) | |
text(count, width / 2, height / 2) | |
} | |
function record() { | |
HME.createH264MP4Encoder().then(async encoder => { | |
encoder.outputFilename = 'test' | |
encoder.width = cwidth | |
encoder.height = cheight | |
encoder.frameRate = frate | |
encoder.kbps = 50000 // video quality | |
encoder.groupOfPictures = 10 // lower if you have fast actions. | |
encoder.initialize() | |
for (let i = 0; i < numFrames; i++) { | |
anim(i) | |
encoder.addFrameRgba(drawingContext.getImageData(0, 0, canvas.width, canvas.height).data) | |
await new Promise(resolve => window.requestAnimationFrame(resolve)) | |
} | |
encoder.finalize() | |
if (recording) { | |
const uint8Array = encoder.FS.readFile(encoder.outputFilename); | |
const anchor = document.createElement('a') | |
anchor.href = URL.createObjectURL(new Blob([uint8Array], { type: 'video/mp4' })) | |
anchor.download = encoder.outputFilename | |
anchor.click() | |
} | |
encoder.delete() | |
}) | |
} | |
</script> | |
</body> | |
</html> |
Within the Regular Draw Loop
The second example is when you want to record a video while the animation is being played. It won’t loop but will just capture the frames as it plays.
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Document</title> | |
<script src="https://unpkg.com/h264-mp4-encoder/embuild/dist/h264-mp4-encoder.web.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js"></script> | |
</head> | |
<body> | |
<script> | |
/* | |
library used: https://github.com/TrevorSundberg/h264-mp4-encoder | |
a simple example exporting mp4 with p5js. | |
record video while animation is being played. | |
*/ | |
let cwidth = 640 | |
let cheight = 360 | |
let button | |
let encoder | |
const frate = 30 // frame rate | |
const numFrames = 100 // num of frames to record | |
let recording = false | |
let recordedFrames = 0 | |
let count = 0 | |
// make sure encoder is ready before use | |
function preload() { | |
HME.createH264MP4Encoder().then(enc => { | |
encoder = enc | |
encoder.outputFilename = 'test' | |
encoder.width = cwidth | |
encoder.height = cheight | |
encoder.frameRate = frate | |
encoder.kbps = 50000 // video quality | |
encoder.groupOfPictures = 10 // lower if you have fast actions. | |
encoder.initialize() | |
}) | |
} | |
function setup() { | |
createCanvas(cwidth, cheight) | |
frameRate(frate) | |
button = createButton('record') | |
button.mousePressed(() => recording = true) | |
} | |
function draw() { | |
background(220) | |
textSize(128) | |
textAlign(CENTER, CENTER) | |
text(count, width / 2, height / 2) | |
count++ | |
// keep adding new frame | |
if (recording) { | |
console.log('recording') | |
encoder.addFrameRgba(drawingContext.getImageData(0, 0, encoder.width, encoder.height).data); | |
recordedFrames++ | |
} | |
// finalize encoding and export as mp4 | |
if (recordedFrames === numFrames) { | |
recording = false | |
recordedFrames = 0 | |
console.log('recording stopped') | |
encoder.finalize() | |
const uint8Array = encoder.FS.readFile(encoder.outputFilename); | |
const anchor = document.createElement('a') | |
anchor.href = URL.createObjectURL(new Blob([uint8Array], { type: 'video/mp4' })) | |
anchor.download = encoder.outputFilename | |
anchor.click() | |
encoder.delete() | |
preload() // reinitialize encoder | |
} | |
} | |
</script> | |
</body> | |
</html> |
It’s just amazing to think about how much a web browser can do these days. And thanks to people who generously share these libraries, I can have more fun with coding. Here are some resources that I found useful:
- The original repo: https://github.com/TrevorSundberg/h264-mp4-encoder
- Mattdesl’s fork: https://github.com/mattdesl/h264-mp4-encoder (I haven’t tried but it says it will be faster than the original)
- An Observable notebook: https://observablehq.com/@rreusser/h264-mp4-encoder