opentok-annotation
Version:
OpenTok annotation accelerator pack
1,428 lines (1,205 loc) • 91.4 kB
JavaScript
/* global OT OTSolution OTKAnalytics ScreenSharingAccPack define */
(function () {
/** Include external dependencies */
var _;
var $;
var OTKAnalytics;
if (typeof module === 'object' && typeof module.exports === 'object') {
/* eslint-disable import/no-unresolved */
_ = require('underscore');
$ = require('jquery');
OTKAnalytics = require('opentok-solutions-logging');
/* eslint-enable import/no-unresolved */
} else {
_ = this._;
$ = this.$;
OTKAnalytics = this.OTKAnalytics;
}
/** Private variables */
var _this;
var _accPack;
var _session;
var _canvas;
var _subscribingToMobileScreen;
var _elements = {};
/** Analytics */
var _otkanalytics;
// vars for the analytics logs. Internal use
var _logEventData = {
clientVersion: 'js-vsol-2.0.59', // x.y.z filled by npm build script
componentId: 'annotationsAccPack',
name: 'guidAnnotationsKit',
actionInitialize: 'Init',
actionStart: 'Start',
actionEnd: 'End',
actionFreeHand: 'FreeHand',
actionPickerColor: 'PickerColor',
actionText: 'Text',
actionScreenCapture: 'ScreenCapture',
actionErase: 'Erase',
actionUseToolbar: 'UseToolbar',
variationAttempt: 'Attempt',
variationError: 'Failure',
variationSuccess: 'Success',
};
var _logAnalytics = function () {
// init the analytics logs
var _source = window.location.href;
var otkanalyticsData = {
clientVersion: _logEventData.clientVersion,
source: _source,
componentId: _logEventData.componentId,
name: _logEventData.name
};
_otkanalytics = new OTKAnalytics(otkanalyticsData);
var sessionInfo = {
sessionId: _session.id,
connectionId: _session.connection.connectionId,
partnerId: _session.apiKey
};
_otkanalytics.addSessionInfo(sessionInfo);
};
var _log = function (action, variation) {
var data = {
action: action,
variation: variation
};
_otkanalytics.logEvent(data);
};
/** End Analytics */
// Check for DOM element or string. Return element.
var _getElem = function (el) {
return typeof el === 'string' ? document.querySelector(el) : el;
};
// Trigger event via common layer API
var _triggerEvent = function (event, data) {
if (_accPack) {
_accPack.triggerEvent(event, data);
}
};
var _registerEvents = function () {
if (_accPack) {
var events = [
'startAnnotation',
'linkAnnotation',
'resizeCanvas',
'annotationWindowClosed',
'endAnnotation'
];
_accPack.registerEvents(events);
}
};
var _setupUI = function () {
var toolbar = [
'<div id="annotationToolbarContainer" class="ots-annotation-toolbar-container">',
'<div id="toolbar"></div>',
'</div>'
].join('\n');
$('body').append(toolbar);
_log(_logEventData.actionUseToolbar, _logEventData.variationSuccess);
};
var _palette = [
'#1abc9c',
'#2ecc71',
'#3498db',
'#9b59b6',
'#8e44ad',
'#f1c40f',
'#e67e22',
'#e74c3c',
'#ded5d5'
];
var _aspectRatio = (10 / 6);
/** Private methods */
var _refreshCanvas = _.throttle(function () {
_canvas.onResize();
}, 1000);
/** Resize the canvas to match the size of its container */
var _resizeCanvas = _.throttle(function () {
var width;
var height;
var cobrowsing = !!_elements.cobrowsingImage;
if (cobrowsing) {
// Cobrowsing images are currently fixed size, so resize isn't needed
return;
}
if (_elements.cobrowsingImage === null) {
var el = _elements.absoluteParent || _elements.canvasContainer;
width = el.clientWidth;
height = el.clientHeight;
}
try {
var videoDimensions = _canvas.videoFeed.stream.videoDimensions;
} catch (e) {
console.log('OT Annotation: Annotation video stream no longer exists');
return;
}
// Override dimensions when subscribing to a mobile screen
if (_subscribingToMobileScreen) {
videoDimensions.width = width;
videoDimensions.height = height;
}
var origRatio = videoDimensions.width / videoDimensions.height;
var destRatio = width / height;
var calcDimensions = {
top: 0,
left: 0,
height: height,
width: width
};
if (!_elements.externalWindow) {
if (origRatio < destRatio) {
// height is the limiting prop, we'll get vertical bars
calcDimensions.width = calcDimensions.height * origRatio;
calcDimensions.left = (width - calcDimensions.width) / 2;
} else {
calcDimensions.height = calcDimensions.width / origRatio;
calcDimensions.top = (height - calcDimensions.height) / 2;
}
}
$(_elements.canvasContainer).find('canvas').css(calcDimensions);
$(_elements.canvasContainer).find('canvas').attr(calcDimensions);
_refreshCanvas();
_triggerEvent('resizeCanvas');
}, 500, {trailing: false});
var _changeColorByIndex = function (colorIndex) {
_canvas.changeColorByIndex(colorIndex);
};
var _takeScreenShot = function () {
_canvas.captureScreenshot(true);
};
var _listenForResize = function () {
$(_elements.resizeSubject).on('resize', _resizeCanvas);
};
var _createToolbar = function (session, options, externalWindow) {
_setupUI();
var toolbarId = _.property('toolbarId')(options) || 'toolbar';
var items = _.property('toolbarItems')(options) || [];
var shapes = _.property('toolbarShapes')(options) || [];
var colors = _.property('colors')(options) || _palette;
var imageAssets = _.property('imageAssets')(options) || null;
var backgroundColor = _.property('backgroundColor')(options) || null;
var container = function () {
var w = !!externalWindow ? externalWindow : window;
return w.document.getElementById(toolbarId);
};
/* eslint-disable no-native-reassign */
toolbar = new OTSolution.Annotations.Toolbar({
session: session,
container: container(),
colors: colors,
items: items.length ? items : ['*'],
shapes: shapes.length ? shapes : ['rectangle', 'oval', 'star', 'arrow', 'line'],
externalWindow: externalWindow || null,
imageAssets: imageAssets,
backgroundColor: backgroundColor,
OTKAnalytics: OTKAnalytics
});
toolbar.itemClicked(function (id) {
var actions = {
OT_pen: _logEventData.actionFreeHand,
OT_colors: _logEventData.actionPickerColor,
OT_text: _logEventData.actionText,
OT_clear: _logEventData.actionErase
};
var action = actions[id];
if (!!action) {
_log(action, _logEventData.variationSuccess);
}
});
/* eslint-enable no-native-reassign */
};
// Create external screen sharing window
var _createExternalWindow = function () {
var deferred = $.Deferred();
var width = screen.width * 0.80 | 0;
var height = width / (_aspectRatio);
var externalWindowHTML = '<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-type" content="text/html; charset=utf-8"><title>OpenTok Screen Sharing Solution Annotation</title><link rel="stylesheet" href="https://assets.tokbox.com/solutions/css/style.css"><style type="text/css" media="screen"> body{margin:0;background-color:rgba(0,153,203,.7);box-sizing:border-box;height:100vh}canvas{top:0;z-index:1000}.hidden{display:none}.ots-hidden{display:none !important}.main-wrap{width:100%;height:100%;-ms-box-orient:horizontal;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-moz-flex;display:-webkit-flex;display:flex;-webkit-justify-content:center;justify-content:center;-webkit-align-items:center;align-items:center}.inner-wrap{position:relative;border-radius:8px;overflow:hidden}.publisherContainer{display:block;background-color:#000;position:absolute}.publisher-wrap{height:100%;width:100%}.subscriberContainer{position:absolute;display:flex;top:20px;left:20px;width:200px;height:120px;background-color:#000;border:2px solid #fff;border-radius:6px}.subscriberContainer .OT_video-poster{width:100%;height:100%;opacity:.25;background-repeat:no-repeat;background-image:url(https://static.opentok.com/webrtc/v2.8.2/images/rtc/audioonly-silhouette.svg);background-size:50%;background-position:center}.OT_video-element{height:100%;width:100%}.OT_edge-bar-item{display:none}</style></head><body> <div class="main-wrap"> <div id="annotationContainer" class="inner-wrap"></div></div><div id="toolbarContainer" class="ots-annotation-toolbar-container"> <div id="toolbar" class="toolbar-wrap"></div></div><div id="subscriberVideo" class="subscriberContainer hidden"></div><script type="text/javascript" charset="utf-8"> /** Must use double-quotes since everything must be converted to a string */ var opener; var canvas; if (!toolbar){alert("Something went wrong: You must pass an OpenTok annotation toolbar object into the window.")}else{opener=window.opener; window.onbeforeunload=window.triggerCloseEvent;}var localScreenProperties={insertMode: "append", width: "100%", height: "100%", videoSource: "window", showControls: false, style:{buttonDisplayMode: "off"}, subscribeToVideo: "true", subscribeToAudio: "false", fitMode: "contain"}; var createContainerElements=function(){var parentDiv=document.getElementById("annotationContainer"); var publisherContainer=document.createElement("div"); publisherContainer.setAttribute("id", "screenshare_publisher"); publisherContainer.classList.add("publisher-wrap"); parentDiv.appendChild(publisherContainer); return{annotation: parentDiv, publisher: publisherContainer};}; var addSubscriberVideo=function(stream){var container=document.getElementById("subscriberVideo"); var subscriber=session.subscribe(stream, container, localScreenProperties, function(error){if (error){console.log("Failed to add subscriber video", error);}container.classList.remove("hidden");});}; if (navigator.userAgent.indexOf("Firefox") !==-1){var ghost=window.open("about:blank"); ghost.focus(); ghost.close();}</script></body></html>';
/* eslint-disable max-len */
var windowFeatures = [
'toolbar=no',
'location=no',
'directories=no',
'status=no',
'menubar=no',
'scrollbars=no',
'resizable=no',
'copyhistory=no', ['width=', width].join(''), ['height=', height].join(''), ['left=', ((screen.width / 2) - (width / 2))].join(''), ['top=', ((screen.height / 2) - (height / 2))].join('')
].join(',');
/* eslint-enable max-len */
var annotationWindow = window.open('about:blank', '', windowFeatures);
annotationWindow.document.write(externalWindowHTML);
window.onbeforeunload = function () {
annotationWindow.close();
};
// External window needs access to certain globals
annotationWindow.toolbar = toolbar;
annotationWindow.OT = OT;
annotationWindow.session = _session;
annotationWindow.$ = $;
annotationWindow.triggerCloseEvent = function () {
_triggerEvent('annotationWindowClosed');
};
annotationWindow.onbeforeunload = function () {
_triggerEvent('annotationWindowClosed');
};
// TODO Find something better.
var windowReady = function () {
if (!!annotationWindow.createContainerElements) {
$(annotationWindow.document).ready(function () {
deferred.resolve(annotationWindow);
});
} else {
setTimeout(windowReady, 100);
}
};
windowReady();
return deferred.promise();
};
// Remove the toolbar and cancel event listeners
var _removeToolbar = function () {
_canvas.onClearAnnotation();
$(_elements.resizeSubject).off('resize', _resizeCanvas);
toolbar.remove();
$('#annotationToolbarContainer').remove();
};
// Determine whether or not the subscriber stream is from a mobile device
var _requestPlatformData = function (pubSub, mobileInitiator) {
if (!!pubSub.stream) {
var isPublisher = Object.prototype.hasOwnProperty.call(pubSub, 'accessAllowed');
_session.signal({
type: 'otAnnotation_requestPlatform',
to: pubSub.stream.connection,
});
_session.on('signal:otAnnotation_mobileScreenShare', function (event) {
var platform = event.data ? JSON.parse(event.data).platform : null;
var isMobile = (platform == 'ios' || platform === 'android');
_subscribingToMobileScreen = isMobile;
_canvas.onMobileScreenShare(isMobile, isPublisher);
});
}
if (mobileInitiator) {
_subscribingToMobileScreen = true;
_canvas.onMobileScreenShare(true);
}
};
/**
* Creates an external window (if required) and links the annotation toolbar
* to the session
* @param {object} session
* @param {object} [options]
* @param {boolean} [options.screensharing] - Using an external window
* @param {string} [options.toolbarId] - If the container has an id other than 'toolbar'
* @param {array} [options.items] - Custom set of tools
* @param {array} [options.colors] - Custom color palette
* @returns {promise} < Resolve: undefined | {object} Reference to external annotation window >
*/
var start = function (session, options) {
var deferred = $.Deferred();
if (_.property('screensharing')(options)) {
_createExternalWindow()
.then(function (externalWindow) {
_createToolbar(session, options, externalWindow);
toolbar.createPanel(externalWindow);
_triggerEvent('startAnnotation', externalWindow);
_log(_logEventData.actionStart, _logEventData.variationSuccess);
deferred.resolve(externalWindow);
});
} else {
_createToolbar(session, options);
_triggerEvent('startAnnotation');
_log(_logEventData.actionStart, _logEventData.variationSuccess);
deferred.resolve();
}
return deferred.promise();
};
/**
* @param {object} pubSub - Either the publisher(sharing) or subscriber(viewing)
* @ param {object} container - The parent container for the canvas element
* @ param {object} options
* @param {object} options.canvasContainer - The id of the parent for the annotation canvas
* @param {object | string} [options.externalWindow] - Reference to the annotation window (or query selector) if publishing
* @param {array | string} [options.absoluteParent] - Reference to element (or query selector) for resize if other than container
* * @param {Boolean} [options.mobileInitiator] - Is cobrowsing being initiated by a mobile device
*/
var linkCanvas = function (pubSub, container, options) {
/**
* jQuery only allows listening for a resize event on the window or a
* jQuery resizable element, like #wmsFeedWrap. windowRefernce is a
* reference to the popup window created for annotation. If this doesn't
* exist, we are watching the canvas belonging to the party viewing the
* shared screen
*/
_elements.resizeSubject = _getElem(_.property('externalWindow')(options) || window);
_elements.externalWindow = _getElem(_.property('externalWindow')(options) || null);
_elements.absoluteParent = _getElem(_.property('absoluteParent')(options) || null);
_elements.cobrowsingImage = _getElem(_.property('cobrowsingImage')(options) || null);
_elements.canvasContainer = _getElem(container);
// The canvas object
_canvas = new OTSolution.Annotations({
feed: pubSub,
container: _elements.canvasContainer,
externalWindow: _elements.externalWindow
});
toolbar.addCanvas(_canvas, _elements.externalWindow);
var onScreenCapture = _this.options.onScreenCapture ? _this.options.onScreenCapture :
function (dataUrl) {
var win = window.open(dataUrl, '_blank');
win.focus();
};
_canvas.onScreenCapture(onScreenCapture);
_requestPlatformData(pubSub, options && options.mobileInitiator);
var context = _elements.externalWindow ? _elements.externalWindow : window;
// The canvas DOM element
_elements.canvas = $(_.first(context.document.getElementsByTagName('canvas')));
_listenForResize();
_resizeCanvas();
_triggerEvent('linkAnnotation');
};
/**
* Manually update the size of the canvas to match it's container, or the
* absolute parent, if defined.
*/
var resizeCanvas = function () {
_resizeCanvas();
};
/**
* Change the annotation color of the toolbar passing the colorIndex
* @param {Integer} colorIndex - The color index number
*/
var changeColorByIndex = function (colorIndex) {
_changeColorByIndex(colorIndex);
};
var takeScreenShot = function () {
_takeScreenShot();
};
/**
* Adds a subscriber's video the external annotation window
* @param {Object} stream - The subscriber stream object
*/
var addSubscriberToExternalWindow = function (stream) {
if (!_elements.externalWindow) {
console.log('OT Annotation: External window does not exist. Cannot add subscriber video.');
} else {
_elements.externalWindow.addSubscriberVideo(stream);
}
};
/**
* Stop annotation and clean up components
* @param {Boolean} publisher Are we the publisher?
*/
var end = function () {
_removeToolbar();
_elements.canvas = null;
if (!!_elements.externalWindow) {
_elements.externalWindow.close();
_elements.externalWindow = null;
_elements.resizeSubject = null;
}
_triggerEvent('endAnnotation');
_log(_logEventData.actionEnd, _logEventData.variationSuccess);
};
var hideToolbar = function () {
$(toolbar.parent).hide();
};
var showToolbar = function () {
$(toolbar.parent).show();
};
/**
* @constructor
* Represents an annotation component, used for annotation over video or a shared screen
* @param {object} options
* @param {object} options.session - An OpenTok session
* @param {object} options.canvasContainer - The id of the parent for the annotation canvas
* @param {object} options.watchForResize - The DOM element to watch for resize
* @param {object} options.onScreenCapture- A callback function to be invoked on screen capture
*/
var AnnotationAccPack = function (options) {
_this = this;
_this.options = _.omit(options, 'accPack', 'session');
_accPack = _.property('accPack')(options);
_session = _.property('session')(options);
if (!_session) {
throw new Error('OpenTok Annotation Accelerator Pack requires an OpenTok session');
}
_registerEvents();
// init analytics logs
_logAnalytics();
_log(_logEventData.actionInitialize, _logEventData.variationSuccess);
};
AnnotationAccPack.prototype = {
constructor: AnnotationAccPack,
start: start,
linkCanvas: linkCanvas,
resizeCanvas: resizeCanvas,
addSubscriberToExternalWindow: addSubscriberToExternalWindow,
end: end,
hideToolbar: hideToolbar,
showToolbar: showToolbar,
changeColorByIndex: changeColorByIndex,
takeScreenShot: takeScreenShot
};
if (typeof exports === 'object') {
module.exports = AnnotationAccPack;
} else if (typeof define === 'function' && define.amd) {
define(function () {
return AnnotationAccPack;
});
} else {
this.AnnotationAccPack = AnnotationAccPack;
}
}.call(this));
/*!
* Annotation Plugin for OpenTok
*
* @Author: Trevor Boyer
* @Copyright (c) 2015 TokBox, Inc
**/
/* eslint-disable */
/** Analytics */
(function () {
var _OTKAnalytics;
var _otkanalytics;
var _session;
// vars for the analytics logs. Internal use
var _logEventData = {
clientVersion: 'js-vsol-2.0.59', // x.y.z filled by npm build script
componentId: 'annotationsAccPack',
name: 'guidAnnotationsKit',
actionStartDrawing: 'StartDrawing',
actionEndDrawing: 'EndDrawing',
variationSuccess: 'Success',
};
var _logAnalytics = function () {
// init the analytics logs
var _source = window.location.href;
var otkanalyticsData = {
clientVersion: _logEventData.clientVersion,
source: _source,
componentId: _logEventData.componentId,
name: _logEventData.name
};
_otkanalytics = new _OTKAnalytics(otkanalyticsData);
var sessionInfo = {
sessionId: _session.id,
connectionId: _session.connection.connectionId,
partnerId: _session.apiKey
};
_otkanalytics.addSessionInfo(sessionInfo);
};
var _log = function (action, variation) {
var data = {
action: action,
variation: variation
};
_otkanalytics.logEvent(data);
};
/** End Analytics */
//--------------------------------------
// OPENTOK ANNOTATION CANVAS/VIEW
//--------------------------------------
var DEFAULT_ASSET_URL = 'https://assets.tokbox.com/solutions/images/';
OTSolution = this.OTSolution || {};
OTSolution.Annotations = function (options) {
options = options || {};
this.widgetVersion = 'js-1.0.0-beta';
this.parent = options.container;
this.videoFeed = options.feed;
this.imageAssets = options.imageAssets || DEFAULT_ASSET_URL;
_OTKAnalytics = _OTKAnalytics || options.OTKAnalytics;
if (!_otkanalytics) {
_logAnalytics();
}
if (typeof module === 'object' && typeof module.exports === 'object') {
$ = require('jquery');
}
var context = options.externalWindow ? options.externalWindow.document : window.document;
var self = this;
if (this.parent) {
var canvas = document.createElement('canvas');
canvas.setAttribute('id', 'opentok_canvas'); // session.connection.id?
canvas.style.position = 'absolute';
this.parent.appendChild(canvas);
canvas.setAttribute('width', this.parent.clientWidth + 'px');
canvas.style.width = window.getComputedStyle(this.parent).width;
canvas.setAttribute('height', this.parent.clientHeight + 'px');
canvas.style.height = window.getComputedStyle(this.parent).height;
}
function VideoRelativeCoordinateSet(update) {
var returnedObj = {};
var scale = {
get X() {
if (publishingScreenToMobileDevice || (cobrowsing && subscribingToMobileScreen)) {
return update.canvasWidth / canvas.width;
}
var width = cobrowsing ? canvas.width : self.videoFeed.stream.videoDimensions.width;
return width / canvas.width;
},
get Y() {
if (publishingScreenToMobileDevice || (cobrowsing && subscribingToMobileScreen)) {
return update.canvasHeight / canvas.height;
}
var height = cobrowsing ? canvas.height : self.videoFeed.stream.videoDimensions.height;
return height / canvas.height;
}
};
Object.keys(update).forEach(function (attr) {
returnedObj[attr] = update[attr];
});
['X', 'Y'].forEach(function (coord) {
['to', 'from', 'last', 'm', 'start', 'point'].forEach(function (verb) {
var attr = verb + coord;
returnedObj['_' + attr] = returnedObj[attr];
Object.defineProperty(returnedObj, attr, {
get: function () {
return returnedObj['_' + attr] / scale[coord];
},
set: function (newVal) {
returnedObj['_' + attr] = newVal; // * scale[coord];
}
});
});
});
return returnedObj;
}
var self = this;
var ctx;
var cbs = [];
var isPublisher;
var mirrored;
var scaledToFill;
var batchUpdates = [];
var drawHistory = [];
var drawHistoryReceivedFrom = [];
var updateHistory = [];
var eventHistory = [];
var isStartPoint = false;
var isVideo = self.videoFeed && self.videoFeed.element ? true : false;
var cobrowsing = !self.videoFeed.stream;
var subscribingToMobileScreen = false;
var publishingScreenToMobileDevice = false;
var client = new VideoRelativeCoordinateSet({
dragging: false
});
// INFO Mirrored feeds contain the OT_mirrored class
if (isVideo) {
isPublisher = (' ' + self.videoFeed.element.className + ' ').indexOf(' ' + 'OT_publisher' + ' ') > -1;
mirrored = isPublisher ? (' ' + self.videoFeed.element.className + ' ').indexOf(' ' + 'OT_mirrored' + ' ') > -1 : false;
scaledToFill = (' ' + self.videoFeed.element.className + ' ').indexOf(' ' + 'OT_fit-mode-cover' + ' ') > -1;
} else {
mirrored = false;
scaledToFill = false;
}
this.canvas = function () {
return canvas;
};
/**
* Links an OpenTok session to the annotation canvas. Typically, this is automatically linked
* when using {@link Toolbar#addCanvas}.
* @param session The OpenTok session.
*/
this.link = function (session) {
this.session = session;
};
/**
* Changes the active annotation color for the canvas.
* @param color The hex string representation of the color (#rrggbb).
*/
this.changeColor = function (color) {
self.userColor = color;
if (!self.lineWidth) {
self.lineWidth = 2; // TODO Default to first option in list of line widths
}
};
/**
* Changes the active annotation color for the canvas.
* @param colorIndex - the index regarding the colors array
*/
this.changeColorByIndex = function (colorIndex) {
//set the user color
self.userColor = this.colors[colorIndex];
//activate the change on the toolbar
var colorChoices = context.querySelectorAll('.color-choice');
colorChoices[colorIndex].classList.add('active');
var button = context.getElementById('OT_colors');
button.setAttribute('class', 'OT_color annotation-btn colors');
button.style.borderRadius = '50%';
button.style.backgroundColor = this.colors[colorIndex];
};
/**
* Changes the line/stroke width of the active annotation for the canvas.
* @param size The size in pixels.
*/
this.changeLineWidth = function (size) {
this.lineWidth = size;
};
/**
* Sets the selected menu item from the toolbar. This is typically handled
* automatically by the toolbar, but can be used to programmatically select an item.
* @param item The menu item to set as selected.
*/
this.selectItem = function (item) {
if (self.overlay) {
self.parent.removeChild(self.overlay);
self.overlay = null;
}
/**
* Update classes for toolbar items
*/
var updateSelected = function () {
// Remove the 'selected' class from the currently selected item (or parent)
var current = context.getElementById(self.selectedItem.id);
var shapesBtn = context.getElementById('OT_shapes');
var currentIsShape = shapesBtn.classList.contains('selected');
currentIsShape ? shapesBtn.classList.remove('selected') : current.classList.remove('selected');
// If the newly selected item is a shape, update the shapes subpanel button
var newlySelected = context.getElementById(item.id);
if (newlySelected.parentElement.classList.contains('shapes')) {
shapesBtn.classList.add('selected');
} else {
newlySelected.classList.add('selected');
}
}
if (item && item.id === 'OT_capture') {
self.captureScreenshot();
} else if (item && item.id.indexOf('OT_line_width') !== -1) {
if (item.size) {
self.changeLineWidth(item.size);
}
// 'undo' and 'clear' are actions, not items that can be selected
} else if (item.id !== 'OT_undo' && item.id !== 'OT_clear') {
updateSelected();
self.selectedItem = item;
}
};
/**
* Sets the color palette for the color picker
* @param colors The array of hex color strings (#rrggbb).
*/
this.colors = function (colors) {
this.colors = colors;
this.changeColor(colors[0]);
};
/**
* Clears the canvas for the active user. Only annotations added by the active OpenTok user will
* be removed, leaving the history of all other annotations.
*/
this.clear = function () {
clearCanvas(false, self.session.connection.connectionId);
if (self.session) {
self.session.signal({
type: 'otAnnotation_clear'
});
}
};
this.undo = function () {
undoLast(false, self.session.connection.connectionId);
}
// TODO Allow the user to choose the image type? (jpg, png) Also allow size?
/**
* Captures a screenshot of the annotations displayed on top of the active video feed.
*/
this.captureScreenshot = function (isAnnotationEnd) {
var canvasCopy = document.createElement('canvas');
canvasCopy.width = canvas.width;
canvasCopy.height = canvas.height;
var width = isVideo ? self.videoFeed.videoWidth() : canvas.width;
var height = isVideo ? self.videoFeed.videoHeight() : canvas.height;
var scale = 1;
var offsetX = 0;
var offsetY = 0;
if (scaledToFill) {
if (width < height) {
scale = canvas.width / width;
width = canvas.width;
height = height * scale;
} else {
scale = canvas.height / height;
height = canvas.height;
width = width * scale;
}
// If stretched to fill, we need an offset to center the image
offsetX = (width - canvas.width) / 2;
offsetY = (height - canvas.height) / 2;
} else {
if (width > height) {
scale = canvas.width / width;
width = canvas.width;
height = height * scale;
offsetX = 0;
offsetY = (canvas.height - height) / 2;
} else {
scale = canvas.height / height;
height = canvas.height;
width = width * scale;
offsetX = (canvas.width - width) / 2;
offsetY = 0;
}
}
// Combine the video and annotation images
var image = new Image();
image.onload = function () {
var ctxCopy = canvasCopy.getContext('2d');
if (mirrored) {
ctxCopy.translate(width, 0);
ctxCopy.scale(-1, 1);
}
ctxCopy.drawImage(image, offsetX, offsetY, width, height);
// We want to make sure we draw the annotations the same way, so we need to flip back
if (mirrored) {
ctxCopy.translate(width, 0);
ctxCopy.scale(-1, 1);
}
ctxCopy.drawImage(canvas, 0, 0);
cbs.forEach(function (cb) {
var data = { src: canvasCopy.toDataURL(), isAnnotationEnd: isAnnotationEnd };
cb.call(self, data);
});
// Clear and destroy the canvas copy
canvasCopy = null;
};
if (isVideo) {
imgData = 'data:image/png;base64,' + self.videoFeed.getImgData();
image.src = imgData;
} else {
var currentWindow = options.externalWindow ? options.externalWindow : window;
image.src = currentWindow.getComputedStyle(self.parent)['background-image'].replace(/url\("|"\)/g, '');
}
};
this.onScreenCapture = function (cb) {
cbs.push(cb);
};
/**
* Set flags for sharing with mobile devices
* @param {Boolean} mobile - Is the other party using a mobile device
* @param {Boolean} publishing - Are we publishing our screen?
*/
this.onMobileScreenShare = function (mobile, publishing) {
if (publishing) {
publishingScreenToMobileDevice = mobile;
} else {
subscribingToMobileScreen = mobile;
}
};
this.onResize = function () {
drawUpdates(updateHistory, true);
draw(null, true);
};
this.onClearAnnotation = function () {
dismissTextAnnotation();
};
/** Canvas Handling **/
function addEventListeners(el, s, fn) {
var evts = s.split(' ');
for (var i = 0, iLen = evts.length; i < iLen; i++) {
el.addEventListener(evts[i], fn, true);
}
}
function updateCanvas(event, resizeEvent) {
// Ensure that our canvas has been properly sized
if (canvas.width === 0) {
canvas.width = self.parent.getBoundingClientRect().width;
}
if (canvas.height === 0) {
canvas.height = self.parent.getBoundingClientRect().height;
}
var offsetLeft = !!resizeEvent ? event.canvas.offsetLeft : canvas.offsetLeft;
var offsetTop = !!resizeEvent ? event.canvas.offsetTop : canvas.offsetTop;
if (cobrowsing) {
var baseWidth = !!resizeEvent ? event.canvas.width : self.parent.clientWidth;
var baseHeight = !!resizeEvent ? event.canvas.height : self.parent.clientHeight;
var scaleX = canvas.width / baseWidth;
var scaleY = canvas.height / baseHeight;
var offsetX = event.offsetX || event.pageX - offsetLeft ||
(event.changedTouches && event.changedTouches[0].pageX - offsetLeft);
var offsetY = event.offsetY || event.pageY - offsetTop ||
(event.changedTouches && event.changedTouches[0].pageY - offsetTop);
var x = offsetX * scaleX;
var y = offsetY * scaleY;
} else {
var videoDimensions = self.videoFeed.stream.videoDimensions;
var scaleX = videoDimensions.width / canvas.width;
var scaleY = videoDimensions.height / canvas.height;
if (subscribingToMobileScreen) {
scaleX = 1;
scaleY = 1;
}
var offsetX = event.offsetX || event.pageX - offsetLeft ||
(event.changedTouches && event.changedTouches[0].pageX - offsetLeft);
var offsetY = event.offsetY || event.pageY - offsetTop ||
(event.changedTouches && event.changedTouches[0].pageY - offsetTop);
var x = offsetX * scaleX;
var y = offsetY * scaleY;
}
var update;
var selectedItem = resizeEvent ? event.selectedItem : self.selectedItem;
if (selectedItem) {
if (selectedItem.id === 'OT_pen') {
switch (event.type) {
case 'mousedown':
case 'touchstart':
client.dragging = true;
client.lastX = x;
client.lastY = y;
self.isStartPoint = true;
!resizeEvent && _log(_logEventData.actionStartDrawing, _logEventData.variationSuccess);
break;
case 'mousemove':
case 'touchmove':
if (client.dragging) {
update = {
id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId,
fromId: self.session.connection.connectionId,
fromX: client._lastX,
fromY: client._lastY,
toX: x,
toY: y,
color: resizeEvent ? event.userColor : self.userColor,
lineWidth: self.lineWidth,
videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width,
videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
mirrored: mirrored,
startPoint: self.isStartPoint, // Each segment is treated as a new set of points
endPoint: false,
selectedItem: selectedItem,
platform: 'web',
guid: event.guid
};
draw(new VideoRelativeCoordinateSet(update), true);
client.lastX = x;
client.lastY = y;
!resizeEvent && sendUpdate(update);
self.isStartPoint = false;
}
break;
case 'mouseup':
case 'touchend':
client.dragging = false;
update = {
id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId,
fromId: self.session.connection.connectionId,
fromX: client._lastX,
fromY: client._lastY,
toX: x,
toY: y,
color: resizeEvent ? event.userColor : self.userColor,
lineWidth: self.lineWidth,
videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width,
videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
mirrored: mirrored,
startPoint: self.isStartPoint, // Each segment is treated as a new set of points
endPoint: true,
selectedItem: selectedItem,
platform: 'web',
guid: event.guid
};
draw(new VideoRelativeCoordinateSet(update), true);
client.lastX = x;
client.lastY = y;
!resizeEvent && sendUpdate(update);
self.isStartPoint = false;
!resizeEvent && _log(_logEventData.actionEndDrawing, _logEventData.variationSuccess);
break;
case 'mouseout':
client.dragging = false;
}
} else if (selectedItem.id === 'OT_text') {
update = {
id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId,
fromId: self.session.connection.connectionId,
fromX: x,
fromY: y + event.inputHeight, // Account for the height of the text input
color: event.userColor,
font: event.font,
text: event.text,
videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width,
videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
mirrored: mirrored,
selectedItem: selectedItem,
platform: 'web',
guid: event.guid
};
draw(new VideoRelativeCoordinateSet(update));
!resizeEvent && sendUpdate(update);
} else {
// We have a shape or custom object
// We are currently using a constant default width for shapes
var shapeLineWidth = 2;
if (selectedItem && selectedItem.points) {
client.mX = x;
client.mY = y;
switch (event.type) {
case 'mousedown':
case 'touchstart':
client.isDrawing = true;
client.dragging = true;
client.startX = x;
client.startY = y;
break;
case 'mousemove':
case 'touchmove':
if (client.dragging) {
update = {
color: resizeEvent ? event.userColor : self.userColor,
lineWidth: resizeEvent ? event.lineWidth : shapeLineWidth,
selectedItem: selectedItem
// INFO The points for scaling will get added when drawing is complete
};
draw(new VideoRelativeCoordinateSet(update), true);
}
break;
case 'mouseup':
case 'touchend':
client.isDrawing = false;
var points = selectedItem.points;
if (points.length === 2) {
update = {
id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId,
fromId: self.session.connection.connectionId,
fromX: client._startX,
fromY: client._startY,
toX: client._mX,
toY: client._mY,
color: resizeEvent ? event.userColor : self.userColor,
lineWidth: resizeEvent ? event.lineWidth : shapeLineWidth,
videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width,
videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
mirrored: mirrored,
smoothed: false,
startPoint: true,
selectedItem: selectedItem,
platform: 'web',
guid: event.guid
};
drawHistory.push(new VideoRelativeCoordinateSet(update));
!resizeEvent && sendUpdate(update);
} else {
var scale = scaleForPoints(points);
for (var i = 0; i < points.length; i++) {
var firstPoint = false;
var endPoint = false;
// Scale the points according to the difference between the start and end points
var pointX = client._startX + (scale.x * points[i][0]);
var pointY = client._startY + (scale.y * points[i][1]);
if (i === 0) {
client.lastX = pointX;
client.lastY = pointY;
firstPoint = true;
} else if (i === points.length - 1) {
endPoint = true;
}
update = {
id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId,
fromId: self.session.connection.connectionId,
fromX: client._lastX,
fromY: client._lastY,
toX: pointX,
toY: pointY,
color: resizeEvent ? event.userColor : self.userColor,
lineWidth: resizeEvent ? event.lineWidth : shapeLineWidth,
videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width,
videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
mirrored: mirrored,
smoothed: selectedItem.enableSmoothing,
startPoint: firstPoint,
endPoint: endPoint,
selectedItem: selectedItem,
platform: 'web',
guid: event.guid
};
drawHistory.push(new VideoRelativeCoordinateSet(update));
!resizeEvent && sendUpdate(update);
client.lastX = pointX; // SCALE BACK!
client.lastY = pointY;
}
draw(null);
}
client.dragging = false;
}
}
}
}
}
function guid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
addEventListeners(canvas, 'mousedown mousemove mouseup mouseout touchstart touchmove touchend', function (event) {
// Handle text annotation separately and ignore mouse movements if we're not dragging.
var istextEvent = self.selectedItem && self.selectedItem.id === 'OT_text';
var notDragging = event.type === 'mousemove' && !client.dragging;
if (istextEvent || notDragging) {
return;
}
event.preventDefault();
// Save raw events to reprocess on canvas resize
event.selectedItem = self.selectedItem;
if (event.selectedItem) {
event.canvas = {
width: canvas.width,
height: canvas.height,
offsetLeft: canvas.offsetLeft,
offsetTop: canvas.offsetTop
};
event.userColor = self.userColor;
event.lineWidth = self.lineWidth;
event.guid = guid();
eventHistory.push(event);
}
updateCanvas(event);
});
/**
* We need intermediate event handling for text annotation since the user is adding
* text to an input element before it is actually added to the canvas. The original
* click event is assigned to textEvent, which is then updated before being passed
* to updateCanvas.
*/
/** Listen for a click on the canvas. When it occurs, append a text input
* that the user can edit and listen for keydown on the enter key. When enter is
* pressed, processTextEvent is called, the input element is removed, and the text
* is appended to the canvas.
*/
var textEvent;
var textInputId = 'textAnnotation';
var commitPopId = 'commitTextPop';
var commitPopClickId = 'confirm-btn';
var dismissPopId = 'dismiss-btn';
var ignoreClicks = false;
var handleClick = function (event) {
event.preventDefault();
if (!self.selectedItem || self.selectedItem.id !== 'OT_text' || ignoreClicks) {
return;
}
ignoreClicks = true;
// Save raw events to reprocess on canvas resize
event.selectedItem = self.selectedItem;
createTextInput(event);
};
/**
* Get the value of the text input and use it to create an "event".
*/
var processTextEvent = function () {
var textInput = context.getElementById(textInputId);
var inputheight = textInput.clientHeight;
if (!textInput.value) {
textEvent = null;
return;
}
textEvent.text = textInput.value;
textEvent.font = '16px Arial';
textEvent.userColor = self.userColor;
textEvent.canvas = {
width: canvas.width,
height: canvas.height,
offsetLeft: canvas.offsetLeft,
offsetTop: canvas.offsetTop
}
eventHistory.push(textEvent);
updateCanvas(textEvent);
dismissTextAnnotation();
ignoreClicks = false;
};
var createTextInput = function (event) {
var textInput = context.createElement('input');
textInput.setAttribute('type', 'text');
textInput.style.position = 'fixed';
textInput.style.top = event.clientY + 'px';
textInput.style.left = event.clientX + 'px';
textInput.style.background = 'rgba(255,255,255, .5)';
textInput.style.width = '100px';
textInput.style.maxWidth = '200px';
textInput.style.border = '1px dashed red';
textInput.style.fontSize = '16px';
textInput.style.color = self.userColor;
textInput.style.fontFamily = 'Arial';
textInput.style.zIndex = '1001';
textInput.setAttribute('data-canvas-origin', JSON.stringify({
x: event.offsetX,
y: event.offsetY
}));
textInput.setAttribute('data-top', event.clientY - 50)
textInput.id = textInputId;
context.body.appendChild(textInput);
textInput.focus();
textInput.addEventListener('keydown', function (event) {
//If its Enter
if (event.which === 13) {
creteCommitPop(textInput)
}
})
textInput.addEventListener('blur', function () {
creteCommitPop(textInput)
})
textEvent = event;
textEvent.inputHeight = textInput.clientHeight;
ignoreClicks = true;
};
var creteCommitPop = function (textInput) {
if (context.getElementById(commitPopId)) return;
var commitPop = context.createElement('div');
commitPop.style.position = 'fixed';
commitPop.style.top = textInput.dataset.top + 'px';
commitPop.style.left = textInput.style.left;
commitPop.style.width = '200px';
commitPop.style.fontSize = '16px';
commitPop.style.fontFamily = 'Arial';
commitPop.style.zIndex = '2000';
commitPop.style.border = '1px solid grey';
commitPop.style.height = '40px';
commitPop.className = 'ots-annotation-prompt';
commitPop.id = commitPopId;
var commitPopText = context.createElement('span');
var text = context.createTextNode('Commit type?');
commitPopText.appendChild(text);
commitPop.append(commitPopText)
var commitPopClick = context.createElement('div');
commitPopClick.id = commitPopClickId;
commitPopClick.className = commitPopClickId;
var dismissDiv = context.createElement('div');
dismissDiv.id = dismissPopId;
dismissDiv.className = dismissPopId;
commitPop.appendChild(commitPopClick);
commitPop.appendChild(dismissDiv);
context.body.appendChild(commitPop);
dismissDiv.addEventListener('click', dismissTextAnnotation);
commitPopClick.addEventListener('click', function () {
processTextEvent();
});
};
var dismissTextAnnotation = function () {
if (context.getElementById(textInputId)) context.getElementById(textInputId).remove();
if (context.getElementById(commitPopId)) context.getElementById(commitPopId).remove();
textEvent = null;
ignoreClicks = false;
}
addEventListeners(canvas, 'click', handleClick);
/**
* End Handle text markup
*/
var draw = function (update, resizeEvent) {
if (!ctx) {
ctx = canvas.getContext('2d');
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.fillStyle = 'solid';
}
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Repopulate the canvas with items from drawHistory
drawHistory.forEach(function (history) {
ctx.strokeStyle = history.color;
ctx.lineWidth = history.lineWidth;
// IN