topical media & game development

talk show tell print

mobile-query-three-plugins-whammy-vendor-whammy.js / js



  /*
          var vid = new Whammy.Video();
          vid.add(canvas or data url)
          vid.compile()
  */
  
  var Whammy = (function(){
          // in this case, frames has a very specific meaning, which will be 
          // detailed once i finish writing the code
  
          function toWebM(frames, outputAsArray){
                  var info = checkFrames(frames);
  
                  //max duration by cluster in milliseconds
                  var CLUSTER_MAX_DURATION = 30000;
                  
                  var EBML = [
                          {
                                  "id": 0x1a45dfa3, // EBML
                                  "data": [
                                          { 
                                                  "data": 1,
                                                  "id": 0x4286 // EBMLVersion
                                          },
                                          { 
                                                  "data": 1,
                                                  "id": 0x42f7 // EBMLReadVersion
                                          },
                                          { 
                                                  "data": 4,
                                                  "id": 0x42f2 // EBMLMaxIDLength
                                          },
                                          { 
                                                  "data": 8,
                                                  "id": 0x42f3 // EBMLMaxSizeLength
                                          },
                                          { 
                                                  "data": "webm",
                                                  "id": 0x4282 // DocType
                                          },
                                          { 
                                                  "data": 2,
                                                  "id": 0x4287 // DocTypeVersion
                                          },
                                          { 
                                                  "data": 2,
                                                  "id": 0x4285 // DocTypeReadVersion
                                          }
                                  ]
                          },
                          {
                                  "id": 0x18538067, // Segment
                                  "data": [
                                          { 
                                                  "id": 0x1549a966, // Info
                                                  "data": [
                                                          {  
                                                                  "data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
                                                                  "id": 0x2ad7b1 // TimecodeScale
                                                          },
                                                          { 
                                                                  "data": "whammy",
                                                                  "id": 0x4d80 // MuxingApp
                                                          },
                                                          { 
                                                                  "data": "whammy",
                                                                  "id": 0x5741 // WritingApp
                                                          },
                                                          { 
                                                                  "data": doubleToString(info.duration),
                                                                  "id": 0x4489 // Duration
                                                          }
                                                  ]
                                          },
                                          {
                                                  "id": 0x1654ae6b, // Tracks
                                                  "data": [
                                                          {
                                                                  "id": 0xae, // TrackEntry
                                                                  "data": [
                                                                          {  
                                                                                  "data": 1,
                                                                                  "id": 0xd7 // TrackNumber
                                                                          },
                                                                          { 
                                                                                  "data": 1,
                                                                                  "id": 0x63c5 // TrackUID
                                                                          },
                                                                          { 
                                                                                  "data": 0,
                                                                                  "id": 0x9c // FlagLacing
                                                                          },
                                                                          { 
                                                                                  "data": "und",
                                                                                  "id": 0x22b59c // Language
                                                                          },
                                                                          { 
                                                                                  "data": "V_VP8",
                                                                                  "id": 0x86 // CodecID
                                                                          },
                                                                          { 
                                                                                  "data": "VP8",
                                                                                  "id": 0x258688 // CodecName
                                                                          },
                                                                          { 
                                                                                  "data": 1,
                                                                                  "id": 0x83 // TrackType
                                                                          },
                                                                          {
                                                                                  "id": 0xe0,  // Video
                                                                                  "data": [
                                                                                          {
                                                                                                  "data": info.width,
                                                                                                  "id": 0xb0 // PixelWidth
                                                                                          },
                                                                                          { 
                                                                                                  "data": info.height,
                                                                                                  "id": 0xba // PixelHeight
                                                                                          }
                                                                                  ]
                                                                          }
                                                                  ]
                                                          }
                                                  ]
                                          },
  
                                          //cluster insertion point
                                  ]
                          }
                   ];
  
                                                  
                  //Generate clusters (max duration)
                  var frameNumber = 0;
                  var clusterTimecode = 0;
                  while(frameNumber < frames.length){
                          
                          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, // Cluster
                                          "data": [
                                                  {  
                                                          "data": clusterTimecode,
                                                          "id": 0xe7 // Timecode
                                                  }
                                          ].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
                                                  };
                                          }))
                                  }
                          
                          //Add cluster to segment
                          EBML[1].data.push(cluster);                        
                          clusterTimecode += clusterDuration;
                  }
                                                  
                  return generateEBML(EBML, outputAsArray)
          }
  
          // sums the lengths of all the frames and gets the duration, woo
  
          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 strToBuffer(str){
                  // return new Blob([str]);
  
                  var arr = new Uint8Array(str.length);
                  for(var i = 0; i < str.length; i++){
                          arr[i] = str.charCodeAt(i)
                  }
                  return arr;
                  // this is slower
                  // return new Uint8Array(str.split('').map(function(e){
                  //         return e.charCodeAt(0)
                  // }))
          }
  
          //sorry this is ugly, and sort of hard to understand exactly why this was done
          // at all really, but the reason is that there's some code below that i dont really
          // feel like understanding, and this is easier than using my brain.
  
          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++){
                          var data = json[i].data;
                          if(typeof data == 'object') data = generateEBML(data, outputAsArray);                                        
                          if(typeof data == 'number') data = 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;
                          
                          //i actually dont quite understand what went on up there, so I'm not really
                          //going to fix this, i'm probably just going to write some hacky thing which
                          //converts that string into a buffer-esque thing
  
                          ebml.push(numToBuffer(json[i].id));
                          ebml.push(bitsToBuffer(size));
                          ebml.push(data)
                          
  
                  }
                  
                  //output as blob or byteArray
                  if(outputAsArray){
                          //convert ebml to an array
                          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'){
                                  //an array
                                  toFlatArray(arr[i], outBuffer)
                          }else{
                                  //a simple element
                                  outBuffer.push(arr[i]);
                          }
                  }
                  return outBuffer;
          }
          
          //OKAY, so the following two functions are the string-based old stuff, the reason they're
          //still sort of in here, is that they're actually faster than the new blob stuff because
          //getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the
          // only browser which supports get as webp
  
          //Converting between a string of 0010101001's and binary back and forth is probably inefficient
          //TODO: get rid of this function
          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;
          }
  
          //woot, a function that's actually written for this project!
          //this parses some json markup and makes it into that binary magic
          //which can then get shoved into the matroska comtainer (peaceably)
  
          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;
          }
  
          // here's something else taken verbatim from weppy, awesome rite?
  
          function parseWebP(riff){
                  var VP8 = riff.RIFF[0].WEBP[0];
                  
                  var frame_start = VP8.indexOf('\x9d\x01\x2a'); //A VP8 keyframe starts with the 0x9d012a header
                  for(var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);
                  
                  var width, horizontal_scale, height, vertical_scale, tmp;
                  
                  //the code below is literally copied verbatim from the bitstream spec
                  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
                  }
          }
  
          // i think i'm going off on a riff by pretending this is some known
          // idiom which i'm making a casual and brilliant pun about, but since
          // i can't find anything on google which conforms to this idiomatic
          // usage, I'm assuming this is just a consequence of some psychotic
          // break which makes me make up puns. well, enough riff-raff (aha a
          // rescue of sorts), this function was ripped wholesale from weppy
  
          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;
          }
  
          // here's a little utility function that acts as a utility for other functions
          // basically, the only purpose is for encoding "Duration", which is encoded as
          // a double (considerably more difficult to encode than an integer)
          function doubleToString(num){
                  return [].slice.call(
                          new Uint8Array(
                                  (
                                          new Float64Array([num]) //create a float64 array
                                  ).buffer) //extract the array buffer
                          , 0) // convert the Uint8Array into a regular array
                          .map(function(e){ //since it's a regular array, we can now use map
                                  return String.fromCharCode(e) // encode all the bytes individually
                          })
                          .reverse() //correct the byte endianness (assume it's little endian for now)
                          .join('') // join the bytes in holy matrimony as a string
          }
  
          function WhammyVideo(speed, quality){ // a more abstract-ish API
                  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('canvas' in frame){ //CanvasRenderingContext2D
                          frame = frame.canvas;        
                  }
                  if('toDataURL' in frame){
                          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
                  // expose methods of madness
          }
  })()


(C) Æliens 04/09/2009

You may not copy or print any of this material without explicit permission of the author or the publisher. In case of other copyright issues, contact the author.