shithub: h264bsd

Download patch

ref: b7dd8be76e7dddd5ae39a6c3859b5d01bf9b160c
parent: 8bf8b2e9a4963cb93ce511062752b2bd8a18c221
author: Sam Leitch <conceptualalchemist@gmail.com>
date: Sat Jan 20 21:48:29 EST 2018

Added WebAssembly implementation

diff: cannot open b/wasm//null: 'b/wasm//null' does not exist
--- /dev/null
+++ b/wasm/README.md
@@ -1,0 +1,97 @@
+# Javascript WebAssembly implementation
+
+This implementation was compiled with emscripten and includes tools for rendering to a canvas element with WebGL acceleration.
+
+Here's an example of how to use it:
+```
+var decoder = new H264bsdDecoder();
+var display = new H264bsdCanvas(myCanvasElement);
+
+// Render output to the canvas when a new picture is ready
+decoder.onPictureReady = function() {
+    var width = decoder.outputPictureWidth();
+    var height = decoder.outputPictureHeight();
+    var croppingParams = decoder.croppingParams();
+    var data = decoder.nextOutputPicture();
+    display.drawNextOutputPicture(width, height, croppingParams, data);
+}
+
+// Resize the canvas to display uncropped content.
+decoder.onHeadersReady = function() {
+    myCanvasElement.width = decoder.outputPictureWidth();
+    myCanvasElement.height = decoder.outputPictureHeight();
+}
+
+// Queue input data
+decoder.queueInput(myUint8Array);
+
+// Pump the decode loop
+var status = H264bsdDecoder.RDY;
+while(status != H264bsdDecoder.NO_INPUT) {
+    status = decoder.decode();
+}
+```
+
+This code will decode H.264 annex B encoded bytes stored in a Uint8Array. Each call to `decode()` will decode a single NAL unit, so you need to keep calling it until all of the input data is consumed. Note that each call to `decode()` is synchronous and blocking, so you may want to delay subsequent calls or wrap the whole things in a web worker to keep your app responsive.
+
+`decode()` returns H264bsdDecoder.HDRS_RDY when the output size and cropping information are available and H264bsdDecoder.PIC_RDY when there is a picture ready to render. The decoder will also call the callbacks onHeadersReady and onPictureReady to simplify your code. Use nextOutputPicture or nextOutputPictureRGBA to retrieve i420 or RGBA encoded bytes for the next picture.
+
+H264bsdCanvas will create a 3d context and use a shader program to display YUV encoded data obtained directly from `decoder.nextOutputPicture()`. If WebGL is not available (`isWebGL()` returns false), it will use a 2d context to draw the output, which requires data obtained from `decoder.nextOutputPictureRGBA()`.
+
+## Using the web worker
+
+The project also contains code for a web worker implementation. Here's an example of how it's used:
+```
+var decoder = new Worker("h264bsd_worker.js");
+var display = new H264bsdCanvas(myCanvasElement);
+
+decoder.addEventListener('message', function(e) {
+    var message = e.data;
+    if (!message.hasOwnProperty('type')) return;
+
+    switch(message.type) {
+
+    // Posted when onHeadersReady is called on the worker
+    case 'pictureParams':
+        croppingParams = message.croppingParams;
+        if(croppingParams === null) {
+            canvas.width = message.width;
+            canvas.height = message.height;
+        } else {
+            canvas.width = croppingParams.width;
+            canvas.height = croppingParams.height;
+        }
+        break;
+
+    // Posted when onPictureReady is called on the worker
+    case 'pictureReady':
+        display.drawNextOutputPicture(
+            message.width,
+            message.height,
+            message.croppingParams,
+            new Uint8Array(message.data));
+        ++pictureCount;
+        break;
+
+    // Posted after all of the queued data has been decoded
+    case 'noInput':
+        break;
+
+    // Posted after the worker creates and configures a decoder
+    case 'decoderReady':
+        break;
+
+    // Error messages that line up with error codes returned by decode()
+    case 'decodeError':
+    case 'paramSetError':
+    case 'memAllocError':
+        break;
+    }
+});
+
+// Queue input data.
+// The queued data will be immediately decoded.
+// Once all of the data has been decoded, a "noInput" message will be posted.
+decoder.postMessage({'type' : 'queueInput', 'data' : myUint8Array.buffer}, [myUint8Array.buffer]);
+
+```
--- /dev/null
+++ b/wasm/Rakefile
@@ -1,0 +1,52 @@
+require 'json'
+
+EMCC_FLAGS = ENV["EMCC_FLAGS"] || "-O3 -D_ERROR_PRINT -s ALLOW_MEMORY_GROWTH=1 -s WASM=1"
+
+EMCC_FLAGS = "-O0 -g4 -D_ASSERT_USED -D_ERROR_PRINT -s ALLOW_MEMORY_GROWTH=1 -s WASM=1" if ENV["DEBUG"]
+
+c_files = FileList["../src/*.c"]
+
+exported_functions = [
+	"_malloc",
+	"_free",
+	"_memcpy",
+	"_h264bsdAlloc",
+	"_h264bsdFree",
+	"_h264bsdInit",
+	"_h264bsdDecode",
+	"_h264bsdShutdown",
+	"_h264bsdNextOutputPicture",
+	"_h264bsdNextOutputPictureRGBA",
+	"_h264bsdPicWidth",
+	"_h264bsdPicHeight",
+	"_h264bsdCroppingParams",
+]
+
+exported_runtime_methods = [
+	'getValue',
+	'setValue'
+]
+
+EXPORT_FLAGS = "-s EXTRA_EXPORTED_RUNTIME_METHODS='#{JSON.generate(exported_runtime_methods)}' -s EXPORTED_FUNCTIONS='#{JSON.generate(exported_functions)}'"
+
+file "h264bsd_asm.js" => c_files do
+	sh "emcc #{c_files.join(' ')} #{EMCC_FLAGS} #{EXPORT_FLAGS} -o h264bsd_asm.js"
+end
+
+task :clean do
+	FileUtils.rm_f("h264bsd_asm.js")
+	FileUtils.rm_f("h264bsd_asm.wasm")
+end
+
+desc "Check for prereq tools"
+task :setup do
+	sh("emcc --version") { |ok, res| fail("Can't find emscripten binaries.") unless ok }
+	puts("Ready to go")
+end
+
+task :server do
+	require 'webrick'
+	WEBrick::HTTPServer.new(:Port => 8000, :DocumentRoot => Dir.pwd).start
+end
+
+task :default => [:setup, 'h264bsd_asm.js']
--- /dev/null
+++ b/wasm/h264bsd_asm.js
@@ -1,0 +1,4 @@
+var Module=typeof Module!=="undefined"?Module:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;if(Module["ENVIRONMENT"]){if(Module["ENVIRONMENT"]==="WEB"){ENVIRONMENT_IS_WEB=true}else if(Module["ENVIRONMENT"]==="WORKER"){ENVIRONMENT_IS_WORKER=true}else if(Module["ENVIRONMENT"]==="NODE"){ENVIRONMENT_IS_NODE=true}else if(Module["ENVIRONMENT"]==="SHELL"){ENVIRONMENT_IS_SHELL=true}else{throw new Error("The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.")}}else{ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof require==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER}if(ENVIRONMENT_IS_NODE){if(!Module["print"])Module["print"]=console.log;if(!Module["printErr"])Module["printErr"]=console.warn;var nodeFS;var nodePath;Module["read"]=function shell_read(filename,binary){var ret;if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(!Module["thisProgram"]){if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}else{Module["thisProgram"]="unknown-program"}}Module["arguments"]=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",(function(ex){if(!(ex instanceof ExitStatus)){throw ex}}));process["on"]("unhandledRejection",(function(reason,p){process["exit"](1)}));Module["inspect"]=(function(){return"[Emscripten Module object]"})}else if(ENVIRONMENT_IS_SHELL){if(!Module["print"])Module["print"]=print;if(typeof printErr!="undefined")Module["printErr"]=printErr;if(typeof read!="undefined"){Module["read"]=function shell_read(f){return read(f)}}else{Module["read"]=function shell_read(){throw"no read() available"}}Module["readBinary"]=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=(function(status,toThrow){quit(status)})}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){Module["read"]=function shell_read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){Module["readBinary"]=function readBinary(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}Module["readAsync"]=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)};if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof console!=="undefined"){if(!Module["print"])Module["print"]=function shell_print(x){console.log(x)};if(!Module["printErr"])Module["printErr"]=function shell_printErr(x){console.warn(x)}}else{var TRY_USE_DUMP=false;if(!Module["print"])Module["print"]=TRY_USE_DUMP&&typeof dump!=="undefined"?(function(x){dump(x)}):(function(x){})}if(typeof Module["setWindowTitle"]==="undefined"){Module["setWindowTitle"]=(function(title){document.title=title})}}else{throw new Error("Unknown runtime environment. Where are we?")}if(!Module["print"]){Module["print"]=(function(){})}if(!Module[
\ No newline at end of file
+
+
+
binary files /dev/null b/wasm/h264bsd_asm.wasm differ
--- /dev/null
+++ b/wasm/h264bsd_canvas.js
@@ -1,0 +1,290 @@
+//
+//  Copyright (c) 2014 Sam Leitch. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+//  IN THE SOFTWARE.
+//
+
+/**
+ * This class can be used to render output pictures from an H264bsdDecoder to a canvas element.
+ * If available the content is rendered using WebGL.
+ */
+function H264bsdCanvas(canvas, forceNoGL) {
+    this.canvasElement = canvas;
+
+    if(!forceNoGL) this.initContextGL();
+
+    if(this.contextGL) {
+        this.initProgram();
+        this.initBuffers();
+        this.initTextures();
+    }
+}
+
+/**
+ * Returns true if the canvas supports WebGL
+ */
+H264bsdCanvas.prototype.isWebGL = function() {
+    return this.contextGL;
+}
+
+/**
+ * Create the GL context from the canvas element
+ */
+H264bsdCanvas.prototype.initContextGL = function() {
+    var canvas = this.canvasElement;
+    var gl = null;
+
+    var validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"];
+    var nameIndex = 0;
+
+    while(!gl && nameIndex < validContextNames.length) {
+        var contextName = validContextNames[nameIndex];
+        
+        try {
+            gl = canvas.getContext(contextName);
+        } catch (e) {
+            gl = null;
+        }
+
+        if(!gl || typeof gl.getParameter !== "function") {
+            gl = null;
+        }    
+
+        ++nameIndex;
+    }
+ 
+    this.contextGL = gl;
+}
+
+/**
+ * Initialize GL shader program
+ */
+H264bsdCanvas.prototype.initProgram = function() {
+    var gl = this.contextGL;
+
+    var vertexShaderScript = [
+        'attribute vec4 vertexPos;',
+        'attribute vec4 texturePos;',
+        'varying vec2 textureCoord;',
+
+        'void main()',
+        '{',
+            'gl_Position = vertexPos;',
+            'textureCoord = texturePos.xy;',
+        '}'
+        ].join('\n');
+
+    var fragmentShaderScript = [
+        'precision highp float;',
+        'varying highp vec2 textureCoord;',
+        'uniform sampler2D ySampler;',
+        'uniform sampler2D uSampler;',
+        'uniform sampler2D vSampler;',
+        'const mat4 YUV2RGB = mat4',
+        '(',
+            '1.1643828125, 0, 1.59602734375, -.87078515625,',
+            '1.1643828125, -.39176171875, -.81296875, .52959375,',
+            '1.1643828125, 2.017234375, 0, -1.081390625,',
+            '0, 0, 0, 1',
+        ');',
+      
+        'void main(void) {',
+            'highp float y = texture2D(ySampler,  textureCoord).r;',
+            'highp float u = texture2D(uSampler,  textureCoord).r;',
+            'highp float v = texture2D(vSampler,  textureCoord).r;',
+            'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;',
+        '}'
+        ].join('\n');
+
+    var vertexShader = gl.createShader(gl.VERTEX_SHADER);
+    gl.shaderSource(vertexShader, vertexShaderScript);
+    gl.compileShader(vertexShader);
+    if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
+        console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader));
+    }
+
+    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+    gl.shaderSource(fragmentShader, fragmentShaderScript);
+    gl.compileShader(fragmentShader);
+    if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
+        console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader));
+    }
+
+    var program = gl.createProgram();
+    gl.attachShader(program, vertexShader);
+    gl.attachShader(program, fragmentShader);
+    gl.linkProgram(program);
+    if(!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+        console.log('Program failed to compile: ' + gl.getProgramInfoLog(program));
+    }
+
+    gl.useProgram(program);
+    
+    this.shaderProgram = program;
+}
+
+/**
+ * Initialize vertex buffers and attach to shader program
+ */
+H264bsdCanvas.prototype.initBuffers = function() {
+    var gl = this.contextGL;
+    var program = this.shaderProgram;
+
+    var vertexPosBuffer = gl.createBuffer();
+    gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
+    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);
+
+    var vertexPosRef = gl.getAttribLocation(program, 'vertexPos');
+    gl.enableVertexAttribArray(vertexPosRef);
+    gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0);
+
+    var texturePosBuffer = gl.createBuffer();
+    gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
+    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
+
+    var texturePosRef = gl.getAttribLocation(program, 'texturePos');
+    gl.enableVertexAttribArray(texturePosRef);
+    gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0);
+
+    this.texturePosBuffer = texturePosBuffer;
+}
+
+/**
+ * Initialize GL textures and attach to shader program
+ */
+H264bsdCanvas.prototype.initTextures = function() {
+    var gl = this.contextGL;
+    var program = this.shaderProgram;
+
+    var yTextureRef = this.initTexture();
+    var ySamplerRef = gl.getUniformLocation(program, 'ySampler');
+    gl.uniform1i(ySamplerRef, 0);
+    this.yTextureRef = yTextureRef;
+
+    var uTextureRef = this.initTexture();
+    var uSamplerRef = gl.getUniformLocation(program, 'uSampler');
+    gl.uniform1i(uSamplerRef, 1);
+    this.uTextureRef = uTextureRef;
+
+    var vTextureRef = this.initTexture();
+    var vSamplerRef = gl.getUniformLocation(program, 'vSampler');
+    gl.uniform1i(vSamplerRef, 2);
+    this.vTextureRef = vTextureRef;
+}
+
+/**
+ * Create and configure a single texture
+ */
+H264bsdCanvas.prototype.initTexture = function() {
+    var gl = this.contextGL;
+
+    var textureRef = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, textureRef);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+    gl.bindTexture(gl.TEXTURE_2D, null);
+
+    return textureRef;
+}
+
+/**
+ * Draw picture data to the canvas.
+ * If this object is using WebGL, the data must be an I420 formatted ArrayBuffer,
+ * Otherwise, data must be an RGBA formatted ArrayBuffer.
+ */
+H264bsdCanvas.prototype.drawNextOutputPicture = function(width, height, croppingParams, data) {
+    var gl = this.contextGL;
+
+    if(gl) {
+        this.drawNextOuptutPictureGL(width, height, croppingParams, data);
+    } else {
+        this.drawNextOuptutPictureRGBA(width, height, croppingParams, data);
+    }
+}
+
+/**
+ * Draw the next output picture using WebGL
+ */
+H264bsdCanvas.prototype.drawNextOuptutPictureGL = function(width, height, croppingParams, data) {
+    var gl = this.contextGL;
+    var texturePosBuffer = this.texturePosBuffer;
+    var yTextureRef = this.yTextureRef;
+    var uTextureRef = this.uTextureRef;
+    var vTextureRef = this.vTextureRef;    
+
+    if(croppingParams === null) {
+        gl.viewport(0, 0, width, height);
+    } else {
+        gl.viewport(0, 0, croppingParams.width, croppingParams.height);
+
+        var tTop = croppingParams.top / height;
+        var tLeft = croppingParams.left / width;
+        var tBottom = croppingParams.height / height;
+        var tRight = croppingParams.width / width;
+        var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW);
+    }
+
+    var i420Data = data;
+
+    var yDataLength = width * height;
+    var yData = i420Data.subarray(0, yDataLength);
+    gl.activeTexture(gl.TEXTURE0);
+    gl.bindTexture(gl.TEXTURE_2D, yTextureRef);
+    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, yData);
+
+    var cbDataLength = width/2 * height/2;
+    var cbData = i420Data.subarray(yDataLength, yDataLength + cbDataLength);
+    gl.activeTexture(gl.TEXTURE1);
+    gl.bindTexture(gl.TEXTURE_2D, uTextureRef);
+    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width/2, height/2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, cbData);
+
+    var crDataLength = cbDataLength;
+    var crData = i420Data.subarray(yDataLength + cbDataLength, yDataLength + cbDataLength + crDataLength);
+    gl.activeTexture(gl.TEXTURE2);
+    gl.bindTexture(gl.TEXTURE_2D, vTextureRef);
+    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width/2, height/2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, crData);
+
+    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
+}
+
+/**
+ * Draw next output picture using ARGB data on a 2d canvas.
+ */
+H264bsdCanvas.prototype.drawNextOuptutPictureRGBA = function(width, height, croppingParams, data) {
+    var canvas = this.canvasElement;
+
+    var croppingParams = null;
+
+    var argbData = data;
+
+    var ctx = canvas.getContext('2d');
+    var imageData = ctx.getImageData(0, 0, width, height);
+    imageData.data.set(argbData);
+
+    if(croppingParams === null) {
+        ctx.putImageData(imageData, 0, 0);
+    } else {
+        ctx.putImageData(imageData, -croppingParams.left, -croppingParams.top, 0, 0, croppingParams.width, croppingParams.height);
+    }
+}
--- /dev/null
+++ b/wasm/h264bsd_decoder.js
@@ -1,0 +1,331 @@
+//
+//  Copyright (c) 2013 Sam Leitch. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+//  IN THE SOFTWARE.
+//
+
+/**
+ * This class wraps the details of the h264bsd library.
+ * Module object is an Emscripten module provided globally by h264bsd_asm.js
+ *
+ * In order to use this class, you first queue encoded data using queueData.
+ * Each call to decode() will decode a single encoded element.
+ * When decode() returns H264bsdDecoder.PIC_RDY, a picture is ready in the output buffer.
+ * You can also use the onPictureReady() function to determine when a picture is ready.
+ * The output buffer can be accessed by calling getNextOutputPicture()
+ * An output picture may also be decoded using an H264bsdCanvas.
+ * When you're done decoding, make sure to call release() to clean up internal buffers.
+ */
+
+window = this;
+
+function H264bsdDecoder(module) {
+    this.module = module;
+    this.released = false;
+
+    this.pInput = 0;
+    this.inputLength = 0;
+    this.inputOffset = 0;
+
+    this.onPictureReady = null;
+    this.onHeadersReady = null;
+
+    this.pBytesRead = module._malloc(4);
+    this.pPicId = module._malloc(4);
+    this.pIsIdrPic = module._malloc(4);
+    this.pNumErrMbs = module._malloc(4);
+    this.pCroppingFlag = module._malloc(4);
+    this.pLeftOffset = module._malloc(4);
+    this.pWidth = module._malloc(4);
+    this.pTopOffset = module._malloc(4);
+    this.pHeight = module._malloc(4);
+
+    this.pStorage = module._h264bsdAlloc();
+    module._h264bsdInit(this.pStorage, 0);
+};
+
+H264bsdDecoder.RDY = 0;
+H264bsdDecoder.PIC_RDY = 1;
+H264bsdDecoder.HDRS_RDY = 2;
+H264bsdDecoder.ERROR = 3;
+H264bsdDecoder.PARAM_SET_ERROR = 4;
+H264bsdDecoder.MEMALLOC_ERROR = 5;
+H264bsdDecoder.NO_INPUT = 1024;
+
+/**
+ * Clean up memory used by the decoder
+ */
+H264bsdDecoder.prototype.release = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+    var pInput = this.pInput;
+    var pPicId = this.pPicId;
+    var pIsIdrPic = this.pIsIdrPic;
+    var pNumErrMbs = this.pNumErrMbs;
+    var pBytesRead = this.pBytesRead;
+    var pCroppingFlag = this.pCroppingFlag;
+    var pLeftOffset = this.pLeftOffset;
+    var pWidth = this.pWidth;
+    var pTopOffset = this.pTopOffset;
+    var pHeight = this.pHeight;
+
+    if(pStorage != 0) {
+        module._h264bsdShutdown(pStorage);
+        module._h264bsdFree(pStorage);
+    }
+
+    if(pInput != 0) {
+        module._free(pInput);
+    }
+
+    module._free(pPicId);
+    module._free(pIsIdrPic);
+    module._free(pNumErrMbs);
+    module._free(pBytesRead);
+    module._free(pCroppingFlag);
+    module._free(pLeftOffset);
+    module._free(pWidth);
+    module._free(pTopOffset);
+    module._free(pHeight);
+
+    this.pStorage = 0;
+    this.pInput = 0;
+    this.inputLength = 0;
+    this.inputOffset = 0;
+
+    this.pPicId = 0;
+    this.pIsIdrPic = 0;
+    this.pNumErrMbs = 0;
+    this.pBytesRead = 0;
+    this.pCroppingFlag = 0;
+    this.pLeftOffset = 0;
+    this.pWidth = 0;
+    this.pTopOffset = 0;
+    this.pHeight = 0;
+};
+
+/**
+ * Queue ArrayBuffer data to be decoded
+ */
+H264bsdDecoder.prototype.queueInput = function(data) {
+    var module = this.module
+    var pInput = this.pInput;
+    var inputLength = this.inputLength;
+    var inputOffset = this.inputOffset;
+
+    if (data instanceof ArrayBuffer) {
+        data = new Uint8Array(data)
+    }
+
+    if(pInput === 0) {
+        inputLength = data.byteLength;
+        pInput = module._malloc(inputLength);
+        inputOffset = 0;
+
+        module.HEAPU8.set(data, pInput);
+    } else {
+        var remainingInputLength = inputLength - inputOffset;
+        var newInputLength = remainingInputLength + data.byteLength;
+        var pNewInput = module._malloc(newInputLength);
+
+        module._memcpy(pNewInput, pInput + inputOffset, remainingInputLength);
+        module.HEAPU8.set(data, pNewInput + remainingInputLength);
+
+        module._free(pInput);
+
+        pInput = pNewInput;
+        inputLength = newInputLength;
+        inputOffset = 0;
+    }
+
+    this.pInput = pInput;
+    this.inputLength = inputLength;
+    this.inputOffset = inputOffset;
+}
+
+/**
+ * Returns the numbre of bytes remaining in the decode queue.
+ */
+H264bsdDecoder.prototype.inputBytesRemaining = function() {
+    return this.inputLength - this.inputOffset;
+};
+
+/**
+ * Decodes the next NAL unit from the queued data.
+ * Returns H264bsdDecoder.PIC_RDY when a new picture is ready.
+ * Pictures can be accessed using nextOutputPicture() or nextOutputPictureRGBA()
+ * decode() will return H264bsdDecoder.NO_INPUT when there is no more data to be decoded.
+ */
+H264bsdDecoder.prototype.decode = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+    var pInput = this.pInput;
+    var pBytesRead = this.pBytesRead;
+    var inputLength = this.inputLength;
+    var inputOffset = this.inputOffset;
+
+    if(pInput == 0) return H264bsdDecoder.NO_INPUT;
+
+    var bytesRead = 0;
+    var retCode = module._h264bsdDecode(pStorage, pInput + inputOffset, inputLength - inputOffset, 0, pBytesRead);
+
+    if (retCode == H264bsdDecoder.RDY ||
+        retCode == H264bsdDecoder.PIC_RDY ||
+        retCode == H264bsdDecoder.HDRS_RDY) {
+        bytesRead = module.getValue(pBytesRead, 'i32');
+    }
+
+    inputOffset += bytesRead;
+
+    if(inputOffset >= inputLength) {
+        module._free(pInput);
+        pInput = 0;
+        inputOffset = 0;
+        inputLength = 0;
+    }
+
+    this.pInput = pInput;
+    this.inputLength = inputLength;
+    this.inputOffset = inputOffset;
+
+    if(retCode == H264bsdDecoder.PIC_RDY && this.onPictureReady instanceof Function) {
+        this.onPictureReady();
+    }
+
+    if(retCode == H264bsdDecoder.HDRS_RDY && this.onHeadersReady instanceof Function) {
+        this.onHeadersReady();
+    }
+
+    return retCode;
+};
+
+/**
+ * Returns the next output picture as an I420 encoded image.
+ */
+H264bsdDecoder.prototype.nextOutputPicture = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+    var pPicId = this.pPicId;
+    var pIsIdrPic = this.pIsIdrPic;
+    var pNumErrMbs = this.pNumErrMbs;
+
+    var pBytes = module._h264bsdNextOutputPicture(pStorage, pPicId, pIsIdrPic, pNumErrMbs);
+
+    var outputLength = this.outputPictureSizeBytes();
+    var outputBytes = new Uint8Array(module.HEAPU8.subarray(pBytes, pBytes + outputLength));
+
+    return outputBytes;
+};
+
+/**
+ * Returns the next output picture as an RGBA encoded image.
+ * Note: There is extra overhead required to convert the image to RGBA.
+ * This method should be avoided if possible.
+ */
+H264bsdDecoder.prototype.nextOutputPictureRGBA = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+    var pPicId = this.pPicId;
+    var pIsIdrPic = this.pIsIdrPic;
+    var pNumErrMbs = this.pNumErrMbs;
+
+    var pBytes = module._h264bsdNextOutputPictureRGBA(pStorage, pPicId, pIsIdrPic, pNumErrMbs);
+
+    var outputLength = this.outputPictureSizeBytesRGBA();
+    var outputBytes = new Uint8Array(module.HEAPU8.subarray(pBytes, pBytes + outputLength));
+
+    return outputBytes;
+};
+
+/**
+ * Returns an object containing the width and height of output pictures in pixels.
+ * This value is only valid after at least one call to decode() has returned H264bsdDecoder.HDRS_RDY
+ * You can also use onHeadersReady callback to determine when this value changes.
+ */
+H264bsdDecoder.prototype.outputPictureWidth = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+
+    return module._h264bsdPicWidth(pStorage) * 16;
+};
+
+/**
+ * Returns an object containing the width and height of output pictures in pixels.
+ * This value is only valid after at least one call to decode() has returned H264bsdDecoder.HDRS_RDY
+ * You can also use onHeadersReady callback to determine when this value changes.
+ */
+H264bsdDecoder.prototype.outputPictureHeight = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+
+    return module._h264bsdPicHeight(pStorage) * 16;
+};
+
+/**
+ * Returns integer byte length of output pictures in bytes.
+ * This value is only valid after at least one call to decode() has returned H264bsdDecoder.HDRS_RDY
+ */
+H264bsdDecoder.prototype.outputPictureSizeBytes = function() {
+    var width = this.outputPictureWidth();
+    var height = this.outputPictureHeight();
+
+    return (width * height) * 3 / 2;
+};
+
+/**
+ * Returns integer byte length of RGBA output pictures in bytes.
+ * This value is only valid after at least one call to decode() has returned H264bsdDecoder.HDRS_RDY
+ */
+H264bsdDecoder.prototype.outputPictureSizeBytesRGBA = function() {
+    var width = this.outputPictureWidth();
+    var height = this.outputPictureHeight();
+
+    return (width * height) * 4;
+};
+
+/**
+ * Returns the info used to crop output images to there final viewing dimensions.
+ * If this method returns null no cropping info is provided and the full image should be presented.
+ */
+H264bsdDecoder.prototype.croppingParams = function() {
+    var module = this.module;
+    var pStorage = this.pStorage;
+    var pCroppingFlag = this.pCroppingFlag;
+    var pLeftOffset = this.pLeftOffset;
+    var pWidth = this.pWidth;
+    var pTopOffset = this.pTopOffset;
+    var pHeight = this.pHeight;
+
+    module._h264bsdCroppingParams(pStorage, pCroppingFlag, pLeftOffset, pWidth, pTopOffset, pHeight);
+
+    var croppingFlag = module.getValue(pCroppingFlag, 'i32');
+    var leftOffset = module.getValue(pLeftOffset, 'i32');
+    var width = module.getValue(pWidth, 'i32');
+    var topOffset = module.getValue(pTopOffset, 'i32');
+    var height = module.getValue(pHeight, 'i32');
+
+    if(croppingFlag === 0) return null;
+
+    return {
+        'width': width,
+        'height': height,
+        'top': topOffset,
+        'left': leftOffset
+    };
+};
--- /dev/null
+++ b/wasm/h264bsd_worker.js
@@ -1,0 +1,78 @@
+var Module = {
+  onRuntimeInitialized: function() {
+    decoder = new H264bsdDecoder(Module);
+    decoder.onPictureReady = onPictureReady;
+    decoder.onHeadersReady = onHeadersReady;
+    postMessage({'type': 'decoderReady'});
+  }
+};
+
+importScripts('h264bsd_decoder.js', 'h264bsd_asm.js')
+
+var noInput = true;
+var decoder = null;
+
+function onMessage(e) {
+    var message = e.data;
+    switch(message.type) {
+    case 'queueInput' :
+        decoder.queueInput(message.data);
+        if(noInput) {
+          noInput = false;
+          decodeLoop();
+        }
+        break;
+    }
+}
+
+function onPictureReady() {
+    var width = decoder.outputPictureWidth();
+    var height = decoder.outputPictureHeight();
+    var croppingParams = decoder.croppingParams();
+    var output = decoder.nextOutputPicture();
+
+    postMessage({
+      'type' : 'pictureReady',
+      'width' : width,
+      'height' : height,
+      'croppingParams' : croppingParams,
+      'data' : output.buffer,
+    }, [output.buffer]);
+}
+
+function onHeadersReady() {
+    var width = decoder.outputPictureWidth();
+    var height = decoder.outputPictureHeight();
+    var croppingParams = decoder.croppingParams();
+
+    postMessage({
+      'type' : 'pictureParams',
+      'width' : width,
+      'height' : height,
+      'croppingParams' : croppingParams,
+    });
+}
+
+function decodeLoop() {
+    var result = decoder.decode();
+
+    switch(result) {
+    case H264bsdDecoder.ERROR:
+        postMessage({'type': 'decodeError'});
+        break;
+    case H264bsdDecoder.PARAM_SET_ERROR:
+        postMessage({'type': 'paramSetError'});
+        break;
+    case H264bsdDecoder.MEMALLOC_ERROR:
+        postMessage({'type': 'memAllocError'});
+        break;
+    case H264bsdDecoder.NO_INPUT:
+        noInput = true;
+        postMessage({'type': 'noInput'});
+        break;
+    default:
+        setTimeout(decodeLoop, 0);
+    }
+}
+
+addEventListener('message', onMessage);
--- /dev/null
+++ b/wasm/test.html
@@ -1,0 +1,106 @@
+<!doctype html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>h.264bsd test</title>
+</head>
+<body>
+    <input type="file" id="file" name="file" />
+    <span id="fps_display"></span>
+    <br/>
+
+    <!--This is where we will display decoded frames-->
+    <canvas id="canvas" width="640" height="480" style="border:solid;"></canvas>
+
+    <script src="h264bsd_canvas.js"></script>
+
+    <script type="text/javascript">
+        var canvas = document.getElementById('canvas');
+
+        var pictureCount = 0;
+        var lastPictureCount = 0;
+
+        // Create the decoder and canvas
+        var decoder = new Worker('h264bsd_worker.js');
+        var display = new H264bsdCanvas(canvas);
+
+        console.log('Created decoder and canvas');
+
+        decoder.addEventListener('error', function(e) {
+            console.log('Decoder error', e);
+        })
+
+        decoder.addEventListener('message', function(e) {
+            var message = e.data;
+            if (!message.hasOwnProperty('type')) return;
+
+            switch(message.type) {
+            case 'pictureParams':
+                croppingParams = message.croppingParams;
+                if(croppingParams === null) {
+                    canvas.width = message.width;
+                    canvas.height = message.height;
+                } else {
+                    canvas.width = croppingParams.width;
+                    canvas.height = croppingParams.height;
+                }
+                break;
+            case 'noInput':
+                var copy = new Uint8Array(buf);
+                decoder.postMessage({
+                    'type' : 'queueInput',
+                    'data' : copy.buffer
+                }, [copy.buffer]);
+                break;
+            case 'pictureReady':
+                display.drawNextOutputPicture(
+                    message.width,
+                    message.height,
+                    message.croppingParams,
+                    new Uint8Array(message.data));
+                ++pictureCount;
+                break;
+            case 'decoderReady':
+                console.log('Decoder ready');
+                break;
+            }
+        });
+
+        function updateFpsCount() {
+            var picturesSinceLastUpdate = pictureCount - lastPictureCount;
+            var fpsDisplay = document.getElementById('fps_display');
+
+            fps_display.innerHTML = 'FPS: ' + picturesSinceLastUpdate;
+
+            lastPictureCount = pictureCount;
+        }
+
+        var buf = null;
+
+        // Use the FileReader to get the bytes into the decoder
+        function handleFileSelect(evt) {
+            var f = evt.target.files[0]; // FileList object
+
+            var reader = new FileReader();
+
+            // Closure to capture the file information.
+            reader.onload = function(e) {
+                buf = new Uint8Array(e.target.result);
+
+                var copy = new Uint8Array(buf)
+                decoder.postMessage(
+                    {'type' : 'queueInput', 'data' : copy.buffer},
+                    [copy.buffer]);
+                setInterval(updateFpsCount, 1000);
+
+                console.log('Starting encode loop');
+            };
+
+            // Read in the image file as a data URL.
+            reader.readAsArrayBuffer(f);
+        }
+
+        document.getElementById('file').addEventListener('change', handleFileSelect, false);
+    </script>
+</body>
+</html>