Skip to content

Instantly share code, notes, and snippets.

@techslides
Created April 3, 2015 21:13
Show Gist options
  • Save techslides/a9e1cd15d3a562ea76e3 to your computer and use it in GitHub Desktop.
Save techslides/a9e1cd15d3a562ea76e3 to your computer and use it in GitHub Desktop.
HML5 Canvas to WebM Video with JavaScript
//Whammy: https://github.com/antimatter15/whammy
window.Whammy=(function(){function toWebM(frames,outputAsArray){var info=checkFrames(frames);var CLUSTER_MAX_DURATION=30000;var EBML=[{"id":0x1a45dfa3,"data":[{"data":1,"id":0x4286},{"data":1,"id":0x42f7},{"data":4,"id":0x42f2},{"data":8,"id":0x42f3},{"data":"webm","id":0x4282},{"data":2,"id":0x4287},{"data":2,"id":0x4285}]},{"id":0x18538067,"data":[{"id":0x1549a966,"data":[{"data":1e6,"id":0x2ad7b1},{"data":"whammy","id":0x4d80},{"data":"whammy","id":0x5741},{"data":doubleToString(info.duration),"id":0x4489}]},{"id":0x1654ae6b,"data":[{"id":0xae,"data":[{"data":1,"id":0xd7},{"data":1,"id":0x73c5},{"data":0,"id":0x9c},{"data":"und","id":0x22b59c},{"data":"V_VP8","id":0x86},{"data":"VP8","id":0x258688},{"data":1,"id":0x83},{"id":0xe0,"data":[{"data":info.width,"id":0xb0},{"data":info.height,"id":0xba}]}]}]},{"id":0x1c53bb6b,"data":[]}]}];var segment=EBML[1];var cues=segment.data[2];var frameNumber=0;var clusterTimecode=0;while(frameNumber<frames.length){var cuePoint={"id":0xbb,"data":[{"data":Math.round(clusterTimecode),"id":0xb3},{"id":0xb7,"data":[{"data":1,"id":0xf7},{"data":0,"size":8,"id":0xf1}]}]};cues.data.push(cuePoint);var clusterFrames=[];var clusterDuration=0;do{clusterFrames.push(frames[frameNumber]);clusterDuration+=frames[frameNumber].duration;frameNumber++;}while(frameNumber<frames.length&&clusterDuration<CLUSTER_MAX_DURATION);var clusterCounter=0;var cluster={"id":0x1f43b675,"data":[{"data":Math.round(clusterTimecode),"id":0xe7}].concat(clusterFrames.map(function(webp){var block=makeSimpleBlock({discardable:0,frame:webp.data.slice(4),invisible:0,keyframe:1,lacing:0,trackNum:1,timecode:Math.round(clusterCounter)});clusterCounter+=webp.duration;return{data:block,id:0xa3};}))}
segment.data.push(cluster);clusterTimecode+=clusterDuration;}
var position=0;for(var i=0;i<segment.data.length;i++){if(i>=3){cues.data[i-3].data[1].data[1].data=position;}
var data=generateEBML([segment.data[i]],outputAsArray);position+=data.size||data.byteLength||data.length;if(i!=2){segment.data[i]=data;}}
return generateEBML(EBML,outputAsArray)}
function checkFrames(frames){var width=frames[0].width,height=frames[0].height,duration=frames[0].duration;for(var i=1;i<frames.length;i++){if(frames[i].width!=width)throw"Frame "+(i+1)+" has a different width";if(frames[i].height!=height)throw"Frame "+(i+1)+" has a different height";if(frames[i].duration<0||frames[i].duration>0x7fff)throw"Frame "+(i+1)+" has a weird duration (must be between 0 and 32767)";duration+=frames[i].duration;}
return{duration:duration,width:width,height:height};}
function numToBuffer(num){var parts=[];while(num>0){parts.push(num&0xff)
num=num>>8}
return new Uint8Array(parts.reverse());}
function numToFixedBuffer(num,size){var parts=new Uint8Array(size);for(var i=size-1;i>=0;i--){parts[i]=num&0xff;num=num>>8;}
return parts;}
function strToBuffer(str){var arr=new Uint8Array(str.length);for(var i=0;i<str.length;i++){arr[i]=str.charCodeAt(i)}
return arr;}
function bitsToBuffer(bits){var data=[];var pad=(bits.length%8)?(new Array(1+8-(bits.length%8))).join('0'):'';bits=pad+bits;for(var i=0;i<bits.length;i+=8){data.push(parseInt(bits.substr(i,8),2))}
return new Uint8Array(data);}
function generateEBML(json,outputAsArray){var ebml=[];for(var i=0;i<json.length;i++){if(!('id'in json[i])){ebml.push(json[i]);continue;}
var data=json[i].data;if(typeof data=='object')data=generateEBML(data,outputAsArray);if(typeof data=='number')data=('size'in json[i])?numToFixedBuffer(data,json[i].size):bitsToBuffer(data.toString(2));if(typeof data=='string')data=strToBuffer(data);if(data.length){var z=z;}
var len=data.size||data.byteLength||data.length;var zeroes=Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);var size_str=len.toString(2);var padded=(new Array((zeroes*7+7+1)-size_str.length)).join('0')+size_str;var size=(new Array(zeroes)).join('0')+'1'+padded;ebml.push(numToBuffer(json[i].id));ebml.push(bitsToBuffer(size));ebml.push(data)}
if(outputAsArray){var buffer=toFlatArray(ebml)
return new Uint8Array(buffer);}else{return new Blob(ebml,{type:"video/webm"});}}
function toFlatArray(arr,outBuffer){if(outBuffer==null){outBuffer=[];}
for(var i=0;i<arr.length;i++){if(typeof arr[i]=='object'){toFlatArray(arr[i],outBuffer)}else{outBuffer.push(arr[i]);}}
return outBuffer;}
function toBinStr_old(bits){var data='';var pad=(bits.length%8)?(new Array(1+8-(bits.length%8))).join('0'):'';bits=pad+bits;for(var i=0;i<bits.length;i+=8){data+=String.fromCharCode(parseInt(bits.substr(i,8),2))}
return data;}
function generateEBML_old(json){var ebml='';for(var i=0;i<json.length;i++){var data=json[i].data;if(typeof data=='object')data=generateEBML_old(data);if(typeof data=='number')data=toBinStr_old(data.toString(2));var len=data.length;var zeroes=Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);var size_str=len.toString(2);var padded=(new Array((zeroes*7+7+1)-size_str.length)).join('0')+size_str;var size=(new Array(zeroes)).join('0')+'1'+padded;ebml+=toBinStr_old(json[i].id.toString(2))+toBinStr_old(size)+data;}
return ebml;}
function makeSimpleBlock(data){var flags=0;if(data.keyframe)flags|=128;if(data.invisible)flags|=8;if(data.lacing)flags|=(data.lacing<<1);if(data.discardable)flags|=1;if(data.trackNum>127){throw"TrackNumber > 127 not supported";}
var out=[data.trackNum|0x80,data.timecode>>8,data.timecode&0xff,flags].map(function(e){return String.fromCharCode(e)}).join('')+data.frame;return out;}
function parseWebP(riff){var VP8=riff.RIFF[0].WEBP[0];var frame_start=VP8.indexOf('\x9d\x01\x2a');for(var i=0,c=[];i<4;i++)c[i]=VP8.charCodeAt(frame_start+3+i);var width,horizontal_scale,height,vertical_scale,tmp;tmp=(c[1]<<8)|c[0];width=tmp&0x3FFF;horizontal_scale=tmp>>14;tmp=(c[3]<<8)|c[2];height=tmp&0x3FFF;vertical_scale=tmp>>14;return{width:width,height:height,data:VP8,riff:riff}}
function parseRIFF(string){var offset=0;var chunks={};while(offset<string.length){var id=string.substr(offset,4);var len=parseInt(string.substr(offset+4,4).split('').map(function(i){var unpadded=i.charCodeAt(0).toString(2);return(new Array(8-unpadded.length+1)).join('0')+unpadded}).join(''),2);var data=string.substr(offset+4+4,len);offset+=4+4+len;chunks[id]=chunks[id]||[];if(id=='RIFF'||id=='LIST'){chunks[id].push(parseRIFF(data));}else{chunks[id].push(data);}}
return chunks;}
function doubleToString(num){return[].slice.call(new Uint8Array((new Float64Array([num])).buffer),0).map(function(e){return String.fromCharCode(e)}).reverse().join('')}
function WhammyVideo(speed,quality){this.frames=[];this.duration=1000 / speed;this.quality=quality||0.8;}
WhammyVideo.prototype.add=function(frame,duration){if(typeof duration!='undefined'&&this.duration)throw"you can't pass a duration if the fps is set";if(typeof duration=='undefined'&&!this.duration)throw"if you don't have the fps set, you ned to have durations here.";if(frame.canvas){frame=frame.canvas;}
if(frame.toDataURL){frame=frame.toDataURL('image/webp',this.quality)}else if(typeof frame!="string"){throw"frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"}
if(!(/^data:image\/webp;base64,/ig).test(frame)){throw"Input must be formatted properly as a base64 encoded DataURI of type image/webp";}
this.frames.push({image:frame,duration:duration||this.duration})}
WhammyVideo.prototype.compile=function(outputAsArray){return new toWebM(this.frames.map(function(frame){var webp=parseWebP(parseRIFF(atob(frame.image.slice(23))));webp.duration=frame.duration;return webp;}),outputAsArray)}
return{Video:WhammyVideo,fromImageArray:function(images,fps,outputAsArray){return toWebM(images.map(function(image){var webp=parseWebP(parseRIFF(atob(image.slice(23))))
webp.duration=1000 / fps;return webp;}),outputAsArray)},toWebM:toWebM}})();
//Convert Canvas to webp image with background color
function canvasToImage(backgroundColor){
//cache height and width
var w = canvas.width;
var h = canvas.height;
var data;
if(backgroundColor)
{
//get the current ImageData for the canvas.
data = context.getImageData(0, 0, w, h);
//store the current globalCompositeOperation
var compositeOperation = context.globalCompositeOperation;
//set to draw behind current content
context.globalCompositeOperation = "destination-over";
//set background color
context.fillStyle = backgroundColor;
//draw background / rect on entire canvas
context.fillRect(0,0,w,h);
}
//get the image data from the canvas
var imageData = this.canvas.toDataURL("image/webp");
if(backgroundColor)
{
//clear the canvas
context.clearRect (0,0,w,h);
//restore it with original / cached ImageData
context.putImageData(data, 0,0);
//reset the globalCompositeOperation to what it was
context.globalCompositeOperation = compositeOperation;
}
//return the Base64 encoded data url string
return imageData;
}
//Main Setup for Dat.GUI Canvas with FizzyText: http://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
var canvas = document.getElementById("fizzy-0")
var context = canvas.getContext("2d");
var video = new Whammy.Video(15);
//setInterval loop
var counter = 0;
var i = setInterval(function(){
// do your thing
video.add(canvasToImage("#FFFFFF"));
counter++;
if(counter === 50) {
clearInterval(i);
}
}, 200);
//when the loop above finishes running, run the the commands below (url is the generated webm video file)
//var output = video.compile();
//var url = URL.createObjectURL(output);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment