API Docs for: 2.0.0

public/src/SAGE2_BlockStreamingApp.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014

/* global mat4 */

"use strict";


/**
 * @module client
 * @submodule SAGE2_BlockStreamingApp
 */

/**
 * Base class for block streaming applications
 *
 * @class SAGE2_BlockStreamingApp
 */
var SAGE2_BlockStreamingApp = SAGE2_App.extend({

	/**
	* Init method, creates an 'canvas' tag in the DOM and setups up WebGL
	*
	* @method init
	* @param data {Object} contains initialization values (id, width, height, ...)
	*/
	blockStreamInit: function(data) {
		this.SAGE2Init("div", data);

		this.resizeEvents     = "onfinish";
		this.moveEvents       = "continuous"; // "onfinish";
		this.enableControls   = true;

		this.canvas           = null;
		this.gl               = null;
		this.shaderProgram    = null;
		this.pMatrix          = null;

		this.ctx              = null;
		this.maxSize          = null;
		this.verticalBlocks   = null;
		this.horizontalBlocks = null;
		// this.rgbaBuffer       = [];
		this.rgbaTexture      = [];
		// this.rgbBuffer        = [];
		this.rgbTexture       = [];
		// this.yuvBuffer        = [];
		this.yTexture         = [];
		this.uTexture         = [];
		this.vTexture         = [];
		this.changeBlockList  = false;
		this.newBlockList     = null;
		this.validBlocks      = [];
		this.receivedBlocks   = [];

		this.squareVertexPositionBuffer     = [];
		this.squareVertexTextureCoordBuffer = [];
		this.squareVertexIndexBuffer        = [];

		this.canvas = document.createElement('canvas');
		this.canvas.id = data.id + "_canvas";
		this.canvas.style.position = "absolute";
		this.canvas.width  = ui.json_cfg.resolution.width;
		this.canvas.height = ui.json_cfg.resolution.height;
		this.element.appendChild(this.canvas);

		// application specific 'init'
		this.maxSize = 512; // block size
		this.maxFPS  = 60;

		// Setup a function for RAF call
		this.drawFunc = this.mydraw.bind(this);

		this.initGL();
		if (this.gl) {
			var _this = this;
			this.div.style.backgroundColor = "#000000";

			this.gl.clearColor(0.5, 0.5, 0.5, 1.0);
			this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);

			this.pMatrix = mat4.create();

			this.initShaders(function() {
				// shaders compiled
				_this.resizeCanvas();
				_this.refresh(data.date);
			});
		}
		this.setValidBlocksFalse();
	},

	/**
	* Gets a WebGL context from the canvas
	*
	* @method initGL
	*/
	initGL: function() {
		this.gl = this.canvas.getContext("webgl");
		if (!this.gl) {
			this.gl = this.canvas.getContext("experimental-webgl");
		}
		if (!this.gl) {
			this.log("Unable to initialize WebGL. Your browser may not support it.");
		}
	},

	/**
	* Reset valid blocks to false
	*
	* @method setValidBlocksFalse
	*/
	setValidBlocksFalse: function() {
		for (var i = 0; i < this.receivedBlocks.length; i++) {
			if (this.validBlocks.indexOf(i) >= 0) {
				this.receivedBlocks[i] = false;
			} else {
				this.receivedBlocks[i] = true;
			}
		}
	},

	/**
	* Loads the yuv2rgb shaders
	*
	* @method initShaders
	* @param callback {Function} to be executed when shaders are loaded
	*/
	initShaders: function(callback) {
		if (this.state.colorspace === "RGBA" || this.state.colorspace === "RGB") {
			this.initRGBAShaders(callback);
		} else if (this.state.colorspace === "BGR") {
			this.initBGRShaders(callback);
		} else if (this.state.colorspace === "YUV420p") {
			this.initYUV420pShaders(callback);
		}
	},

	initRGBAShaders: function(callback) {
		var _this = this;
		var vertFile = "shaders/rgb.vert";
		var fragFile = "shaders/rgb.frag";

		this.getShaders(vertFile, fragFile, function(vertexShader, fragmentShader) {
			// Create the shader program
			_this.shaderProgram = _this.gl.createProgram();
			_this.gl.attachShader(_this.shaderProgram, vertexShader);
			_this.gl.attachShader(_this.shaderProgram, fragmentShader);
			_this.gl.linkProgram(_this.shaderProgram);

			// If creating the shader program failed, alert
			if (!_this.gl.getProgramParameter(_this.shaderProgram, _this.gl.LINK_STATUS)) {
				throw new Error('Unable to initialize the shader program');
			}

			_this.gl.useProgram(_this.shaderProgram);

			// set vertex array
			_this.shaderProgram.vertexPositionAttribute = _this.gl.getAttribLocation(_this.shaderProgram, "a_position");
			_this.gl.enableVertexAttribArray(_this.shaderProgram.vertexPositionAttribute);
			// set texture coord array
			_this.shaderProgram.textureCoordAttribute = _this.gl.getAttribLocation(_this.shaderProgram, "a_texCoord");
			_this.gl.enableVertexAttribArray(_this.shaderProgram.textureCoordAttribute);

			// set view matrix
			_this.shaderProgram.pMatrixUniform = _this.gl.getUniformLocation(_this.shaderProgram, "p_matrix");

			// set image texture
			_this.shaderProgram.samplerUniform1 = _this.gl.getUniformLocation(_this.shaderProgram, "rgb_image");

			callback();
		});
	},

	initBGRShaders: function(callback) {
		var _this = this;
		var vertFile = "shaders/rgb.vert";
		var fragFile = "shaders/bgr.frag";

		this.getShaders(vertFile, fragFile, function(vertexShader, fragmentShader) {
			// Create the shader program
			_this.shaderProgram = _this.gl.createProgram();
			_this.gl.attachShader(_this.shaderProgram, vertexShader);
			_this.gl.attachShader(_this.shaderProgram, fragmentShader);
			_this.gl.linkProgram(_this.shaderProgram);

			// If creating the shader program failed, alert
			if (!_this.gl.getProgramParameter(_this.shaderProgram, _this.gl.LINK_STATUS)) {
				throw new Error('Unable to initialize the shader program');
			}

			_this.gl.useProgram(_this.shaderProgram);

			// set vertex array
			_this.shaderProgram.vertexPositionAttribute = _this.gl.getAttribLocation(_this.shaderProgram, "a_position");
			_this.gl.enableVertexAttribArray(_this.shaderProgram.vertexPositionAttribute);
			// set texture coord array
			_this.shaderProgram.textureCoordAttribute = _this.gl.getAttribLocation(_this.shaderProgram, "a_texCoord");
			_this.gl.enableVertexAttribArray(_this.shaderProgram.textureCoordAttribute);

			// set view matrix
			_this.shaderProgram.pMatrixUniform = _this.gl.getUniformLocation(_this.shaderProgram, "p_matrix");

			// set image texture
			_this.shaderProgram.samplerUniform1 = _this.gl.getUniformLocation(_this.shaderProgram, "rgb_image");

			callback();
		});
	},

	initYUV420pShaders: function(callback) {
		var _this = this;
		var vertFile = "shaders/yuv2rgb.vert";
		var fragFile = "shaders/yuv2rgb.frag";

		this.getShaders(vertFile, fragFile, function(vertexShader, fragmentShader) {
			// Create the shader program
			_this.shaderProgram = _this.gl.createProgram();
			_this.gl.attachShader(_this.shaderProgram, vertexShader);
			_this.gl.attachShader(_this.shaderProgram, fragmentShader);
			_this.gl.linkProgram(_this.shaderProgram);

			// If creating the shader program failed, alert
			if (!_this.gl.getProgramParameter(_this.shaderProgram, _this.gl.LINK_STATUS)) {
				throw new Error('Unable to initialize the shader program');
			}

			_this.gl.useProgram(_this.shaderProgram);

			// set vertex array
			_this.shaderProgram.vertexPositionAttribute = _this.gl.getAttribLocation(_this.shaderProgram, "a_position");
			_this.gl.enableVertexAttribArray(_this.shaderProgram.vertexPositionAttribute);
			// set texture coord array
			_this.shaderProgram.textureCoordAttribute = _this.gl.getAttribLocation(_this.shaderProgram, "a_texCoord");
			_this.gl.enableVertexAttribArray(_this.shaderProgram.textureCoordAttribute);

			// set view matrix
			_this.shaderProgram.pMatrixUniform = _this.gl.getUniformLocation(_this.shaderProgram, "p_matrix");

			// set image texture
			_this.shaderProgram.samplerUniform1 = _this.gl.getUniformLocation(_this.shaderProgram, "y_image");
			_this.shaderProgram.samplerUniform2 = _this.gl.getUniformLocation(_this.shaderProgram, "u_image");
			_this.shaderProgram.samplerUniform3 = _this.gl.getUniformLocation(_this.shaderProgram, "v_image");

			callback();
		});
	},

	/**
	* Loads the shaders files from the server and creates the shaders
	*
	* @method getShaders
	* @param vertFile {String} filename of the vertex shader
	* @param fragFile {String} filename of the fragment shader
	* @param callback {Function} to be executed when shaders are loaded
	*/
	getShaders: function(vertFile, fragFile, callback) {
		var _this = this;

		var vertShader;
		var fragShader;

		var vertReadComplete = false;
		var fragReadComplete = false;

		readFile(vertFile, function(err, text) {
			if (err) {
				this.log(err);
			}

			vertShader = _this.gl.createShader(_this.gl.VERTEX_SHADER);

			// Send the source to the shader object
			_this.gl.shaderSource(vertShader, text);

			// Compile the shader program
			_this.gl.compileShader(vertShader);

			// See if it compiled successfully
			if (!_this.gl.getShaderParameter(vertShader, _this.gl.COMPILE_STATUS)) {
				this.log("An error occurred compiling the vertex shader: " + _this.gl.getShaderInfoLog(vertShader));
			}

			if (fragReadComplete) {
				callback(vertShader, fragShader);
			}

			vertReadComplete = true;
		});
		readFile(fragFile, function(err, text) {
			if (err) {
				this.log(err);
			}

			fragShader = _this.gl.createShader(_this.gl.FRAGMENT_SHADER);

			// Send the source to the shader object
			_this.gl.shaderSource(fragShader, text);

			// Compile the shader program
			_this.gl.compileShader(fragShader);

			// See if it compiled successfully
			if (!_this.gl.getShaderParameter(fragShader, _this.gl.COMPILE_STATUS)) {
				_this.log("An error occurred compiling the fragment shader: " + _this.gl.getShaderInfoLog(fragShader));
			}

			if (vertReadComplete) {
				callback(vertShader, fragShader);
			}

			fragReadComplete = true;
		});
	},

	/**
	* Initializes the GL buffers for the blocks of pixel
	*
	* @method initBuffers
	*/
	initBuffers: function() {
		var i, j;
		if (this.state.colorspace === "RGB" || this.state.colorspace === "BGR") {
			// Go bottom up
			// for (i = this.verticalBlocks - 1; i >= 0; i--) {
			// 	for (j = 0; j < this.horizontalBlocks; j++) {
			// 		this.initABlock(i, j);
			// 	}
			// }
			for (i = 0; i < this.verticalBlocks; i++) {
				for (j = 0; j < this.horizontalBlocks; j++) {
					this.initABlock(i, j);
				}
			}
		} else {
			// Go top down
			for (i = 0; i < this.verticalBlocks; i++) {
				for (j = 0; j < this.horizontalBlocks; j++) {
					this.initABlock(i, j);
				}
			}
		}
	},

	initABlock: function(i, j) {
		var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width - (j * this.maxSize) : this.maxSize;
		var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;
		var bX = j * this.maxSize;
		var bY = i * this.maxSize;

		var left   =  (bX / this.state.width * 2.0) - 1.0;
		var right  = ((bX + bWidth) / this.state.width * 2.0) - 1.0;
		var bottom = -1 *  ((bY / this.state.height * 2.0) - 1.0);
		var top    = -1 * (((bY + bHeight) / this.state.height * 2.0) - 1.0);

		// vertices
		var squareVertexPositionBuffer = this.gl.createBuffer();
		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, squareVertexPositionBuffer);
		var vertices = [
			left,  bottom,
			right, bottom,
			left,  top,
			right, top
		];
		this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(vertices), this.gl.STATIC_DRAW);
		squareVertexPositionBuffer.itemSize = 2;
		squareVertexPositionBuffer.numItems = 4;

		// texture
		var squareVertexTextureCoordBuffer = this.gl.createBuffer();
		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, squareVertexTextureCoordBuffer);
		var textureCoords = [
			0.0,  1.0,
			1.0,  1.0,
			0.0,  0.0,
			1.0,  0.0
		];
		this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(textureCoords), this.gl.STATIC_DRAW);
		squareVertexTextureCoordBuffer.itemSize = 2;
		squareVertexTextureCoordBuffer.numItems = 4;

		// faces of triangles
		var squareVertexIndexBuffer = this.gl.createBuffer();
		this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, squareVertexIndexBuffer);
		var vertexIndices = [0, 1, 2,   2, 1, 3];
		this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexIndices), this.gl.STATIC_DRAW);
		squareVertexIndexBuffer.itemSize = 1;
		squareVertexIndexBuffer.numItems = 6;

		this.squareVertexPositionBuffer.push(squareVertexPositionBuffer);
		this.squareVertexTextureCoordBuffer.push(squareVertexTextureCoordBuffer);
		this.squareVertexIndexBuffer.push(squareVertexIndexBuffer);
	},

	/**
	* Initializes the GL textures for the blocks of pixel
	*
	* @method initTextures
	*/
	initTextures: function() {
		this.horizontalBlocks = Math.ceil(this.state.width / this.maxSize);
		this.verticalBlocks   = Math.ceil(this.state.height / this.maxSize);

		// Global settings for WebGL textures
		this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT,    1);
		// Flips the source data along its vertical axis if true
		this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, true);

		if (this.state.colorspace === "RGBA") {
			this.initRGBATextures();
		} else if (this.state.colorspace === "RGB" || this.state.colorspace === "BGR") {
			this.initRGBTextures();
		} else if (this.state.colorspace === "YUV420p") {
			this.initYUV420pTextures();
		}
	},

	initRGBATextures: function() {
		for (var i = 0; i < this.verticalBlocks; i++) {
			for (var j = 0; j < this.horizontalBlocks; j++) {
				var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
				var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

				var rgbaTexture = this.gl.createTexture();

				var rgbaBuffer = new Uint8Array(bWidth * bHeight * 4);

				this.gl.activeTexture(this.gl.TEXTURE0);
				this.gl.bindTexture(this.gl.TEXTURE_2D, rgbaTexture);
				this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, bWidth, bHeight,
					0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, rgbaBuffer);

				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);

				this.rgbaTexture.push(rgbaTexture);

				// this.rgbaBuffer.push(rgbaBuffer);
			}
		}
	},

	initRGBTextures: function() {
		// Flips the source data along its vertical axis if true
		// this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);

		for (var i = 0; i < this.verticalBlocks; i++) {
			for (var j = 0; j < this.horizontalBlocks; j++) {
				var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
				var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

				var rgbTexture = this.gl.createTexture();

				var rgbBuffer = new Uint8Array(bWidth * bHeight * 3);

				this.gl.activeTexture(this.gl.TEXTURE0);
				this.gl.bindTexture(this.gl.TEXTURE_2D, rgbTexture);
				this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGB, bWidth, bHeight,
					0, this.gl.RGB, this.gl.UNSIGNED_BYTE, rgbBuffer);

				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);

				this.rgbTexture.push(rgbTexture);

				// this.rgbBuffer.push(rgbBuffer);
			}
		}
	},

	initYUV420pTextures: function() {
		for (var i = 0; i < this.verticalBlocks; i++) {
			for (var j = 0; j < this.horizontalBlocks; j++) {
				var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width - (j * this.maxSize) : this.maxSize;
				var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

				var yTexture = this.gl.createTexture();
				var uTexture = this.gl.createTexture();
				var vTexture = this.gl.createTexture();

				var yBuffer = new Uint8Array(bWidth * bHeight);
				var uBuffer = new Uint8Array(bWidth * bHeight / 4);
				var vBuffer = new Uint8Array(bWidth * bHeight / 4);

				this.gl.bindTexture(this.gl.TEXTURE_2D, yTexture);
				this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.LUMINANCE, bWidth, bHeight, 0,
					this.gl.LUMINANCE, this.gl.UNSIGNED_BYTE, yBuffer);

				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);

				this.gl.bindTexture(this.gl.TEXTURE_2D, uTexture);
				this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.LUMINANCE, bWidth / 2, bHeight / 2, 0,
					this.gl.LUMINANCE, this.gl.UNSIGNED_BYTE, uBuffer);

				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);

				this.gl.bindTexture(this.gl.TEXTURE_2D, vTexture);
				this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.LUMINANCE, bWidth / 2, bHeight / 2, 0,
					this.gl.LUMINANCE, this.gl.UNSIGNED_BYTE, vBuffer);

				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
				this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);

				this.gl.bindTexture(this.gl.TEXTURE_2D, null);

				this.yTexture.push(yTexture);
				this.uTexture.push(uTexture);
				this.vTexture.push(vTexture);


				// var yuvBuffer = new Uint8Array(bWidth * bHeight * 1.5);
				// yuvBuffer.set(yBuffer, 0);
				// yuvBuffer.set(uBuffer, bWidth * bHeight);
				// yuvBuffer.set(vBuffer, bWidth * bHeight + bWidth * bHeight / 4);
				// this.yuvBuffer.push(yuvBuffer);
			}
		}
	},

	/**
	* Sets a block of pixels into a buffer
	*
	* @method textureData
	* @param blockIdx {Number} index to the block
	* @param yuvBuffer {Object} pixel data
	*/
	textureData: function(blockIdx, buffer) {
		if (this.state.colorspace === "RGBA") {
			this.textureDataRGBA(blockIdx, buffer);
		} else if (this.state.colorspace === "RGB" || this.state.colorspace === "BGR") {
			this.textureDataRGB(blockIdx, buffer);
		} else if (this.state.colorspace === "YUV420p") {
			this.textureDataYUV420p(blockIdx, buffer);
		}
	},

	textureDataRGBA: function(blockIdx, rgbaBuffer) {
		// this.rgbaBuffer[blockIdx] = rgbaBuffer;
		this.receivedBlocks[blockIdx] = true;


		// Updating the texture right away
		var i, j;
		i = Math.floor(blockIdx / this.horizontalBlocks);
		j = blockIdx % this.horizontalBlocks;
		var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
		var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;
		this.gl.bindTexture(this.gl.TEXTURE_2D, this.rgbaTexture[blockIdx]);
		this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth, bHeight, this.gl.RGBA,
			this.gl.UNSIGNED_BYTE, rgbaBuffer);
	},

	textureDataRGB: function(blockIdx, rgbBuffer) {
		// this.rgbBuffer[blockIdx] = rgbBuffer;
		this.receivedBlocks[blockIdx] = true;


		// Updating the texture right away
		var i, j;
		i = Math.floor(blockIdx / this.horizontalBlocks);
		j = blockIdx % this.horizontalBlocks;
		var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
		var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;
		this.gl.bindTexture(this.gl.TEXTURE_2D, this.rgbTexture[blockIdx]);
		this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth, bHeight, this.gl.RGB,
			this.gl.UNSIGNED_BYTE, rgbBuffer);
	},

	textureDataYUV420p: function(blockIdx, yuvBuffer) {
		// this.yuvBuffer[blockIdx] = yuvBuffer;
		this.receivedBlocks[blockIdx] = true;

		// Updating the texture right away
		var i, j;
		i = Math.floor(blockIdx / this.horizontalBlocks);
		j = blockIdx % this.horizontalBlocks;

		var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
		var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

		var yEnd = bWidth * bHeight;
		var uEnd = yEnd + bWidth * bHeight / 4;
		var vEnd = uEnd + bWidth * bHeight / 4;

		// var yBuffer = yuvBuffer.subarray(0,    yEnd);
		// var uBuffer = yuvBuffer.subarray(yEnd, uEnd);
		// var vBuffer = yuvBuffer.subarray(uEnd, vEnd);

		this.gl.bindTexture(this.gl.TEXTURE_2D, this.yTexture[blockIdx]);
		this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth, bHeight, this.gl.LUMINANCE,
			this.gl.UNSIGNED_BYTE, yuvBuffer);

		this.gl.bindTexture(this.gl.TEXTURE_2D, this.uTexture[blockIdx]);
		this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth / 2, bHeight / 2, this.gl.LUMINANCE,
			this.gl.UNSIGNED_BYTE, yuvBuffer.subarray(yEnd, uEnd));

		this.gl.bindTexture(this.gl.TEXTURE_2D, this.vTexture[blockIdx]);
		this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth / 2, bHeight / 2, this.gl.LUMINANCE,
			this.gl.UNSIGNED_BYTE, yuvBuffer.subarray(uEnd, vEnd));
	},

	/**
	* Update the textures with the new pixel data
	*
	* @method updateTextures
	*/
	updateTextures: function() {
		if (this.state.colorspace === "RGBA") {
			this.updateTexturesRGBA();
		} else if (this.state.colorspace === "RGB" || this.state.colorspace === "BGR") {
			this.updateTexturesRGB();
		} else if (this.state.colorspace === "YUV420p") {
			this.updateTexturesYUV420p();
		}
	},

	updateTexturesRGBA: function() {
		// for (var i = 0; i < this.verticalBlocks; i++) {
		// 	for (var j = 0; j < this.horizontalBlocks; j++) {
		// 		var blockIdx = i * this.horizontalBlocks + j;
		// 		// Update only valid bocks
		// 		if (this.validBlocks.indexOf(blockIdx) >= 0) {
		// 			var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
		// 			var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, this.rgbaTexture[blockIdx]);
		// 			this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth, bHeight, this.gl.RGBA,
		// 						this.gl.UNSIGNED_BYTE, this.rgbaBuffer[blockIdx]);
		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, null);
		// 		}
		// 	}
		// }
	},

	updateTexturesRGB: function() {
		// for (var i = 0; i < this.verticalBlocks; i++) {
		// 	for (var j = 0; j < this.horizontalBlocks; j++) {
		// 		var blockIdx = i * this.horizontalBlocks + j;
		// 		// Update only valid bocks
		// 		if (this.validBlocks.indexOf(blockIdx) >= 0) {
		// 			var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
		// 			var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, this.rgbTexture[blockIdx]);
		// 			this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth, bHeight, this.gl.RGB,
		// 						this.gl.UNSIGNED_BYTE, this.rgbBuffer[blockIdx]);
		// 		}
		// 	}
		// }
	},

	updateTexturesYUV420p: function() {
		// for (var i = 0; i < this.verticalBlocks; i++) {
		// 	for (var j = 0; j < this.horizontalBlocks; j++) {
		// 		var blockIdx = i * this.horizontalBlocks + j;
		// 		// Update only valid bocks
		// 		if (this.validBlocks.indexOf(blockIdx) >= 0) {
		// 			var bWidth  = (j + 1) * this.maxSize > this.state.width  ? this.state.width  - (j * this.maxSize) : this.maxSize;
		// 			var bHeight = (i + 1) * this.maxSize > this.state.height ? this.state.height - (i * this.maxSize) : this.maxSize;

		// 			var yEnd = bWidth * bHeight;
		// 			var uEnd = yEnd + bWidth * bHeight / 4;
		// 			var vEnd = uEnd + bWidth * bHeight / 4;

		// 			var yBuffer = this.yuvBuffer[blockIdx].subarray(0,    yEnd);
		// 			var uBuffer = this.yuvBuffer[blockIdx].subarray(yEnd, uEnd);
		// 			var vBuffer = this.yuvBuffer[blockIdx].subarray(uEnd, vEnd);

		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, this.yTexture[blockIdx]);
		// 			this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth, bHeight, this.gl.LUMINANCE,
		// 						this.gl.UNSIGNED_BYTE, yBuffer);

		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, this.uTexture[blockIdx]);
		// 			this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth / 2, bHeight / 2, this.gl.LUMINANCE,
		// 						this.gl.UNSIGNED_BYTE, uBuffer);

		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, this.vTexture[blockIdx]);
		// 			this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, bWidth / 2, bHeight / 2, this.gl.LUMINANCE,
		// 						this.gl.UNSIGNED_BYTE, vBuffer);

		// 			this.gl.bindTexture(this.gl.TEXTURE_2D, null);
		// 		}
		// 	}
		// }
	},

	/**
	* Loads the app from a previous state and initializes the buffers and textures
	*
	* @method firstLoad
	*/
	firstLoad: function() {
		this.horizontalBlocks = Math.ceil(this.state.width / this.maxSize);
		this.verticalBlocks   = Math.ceil(this.state.height / this.maxSize);
		this.receivedBlocks   = initializeArray(this.horizontalBlocks * this.verticalBlocks, false);

		this.initBuffers();
		this.initTextures();
	},

	/**
	* Draw function, draws each blocks
	*
	* @method draw
	* @param date {Date} current time from the server
	*/
	draw: function(date) {
		// Schedule a draw call
		requestAnimationFrame(this.drawFunc);
	},

	mydraw: function(date) {
		if (this.shaderProgram === undefined || this.shaderProgram === null) {
			// this.log("waiting for shaders to load");
			return;
		}

		// if (this.state.colorspace === "RGBA" && (this.rgbaBuffer === undefined || this.rgbaBuffer === null)) {
		// 	this.log("no RGBA texture loaded");
		// 	return;
		// }
		// if (this.state.colorspace === "RGB" && (this.rgbBuffer === undefined || this.rgbBuffer === null)) {
		// 	this.log("no RGB texture loaded");
		// 	return;
		// }
		// if (this.state.colorspace === "BGR" && (this.rgbBuffer === undefined || this.rgbBuffer === null)) {
		// 	this.log("no BGR texture loaded");
		// 	return;
		// }
		// if (this.state.colorspace === "YUV420p" && (this.yuvBuffer === undefined || this.yuvBuffer === null)) {
		// 	this.log("no YUV420p texture loaded");
		// 	return;
		// }

		// this.gl.clear(this.gl.COLOR_BUFFER_BIT);
		// this.updateTextures();

		if (this.state.colorspace === "RGBA") {
			this.drawRGBA();
		} else if (this.state.colorspace === "RGB" || this.state.colorspace === "BGR") {
			this.drawRGB();
		} else if (this.state.colorspace === "YUV420p") {
			this.drawYUV420p();
		}
	},

	drawRGBA: function() {
		for (var i = 0; i < this.verticalBlocks; i++) {
			for (var j = 0; j < this.horizontalBlocks; j++) {
				var blockIdx = i * this.horizontalBlocks + j;
				// Draw only valid bocks
				if (this.validBlocks.indexOf(blockIdx) >= 0) {

					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.squareVertexPositionBuffer[blockIdx]);
					this.gl.vertexAttribPointer(this.shaderProgram.vertexPositionAttribute,
						this.squareVertexPositionBuffer[blockIdx].itemSize, this.gl.FLOAT, false, 0, 0);

					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.squareVertexTextureCoordBuffer[blockIdx]);
					this.gl.vertexAttribPointer(this.shaderProgram.textureCoordAttribute,
						this.squareVertexTextureCoordBuffer[blockIdx].itemSize, this.gl.FLOAT, false, 0, 0);

					this.gl.bindTexture(this.gl.TEXTURE_2D, this.rgbaTexture[blockIdx]);
					this.gl.uniform1i(this.shaderProgram.samplerUniform1, 0);

					this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.squareVertexIndexBuffer[blockIdx]);
					this.gl.drawElements(this.gl.TRIANGLES, this.squareVertexIndexBuffer[blockIdx].numItems,
						this.gl.UNSIGNED_SHORT, 0);
				}
			}
		}
	},

	drawRGB: function() {
		for (var i = 0; i < this.verticalBlocks; i++) {
			for (var j = 0; j < this.horizontalBlocks; j++) {
				var blockIdx   = i * this.horizontalBlocks + j;
				// Draw only valid bocks
				if (this.validBlocks.indexOf(blockIdx) >= 0) {

					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.squareVertexPositionBuffer[blockIdx]);
					this.gl.vertexAttribPointer(this.shaderProgram.vertexPositionAttribute,
						this.squareVertexPositionBuffer[blockIdx].itemSize, this.gl.FLOAT, false, 0, 0);

					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.squareVertexTextureCoordBuffer[blockIdx]);
					this.gl.vertexAttribPointer(this.shaderProgram.textureCoordAttribute,
						this.squareVertexTextureCoordBuffer[blockIdx].itemSize, this.gl.FLOAT, false, 0, 0);

					this.gl.bindTexture(this.gl.TEXTURE_2D, this.rgbTexture[blockIdx]);
					this.gl.uniform1i(this.shaderProgram.samplerUniform1, 0);

					this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.squareVertexIndexBuffer[blockIdx]);
					this.gl.drawElements(this.gl.TRIANGLES, this.squareVertexIndexBuffer[blockIdx].numItems,
						this.gl.UNSIGNED_SHORT, 0);
				}
			}
		}
	},

	drawYUV420p: function() {
		for (var i = 0; i < this.verticalBlocks; i++) {
			for (var j = 0; j < this.horizontalBlocks; j++) {
				var blockIdx = i * this.horizontalBlocks + j;
				// Draw only valid bocks
				if (this.validBlocks.indexOf(blockIdx) >= 0) {
					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.squareVertexPositionBuffer[blockIdx]);
					this.gl.vertexAttribPointer(this.shaderProgram.vertexPositionAttribute,
						this.squareVertexPositionBuffer[blockIdx].itemSize, this.gl.FLOAT, false, 0, 0);

					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.squareVertexTextureCoordBuffer[blockIdx]);
					this.gl.vertexAttribPointer(this.shaderProgram.textureCoordAttribute,
						this.squareVertexTextureCoordBuffer[blockIdx].itemSize, this.gl.FLOAT, false, 0, 0);

					this.gl.activeTexture(this.gl.TEXTURE0);
					this.gl.bindTexture(this.gl.TEXTURE_2D, this.yTexture[blockIdx]);
					this.gl.uniform1i(this.shaderProgram.samplerUniform1, 0);

					this.gl.activeTexture(this.gl.TEXTURE1);
					this.gl.bindTexture(this.gl.TEXTURE_2D, this.uTexture[blockIdx]);
					this.gl.uniform1i(this.shaderProgram.samplerUniform2, 1);

					this.gl.activeTexture(this.gl.TEXTURE2);
					this.gl.bindTexture(this.gl.TEXTURE_2D, this.vTexture[blockIdx]);
					this.gl.uniform1i(this.shaderProgram.samplerUniform3, 2);

					this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.squareVertexIndexBuffer[blockIdx]);
					this.gl.drawElements(this.gl.TRIANGLES, this.squareVertexIndexBuffer[blockIdx].numItems,
						this.gl.UNSIGNED_SHORT, 0);
				}
			}
		}
	},

	/**
	* Resize the canvas in local (client) coordinates, never bigger than the local screen
	*
	* @method resizeCanvas
	*/
	resizeCanvas: function() {
		if (this.shaderProgram === undefined || this.shaderProgram === null) {
			return;
		}

		var checkWidth  = this.config.resolution.width;
		var checkHeight = this.config.resolution.height;
		// Overview client covers all
		if (clientID === -1) {
			// set the resolution to be the whole display wall
			checkWidth  *= this.config.layout.columns;
			checkHeight *= this.config.layout.rows;
		} else {
			checkWidth  *= (ui.json_cfg.displays[clientID].width || 1);
			checkHeight *= (ui.json_cfg.displays[clientID].height || 1);
		}

		var localX = this.sage2_x - ui.offsetX;
		var localY = this.sage2_y - ui.offsetY;
		var localRight  = localX + this.sage2_width;
		var localBottom = localY + this.sage2_height;
		var viewX = Math.max(localX, 0);
		var viewY = Math.max(localY, 0);
		var viewRight   = Math.min(localRight,  checkWidth);
		var viewBottom  = Math.min(localBottom, checkHeight);
		var localWidth  = viewRight  - viewX;
		var localHeight = viewBottom - viewY;

		// completely off-screen
		if (localWidth <= 0 || localHeight <= 0) {
			this.canvas.width  = 1;
			this.canvas.height = 1;
			this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
		} else {
			var parentTransform = getTransform(this.div.parentNode);
			this.canvas.width  = localWidth / parentTransform.scale.x;
			this.canvas.height = localHeight / parentTransform.scale.y;
			this.canvas.style.left = ((viewX - localX) / parentTransform.scale.x) + "px";
			this.canvas.style.top  = ((viewY - localY) / parentTransform.scale.y) + "px";

			var left   = ((viewX     - localX) / (localRight - localX) * 2.0) - 1.0;
			var right  = ((viewRight - localX) / (localRight - localX) * 2.0) - 1.0;
			var top    = ((1.0 - (viewY     - localY) / (localBottom - localY)) * 2.0) - 1.0;
			var bottom = ((1.0 - (viewBottom - localY) / (localBottom - localY)) * 2.0) - 1.0;

			mat4.ortho(left, right, bottom, top, -1, 1, this.pMatrix);
			this.gl.uniformMatrix4fv(this.shaderProgram.pMatrixUniform, false, this.pMatrix);
			this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
		}
	},

	/**
	* When a move starts, hide the canvas
	*
	* @method startMove
	* @param date {Date} current time from the server
	*/
	startMove: function(date) {
		this.canvas.style.display = "none";
	},

	/**
	* After move, show the canvas and update the coordinate system (resizeCanvas)
	*
	* @method move
	* @param date {Date} current time from the server
	*/
	move: function(date) {
		this.canvas.style.display = "block";
		this.resizeCanvas();
		this.refresh(date);
	},

	/**
	* When a resize starts, hide the canvas
	*
	* @method startResize
	* @param date {Date} current time from the server
	*/
	startResize: function(date) {
		this.canvas.style.display = "none";
	},

	/**
	* After resize, show the canvas and update the coordinate system (resizeCanvas)
	*
	* @method resize
	* @param date {Date} current time from the server
	*/
	resize: function(date) {
		this.canvas.style.display = "block";
		this.resizeCanvas();
		this.refresh(date);
	},

	/**
	* Handles event processing for the app
	*
	* @method event
	* @param eventType {String} the type of event
	* @param position {Object} contains the x and y positions of the event
	* @param user_id {Object} data about the user who triggered the event
	* @param data {Object} object containing extra data about the event,
	* @param date {Date} current time from the server
	*/
	event: function(type, position, user, data, date) {
	}

});