API Docs for: 2.0.0

public/src/sagePointer.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 sagePointerEnabled, sagePointerDisabled */

"use strict";

/**
 * @module client
 * @submodule sagePointer
 */

/**
 * Class repsenting a pointer, used in the sagePointerApp.html page
 *
 * @class sagePointer
 * @constructor
 */
function sagePointer(wsio) {
	this.wsio = wsio;

	this.uniqueID    = null;
	this.sensitivity = null;
	this.mediaStream = null;

	this.fileDrop         = document.getElementById('fileDrop');
	this.fileDropText     = document.getElementById('fileDropText');
	this.fileDropProgress = document.getElementById('fileDropProgress');
	this.sagePointerBtn   = document.getElementById('sagePointerBtn');
	this.screenShareBtn   = document.getElementById('screenShareBtn');

	this.sagePointerLabel      = document.getElementById('sagePointerLabel');
	this.sagePointerColor      = document.getElementById('sagePointerColor');
	this.screenShareResolution = document.getElementById('screenShareResolution');
	this.screenShareQuality    = document.getElementById('screenShareQuality');
	this.windowManager         = document.getElementById('winMgr');
	this.screenShareQualityIndicator = document.getElementById('screenShareQualityIndicator');

	this.mediaVideo      = document.getElementById('mediaVideo');
	this.mediaCanvas     = document.getElementById('mediaCanvas');
	this.mediaCtx        = this.mediaCanvas.getContext('2d');
	this.mediaResolution = this.screenShareResolution.selectedIndex;
	this.mediaQuality    = this.screenShareQuality.value;
	this.broadcasting    = false;
	this.desktopId       = null;

	this.desktopCaptureEnabled = false;

	this.chunk = 32 * 1024; // 32 KB
	this.maxUploadSize = 20 * (1024 * 1024 * 1024); // 20GB just as a precaution

	if (localStorage.SAGE2_ptrName  === undefined ||
		localStorage.SAGE2_ptrName  === null ||
		localStorage.SAGE2_ptrName  === "Default") {
		localStorage.SAGE2_ptrName  = "SAGE2_user";
	}
	if (localStorage.SAGE2_ptrColor === undefined ||
		localStorage.SAGE2_ptrColor === null) {
		localStorage.SAGE2_ptrColor = "#B4B4B4";
	}

	this.sagePointerLabel.value = localStorage.SAGE2_ptrName;
	this.sagePointerColor.value = localStorage.SAGE2_ptrColor;

	var _this = this;

	// Capture the changes in the pointer name
	this.sagePointerLabel.addEventListener('input', function() {
		localStorage.SAGE2_ptrName = _this.sagePointerLabel.value;
	});

	// Capture the changes in the pointer color
	this.sagePointerColor.addEventListener('change', function() {
		localStorage.SAGE2_ptrColor = _this.sagePointerColor.value;
	});


	/**
	*
	* @method setPointerId
	*/
	this.setPointerId = function(id) {
		this.uniqueID = id;
	};

	/**
	*
	* @method setPointerSensitivity
	*/
	this.setPointerSensitivity = function(value) {
		this.sensitivity = value;
	};

	/**
	*
	* @method preventDefaultMethod
	*/
	this.preventDefaultMethod = function(event) {
		event.preventDefault();
	};

	/**
	*
	* @method startSagePointerMethod
	*/
	this.startSagePointerMethod = function() {
		this.sagePointerBtn.requestPointerLock = this.sagePointerBtn.requestPointerLock      ||
												this.sagePointerBtn.mozRequestPointerLock    ||
												this.sagePointerBtn.webkitRequestPointerLock;
		// Ask the browser to lock the pointer
		this.sagePointerBtn.requestPointerLock();
	};

	/**
	*
	* @method pointerLockChangeMethod
	*/
	this.pointerLockChangeMethod = function() {
		if (document.pointerLockElement === this.sagePointerBtn ||
				document.mozPointerLockElement === this.sagePointerBtn ||
				document.webkitPointerLockElement === this.sagePointerBtn) {
			this.wsio.emit('startSagePointer', {label: localStorage.SAGE2_ptrName, color: localStorage.SAGE2_ptrColor});

			document.addEventListener('mousedown',           this.pointerPress,     false);
			document.addEventListener('mousemove',           this.pointerMove,      false);
			document.addEventListener('mouseup',             this.pointerRelease,   false);
			document.addEventListener('dblclick',            this.pointerDblClick,  false);
			document.addEventListener('mousewheel',          this.pointerScroll,    false);
			document.addEventListener('DOMMouseScroll',      this.pointerScrollFF,  false);
			document.addEventListener('keydown',             this.pointerKeyDown,   false);
			document.addEventListener('keyup',               this.pointerKeyUp,     false);
			document.addEventListener('keypress',            this.pointerKeyPress,  false);

			this.sagePointerBtn.removeEventListener('click', this.startSagePointer, false);

			sagePointerEnabled();
		} else {
			this.wsio.emit('stopSagePointer');

			document.removeEventListener('mousedown',        this.pointerPress,     false);
			document.removeEventListener('mousemove',        this.pointerMove,      false);
			document.removeEventListener('mouseup',          this.pointerRelease,   false);
			document.removeEventListener('dblclick',         this.pointerDblClick,  false);
			document.removeEventListener('mousewheel',       this.pointerScroll,    false);
			document.removeEventListener('DOMMouseScroll',   this.pointerScrollFF,  false);
			document.removeEventListener('keydown',          this.pointerKeyDown,   false);
			document.removeEventListener('keyup',            this.pointerKeyUp,     false);
			document.removeEventListener('keypress',         this.pointerKeyPress,  false);

			this.sagePointerBtn.addEventListener('click',    this.startSagePointer, false);

			sagePointerDisabled();
		}
	};

	/**
	*
	* @method pointerPressMethod
	*/
	this.pointerPressMethod = function(event) {
		var btn = (event.button === 0) ? "left" : (event.button === 1) ? "middle" : "right";
		this.wsio.emit('pointerPress', {button: btn});
		event.preventDefault();
	};

	/**
	*
	* @method pointerMoveMethod
	*/
	this.pointerMoveMethod = function(event) {
		var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
		var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
		this.wsio.emit('ptm', {dx: Math.round(movementX * this.sensitivity), dy: Math.round(movementY * this.sensitivity)});
		event.preventDefault();
	};

	/**
	*
	* @method pointerReleaseMethod
	*/
	this.pointerReleaseMethod = function(event) {
		var btn = (event.button === 0) ? "left" : (event.button === 1) ? "middle" : "right";
		this.wsio.emit('pointerRelease', {button: btn});
		event.preventDefault();
	};

	/**
	*
	* @method pointerDblClickMethod
	*/
	this.pointerDblClickMethod = function(event) {
		this.wsio.emit('pointerDblClick');
		event.preventDefault();
	};

	/**
	*
	* @method pointerScrollMethod
	*/
	this.pointerScrollMethod = function(event) {
		this.wsio.emit('pointerScrollStart');
		this.wsio.emit('pointerScroll', {wheelDelta: event.wheelDelta});
		event.preventDefault();
	};

	/**
	*
	* @method pointerScrollFFMethod
	*/
	this.pointerScrollFFMethod = function(event) {
		var wheelDelta = -120 * event.detail;
		this.wsio.emit('pointerScrollStart');
		this.wsio.emit('pointerScroll', {wheelDelta: wheelDelta});
		event.preventDefault();
	};

	/**
	*
	* @method pointerKeyDownMethod
	*/
	this.pointerKeyDownMethod = function(event) {
		var code = parseInt(event.keyCode);
		this.wsio.emit('keyDown', {code: code});
		// tab is a special case - no keypress event called (do we need to change code?)
		if (code  === 9) {
			this.wsio.emit('keyPress', {code: code, character: String.fromCharCode(code)});
		}
		// if a special key - prevent default (otherwise let continue to keyPress)
		if (code  === 8 || code  === 9 || (code >= 16 && code <= 46 && code !== 32) ||
				(code >= 91 && code <= 93) || (code >= 112 && code <= 145)) {
			event.preventDefault();
		}
	};

	/**
	*
	* @method pointerKeyUpMethod
	*/
	this.pointerKeyUpMethod = function(event) {
		var code = parseInt(event.keyCode);
		this.wsio.emit('keyUp', {code: code});
		event.preventDefault();
	};

	/**
	*
	* @method pointerKeyPressMethod
	*/
	this.pointerKeyPressMethod = function(event) {
		var code = parseInt(event.charCode);
		this.wsio.emit('keyPress', {code: code, character: String.fromCharCode(code)});
		event.preventDefault();
	};

	/**
	*
	* @method startScreenShareMethod
	*/
	this.startScreenShareMethod = function() {
		if (this.desktopCaptureEnabled === false) {
			alert("Cannot share screen: \"SAGE2 Screen Capture\" Extension not enabled.");
			return;
		}
		// start screen share
		window.postMessage('capture_desktop', '*');
	};

	/**
	*
	* @method captureDesktop
	*/
	this.captureDesktop = function(mediaSourceId) {
		var constraints = {chromeMediaSource: 'desktop', chromeMediaSourceId: mediaSourceId, maxWidth: 3840, maxHeight: 2160};
		navigator.getUserMedia({
			video: {mandatory: constraints, optional: []},
			audio: false
		}, this.streamSuccess, this.streamFail);
	};

	/**
	*
	* @method streamSuccessMethod
	*/
	this.streamSuccessMethod = function(stream) {
		console.log("media capture success!");

		this.screenShareBtn.disabled = true;
		this.mediaStream = stream;
		this.mediaStream.onended = this.streamEnded;
		this.mediaVideo.src = window.URL.createObjectURL(this.mediaStream);
		this.mediaVideo.play();
	};

	/**
	*
	* @method streamFailMethod
	*/
	this.streamFailMethod = function() {
		console.log("no access to media capture");
	};

	/**
	*
	* @method streamEndedMethod
	*/
	this.streamEndedMethod = function() {
		console.log("media stream ended");
		this.broadcasting = false;
		this.screenShareBtn.disabled = false;
		this.wsio.emit('stopMediaStream', {id: this.uniqueID + "|0"});
	};

	/**
	*
	* @method streamMetaDataMethod
	*/
	this.streamMetaDataMethod = function() {
		var widths = [
			Math.min(852, this.mediaVideo.videoWidth),
			Math.min(1280, this.mediaVideo.videoWidth),
			Math.min(1920, this.mediaVideo.videoWidth),
			this.mediaVideo.videoWidth
		];

		for (var i = 0; i < 4; i++) {
			var height = parseInt(widths[i] * this.mediaVideo.videoHeight / this.mediaVideo.videoWidth, 10);
			this.screenShareResolution.options[i].value = widths[i] + "x" + height;
		}

		var res = this.screenShareResolution.options[this.mediaResolution].value.split("x");
		this.mediaWidth  = parseInt(res[0], 10);
		this.mediaHeight = parseInt(res[1], 10);
		this.mediaCanvas.width  = this.mediaWidth;
		this.mediaCanvas.height = this.mediaHeight;

		var frame = this.captureMediaFrame();
		var raw = this.base64ToString(frame.split(",")[1]);
		this.wsio.emit('startNewMediaStream', {
			id: this.uniqueID + "|0",
			title: localStorage.SAGE2_ptrName + ": Shared Screen",
			src: raw, type: "image/jpeg", encoding: "binary",
			width: this.mediaVideo.videoWidth,
			height: this.mediaVideo.videoHeight
		});

		this.broadcasting = true;
	};

	/**
	*
	* @method captureMediaFrame
	*/
	this.captureMediaFrame = function() {
		this.mediaCtx.clearRect(0, 0, this.mediaWidth, this.mediaHeight);
		this.mediaCtx.drawImage(this.mediaVideo, 0, 0, this.mediaWidth, this.mediaHeight);
		return this.mediaCanvas.toDataURL("image/jpeg", (this.mediaQuality / 10));
	};

	/**
	*
	* @method sendMediaStreamFrame
	*/
	this.sendMediaStreamFrame = function() {
		if (this.broadcasting) {
			var frame = this.captureMediaFrame();
			var raw = this.base64ToString(frame.split(",")[1]);

			if (raw.length > this.chunk) {
				var nchunks = Math.ceil(raw.length / this.chunk);

				/*eslint-disable */
				function updateMediaStreamChunk(index, msg_chunk) { // jshint ignore:line
					setTimeout(function() {
						_this.wsio.emit('updateMediaStreamChunk', {id: _this.uniqueID + "|0",
							state: {src: msg_chunk, type: "image/jpeg", encoding: "binary"},
							piece: index, total: nchunks});
					}, 4);
				}
				/*eslint-enable */
				for (var i = 0; i < nchunks; i++) {
					var start = i * this.chunk;
					var end = (i + 1) * this.chunk < raw.length ? (i + 1) * this.chunk : raw.length;
					updateMediaStreamChunk(i, raw.substring(start, end));
				}
			} else {
				this.wsio.emit('updateMediaStreamFrame', {id: this.uniqueID + "|0", state: {src: raw,
					type: "image/jpeg", encoding: "binary"}});
			}
		}
	};

	/**
	*
	* @method changeScreenShareResolutionMethod
	*/
	this.changeScreenShareResolutionMethod = function() {
		this.mediaResolution = this.screenShareResolution.selectedIndex;
		if (this.screenShareResolution.options[this.mediaResolution].value) {
			var res = this.screenShareResolution.options[this.mediaResolution].value.split("x");
			this.mediaHeight = parseInt(res[0], 10);
			this.mediaWidth  = parseInt(res[1], 10);
			this.mediaCanvas.width  = this.mediaWidth;
			this.mediaCanvas.height = this.mediaHeight;
			console.log("media resolution: " + this.screenShareResolution.options[this.mediaResolution].value);
		}
	};

	/**
	*
	* @method changeScreenShareQualityMethod
	*/
	this.changeScreenShareQualityMethod = function() {
		this.mediaQuality = this.screenShareQuality.value;
		this.screenShareQualityIndicator.textContent = this.mediaQuality;
	};

	/**
	*
	* @method uploadFileToServerMethod
	*/
	this.uploadFileToServerMethod = function(event) {
		event.preventDefault();

		var dropX = 0.0;
		var dropY = 0.0;

		// Check if we are are a sageUI or a sagePointer
		if (event.target.id === "fileDropText") {
			// we are in sagePointer
			dropX = 0.0;
			dropY = 0.0;
		} else if (event.target.id === "winMgr") {
			// we are in sageUI
			dropX = event.offsetX / event.target.clientWidth;
			dropY = event.offsetY / event.target.clientHeight;
		}

		var files = event.dataTransfer.files;
		var url   = event.dataTransfer.getData("Url");
		var text  = event.dataTransfer.getData("Text");

		var total  = {};
		var loaded = {};
		var pc     = 0;

		function onProgress(evt) {
			if (!(evt.srcElement.id in total)) {
				total[evt.srcElement.id] = evt.total;
			}
			loaded[evt.srcElement.id] = evt.loaded;

			var totalSize = 0;
			var uploaded = 0;
			for (var key in total) {
				totalSize += total[key]; uploaded += loaded[key];
			}
			pc = Math.floor((uploaded / totalSize) * 100);
			_this.fileDropText.textContent = "File upload... " + pc.toString() + "%";
			// sagePointerApp has no progress bar
			if (_this.fileDropProgress !== null) {
				_this.fileDropProgress.value = pc;
			}
			if (pc === 100) {
				setTimeout(function() {
					if (pc === 100) {
						_this.fileDropText.textContent = "Drop multimedia files here";
						// sagePointerApp has no progress bar
						if (_this.fileDropProgress !== null) {
							_this.fileDropProgress.value = 0;
						}
					}
				}, 500);
			}
		}

		if (files.length > 0) {
			for (var i = 0; i < files.length; i++) {
				if (files[i].size <= this.maxUploadSize) {
					var formdata = new FormData();
					formdata.append("file" + i.toString(), files[i]);
					formdata.append("dropX", dropX);
					formdata.append("dropY", dropY);

					var xhr = new XMLHttpRequest();
					xhr.open("POST", "upload", true);
					xhr.upload.id = "file" + i.toString();
					xhr.upload.addEventListener('progress', onProgress);
					xhr.send(formdata);
				} else {
					alert("File: " + files[i].name + " is too large (max size is " +
						(this.maxUploadSize / (1024 * 1024 * 1024)) + " GB)");
				}
			}
		} else if (url !== null || text !== null) {
			var dataUrl;
			if (url === null) {
				dataUrl = text;
			} else if (text === null) {
				dataUrl = url;
			} else {
				dataUrl = (url.length > text.length) ? url : text;
			}
			var mimeType = "";
			var youtube  = dataUrl.indexOf("www.youtube.com");
			var ext      = dataUrl.substring(dataUrl.lastIndexOf('.') + 1);
			if (ext.length > 4) {
				ext = ext.substring(0, 4);
			}
			ext = ext.toLowerCase();
			if (youtube >= 0) {
				mimeType = "video/youtube";
			} else if (ext === "jpg" || ext === "jpeg") {
				mimeType = "image/jpeg";
			} else if (ext === "png") {
				mimeType  = "image/png";
			} else if (ext === "mp4") {
				mimeType  = "video/mp4";
			} else if (ext === "m4v") {
				mimeType  = "video/mp4";
			} else if (ext === "webm") {
				mimeType = "video/webm";
			} else if (ext === "pdf") {
				mimeType  = "application/pdf";
			}
			if (mimeType !== "") {
				this.wsio.emit('addNewWebElement', {type: mimeType, url: dataUrl, position: [dropX, dropY]});
			}
		}
	};


	/**
	*
	* @method base64ToString
	*/
	this.base64ToString = function(base64) {
		return atob(base64);
	};

	// convert class methods to functions (to be used as callbacks)
	this.preventDefault              = this.preventDefaultMethod.bind(this);
	this.uploadFileToServer          = this.uploadFileToServerMethod.bind(this);
	this.startSagePointer            = this.startSagePointerMethod.bind(this);
	this.startScreenShare            = this.startScreenShareMethod.bind(this);
	this.changeScreenShareResolution = this.changeScreenShareResolutionMethod.bind(this);
	this.changeScreenShareQuality    = this.changeScreenShareQualityMethod.bind(this);
	this.pointerLockChange           = this.pointerLockChangeMethod.bind(this);
	this.pointerPress                = this.pointerPressMethod.bind(this);
	this.pointerMove                 = this.pointerMoveMethod.bind(this);
	this.pointerRelease              = this.pointerReleaseMethod.bind(this);
	this.pointerDblClick             = this.pointerDblClickMethod.bind(this);
	this.pointerScroll               = this.pointerScrollMethod.bind(this);
	this.pointerScrollFF             = this.pointerScrollFFMethod.bind(this);
	this.pointerKeyDown              = this.pointerKeyDownMethod.bind(this);
	this.pointerKeyUp                = this.pointerKeyUpMethod.bind(this);
	this.pointerKeyPress             = this.pointerKeyPressMethod.bind(this);
	this.streamSuccess               = this.streamSuccessMethod.bind(this);
	this.streamFail                  = this.streamFailMethod.bind(this);
	this.streamEnded                 = this.streamEndedMethod.bind(this);
	this.streamMetaData              = this.streamMetaDataMethod.bind(this);

	// Event Listeners
	window.addEventListener('dragover', this.preventDefault, false);
	window.addEventListener('dragend',  this.preventDefault, false);
	window.addEventListener('drop',     this.preventDefault, false);

	this.fileDrop.addEventListener('dragover', this.preventDefault,     false);
	this.fileDrop.addEventListener('dragend',  this.preventDefault,     false);
	this.fileDrop.addEventListener('drop',     this.uploadFileToServer, false);

	if (this.windowManager) {
		// Not all sagePointer have a windowManager
		this.windowManager.addEventListener('dragover', this.preventDefault,     false);
		this.windowManager.addEventListener('dragend',  this.preventDefault,     false);
		this.windowManager.addEventListener('drop',     this.uploadFileToServer, false);
	}

	this.sagePointerBtn.addEventListener('click', this.startSagePointer, false);
	this.screenShareBtn.addEventListener('click', this.startScreenShare, false);

	this.screenShareResolution.addEventListener('change', this.changeScreenShareResolution, false);
	this.screenShareQuality.addEventListener('change',    this.changeScreenShareQuality,    false);

	document.addEventListener('pointerlockchange',       this.pointerLockChange, false);
	// redudant here (webkit)
	// document.addEventListener('mozpointerlockchange',    this.pointerLockChange, false);
	// document.addEventListener('webkitpointerlockchange', this.pointerLockChange, false);

	this.mediaVideo.addEventListener('loadedmetadata', this.streamMetaData, false);

}