-
-
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
This file contains 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
# 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/[email protected]/ffmpeg-worker-mp4.js` |
This file contains 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
<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