├── .gitignore ├── dist ├── css │ ├── projectorjs.min.css │ └── projectorjs.css └── js │ ├── projector.min.js │ └── projector.js ├── lib ├── css │ └── projectorjs.css └── js │ └── index.js ├── package.json ├── gulpfile.js ├── LICENSE └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /dist/css/projectorjs.min.css: -------------------------------------------------------------------------------- 1 | .vjs-big-play-button,.vjs-control-bar,.vjs-loading-spinner,.vjs-poster,.vjs-text-track-display{z-index:2}.projector-wrapper{position:relative}.projector-overlay{position:absolute;pointer-events:none;z-index:1}.projector-overlay-cover{position:absolute;width:100%;height:100%;top:0;left:0}.projector-overlay-item{position:relative;width:100%;height:100%}.projector-textbox{background:rgba(0,0,0,.8);border-radius:5px;font-size:3em;padding:10px;color:#fff} -------------------------------------------------------------------------------- /dist/css/projectorjs.css: -------------------------------------------------------------------------------- 1 | .vjs-poster, .vjs-text-track-display, 2 | .vjs-loading-spinner, .vjs-big-play-button, 3 | .vjs-control-bar { 4 | z-index: 2; 5 | } 6 | 7 | .projector-wrapper { 8 | position: relative; 9 | } 10 | 11 | .projector-overlay { 12 | position: absolute; 13 | pointer-events: none; 14 | z-index: 1; 15 | } 16 | 17 | .projector-overlay-cover { 18 | position: absolute; 19 | width: 100%; 20 | height: 100%; 21 | top: 0; 22 | left: 0; 23 | } 24 | 25 | .projector-overlay-item { 26 | position: relative; 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .projector-textbox { 32 | background: rgba(0,0,0,0.8); 33 | border-radius: 5px; 34 | font-size: 3em; 35 | padding:10px; 36 | color:white; 37 | } 38 | -------------------------------------------------------------------------------- /lib/css/projectorjs.css: -------------------------------------------------------------------------------- 1 | .vjs-poster, .vjs-text-track-display, 2 | .vjs-loading-spinner, .vjs-big-play-button, 3 | .vjs-control-bar { 4 | z-index: 2; 5 | } 6 | 7 | .projector-wrapper { 8 | position: relative; 9 | } 10 | 11 | .projector-overlay { 12 | position: absolute; 13 | pointer-events: none; 14 | z-index: 1; 15 | } 16 | 17 | .projector-overlay-cover { 18 | position: absolute; 19 | width: 100%; 20 | height: 100%; 21 | top: 0; 22 | left: 0; 23 | } 24 | 25 | .projector-overlay-item { 26 | position: relative; 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .projector-textbox { 32 | background: rgba(0,0,0,0.8); 33 | border-radius: 5px; 34 | font-size: 3em; 35 | padding:10px; 36 | color:white; 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "projectorjs", 3 | "version": "0.1.1", 4 | "description": "A small no-dependencies JavaScript library that enables the display of overlays on native HTML5 video elements, or (optionally) video elements powered by videojs.", 5 | "main": "dist/js/projector.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/FbF/projectorjs.git" 12 | }, 13 | "author": "Adam Thomas (@adamscybot)", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "gulp": "^3.8.10", 17 | "gulp-minify-css": "^0.3.11", 18 | "gulp-rename": "^1.2.0", 19 | "gulp-sourcemaps": "^1.3.0", 20 | "gulp-uglify": "^1.0.2", 21 | "gulp-umd": "^0.1.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | sourcemaps = require('gulp-sourcemaps'), 3 | uglify = require('gulp-uglify'), 4 | rename = require('gulp-rename'), 5 | minifyCSS = require('gulp-minify-css'); 6 | 7 | gulp.task('default', ['js', 'css']); 8 | 9 | gulp.task('css', function() { 10 | gulp.src('./lib/css/projectorjs.css') 11 | .pipe(gulp.dest('./dist/css/')) 12 | .pipe(rename('projectorjs.min.css')) 13 | .pipe(minifyCSS()) 14 | .pipe(gulp.dest('./dist/css/')); 15 | }); 16 | 17 | 18 | gulp.task('js', function() { 19 | gulp.src('./lib/js/index.js') 20 | .pipe(rename('projector.js')) 21 | .pipe(gulp.dest('./dist/js/')) 22 | .pipe(sourcemaps.init({loadMaps: true})) 23 | .pipe(uglify()) 24 | .pipe(sourcemaps.write('./')) 25 | .pipe(rename('projector.min.js')) 26 | .pipe(gulp.dest('./dist/js/')); 27 | }); 28 | 29 | 30 | gulp.task('watch', ['js', 'css'], function() { 31 | gulp.watch('./lib/js/**/*.js', ['js']); 32 | gulp.watch('./lib/css/**/*.js', ['css']); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adam Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/js/projector.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){if("function"==typeof define&&define.amd)define(["videojs"],e);else if("object"==typeof exports){var r=void 0;try{r=require("video.js")}catch(n){}module.exports=e(r)}else t.Projector=e(t.videojs)}(this,function(t){"use strict";var e=0,r=function(t,r,n){n=n||!1;var i=this;if(this.element=t,this.coptions=r,this.overlays=[],this.vjs=n,n)this.wrapper=t.el(),t.addEventListener=t.on,t.projector=this;else{var o=t.parentNode,s=document.createElement("div");s.setAttribute("data-projectorid",e),s.setAttribute("class","projector-wrapper"),o.replaceChild(s,t),s.appendChild(t),this.wrapper=s,t.addEventListener("loadedmetadata",function(){var e=t.offsetHeight,r=t.offsetWidth;i.wrapper.style.height=e+"px",i.wrapper.style.width=r+"px"})}r&&r.overlays&&r.overlays.forEach(function(t){t.overlay;i.addOverlay(t.overlay,t)}),t.addEventListener("fullscreenchange",function(){for(var t=document.getElementsByClassName("projector-overlay"),e=0;e=n.start&&t<=n.end||t>=n.start&&void 0===n.end)?r.__runBeginOverlay(n,t,e):n.active&&(tn.end)&&r.__runEndOverlay(n,t,e)})},t}(),o=function(){var t=function(e){var r=Object.create(t.prototype);return r.init(e),r};return t.prototype=Object.create(i.prototype),t.prototype.class="projector-textbox",t.prototype.init=function(){i.prototype.init.apply(this,arguments)},t.prototype.render=function(){var t=i.prototype.render.call(this);return t.innerHTML=this.options.text,t},t}(),s=function(){var t=function(e){var r=Object.create(t.prototype);return r.init(e),r};return t.prototype=Object.create(i.prototype),t.prototype.init=function(){i.prototype.init.apply(this,arguments)},t.prototype.render=function(){var t=i.prototype.render.call(this);return t.innerHTML=this.options.html,t},t}(),a={VERSION:"0.1.0",init:function(t,e){return"string"==typeof t&&(t=document.getElementById(t)),new r(t,e)},initVjs:function(t){return this.projector=new r(this,t,!0),this.projector}};return t&&t.plugin("projector",a.initVjs),a.Overlay=i,a.TextBox=o,a.HTMLBox=s,a}); 2 | //# sourceMappingURL=projector.js.map -------------------------------------------------------------------------------- /lib/js/index.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['videojs'], factory); 5 | } else if (typeof exports === 'object') { 6 | // Node. Does not work with strict CommonJS, but 7 | // only CommonJS-like environments that support module.exports, 8 | // like Node. 9 | var videojs = undefined; 10 | try { 11 | videojs = require('video.js'); 12 | } catch (e) {} 13 | module.exports = factory(videojs); 14 | } else { 15 | // Browser globals (root is window) 16 | root.Projector = factory(root.videojs); 17 | } 18 | }(this, function (vjs) { 19 | 'use strict'; 20 | 21 | var pCount = 0; 22 | 23 | // The projector instance attached to a single video element. 24 | var pInstance = function (element, options, vjs) { 25 | vjs = vjs || false; 26 | 27 | var that = this; 28 | 29 | this.element = element; 30 | this.coptions = options; 31 | this.overlays = []; 32 | this.vjs = vjs; 33 | 34 | if (!vjs) { 35 | // If not videojs, create wrapper for video in dom. 36 | var parent = element.parentNode; 37 | var wrapper = document.createElement('div'); 38 | wrapper.setAttribute('data-projectorid', pCount); 39 | wrapper.setAttribute('class', 'projector-wrapper'); 40 | parent.replaceChild(wrapper, element); 41 | wrapper.appendChild(element); 42 | this.wrapper = wrapper; 43 | 44 | // Set width and height of wrapper according to video. 45 | element.addEventListener("loadedmetadata", function() { 46 | var h = element.offsetHeight; 47 | var w = element.offsetWidth; 48 | that.wrapper.style.height = h+'px'; 49 | that.wrapper.style.width = w+'px'; 50 | }); 51 | 52 | } else { 53 | // Not videojs? Just get the wrapper vjs put in. 54 | this.wrapper = element.el(); 55 | 56 | // Videojs triggers its even via the on method. 57 | element.addEventListener = element.on; 58 | 59 | element.projector = this; 60 | } 61 | 62 | 63 | // Add overlays already defined in options 64 | if (options && options.overlays) { 65 | options.overlays.forEach(function (overlayDefintion) { 66 | var overlay = overlayDefintion.overlay; 67 | that.addOverlay(overlayDefintion.overlay, overlayDefintion); 68 | }); 69 | } 70 | 71 | // When size of player changes, we should set the font size as 1% of width. 72 | // TODO: Detect video size change beyond full screen. 73 | element.addEventListener("fullscreenchange", function() { 74 | var overlays = document.getElementsByClassName('projector-overlay'); 75 | for (var i = 0; i < overlays.length; i++) { 76 | overlays[i].style.fontSize = that.wrapper.offsetWidth * 0.01+'px'; 77 | } 78 | }); 79 | 80 | // Each time the time updates, we need to tell the overlays. 81 | element.addEventListener("timeupdate", function() { 82 | // If the element is seeking, we pass a "dirty" flag. 83 | // E.g. A user may want to show/hide an overlay without an 84 | // animation if the user seeked straight in. 85 | if (that.getSeeking()) { 86 | that.updateOverlays(true); 87 | } else { 88 | that.updateOverlays(); 89 | } 90 | }); 91 | 92 | pCount++; 93 | }; 94 | 95 | // Notifies each overlay that the time has been updated. 96 | pInstance.prototype.updateOverlays = function (dirty) { 97 | var that = this; 98 | var curTime = this.getCurrentTime(); 99 | this.overlays.forEach(function(overlay) { 100 | overlay.update(curTime, dirty); 101 | }); 102 | }; 103 | 104 | // Set size and position of an element according to available options 105 | pInstance.prototype.setPositionAttributes = function (el, options) { 106 | ['top', 'right', 'left', 'bottom', 'height', 'width'].forEach(function(attr) { 107 | if(options[attr]) { 108 | el.style[attr] = options[attr]; 109 | } 110 | }); 111 | } 112 | 113 | // Parse timings in shorthand form, e.g. 1-10,45-60 114 | // and then create individual timing objects for each range. 115 | pInstance.prototype.unwindTimings = function (timings) { 116 | var that = this; 117 | var newTimings = []; 118 | timings.forEach(function (timing) { 119 | if (timing.timing) { 120 | var multiTimings = timing.timing.split(','); 121 | multiTimings.forEach(function (values) { 122 | var newTiming = {}; 123 | var range = values.split('-'); 124 | newTiming.start = range[0]; 125 | newTiming.end = range[1]; 126 | newTiming.beforeBeginOverlay = timing.beforeBeginOverlay; 127 | newTiming.afterBeginOverlay = timing.afterBeginOverlay; 128 | newTiming.beforeEndOverlay = timing.beforeEndOverlay; 129 | newTiming.afterEndOverlay = timing.afterEndOverlay; 130 | newTimings.push(newTiming); 131 | }); 132 | } else { 133 | newTimings.push(timing); 134 | } 135 | }); 136 | 137 | return newTimings; 138 | } 139 | 140 | // Get the current time elapsed 141 | pInstance.prototype.getCurrentTime = function () { 142 | return this.vjs ? this.element.currentTime() : this.element.currentTime; 143 | } 144 | 145 | // Get the current seeking status 146 | pInstance.prototype.getSeeking = function () { 147 | return this.vjs ? this.element.seeking() : this.element.seeking; 148 | } 149 | 150 | 151 | // Add an overlay to the video 152 | pInstance.prototype.addOverlay = function (overlay, options) { 153 | var that = this; 154 | var div = document.createElement('div'); 155 | div.setAttribute('class', 'projector-overlay'); 156 | 157 | 158 | this.setPositionAttributes(div, options); 159 | 160 | // Each overlay has its own wrapper. 161 | div.appendChild(overlay.el); 162 | div.style.fontSize = this.wrapper.offsetWidth * 0.01+'px'; 163 | overlay.wrapper = div; 164 | 165 | // Unwind timings 166 | overlay.__timings = this.unwindTimings(options.timings); 167 | 168 | 169 | // Allow user to specify player events for start and end values. 170 | overlay.__timings.forEach(function (timing) { 171 | ['start', 'end'].forEach(function(boundary) { 172 | if (typeof timing[boundary] === 'string') { 173 | that.element.addEventListener(timing[boundary], function() { 174 | var curTime = that.getCurrentTime(); 175 | boundary === 'start' ? overlay.__runBeginOverlay(timing, curTime, false) : overlay.__runEndOverlay(timing, curTime, false); 176 | }); 177 | } 178 | }); 179 | }); 180 | 181 | // Add overlay to list of overlays attached to this video. 182 | this.overlays.push(overlay); 183 | 184 | // Insert the overlay into DOM 185 | this.wrapper.insertBefore(div, this.wrapper.childNodes[0]); 186 | 187 | return this; 188 | }; 189 | 190 | // Run a function only if target is actually a function. 191 | var runFunc = function () { 192 | var args = Array.prototype.slice.call(arguments); 193 | args.shift(); 194 | if (typeof(arguments[0]) == "function") { 195 | arguments[0].apply(this, args); 196 | } 197 | }; 198 | 199 | // Base overlay class. All other overlays should extend from this and call 200 | // these methods before doing their own work. 201 | var Overlay = (function(){ 202 | var exports = function(options) { 203 | var ret = Object.create(exports.prototype); 204 | ret.init(options); 205 | return ret; 206 | }; 207 | 208 | exports.prototype = {}; 209 | exports.prototype.options = {}; 210 | exports.prototype.element = undefined; 211 | exports.prototype.class = ''; 212 | exports.prototype.init = function(options) { 213 | this.options = options || {}; 214 | this.options.attrs = this.options.attrs || {}; 215 | this.el = this.render(); 216 | return this; 217 | }; 218 | 219 | // Builds the overlay DOM 220 | exports.prototype.render = function () { 221 | var that = this; 222 | var div = document.createElement("div"); 223 | div.style.display = 'none'; 224 | div.setAttribute('class', that.class+' projector-overlay-item'); 225 | 226 | // Allow user to set attributes in options object. 227 | // Copy them over to style object here. 228 | for (var key in this.options.attrs) { 229 | if (key === 'class') { 230 | div.setAttribute(key, that.class ? that.class+' '+this.options.attrs[key] : this.options.attrs[key]); 231 | } else { 232 | div.setAttribute(key, this.options.attrs[key]); 233 | } 234 | } 235 | return div; 236 | } 237 | 238 | // Check if the overlay is currently active 239 | exports.prototype.isActive = function (timing, curTime, dirty) { 240 | return this.__timings.some(function(timing) { 241 | return timing.active === true; 242 | }); 243 | } 244 | 245 | 246 | // Triggers the userland functions to start overlay. 247 | // Run the overlays start overlay logic. 248 | exports.prototype.__runBeginOverlay = function (timing, curTime, dirty) { 249 | var that = this; 250 | runFunc(timing.beforeBeginOverlay, this, curTime, dirty); 251 | this.beginOverlay(function() { 252 | runFunc(timing.afterBeginOverlay, that, curTime, dirty); 253 | }, curTime, dirty); 254 | 255 | timing.active = true; 256 | } 257 | 258 | // Triggers the userland functions to end overlay. 259 | // Run the overlays end overlay logic. 260 | exports.prototype.__runEndOverlay = function (timing, curTime, dirty) { 261 | var that = this; 262 | runFunc(timing.beforeEndOverlay, this, curTime, dirty); 263 | this.endOverlay(function() { 264 | runFunc(timing.afterEndOverlay, that, curTime, dirty); 265 | }, curTime, dirty); 266 | timing.active = false; 267 | } 268 | 269 | // The function that executes when the overlay starts. 270 | // DOM should be edited here. 271 | exports.prototype.beginOverlay = function (cb, curTime, dirty) { 272 | this.el.style.display = 'block'; 273 | cb(); 274 | } 275 | 276 | // The function that executes when the overlay ends. 277 | // DOM should be edited here. 278 | exports.prototype.endOverlay = function (cb, curTime, dirty) { 279 | this.el.style.display = 'none'; 280 | cb(); 281 | } 282 | 283 | // The function that decides if the overlay should be started or ended. 284 | exports.prototype.update = function (curTime, dirty) { 285 | var that = this; 286 | 287 | dirty = dirty || false; 288 | 289 | this.__timings.forEach(function (timing) { 290 | if (!timing.active && ((curTime >= timing.start && curTime <= timing.end) || (curTime >= timing.start && timing.end === undefined))) { 291 | that.__runBeginOverlay(timing, curTime, dirty); 292 | } else if (timing.active && (curTime < timing.start || curTime > timing.end)){ 293 | that.__runEndOverlay(timing, curTime, dirty); 294 | } 295 | }); 296 | }; 297 | 298 | return exports; 299 | })(); 300 | 301 | // Built in textbox overlay. 302 | var TextBox = (function () { 303 | var exports = function(options) { 304 | var ret = Object.create(exports.prototype); 305 | ret.init(options); 306 | return ret; 307 | }; 308 | 309 | exports.prototype = Object.create(Overlay.prototype); 310 | exports.prototype.class = 'projector-textbox'; 311 | exports.prototype.init = function() { 312 | Overlay.prototype.init.apply(this, arguments); 313 | }; 314 | 315 | exports.prototype.render = function() { 316 | var div = Overlay.prototype.render.call(this); 317 | div.innerHTML = this.options.text; 318 | return div; 319 | }; 320 | 321 | return exports; 322 | })(); 323 | 324 | // Built in HTML box overlay. 325 | var HTMLBox = (function () { 326 | var exports = function(options) { 327 | var ret = Object.create(exports.prototype); 328 | ret.init(options); 329 | return ret; 330 | }; 331 | 332 | exports.prototype = Object.create(Overlay.prototype); 333 | exports.prototype.init = function() { 334 | Overlay.prototype.init.apply(this, arguments); 335 | }; 336 | 337 | exports.prototype.render = function() { 338 | var div = Overlay.prototype.render.call(this); 339 | div.innerHTML = this.options.html; 340 | return div; 341 | }; 342 | 343 | return exports; 344 | })(); 345 | 346 | // The primary export of the module. Provides init functions. 347 | var Projector = { 348 | VERSION: '0.1.0', 349 | // Given an element or ID, create a new projector instance. 350 | init: function (element, options) { 351 | if (typeof element === "string") { 352 | element = document.getElementById(element); 353 | } 354 | return new pInstance(element, options); 355 | }, 356 | // Create a new projector instance on top of a videojs instance. 357 | initVjs: function (options) { 358 | this.projector = new pInstance(this, options, true); 359 | return this.projector; 360 | } 361 | }; 362 | 363 | // Register as videojs plugin 364 | if (vjs) { 365 | vjs.plugin('projector', Projector.initVjs); 366 | } 367 | 368 | Projector.Overlay = Overlay; 369 | Projector.TextBox = TextBox; 370 | Projector.HTMLBox = HTMLBox; 371 | 372 | return Projector; 373 | 374 | })); 375 | -------------------------------------------------------------------------------- /dist/js/projector.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['videojs'], factory); 5 | } else if (typeof exports === 'object') { 6 | // Node. Does not work with strict CommonJS, but 7 | // only CommonJS-like environments that support module.exports, 8 | // like Node. 9 | var videojs = undefined; 10 | try { 11 | videojs = require('video.js'); 12 | } catch (e) {} 13 | module.exports = factory(videojs); 14 | } else { 15 | // Browser globals (root is window) 16 | root.Projector = factory(root.videojs); 17 | } 18 | }(this, function (vjs) { 19 | 'use strict'; 20 | 21 | var pCount = 0; 22 | 23 | // The projector instance attached to a single video element. 24 | var pInstance = function (element, options, vjs) { 25 | vjs = vjs || false; 26 | 27 | var that = this; 28 | 29 | this.element = element; 30 | this.coptions = options; 31 | this.overlays = []; 32 | this.vjs = vjs; 33 | 34 | if (!vjs) { 35 | // If not videojs, create wrapper for video in dom. 36 | var parent = element.parentNode; 37 | var wrapper = document.createElement('div'); 38 | wrapper.setAttribute('data-projectorid', pCount); 39 | wrapper.setAttribute('class', 'projector-wrapper'); 40 | parent.replaceChild(wrapper, element); 41 | wrapper.appendChild(element); 42 | this.wrapper = wrapper; 43 | 44 | // Set width and height of wrapper according to video. 45 | element.addEventListener("loadedmetadata", function() { 46 | var h = element.offsetHeight; 47 | var w = element.offsetWidth; 48 | that.wrapper.style.height = h+'px'; 49 | that.wrapper.style.width = w+'px'; 50 | }); 51 | 52 | } else { 53 | // Not videojs? Just get the wrapper vjs put in. 54 | this.wrapper = element.el(); 55 | 56 | // Videojs triggers its even via the on method. 57 | element.addEventListener = element.on; 58 | 59 | element.projector = this; 60 | } 61 | 62 | 63 | // Add overlays already defined in options 64 | if (options && options.overlays) { 65 | options.overlays.forEach(function (overlayDefintion) { 66 | var overlay = overlayDefintion.overlay; 67 | that.addOverlay(overlayDefintion.overlay, overlayDefintion); 68 | }); 69 | } 70 | 71 | // When size of player changes, we should set the font size as 1% of width. 72 | // TODO: Detect video size change beyond full screen. 73 | element.addEventListener("fullscreenchange", function() { 74 | var overlays = document.getElementsByClassName('projector-overlay'); 75 | for (var i = 0; i < overlays.length; i++) { 76 | overlays[i].style.fontSize = that.wrapper.offsetWidth * 0.01+'px'; 77 | } 78 | }); 79 | 80 | // Each time the time updates, we need to tell the overlays. 81 | element.addEventListener("timeupdate", function() { 82 | // If the element is seeking, we pass a "dirty" flag. 83 | // E.g. A user may want to show/hide an overlay without an 84 | // animation if the user seeked straight in. 85 | if (that.getSeeking()) { 86 | that.updateOverlays(true); 87 | } else { 88 | that.updateOverlays(); 89 | } 90 | }); 91 | 92 | pCount++; 93 | }; 94 | 95 | // Notifies each overlay that the time has been updated. 96 | pInstance.prototype.updateOverlays = function (dirty) { 97 | var that = this; 98 | var curTime = this.getCurrentTime(); 99 | this.overlays.forEach(function(overlay) { 100 | overlay.update(curTime, dirty); 101 | }); 102 | }; 103 | 104 | // Set size and position of an element according to available options 105 | pInstance.prototype.setPositionAttributes = function (el, options) { 106 | ['top', 'right', 'left', 'bottom', 'height', 'width'].forEach(function(attr) { 107 | if(options[attr]) { 108 | el.style[attr] = options[attr]; 109 | } 110 | }); 111 | } 112 | 113 | // Parse timings in shorthand form, e.g. 1-10,45-60 114 | // and then create individual timing objects for each range. 115 | pInstance.prototype.unwindTimings = function (timings) { 116 | var that = this; 117 | var newTimings = []; 118 | timings.forEach(function (timing) { 119 | if (timing.timing) { 120 | var multiTimings = timing.timing.split(','); 121 | multiTimings.forEach(function (values) { 122 | var newTiming = {}; 123 | var range = values.split('-'); 124 | newTiming.start = range[0]; 125 | newTiming.end = range[1]; 126 | newTiming.beforeBeginOverlay = timing.beforeBeginOverlay; 127 | newTiming.afterBeginOverlay = timing.afterBeginOverlay; 128 | newTiming.beforeEndOverlay = timing.beforeEndOverlay; 129 | newTiming.afterEndOverlay = timing.afterEndOverlay; 130 | newTimings.push(newTiming); 131 | }); 132 | } else { 133 | newTimings.push(timing); 134 | } 135 | }); 136 | 137 | return newTimings; 138 | } 139 | 140 | // Get the current time elapsed 141 | pInstance.prototype.getCurrentTime = function () { 142 | return this.vjs ? this.element.currentTime() : this.element.currentTime; 143 | } 144 | 145 | // Get the current seeking status 146 | pInstance.prototype.getSeeking = function () { 147 | return this.vjs ? this.element.seeking() : this.element.seeking; 148 | } 149 | 150 | 151 | // Add an overlay to the video 152 | pInstance.prototype.addOverlay = function (overlay, options) { 153 | var that = this; 154 | var div = document.createElement('div'); 155 | div.setAttribute('class', 'projector-overlay'); 156 | 157 | 158 | this.setPositionAttributes(div, options); 159 | 160 | // Each overlay has its own wrapper. 161 | div.appendChild(overlay.el); 162 | div.style.fontSize = this.wrapper.offsetWidth * 0.01+'px'; 163 | overlay.wrapper = div; 164 | 165 | // Unwind timings 166 | overlay.__timings = this.unwindTimings(options.timings); 167 | 168 | 169 | // Allow user to specify player events for start and end values. 170 | overlay.__timings.forEach(function (timing) { 171 | ['start', 'end'].forEach(function(boundary) { 172 | if (typeof timing[boundary] === 'string') { 173 | that.element.addEventListener(timing[boundary], function() { 174 | var curTime = that.getCurrentTime(); 175 | boundary === 'start' ? overlay.__runBeginOverlay(timing, curTime, false) : overlay.__runEndOverlay(timing, curTime, false); 176 | }); 177 | } 178 | }); 179 | }); 180 | 181 | // Add overlay to list of overlays attached to this video. 182 | this.overlays.push(overlay); 183 | 184 | // Insert the overlay into DOM 185 | this.wrapper.insertBefore(div, this.wrapper.childNodes[0]); 186 | 187 | return this; 188 | }; 189 | 190 | // Run a function only if target is actually a function. 191 | var runFunc = function () { 192 | var args = Array.prototype.slice.call(arguments); 193 | args.shift(); 194 | if (typeof(arguments[0]) == "function") { 195 | arguments[0].apply(this, args); 196 | } 197 | }; 198 | 199 | // Base overlay class. All other overlays should extend from this and call 200 | // these methods before doing their own work. 201 | var Overlay = (function(){ 202 | var exports = function(options) { 203 | var ret = Object.create(exports.prototype); 204 | ret.init(options); 205 | return ret; 206 | }; 207 | 208 | exports.prototype = {}; 209 | exports.prototype.options = {}; 210 | exports.prototype.element = undefined; 211 | exports.prototype.class = ''; 212 | exports.prototype.init = function(options) { 213 | this.options = options || {}; 214 | this.options.attrs = this.options.attrs || {}; 215 | this.el = this.render(); 216 | return this; 217 | }; 218 | 219 | // Builds the overlay DOM 220 | exports.prototype.render = function () { 221 | var that = this; 222 | var div = document.createElement("div"); 223 | div.style.display = 'none'; 224 | div.setAttribute('class', that.class+' projector-overlay-item'); 225 | 226 | // Allow user to set attributes in options object. 227 | // Copy them over to style object here. 228 | for (var key in this.options.attrs) { 229 | if (key === 'class') { 230 | div.setAttribute(key, that.class ? that.class+' '+this.options.attrs[key] : this.options.attrs[key]); 231 | } else { 232 | div.setAttribute(key, this.options.attrs[key]); 233 | } 234 | } 235 | return div; 236 | } 237 | 238 | // Check if the overlay is currently active 239 | exports.prototype.isActive = function (timing, curTime, dirty) { 240 | return this.__timings.some(function(timing) { 241 | return timing.active === true; 242 | }); 243 | } 244 | 245 | 246 | // Triggers the userland functions to start overlay. 247 | // Run the overlays start overlay logic. 248 | exports.prototype.__runBeginOverlay = function (timing, curTime, dirty) { 249 | var that = this; 250 | runFunc(timing.beforeBeginOverlay, this, curTime, dirty); 251 | this.beginOverlay(function() { 252 | runFunc(timing.afterBeginOverlay, that, curTime, dirty); 253 | }, curTime, dirty); 254 | 255 | timing.active = true; 256 | } 257 | 258 | // Triggers the userland functions to end overlay. 259 | // Run the overlays end overlay logic. 260 | exports.prototype.__runEndOverlay = function (timing, curTime, dirty) { 261 | var that = this; 262 | runFunc(timing.beforeEndOverlay, this, curTime, dirty); 263 | this.endOverlay(function() { 264 | runFunc(timing.afterEndOverlay, that, curTime, dirty); 265 | }, curTime, dirty); 266 | timing.active = false; 267 | } 268 | 269 | // The function that executes when the overlay starts. 270 | // DOM should be edited here. 271 | exports.prototype.beginOverlay = function (cb, curTime, dirty) { 272 | this.el.style.display = 'block'; 273 | cb(); 274 | } 275 | 276 | // The function that executes when the overlay ends. 277 | // DOM should be edited here. 278 | exports.prototype.endOverlay = function (cb, curTime, dirty) { 279 | this.el.style.display = 'none'; 280 | cb(); 281 | } 282 | 283 | // The function that decides if the overlay should be started or ended. 284 | exports.prototype.update = function (curTime, dirty) { 285 | var that = this; 286 | 287 | dirty = dirty || false; 288 | 289 | this.__timings.forEach(function (timing) { 290 | if (!timing.active && ((curTime >= timing.start && curTime <= timing.end) || (curTime >= timing.start && timing.end === undefined))) { 291 | that.__runBeginOverlay(timing, curTime, dirty); 292 | } else if (timing.active && (curTime < timing.start || curTime > timing.end)){ 293 | that.__runEndOverlay(timing, curTime, dirty); 294 | } 295 | }); 296 | }; 297 | 298 | return exports; 299 | })(); 300 | 301 | // Built in textbox overlay. 302 | var TextBox = (function () { 303 | var exports = function(options) { 304 | var ret = Object.create(exports.prototype); 305 | ret.init(options); 306 | return ret; 307 | }; 308 | 309 | exports.prototype = Object.create(Overlay.prototype); 310 | exports.prototype.class = 'projector-textbox'; 311 | exports.prototype.init = function() { 312 | Overlay.prototype.init.apply(this, arguments); 313 | }; 314 | 315 | exports.prototype.render = function() { 316 | var div = Overlay.prototype.render.call(this); 317 | div.innerHTML = this.options.text; 318 | return div; 319 | }; 320 | 321 | return exports; 322 | })(); 323 | 324 | // Built in HTML box overlay. 325 | var HTMLBox = (function () { 326 | var exports = function(options) { 327 | var ret = Object.create(exports.prototype); 328 | ret.init(options); 329 | return ret; 330 | }; 331 | 332 | exports.prototype = Object.create(Overlay.prototype); 333 | exports.prototype.init = function() { 334 | Overlay.prototype.init.apply(this, arguments); 335 | }; 336 | 337 | exports.prototype.render = function() { 338 | var div = Overlay.prototype.render.call(this); 339 | div.innerHTML = this.options.html; 340 | return div; 341 | }; 342 | 343 | return exports; 344 | })(); 345 | 346 | // The primary export of the module. Provides init functions. 347 | var Projector = { 348 | VERSION: '0.1.0', 349 | // Given an element or ID, create a new projector instance. 350 | init: function (element, options) { 351 | if (typeof element === "string") { 352 | element = document.getElementById(element); 353 | } 354 | return new pInstance(element, options); 355 | }, 356 | // Create a new projector instance on top of a videojs instance. 357 | initVjs: function (options) { 358 | this.projector = new pInstance(this, options, true); 359 | return this.projector; 360 | } 361 | }; 362 | 363 | // Register as videojs plugin 364 | if (vjs) { 365 | vjs.plugin('projector', Projector.initVjs); 366 | } 367 | 368 | Projector.Overlay = Overlay; 369 | Projector.TextBox = TextBox; 370 | Projector.HTMLBox = HTMLBox; 371 | 372 | return Projector; 373 | 374 | })); 375 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Projector.js 2 | 3 | A small no-dependencies JavaScript library that enables the display of overlays on native HTML5 video elements, or (optionally) video elements powered by [videojs](https://github.com/videojs/video.js/). Overlays can be triggered via time ranges or events. A set of useful overlays is (WIP) included to cover common use cases. 4 | 5 | ## Getting Started 6 | ### Raw browser 7 | 8 | You need to include both the javascript and css files from the project. Both of these are located in the dist directory. 9 | 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | 19 | 34 | 35 | ``` 36 | 37 | Or you can use projector.js with videojs: 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 70 | 71 | 72 | 73 | ``` 74 | 75 | ### Other require libs 76 | 77 | Projector.js uses the [UMD](https://github.com/umdjs/umd) pattern and so should work with both [require.js](https://github.com/jrburke/requirejs) and [browserify](https://github.com/substack/node-browserify). 78 | 79 | ## Considerations 80 | 81 | ### Overlays block clicks 82 | 83 | A common issue with video overlays is that one overlay blocks events from hitting other overlays that are underneath. E.g, you have 2 overlays that both provide user input, but because one is on top, the other is not accessible. 84 | 85 | Projector.js attempts to counter this by assigning the [```pointer-events: none;```](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) CSS property to overlay wrappers (```.projector-overlay```). This allows child elements to be clickable, but the containing overlay div (which covers the whole video) allows events to "pass-through." However, this is a new property and support is limited on older browsers. It is of note that it is unsupported on < IE11. 86 | 87 | A good way to avoid this issue is to keep all individual elements on a separate overlay. I.e. avoid having 2 separate divs in a single overlay. This would cause the overlay container to stretch to fit both, introducing transparent white space which would block clicks if on a browser that does not support pointer-events. 88 | 89 | ### Scaling up videos 90 | 91 | It is important to use percentage values for dimensions/margins/paddings if you want your overlay to scale up if the video size changes (I.e. full screen). 92 | 93 | Projector.js sets the font-size of the overlay wrappers to 1% of the width of the video. This should allow you to use em or percentage values for font-size. You could also use [viewport units](http://caniuse.com/#feat=viewport-units) if you don't care about older browsers. 94 | 95 | ### Sub-second accuracy 96 | 97 | Different browsers report the time elapsed at different intervals. This unfortunately limits the use of milliseconds. Projector.js does not currently support milliseconds, but is being considered for inclusion in the library via setTimeout() hacks. Regardless, **exact** precision is not easily achievable and is out of scope. 98 | 99 | ### Dirty triggers 100 | 101 | Projector.js ensures your overlay is active between the user-specified times. Consequentially, as the users elapsed video time breaches a user specified time period, the relevant overlay is triggered to be active. 102 | 103 | However, the user may have seeked directly into this time period, and not naturally watched the video in real time. For this reason, callbacks such as `beforeBeginOverlay` are passed a "dirty" flag when the user has seeked into one of the specified time ranges. This allows the developer to disable certain affects when the player is used in this way, e.g. fade in animations. 104 | 105 | ## Core API 106 | 107 | ### Projector object 108 | 109 | The projector object provides the intialization function to allow projectorjs to attach to your video element. 110 | 111 | #### init(element, [options]) 112 | Type: `function` 113 | Arguments: `element`, `options` 114 | Returns: `pInstance` 115 | 116 | ##### options 117 | 118 | Instead of using the `addOverlay` function of a `pInstance`, you can use the shorthand method to specify all of your overlays in the initial options object. Please see the [`addOverlay`](#addOverlay) documentation for full description of options. 119 | 120 | 121 | ```javascript 122 | var p = Projector.init(document.getElementById('video'), { 123 | overlays: [ 124 | { 125 | overlay: Projector.TextBox({text: "Test"}), 126 | left:'40%', 127 | top: '50%', 128 | timings: [ 129 | { 130 | timing: '8-14' 131 | } 132 | ] 133 | } 134 | ] 135 | }); 136 | 137 | // Add more overlays programmatically 138 | p.addOverlay(...) 139 | ``` 140 | 141 | Init can also be called via video.js intialization. 142 | 143 | ```javascript 144 | var p = videojs("video").projector({ 145 | // Options 146 | ... 147 | }); 148 | 149 | // Add more overlays programmatically 150 | p.addOverlay(...) 151 | ``` 152 | 153 | ### pInstance object 154 | 155 | The pInstance (projector instance) object is attached to a single HTML or videojs video element. It exposes functions to attach overlays. 156 | 157 | #### addOverlay 158 | Type: `function` 159 | Arguments: `overlay`, `options` 160 | Returns: `pInstance` 161 | 162 | Attach an overlay object (see [Bundled overlays](#bundled-overlays)) with the given options. 163 | 164 | ##### Options 165 | 166 | ###### left 167 | Type: `string` 168 | Default: `undefined` (effectively auto) 169 | 170 | The position of the overlay relative to the left boundary. Include units. 171 | 172 | ###### right 173 | Type: `string` 174 | Default: `undefined` (effectively auto) 175 | 176 | The position of the overlay relative to the right boundary. Include units. 177 | 178 | ###### top 179 | Type: `string` 180 | Default: `undefined` (effectively auto) 181 | 182 | The position of the overlay relative to the top boundary. Include units. 183 | 184 | ###### bottom 185 | Type: `string` 186 | Default: `undefined` (effectively auto) 187 | 188 | The position of the overlay relative to the bottom boundary. Include units. 189 | 190 | ###### height 191 | Type: `string` 192 | Default: `undefined` (effectively auto) 193 | 194 | The width of the overlay. Include units. 195 | 196 | ###### width 197 | Type: `string` 198 | Default: `undefined` 199 | 200 | The height of the overlay. Include units. 201 | 202 | ###### timings 203 | Type: `array` 204 | Default: `[]` 205 | 206 | An array of timing singleton objects. See [Timing object](#timing-object). 207 | 208 | ### Timing object 209 | 210 | A user-provided singleton object passed as part of the options for `Projector.init` or `pInstance.addOverlay`. 211 | 212 | #### timing 213 | Type: `string` 214 | Default: `undefined` 215 | Example: `"3-6,6-10,20"` 216 | 217 | A shorthand string representing time periods for the start and end of overlays. This string is a comma separated list of 218 | 219 | * min/max integer ranges (seconds) delimited by hyphens. 220 | * or singular integers (seconds). 221 | 222 | If a single integer 'x' is specified, this is processed as a timing that starts at 'x' seconds, with no end. 223 | 224 | Overrides start and end values if set. 225 | 226 | #### start 227 | Type: `integer|string` 228 | Default: `undefined` 229 | 230 | The number of seconds elapsed upon which the overlay starts. Alternatively, an event name such as play or pause. Any event fired on the video element (native or videojs) works. 231 | 232 | #### end 233 | Type: `integer|string` 234 | Default: `undefined` 235 | 236 | The number of seconds elapsed upon which the overlay ends. Alternatively, an event name such as play or pause. Any event fired on the video element (native or videojs) works. 237 | 238 | 239 | #### beforeBeginOverlay 240 | Type: `function` 241 | Default: `undefined` 242 | Arguments: `overlay`, `currentTime`, `dirtyTrigger` 243 | 244 | A callback called before the overlay begins. This function is passed the overlay object, the current time elapsed and a "dirty trigger flag" (see [Dirty triggers](#dirty-triggers)). 245 | 246 | #### afterBeginOverlay 247 | Type: `function` 248 | Default: `undefined` 249 | Arguments: `overlay`, `currentTime`, `dirtyTrigger` 250 | 251 | A callback called after the overlay begins. This function is passed the overlay object, the current time elapsed and a "dirty trigger flag" (see [Dirty triggers](#dirty-triggers)). 252 | 253 | #### beforeEndOverlay 254 | Type: `function` 255 | Default: `undefined` 256 | Arguments: `overlay`, `currentTime`, `dirtyTrigger` 257 | 258 | A callback called before the overlay ends. This function is passed the overlay object, the current time elapsed and a "dirty trigger flag" (see [Dirty triggers](#dirty-triggers)). 259 | 260 | #### afterEndOverlay 261 | Type: `function` 262 | Default: `undefined` 263 | Arguments: `overlay`, `currentTime`, `dirtyTrigger` 264 | 265 | A callback called after the overlay ends. This function is passed the overlay object, the current time elapsed and a "dirty trigger flag" (see [Dirty triggers](#dirty-triggers)). 266 | 267 | 268 | ### Overlay object 269 | 270 | The overlay object represents the overlay itself. [Built in overlays](#bundled-overlays) and custom overlays extend this object. 271 | 272 | #### init 273 | Type: `function` 274 | Arguments: `options` 275 | Returns: `boolean` 276 | 277 | Sets up the overlay. No need to call manually as is invoked upon overlay creation. 278 | 279 | ```javascript 280 | // Creates overlay, calls init, and returns overlay object 281 | var o = Projector.TextBox(); 282 | ``` 283 | 284 | ##### Options 285 | 286 | ###### attrs 287 | Type: `object` 288 | 289 | A set of attributes that you wish to be copied over to the styles of the overlay. 290 | 291 | ```javascript 292 | // Creates overlay, calls init, and returns overlay object 293 | var o = Projector.TextBox({ 294 | attrs: { 295 | class: 'custom-class', 296 | id: 'custom-id' 297 | } 298 | }); 299 | ``` 300 | #### render 301 | Type: `function` 302 | Return: `HTMLElement` 303 | 304 | Builds and returns the DOM element representing the overlay. Override in custom overlays. 305 | 306 | #### beginOverlay 307 | Type: `function` 308 | Arguments: `cb`, `curTime`, `dirty` 309 | 310 | Main function to trigger the overlay "on". Default is to unhide DOM element. Override in custom overlays. Execute the callback when done. 311 | 312 | #### endOverlay 313 | Type: `function` 314 | Arguments: `cb`, `curTime`, `dirty` 315 | 316 | Main function to trigger the overlay "off". Default is to hide DOM element. Override in custom overlays. Execute the callback when done. 317 | 318 | #### update 319 | Type: `function` 320 | Arguments: `curTime`, `dirty` 321 | 322 | Given an elapsed time in seconds and the dirty flag (see [Dirty triggers](#dirty-triggers)), check if the overlay should show (according to passed elapsed time). Can override in custom overlays if you wish to trigger your overlay on more complicated logic than time ranges. This function is called each time the "timeupdate" event is triggered on the video. 323 | 324 | #### isActive 325 | Type: `function` 326 | Returns: `boolean` 327 | 328 | Returns true if the overlay is in an active state. 329 | 330 | 331 | #### element 332 | Type: `HTMLElement` 333 | 334 | The HTMLElement representing the overlay. 335 | 336 | #### class 337 | Type: `string` 338 | 339 | The default class of an overlay. Override in custom overlays. 340 | 341 | 342 | ## Bundled overlays 343 | 344 | Projector.js comes with some bundled overlays to cover the most common use cases (WIP). 345 | 346 | ### Projector.TextBox(options) 347 | 348 | #### Options 349 | 350 | #####text 351 | Type: `string` 352 | 353 | The text to display in the text box. 354 | 355 | ### Projector.HTMLBox(options) 356 | 357 | #### Options 358 | 359 | ##### html 360 | Type: `string|HTMLElement` 361 | 362 | The text to display in the text box. 363 | 364 | 365 | ## Building overlays 366 | 367 | Extend the `Projector.Overlay` object. Always call the super function within each method unless you know what you are doing. This example creates a text box overlay that fades in: 368 | 369 | ```javascript 370 | var TextBox = (function () { 371 | var exports = function(options) { 372 | var ret = Object.create(exports.prototype); 373 | ret.init(options); 374 | return ret; 375 | }; 376 | 377 | exports.prototype = Object.create(Overlay.prototype); 378 | exports.prototype.class = 'projector-textbox-fade'; 379 | exports.prototype.init = function() { 380 | Overlay.prototype.init.apply(this, arguments); 381 | }; 382 | 383 | exports.prototype.beginOverlay = function(cb) { 384 | Overlay.prototype.beginOverlay.apply(this, arguments); 385 | $(this.element).fadeIn(); 386 | }; 387 | 388 | exports.prototype.endOverlay = function(cb) { 389 | Overlay.prototype.endOverlay.apply(this, arguments); 390 | $(this.element).fadeOut(); 391 | }; 392 | 393 | exports.prototype.render = function() { 394 | var div = Overlay.prototype.render.call(this); 395 | div.innerHTML = this.options.text; 396 | return div; 397 | }; 398 | 399 | return exports; 400 | })(); 401 | ``` 402 | 403 | ## Build 404 | 405 | To build, use [gulp](https://github.com/gulpjs/gulp/) and simply run `gulp` in the project root. 406 | 407 | ## Release History 408 | - 0.1.1: Strip out demos (move to gh-pages) and minor fixes. 409 | - 0.1.0: Initial release. 410 | --------------------------------------------------------------------------------