Skip to content

Instantly share code, notes, and snippets.

@windwp
Forked from ilblog/README.md
Last active May 4, 2021 07:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save windwp/623f9a6af5a68f33dde910874b9a30ca to your computer and use it in GitHub Desktop.
Save windwp/623f9a6af5a68f33dde910874b9a30ca to your computer and use it in GitHub Desktop.
Create mp4 video from set of images in the browser client side, using ffmpeg.js in worker thread
# Inspiration
* https://semisignal.com/tag/ffmpeg-js/
* https://github.com/antimatter15/whammy
ffmeg as worker can be found at https://github.com/Kagami/ffmpeg.js/
Final build can be obtained via `wget https://unpkg.com/ffmpeg.js@3.1.9001/ffmpeg-worker-mp4.js`
<style>
* {font-family: sans-serif;}
</style>
<progress id="progress" value="0" max="60" min="0" style="width: 300px"></progress>
<br>
<canvas id="canvas" width="150" height="150"></canvas>
<video id="awesome" width="150" height="150" controls autoplay loop></video>
<br>
Status: <span id="status">Idle</span>
<a style="display:none" id="download" download="clock.webm">Download WebM</a>
<pre id="ffmsg"></pre>
<div id="images"></div>
<script>
// use requestanimation frame, woo!
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
//stolen wholesale off mozilla's wiki
// the actual demo code, yaaay
var last_time = +new Date;
var progress = document.getElementById('progress');
const images = []
const $ = id => document.getElementById( id )
const worker = new Worker('/ffmpeg-worker-mp4.js')
function pad(n, width, z) {
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
function nextFrame(){
progress.value++;
var context = clock(last_time += 1000);
const img = new Image()
, mimeType = 'image/jpeg'
const imgString = $('canvas').toDataURL(mimeType,1)
const data = convertDataURIToBinary( imgString )
images.push({
name: `img${ pad( images.length, 3 ) }.jpeg`,
data
})
img.src = imgString
$('images').appendChild( img )
if(progress.value / progress.max < 1){
requestAnimationFrame(nextFrame);
$('status').innerHTML = "Drawing Frames";
}else{
$('status').innerHTML = "Compiling Video";
requestAnimationFrame(finalizeVideo); // well, should probably use settimeout instead
}
}
// https://semisignal.com/tag/ffmpeg-js/
function convertDataURIToBinary(dataURI) {
var base64 = dataURI.replace(/^data[^,]+,/,'');
var raw = window.atob(base64);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for (i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
};
//**blob to dataURL**
function blobToDataURL(blob, callback) {
var a = new FileReader();
a.onload = function(e) {callback(e.target.result);}
a.readAsDataURL(blob);
}
let start_time
function finalizeVideo(){
start_time = +new Date;
const msgs = $('ffmsg')
let messages = '';
worker.onmessage = function(e) {
var msg = e.data;
switch (msg.type) {
case "stdout":
case "stderr":
messages += msg.data + "\n";
break;
case "exit":
console.log("Process exited with code " + msg.data);
//worker.terminate();
break;
case 'done':
const blob = new Blob([msg.data.MEMFS[0].data], {
type: "video/mp4"
});
done( blob )
break;
}
msgs.innerHTML = messages
};
// https://trac.ffmpeg.org/wiki/Slideshow
// https://semisignal.com/tag/ffmpeg-js/
worker.postMessage({
type: 'run',
TOTAL_MEMORY: 268435456,
//arguments: 'ffmpeg -framerate 24 -i img%03d.jpeg output.mp4'.split(' '),
arguments: ["-r", "20", "-i", "img%03d.jpeg", "-c:v", "libx264", "-crf", "1", "-vf", "scale=150:150", "-pix_fmt", "yuv420p", "-vb", "20M", "out.mp4"],
//arguments: '-r 60 -i img%03d.jpeg -c:v libx264 -crf 1 -vf -pix_fmt yuv420p -vb 20M out.mp4'.split(' '),
MEMFS: images
});
// Updated recommented arguments
/*
worker.postMessage({
type: 'run',
TOTAL_MEMORY: 268435456,
arguments: [
//"-r", opts.state.frameRate.toString(),
"-framerate", opts.state.frameRate.toString(),
"-frames:v", imgs.length.toString(),
"-an", // disable sound
"-i", "img%03d.jpeg",
"-c:v", "libx264",
"-crf", "17", // https://trac.ffmpeg.org/wiki/Encode/H.264
"-filter:v",
`scale=${w}:${h}`,
"-pix_fmt", "yuv420p",
"-b:v", "20M",
"out.mp4"],
MEMFS: imgs
});*/
/*video.compile(false, function(output){
$('awesome').src = url; //toString converts it to a URL via Object URLs, falling back to DataURL
$('download').style.display = '';
$('download').href = url;
});*/
}
function done(output) {
const url = webkitURL.createObjectURL(output);
var end_time = +new Date;
$('status').innerHTML = "Compiled Video in " + (end_time - start_time) + "ms, file size: " + Math.ceil(output.size / 1024) + "KB";
$('awesome').src = url; //toString converts it to a URL via Object URLs, falling back to DataURL
$('download').style.display = '';
$('download').href = url;
}
nextFrame();
function clock(time){
var now = new Date();
now.setTime(time);
var ctx = document.getElementById('canvas').getContext('2d');
ctx.save();
ctx.fillStyle = 'white'
ctx.fillRect(0,0,150,150); // videos cant handle transprency
ctx.translate(75,75);
ctx.scale(0.4,0.4);
ctx.rotate(-Math.PI/2);
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.lineWidth = 8;
ctx.lineCap = "round";
// Hour marks
ctx.save();
for (var i=0;i<12;i++){
ctx.beginPath();
ctx.rotate(Math.PI/6);
ctx.moveTo(100,0);
ctx.lineTo(120,0);
ctx.stroke();
}
ctx.restore();
// Minute marks
ctx.save();
ctx.lineWidth = 5;
for (i=0;i<60;i++){
if (i%5!=0) {
ctx.beginPath();
ctx.moveTo(117,0);
ctx.lineTo(120,0);
ctx.stroke();
}
ctx.rotate(Math.PI/30);
}
ctx.restore();
var sec = now.getSeconds();
var min = now.getMinutes();
var hr = now.getHours();
hr = hr>=12 ? hr-12 : hr;
ctx.fillStyle = "black";
// write Hours
ctx.save();
ctx.rotate( hr*(Math.PI/6) + (Math.PI/360)*min + (Math.PI/21600)*sec )
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-20,0);
ctx.lineTo(80,0);
ctx.stroke();
ctx.restore();
// write Minutes
ctx.save();
ctx.rotate( (Math.PI/30)*min + (Math.PI/1800)*sec )
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(-28,0);
ctx.lineTo(112,0);
ctx.stroke();
ctx.restore();
// Write seconds
ctx.save();
ctx.rotate(sec * Math.PI/30);
ctx.strokeStyle = "#D40000";
ctx.fillStyle = "#D40000";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-30,0);
ctx.lineTo(83,0);
ctx.stroke();
ctx.beginPath();
ctx.arc(0,0,10,0,Math.PI*2,true);
ctx.fill();
ctx.beginPath();
ctx.arc(95,0,10,0,Math.PI*2,true);
ctx.stroke();
ctx.fillStyle = "#555";
ctx.arc(0,0,3,0,Math.PI*2,true);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = '#325FA2';
ctx.arc(0,0,142,0,Math.PI*2,true);
ctx.stroke();
ctx.restore();
return ctx;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment