public/src/SAGE2_interaction.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 showSAGE2Message, showDialog */
/* global cancelIdleCallback, requestIdleCallback */
/* global showSAGE2PointerOverlayNoMouse, hideSAGE2PointerOverlayNoMouse */
/* global pointerClick, sagePointerDisabled, sagePointerEnabled */
/* global viewOnlyMode, deleteCookie */
"use strict";
/**
* User interaction for SAGE2
*
* @module client
* @submodule SAGE2_interaction
*/
const SAGE2_interaction = (function() {
var _loggedIn = false;
var _uid = null;
var _userSettings = {};
/**
* Callback any time a login event is attempted
*
* @method handleLoginStateChange
* @param response {Object} data returned by the server
*/
function handleLoginStateChange(response) {
// if user tried to log in: check if successful
if (response.login) {
let user = response.user;
if (response.success) {
_uid = response.uid;
_loggedIn = true;
if ($$("login_form_container")) {
$$("login_form_container").hide();
$$("settings_dialog").show();
}
// store auth credentials in cookies
_userSettings.SAGE2_userName = user.name;
_userSettings.SAGE2_userEmail = user.email;
addCookie('SAGE2_userName', user.name);
addCookie('SAGE2_userEmail', user.email);
}
if (user.SAGE2_ptrName !== _userSettings.SAGE2_ptrName) {
this.changeSage2PointerLabel(user.SAGE2_ptrName);
}
if (user.SAGE2_ptrColor !== _userSettings.SAGE2_ptrColor) {
this.changeSage2PointerColor(user.SAGE2_ptrColor);
}
}
// if user tried to logout
if (response.login === false) {
_uid = null;
_loggedIn = false;
// clear credentials
delete _userSettings.SAGE2_userName;
delete _userSettings.SAGE2_userEmail;
deleteCookie('SAGE2_userName');
deleteCookie('SAGE2_userEmail');
this.changeSage2PointerLabel(response.name);
}
// if an error message is shown
if (response.errorMessage && $$("login_error")) {
$$("login_error").setValue(response.errorMessage);
$$("login_error").show();
}
// on initialization
if (response.init) {
if (!_loggedIn && _userSettings.SAGE2_ptrName && _userSettings.SAGE2_ptrName.startsWith('Anon ')) {
this.settingsDialog('init');
}
}
this.updateLoginButton();
}
/**
* Handles server notification that action was permitted
*
* @method allowAction
*/
function allowAction(action) {
switch (action) {
case 'stream':
this.startScreenShare();
break;
}
}
/**
* Handles server notification that action was canceled due to
* permissions
*
* @method cancelAction
*/
function cancelAction(action) {
let actionFound;
switch (action) {
case 'pointer':
this.stopSAGE2Pointer();
actionFound = true;
break;
case 'stream':
case 'application':
case 'file':
actionFound = true;
break;
}
if (actionFound) {
webix.alert("You don't have permission to do that.");
}
}
/**
* @method randomHexColor
* @return {String} color as a hex string
*/
function randomHexColor() {
let hex = (Math.floor(Math.random() * 0xffffff)).toString(16);
if (hex.length < 6) {
hex = Array(6 - hex.length + 1).join('0') + hex;
}
return '#' + hex;
}
/**
* Deals with pointer, file upload, desktop sharing, ...
*
* @class SAGE2_interaction
* @constructor
*/
function SAGE2_interaction(wsio) {
this.wsio = wsio;
this.uniqueID = null;
this.sensitivity = null;
this.fileUploadStart = null;
this.fileUploadProgress = null;
this.fileUploadComplete = null;
this.mediaStream = null;
this.mediaVideo = null;
this.mediaResolution = 2;
this.mediaQuality = 9;
this.chromeDesktopCaptureEnabled = false;
this.broadcasting = false;
this.gotRequest = false;
this.pix = null;
this.chunk = 32 * 1024; // 32 KB
this.maxUploadSize = 20 * (1024 * 1024 * 1024); // 20GB just as a precaution
this.array_xhr = [];
// Event filtering for mouseMove
this.now = Date.now();
this.cnt = 0;
// accumultor for delta motion of the mouse
this.deltaX = 0;
this.deltaY = 0;
// Send frequency (frames per second)
this.sendFrequency = 30;
// Timeout for when scrolling ends
this.scrollTimeId = null;
/**
* Set a unique ID
*
* @method setInteractionId
* @param id {String} client id
*/
this.setInteractionId = function(id) {
this.uniqueID = id;
};
/**
* Set the pointer scaling factor
*
* @method setPointerSensitivity
* @param value {Number} scaling factor for pointer motion
*/
this.setPointerSensitivity = function(value) {
this.sensitivity = value;
};
/**
* Set the start callback
*
* @method setFileUploadStartCallback
* @param callback {Function} upload start callback
*/
this.setFileUploadStartCallback = function(callback) {
this.fileUploadStart = callback;
};
/**
* Set the progress callback
*
* @method setFileUploadProgressCallback
* @param callback {Function} upload progress callback
*/
this.setFileUploadProgressCallback = function(callback) {
this.fileUploadProgress = callback;
};
/**
* Set the complete callback
*
* @method setFileUploadCompleteCallback
* @param callback {Function} upload complete callback
*/
this.setFileUploadCompleteCallback = function(callback) {
this.fileUploadComplete = callback;
};
/**
* Cancel the file uploads by aborting the XMLHttpRequests
*
* @method cancelUploads
*/
this.cancelUploads = function() {
if (this.array_xhr.length > 0) {
for (var i = 0; i < this.array_xhr.length; i++) {
this.array_xhr[i].abort();
}
this.array_xhr.length = 0;
}
};
/**
* Form processing function
*
* @method uploadFiles
* @param files {Object} array of files dropped
* @param dropX {Number} drop location X
* @param dropY {Number} drop location Y
*/
this.uploadFiles = function(files, dropX, dropY) {
var _this = this;
var loaded = {};
var filesFinished = 0;
var total = 0;
var progressCallback = function(event) {
if (loaded[event.target.id] === undefined) {
total += event.total;
}
loaded[event.target.id] = event.loaded;
var uploaded = 0;
for (var key in loaded) {
uploaded += loaded[key];
}
var pc = uploaded / total;
if (_this.fileUploadProgress) {
_this.fileUploadProgress(pc);
}
};
var loadCallback = function(event) {
var sn = event.target.response.substring(event.target.response.indexOf("name: ") + 7);
var st = event.target.response.substring(event.target.response.indexOf("type: ") + 7);
var name = sn.substring(0, sn.indexOf("\n") - 2);
var type = st.substring(0, st.indexOf("\n") - 2);
// Parse the reply into JSON
var msgFromServer = JSON.parse(event.target.response);
// Check the return values for success/error
Object.keys(msgFromServer.files).map(function(k) {
name = msgFromServer.files[k].name;
type = msgFromServer.files[k].type;
if (!msgFromServer.fields.good) {
showSAGE2Message('Unrecognized file type: ' + name + ' ' + type);
}
});
filesFinished++;
if (_this.fileUploadComplete && filesFinished === files.length) {
_this.fileUploadComplete();
}
_this.wsio.emit('uploadedFile', {name: name, type: type});
};
if (this.fileUploadStart) {
this.fileUploadStart(files);
}
// Clear the upload array
this.array_xhr.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);
formdata.append("open", true);
formdata.append("SAGE2_ptrName", _userSettings.SAGE2_ptrName);
formdata.append("SAGE2_ptrColor", _userSettings.SAGE2_ptrColor);
var xhr = new XMLHttpRequest();
// add the request into the array
this.array_xhr.push(xhr);
xhr.open("POST", "upload", true);
xhr.upload.id = "file" + i.toString();
xhr.upload.addEventListener('progress', progressCallback, false);
xhr.addEventListener('load', loadCallback, false);
xhr.send(formdata);
} else {
// show message for 4 seconds
showSAGE2Message("File: " + files[i].name + " is too large (max size is " +
(this.maxUploadSize / (1024 * 1024 * 1024)) + " GB)");
}
}
};
/**
* Process a URL drag and drop
*
* @method uploadURL
* @param url {String} URL dropped on the UI
* @param dropX {Number} drop location X
* @param dropY {Number} drop location Y
*/
this.uploadURL = function(url, dropX, dropY) {
var mimeType = "";
var youtube = url.indexOf("www.youtube.com");
var ext = url.substring(url.lastIndexOf('.') + 1);
if (ext.length > 4) {
ext = ext.substring(0, 4);
}
if (ext.length === 4 && (ext[3] === '?' || ext[3] === '#')) {
ext = ext.substring(0, 3);
}
ext = ext.toLowerCase();
if (youtube >= 0) {
// mimeType = "video/youtube";
mimeType = "application/url";
} else if (ext === "jpg") {
mimeType = "image/jpeg";
} else if (ext === "jpeg") {
mimeType = "image/jpeg";
} else if (ext === "png") {
mimeType = "image/png";
} else if (ext === "bmp") {
mimeType = "image/bmp";
} 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";
} else {
// madeup mimetype for drag-drop URL
mimeType = "application/url";
}
console.log("URL: " + url + ", type: " + mimeType);
if (mimeType !== "") {
this.wsio.emit('addNewWebElement', {
type: mimeType,
url: url,
position: [dropX, dropY],
id: this.uniqueID,
SAGE2_ptrName: localStorage.SAGE2_ptrName,
SAGE2_ptrColor: localStorage.SAGE2_ptrColor
});
}
};
/**
* Request a pointer lock or assume that's a touch device
*
* @method startSAGE2Pointer
* @param buttonId {String} name of the button triggering the pointer
*/
this.startSAGE2Pointer = function(buttonId) {
if (hasMouse) {
var button = document.getElementById(buttonId);
button.addEventListener('pointerlockchange', function(e) {
console.log('Pointerlockchange>', e);
});
button.requestPointerLock = button.requestPointerLock ||
button.mozRequestPointerLock ||
button.webkitRequestPointerLock;
// Ask the browser to lock the pointer
if (button.requestPointerLock) {
button.requestPointerLock();
} else {
showSAGE2Message("No PointerLock support in this browser.<br> Google Chrome is preferred.");
}
} else {
console.log("No mouse detected - entering touch interface for SAGE2 Pointer");
this.wsio.emit('startSagePointer', this.user);
showSAGE2PointerOverlayNoMouse();
}
};
/**
* Release the pointer
*
* @method stopSAGE2Pointer
*/
this.stopSAGE2Pointer = function() {
if (hasMouse) {
if (document.exitPointerLock) {
document.exitPointerLock();
} else {
console.log("No PointerLock support");
}
} else {
this.wsio.emit('stopSagePointer', this.user);
hideSAGE2PointerOverlayNoMouse();
}
};
/**
* Called if pointer lock failed
*
* @method pointerLockErrorMethod
* @param event {Event} error event
*/
this.pointerLockErrorMethod = function(event) {
console.log("Error locking pointer: ", event);
};
/**
* Called when a pointer lock change is triggered, release or aquire
*
* @method pointerLockChangeMethod
* @param event {Event} event
*/
this.pointerLockChangeMethod = function(event) {
var pointerLockElement = document.pointerLockElement ||
document.mozPointerLockElement ||
document.webkitPointerLockElement;
// disable SAGE2 Pointer
if (pointerLockElement === undefined || pointerLockElement === null) {
this.wsio.emit('stopSagePointer', this.user);
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('wheel', this.pointerScroll, false);
document.removeEventListener('keydown', this.pointerKeyDown, false);
document.removeEventListener('keyup', this.pointerKeyUp, false);
document.removeEventListener('keypress', this.pointerKeyPress, false);
document.addEventListener('click', pointerClick, false);
sagePointerDisabled();
} else {
// enable SAGE2 Pointer
this.wsio.emit('startSagePointer', this.user);
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('wheel', this.pointerScroll, false);
document.addEventListener('keydown', this.pointerKeyDown, false);
document.addEventListener('keyup', this.pointerKeyUp, false);
document.addEventListener('keypress', this.pointerKeyPress, false);
document.removeEventListener('click', pointerClick, false);
sagePointerEnabled();
}
};
this.requestToStartScreenShare = function() {
if (!this.broadcasting) {
wsio.emit('requestToStartMediaStream');
}
};
/**
* Start screen sharing, for Chrome or Firefox
*
* @method startScreenShare
*/
this.startScreenShare = function() {
if (!this.broadcasting) {
if (__SAGE2__.browser.isChrome === true && this.chromeDesktopCaptureEnabled === true) {
// post message to start chrome screen share
window.postMessage('SAGE2_capture_desktop', '*');
} else if (__SAGE2__.browser.isChrome === true && this.chromeDesktopCaptureEnabled !== true) {
/* eslint-disable max-len */
webix.confirm({
title: "Screen sharing",
ok: "Ok",
cancel: "Cancel",
text: "Let's install the SAGE2 screen sharing extension for Chrome (or visit the help page).<br>" +
"Once done, please reload the SAGE UI page",
width: "60%",
position: "center",
callback: function(confirm) {
if (confirm) {
window.open("https://chrome.google.com/webstore/detail/sage2-screen-capture/mbkfcmpjbkmmdcfocaclghbobhnjfpkk",
"Good luck!");
} else {
window.open("help/index.html", "Good luck!");
}
webix.modalbox.hide(this);
}
});
/* eslint-enable max-len */
} else if (__SAGE2__.browser.isFirefox === true) {
// attempt to start firefox screen share
// can replace 'screen' with 'window' (but need user choice ahead of time)
showDialog('ffShareScreenDialog');
} else {
showSAGE2Message("Screen capture not supported in this browser.<br> Google Chrome is preferred.");
}
} else {
var _this = this;
// Create a modal window
webix.confirm({
title: "Screen sharing",
ok: "Confirm",
cancel: "Cancel",
text: "Already sharing content.<br> Press <strong style='font-weight:bold;'>Confirm</strong> " +
"to close the existing window and share another one.<br>" +
"Press <strong style='font-weight:bold;'>Cancel</strong> to continue sharing the existing window.",
width: "60%",
position: "center",
callback: function(confirm) {
if (confirm) {
_this.streamEnded();
_this.startScreenShare();
}
webix.modalbox.hide(this);
}
});
}
};
/**
* Initialize the screen share request, for Chrome or Firefox
*
* @method captureDesktop
* @param data {Object} data
*/
this.captureDesktop = function(data) {
if (__SAGE2__.browser.isChrome === true) {
var constraints = {
chromeMediaSource: 'desktop',
chromeMediaSourceId: data,
maxWidth: 1920, maxHeight: 1080,
maxFrameRate: 24,
minFrameRate: 3
};
navigator.getUserMedia({video: {mandatory: constraints, optional: []}, audio: false},
this.streamSuccess, this.streamFail);
} else if (__SAGE2__.browser.isFirefox === true) {
navigator.getUserMedia({video: {mediaSource: data}, audio: false},
this.streamSuccess, this.streamFail);
}
};
/**
* Screen sharing is a go
*
* @method streamSuccessMethod
* @param stream {Object} media stream
*/
this.streamSuccessMethod = function(stream) {
this.mediaStream = stream;
// deprecated:
// this.mediaStream.onended = this.streamEnded;
// Get list of tracks and set the handler on the track
var tracks = stream.getTracks();
if (tracks.length > 0) {
// Place the callback when the track is ended
tracks[0].onended = this.streamEnded;
}
var mediaVideo = document.getElementById('mediaVideo');
mediaVideo.src = window.URL.createObjectURL(this.mediaStream);
mediaVideo.play();
};
/**
* Screen sharing failed
*
* @method streamFailMethod
* @param event {Object} error event
*/
this.streamFailMethod = function(event) {
console.log("no access to media capture");
if (__SAGE2__.browser.isChrome === true) {
showSAGE2Message('Screen capture failed.<br> Make sure to install and enable the Chrome SAGE2 extension.<br>' +
'See Window/Extension menu');
} else if (__SAGE2__.browser.isFirefox === true) {
showSAGE2Message('Screen capture failed. To enable screen capture in Firefox:<br>1- Open "about:config"<br>' +
'2- Set "media.getusermedia.screensharing.enabled" to true<br>' +
'3- Add your domain (or localhost) in "media.getusermedia.screensharing.allowed_domains"');
} else {
showSAGE2Message("No screen capture support in this browser.<br> Google Chrome is preferred.");
}
};
/**
* Screen sharing has ended
*
* @method streamEndedMethod
* @param event {Object} event
*/
this.streamEndedMethod = function(event) {
this.broadcasting = false;
// cancelAnimationFrame(this.req);
cancelIdleCallback(this.req);
this.wsio.emit('stopMediaStream', Object.assign({id: this.uniqueID + "|0"}, _userSettings));
};
/**
* Using requestIdleCallback from Chrome for screen capture
*
* @method stepMethod
* @param deadline {Object} object containing timing information
*/
this.stepMethod = function(deadline) {
// if more than 10ms of freetime, go for it
if (deadline.timeRemaining() > 10) {
if (this.gotRequest) {
this.pix = this.captureMediaFrame();
this.sendMediaStreamFrame();
}
}
// and request again
this.req = requestIdleCallback(this.step);
};
/**
* The screen sharing can start
*
* @method streamCanPlayMethod
* @param event {Object} event
*/
this.streamCanPlayMethod = function(event) {
// Making sure it's not already sending
if (!this.broadcasting) {
var mediaVideo = document.getElementById('mediaVideo');
var mediaCanvas = document.getElementById('mediaCanvas');
if (mediaVideo.videoWidth === 0 || mediaVideo.videoHeight === 0) {
setTimeout(this.streamCanPlay, 500, event);
return;
}
var widths = [
Math.min(852, mediaVideo.videoWidth),
Math.min(1280, mediaVideo.videoWidth),
Math.min(1920, mediaVideo.videoWidth),
mediaVideo.videoWidth
];
var height = widths[this.mediaResolution] * mediaVideo.videoHeight / mediaVideo.videoWidth;
mediaCanvas.width = widths[this.mediaResolution];
mediaCanvas.height = height;
var frame = this.captureMediaFrame();
this.pix = frame;
var raw = atob(frame.split(",")[1]); // base64 to string
this.wsio.emit('startNewMediaStream', Object.assign({
id: this.uniqueID + "|0",
title: _userSettings.SAGE2_ptrName + ": Shared Screen",
color: _userSettings.SAGE2_ptrColor,
src: raw, type: "image/jpeg", encoding: "binary",
width: mediaVideo.videoWidth, height: mediaVideo.videoHeight
}, _userSettings));
this.broadcasting = true;
// Using requestAnimationFrame
// var _this = this;
// var lastCapture = performance.now();
// function step(timestamp) {
// console.log(' update', timestamp - lastCapture);
// var interval = timestamp - lastCapture;
// // if (_this.broadcasting && interval >= 16) {
// lastCapture = timestamp;
// if (_this.gotRequest) {
// console.log(' Capture', timestamp);
// _this.pix = _this.captureMediaFrame();
// _this.sendMediaStreamFrame();
// }
// _this.req = requestAnimationFrame(step);
// // }
// }
// this.req = requestAnimationFrame(step);
// Request an idle callback for screencapture
this.req = requestIdleCallback(this.step);
}
};
/**
* Received a new frame, capture it and return a JPEG buffer
*
* @method captureMediaFrame
*/
this.captureMediaFrame = function() {
var mediaVideo = document.getElementById('mediaVideo');
var mediaCanvas = document.getElementById('mediaCanvas');
var mediaCtx = mediaCanvas.getContext('2d');
// mediaCtx.clearRect(0, 0, mediaCanvas.width, mediaCanvas.height);
mediaCtx.drawImage(mediaVideo, 0, 0, mediaCanvas.width, mediaCanvas.height);
return mediaCanvas.toDataURL("image/jpeg", (this.mediaQuality / 10));
};
this.requestMediaStreamFrame = function(argument) {
if (this.broadcasting) {
this.gotRequest = true;
}
};
/**
* Send the captured frame to the server
*
* @method sendMediaStreamFrame
*/
this.sendMediaStreamFrame = function() {
if (this.broadcasting) {
// var frame = this.captureMediaFrame();
var frame = this.pix;
var raw = atob(frame.split(",")[1]); // base64 to string
if (raw.length > this.chunk) {
var _this = this;
var nchunks = Math.ceil(raw.length / this.chunk);
var updateMediaStreamChunk = function(index, msg_chunk) {
setTimeout(function() {
_this.wsio.emit('updateMediaStreamChunk', {id: _this.uniqueID + "|0",
state: {src: msg_chunk, type: "image/jpeg", encoding: "binary"},
piece: index, total: nchunks});
}, 4);
};
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));
}
this.gotRequest = false;
} else {
this.wsio.emit('updateMediaStreamFrame', {id: this.uniqueID + "|0", state:
{src: raw, type: "image/jpeg", encoding: "binary"}});
}
}
};
/**
* Handler for mouse press
*
* @method pointerPressMethod
* @param event {Event} press event
*/
this.pointerPressMethod = function(event) {
var btn = (event.button === 0) ? "left" : (event.button === 1) ? "middle" : "right";
this.wsio.emit('pointerPress', {button: btn});
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Handler for mouse move
*
* @method pointerMoveMethod
* @param event {Event} move event
*/
this.pointerMoveMethod = function(event) {
var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
// Event filtering
var now = Date.now();
// time difference since last event
var diff = now - this.now;
// count the events
this.cnt++;
if (diff >= (1000 / this.sendFrequency)) {
// Calculate the offset
// increase the speed for touch devices
var scale = (hasMouse ? this.sensitivity : 3 * this.sensitivity);
var px = this.deltaX * scale;
var py = this.deltaY * scale;
// Send the event
this.wsio.emit('pointerMove', {dx: Math.round(px), dy: Math.round(py)});
// Reset the accumulators
this.deltaX = 0;
this.deltaY = 0;
// Reset the time and count
this.now = now;
this.cnt = 0;
} else {
// if it's not time, just accumulate
this.deltaX += movementX;
this.deltaY += movementY;
}
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Handler for mouse release
*
* @method pointerReleaseMethod
* @param event {Event} release event
*/
this.pointerReleaseMethod = function(event) {
var btn = (event.button === 0) ? "left" : (event.button === 1) ? "middle" : "right";
this.wsio.emit('pointerRelease', {button: btn});
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Handler for double click
*
* @method pointerDblClickMethod
* @param event {Event} double click event
*/
this.pointerDblClickMethod = function(event) {
this.wsio.emit('pointerDblClick');
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Handler for mouse scroll
*
* @method pointerScrollMethod
* @param event {Object} scroll event
*/
this.pointerScrollMethod = function(event) {
if (this.scrollTimeId === null) {
this.wsio.emit('pointerScrollStart');
} else {
clearTimeout(this.scrollTimeId);
}
this.wsio.emit('pointerScroll', {wheelDelta: event.deltaY});
var _this = this;
this.scrollTimeId = setTimeout(function() {
_this.wsio.emit('pointerScrollEnd');
_this.scrollTimeId = null;
}, 500);
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Handler for key down
*
* @method pointerKeyDownMethod
* @param event {Object} key event
*/
this.pointerKeyDownMethod = function(event) {
// Get the code of the event
var code = parseInt(event.keyCode, 10);
// exit if 'esc' key
if (code === 27) {
this.stopSAGE2Pointer();
if (event.preventDefault) {
event.preventDefault();
}
} else {
this.wsio.emit('keyDown', {code: code});
if (code === 9) { // tab is a special case - must emulate keyPress event
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)) {
if (event.preventDefault) {
event.preventDefault();
}
}
}
};
/**
* Handler for key up
*
* @method pointerKeyUpMethod
* @param event {Object} key event
*/
this.pointerKeyUpMethod = function(event) {
var code = parseInt(event.keyCode, 10);
if (code !== 27) {
this.wsio.emit('keyUp', {code: code});
}
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Handler for key press
*
* @method pointerKeyPressMethod
* @param event {Object} key event
*/
this.pointerKeyPressMethod = function(event) {
var code = parseInt(event.charCode, 10);
this.wsio.emit('keyPress', {code: code, character: String.fromCharCode(code)});
if (event.preventDefault) {
event.preventDefault();
}
};
/**
* Simalute Shift-Tab with keys
*
* @method togglePointerMode
*/
this.togglePointerMode = function() {
this.wsio.emit('keyDown', {code: 16});
this.wsio.emit('keyPress', {code: 9, character: String.fromCharCode(9)});
this.wsio.emit('keyUp', {code: 16});
};
/**
* Send a spacebar key, for playing PDF and movies mostly
*
* @method sendPlay
*/
this.sendPlay = function() {
// send spacebar code 32
this.wsio.emit('keyPress', {code: 32, character: String.fromCharCode(32)});
};
/**
* Handler for pointer lable text
*
* @method changeSage2PointerLabelMethod
* @param value {String} new name
*/
this.changeSage2PointerLabelMethod = function(value) {
if (value) {
_userSettings.SAGE2_ptrName = value;
} else if (hasMouse) {
_userSettings.SAGE2_ptrName = "SAGE2_user";
} else {
_userSettings.SAGE2_ptrName = "SAGE2_mobile";
}
addCookie('SAGE2_ptrName', _userSettings.SAGE2_ptrName);
if ($$("settings_dialog")) {
// this should be the same as in settingsdialog
webix.ui({
view: "text",
id: "user_name",
label: "Name",
value: _userSettings.SAGE2_ptrName
}, $$("user_name"));
}
};
/**
* Handler for the pointer color selection
*
* @method changeSage2PointerColorMethod
* @param value {String} new name
*/
this.changeSage2PointerColorMethod = function(value) {
_userSettings.SAGE2_ptrColor = value || randomHexColor();
addCookie('SAGE2_ptrColor', _userSettings.SAGE2_ptrColor);
if ($$("settings_dialog")) {
// this should be the same as in settingsdialog
webix.ui({
view: "colorpicker", id: "user_color", label: "Color", value: _userSettings.SAGE2_ptrColor
}, $$("user_color"));
}
};
/**
* Handler for screen resolution selection
*
* @method changeScreenShareResolutionMethod
* @param value {Number} a value corresponding to a resolution level
* @param resolution {String} resolution in the format "width x height"
*/
this.changeScreenShareResolutionMethod = function(value, resolution) {
this.mediaResolution = value;
var res = resolution.split("x");
if (res.length === 2) {
var mediaCanvas = document.getElementById('mediaCanvas');
mediaCanvas.width = parseInt(res[0], 10);
mediaCanvas.height = parseInt(res[1], 10);
console.log("Media resolution: " + resolution);
}
};
/**
* Handler for screen quality selection
*
* @method changeScreenShareQualityMethod
* @param value {Number} a value corresponding to a quality level
*/
this.changeScreenShareQualityMethod = function(value) {
this.mediaQuality = parseInt(value, 10);
};
/**
* Webix modal for user login
*
* @method loginDialog
* @param callingView {Object} webix view that called this method
*/
this.loginDialog = function(callingView) {
if ($$("login_form_container")) {
$$("login_form_container").show();
} else {
webix.ui({
view: "window",
position: "center",
modal: true,
zIndex: 9999,
id: "login_form_container",
head: "Sign in",
body: {
view: "form",
id: "login_form",
width: 400,
borderless: false,
elements: [
{
view: "text", id: "login_name", label: "Name", name: "name"
},
{
view: "text", id: "login_email", label: "Email", name: "email"
},
{
view: "label", id: "login_error", label: "", align: "center"
},
{
margin: 5, cols: [
{
view: "button", id: "login_cancel", label: "Cancel", type: "prev",
click: function() {
this.getTopParentView().hide();
if (callingView && $$('settings_dialog')) {
$$('settings_dialog').show();
}
}
},
{
view: "button", id: "login_create", label: "Create new user",
click: function() {
let data = $$("login_form").getValues();
if ($$("settings_dialog")) {
data.SAGE2_ptrName = $$("user_name").getValue();
data.SAGE2_ptrColor = $$("user_color").getValue();
} else {
data.SAGE2_ptrName = _userSettings.SAGE2_ptrName;
data.SAGE2_ptrColor = _userSettings.SAGE2_ptrColor;
}
wsio.emit('createUser', data);
}
},
{
view: "button", id: "login_confirm", label: "Sign in", type: "form",
click: function() {
let data = $$("login_form").getValues();
wsio.emit('loginUser', data);
}
}
]
}
]
}
}).show();
}
if (callingView) {
callingView.hide();
}
// clear initial values
let name = $$("login_name");
let email = $$("login_email");
$$("login_form").setValues({
name: "",
email: ""
});
$$("login_cancel").enable();
$$("login_confirm").disable();
$$("login_create").disable();
$$("login_error").hide();
$$("login_name").focus();
// check input
function validate() {
if (name.getValue() && email.getValue()) {
$$("login_create").enable();
$$("login_confirm").enable();
} else {
$$("login_create").disable();
$$("login_confirm").disable();
}
}
if (!name.hasEvent('onTimedKeyPress')) {
$$("login_name").attachEvent("onTimedKeyPress", validate);
}
if (!email.hasEvent('onTimedKeyPress')) {
$$("login_email").attachEvent("onTimedKeyPress", validate);
}
};
this.updateLoginButton = function() {
if ($$('user_confirm')) {
let label = "Ok";
if (!_loggedIn && !$$('user_cancel')) {
label = "Log in as guest";
}
webix.ui({
view: "button", id: "user_confirm", value: label, type: "form",
click: () => {
// When OK button pressed
var uname = $$("user_name").getValue();
var ucolor = $$("user_color").getValue();
// Set the values into _userSettings of browser
this.changeSage2PointerLabel(uname);
this.changeSage2PointerColor(ucolor);
this.wsio.emit('editUser', {
uid: _uid,
properties: {
SAGE2_ptrColor: ucolor,
SAGE2_ptrName: uname
}
});
// Close the UI
$$('settings_dialog').destructor();
}
}, $$('user_confirm'));
}
if ($$("user_login")) {
webix.ui({
view: "button", id: "user_login",
template: "<button style='border:none; color:#555; background:#ddd;'>#label#</button>",
label: _loggedIn ? "Sign out" : "Sign up for more options",
click: () => {
if (_loggedIn) {
wsio.emit('logoutUser', _uid);
} else {
// redirect view to login modal
this.loginDialog($$("settings_dialog"));
}
}
}, $$("user_login"));
}
};
/**
* Webix modal for user settings
*
* @method settingsDialog
* @param type {String} (init | main) indicates how to populate the dialog
*/
this.settingsDialog = function(type) {
if (!type) {
return;
}
// get default or stored values
let username = _userSettings.SAGE2_ptrName || (hasMouse ? "SAGE2_user" : "SAGE2_mobile");
let color = _userSettings.SAGE2_ptrColor || randomHexColor();
let loginElement;
if (type === 'init') {
loginElement = {
view: "button", id: "user_confirm", value: "Log in as guest", type: "form"
};
} else {
loginElement = {
margin: 5, cols: [
{
view: "button", id: "user_cancel", value: "Cancel",
click: function() {
// close the dialog without saving changes
this.getTopParentView().destructor();
}
},
{
view: "button", id: "user_confirm", value: "Ok", type: "form"
}
]
};
}
let webixOptions = {
view: "window",
id: "settings_dialog",
position: "center",
modal: true,
zIndex: 9999,
body: {
view: "form",
width: 400,
borderless: false,
elements: [
{
// this should be the same as in changeSage2PointerLabelMethod
view: "text",
id: "user_name",
label: "Name",
value: username
},
{
// this should be the same as in changeSage2PointerColorMethod
view: "colorpicker",
id: "user_color",
label: "Color",
value: color
},
loginElement,
{
view: "button", id: "user_login"
}
],
elementsConfig: {
// labelPosition: "top"
}
}
};
switch (type) {
case 'init':
webixOptions.head = "Set your pointer name and color";
break;
case 'main':
// also show screen resolution options when called via toolbar
webixOptions.head = "Settings";
var elements = webixOptions.body.elements;
var userElements = elements.splice(0, 2);
elements.unshift(
{
view: "label", label: "Pointer", align: "center"
},
{
type: "space",
rows: userElements
},
{
view: "label", label: "Screen sharing", align: "center"
},
{
type: "space",
rows: [
{
view: "richselect",
label: "Resolution",
id: "screen_resolution",
value: (this.mediaResolution + 1) || 3,
options: [
{id: 1, value: "Low"},
{id: 2, value: "Medium"},
{id: 3, value: "High"},
{id: 4, value: "Full"}
]
},
{
view: "richselect",
label: "Quality",
id: "screen_quality",
value: this.mediaQuality || 9,
options: [
{id: 3, value: "Low"},
{id: 7, value: "Medium"},
{id: 9, value: "High"}
]
}
]
},
{
view: "label"
}
);
break;
default: break;
}
webix.ui.zIndexBase = 10000; // to make the colorboard appear on top of the modal
webix.ui(webixOptions).show();
this.updateLoginButton();
// Attach event handlers
// change screen resolution
if (type === 'main') {
let res = $$("screen_resolution");
res.attachEvent("onChange", () => {
this.changeScreenShareResolution(res.getValue() - 1, res.getText());
});
// change screen quality
let quality = $$("screen_quality");
$$("screen_quality").attachEvent("onChange", () => {
this.changeScreenShareQuality(quality.getValue());
});
}
// Focus the text box
$$('user_name').focus();
};
/**
* Getter for user settings
*
* @property user
*/
Object.defineProperty(this, "user", {
get: function() {
return {
name: _userSettings.SAGE2_userName,
// id: _uid,
label: _userSettings.SAGE2_ptrName,
color: _userSettings.SAGE2_ptrColor
};
}
});
/**
* Getter for user name
*
* @property username
*/
Object.defineProperty(this, "username", {
get: function() {
return _userSettings.SAGE2_userName;
}
});
/**
* Getter for user email
*
* @property email
*/
Object.defineProperty(this, "email", {
get: function() {
return _userSettings.SAGE2_userEmail;
}
});
/**
* Getter for pointer color
*
* @property pointerColor
*/
Object.defineProperty(this, "pointerColor", {
get: function() {
return _userSettings.SAGE2_ptrColor;
}
});
/**
* Getter for pointer label
*
* @property pointerLabel
*/
Object.defineProperty(this, "pointerLabel", {
get: function() {
return _userSettings.SAGE2_ptrName;
}
});
this.streamSuccess = this.streamSuccessMethod.bind(this);
this.streamFail = this.streamFailMethod.bind(this);
this.streamEnded = this.streamEndedMethod.bind(this);
this.streamCanPlay = this.streamCanPlayMethod.bind(this);
this.pointerLockError = this.pointerLockErrorMethod.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.pointerKeyDown = this.pointerKeyDownMethod.bind(this);
this.pointerKeyUp = this.pointerKeyUpMethod.bind(this);
this.pointerKeyPress = this.pointerKeyPressMethod.bind(this);
this.changeSage2PointerLabel = this.changeSage2PointerLabelMethod.bind(this);
this.changeSage2PointerColor = this.changeSage2PointerColorMethod.bind(this);
this.changeScreenShareResolution = this.changeScreenShareResolutionMethod.bind(this);
this.changeScreenShareQuality = this.changeScreenShareQualityMethod.bind(this);
this.step = this.stepMethod.bind(this);
/** init **/
// Check if a domain cookie exists for the name
var cookieName = getCookie('SAGE2_ptrName');
if (cookieName) {
if (cookieName.startsWith('Anon ') ||
cookieName === 'SAGE2_user' ||
cookieName === 'SAGE2_mobile') {
deleteCookie('SAGE2_ptrName');
cookieName = null;
}
_userSettings.SAGE2_ptrName = cookieName;
}
// Check if a domain cookie exists for the color
var cookieColor = getCookie('SAGE2_ptrColor');
if (cookieColor) {
_userSettings.SAGE2_ptrColor = cookieColor;
}
// Post message to the Chrome extension to register the UI
if (__SAGE2__.browser.isChrome === true) {
window.postMessage('SAGE2_registerUI', '*');
}
// Deals with the name and color of the pointer
if (_userSettings.SAGE2_ptrColor === undefined ||
_userSettings.SAGE2_ptrColor === null) {
_userSettings.SAGE2_ptrColor = randomHexColor();
addCookie('SAGE2_ptrColor', _userSettings.SAGE2_ptrColor);
}
// Check if user email / name exists
var cookieUserName = getCookie('SAGE2_userName') || '';
var cookieEmail = getCookie('SAGE2_userEmail') || '';
if (!viewOnlyMode) {
wsio.emit('loginUser', {
name: cookieUserName,
email: cookieEmail,
SAGE2_ptrName: _userSettings.SAGE2_ptrName,
SAGE2_ptrColor: _userSettings.SAGE2_ptrColor,
init: true
});
}
this.wsio.on('loginStateChanged', handleLoginStateChange.bind(this));
this.wsio.on('allowAction', allowAction.bind(this));
this.wsio.on('cancelAction', cancelAction.bind(this));
document.addEventListener('pointerlockerror', this.pointerLockError, false);
document.addEventListener('mozpointerlockerror', this.pointerLockError, false);
document.addEventListener('pointerlockchange', this.pointerLockChange, false);
document.addEventListener('mozpointerlockchange', this.pointerLockChange, false);
document.getElementById('mediaVideo').addEventListener('canplay', this.streamCanPlay, false);
// -----------
// Shim for requestIdleCallback (available on Chrome)
// -----------
window.requestIdleCallback = window.requestIdleCallback || function(cb) {
var start = Date.now();
return setTimeout(function() {
cb({
didTimeout: false,
timeRemaining: function() {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
};
window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
clearTimeout(id);
};
// -----------
}
return SAGE2_interaction;
}());