github.com/insionng/yougam@v0.0.0-20170714101924-2bc18d833463/public/libs/video-js/video.dev.js (about) 1 /** 2 * @fileoverview Main function src. 3 */ 4 5 // HTML5 Shiv. Must be in <head> to support older browsers. 6 document.createElement('video'); 7 document.createElement('audio'); 8 document.createElement('track'); 9 10 /** 11 * Doubles as the main function for users to create a player instance and also 12 * the main library object. 13 * 14 * **ALIASES** videojs, _V_ (deprecated) 15 * 16 * The `vjs` function can be used to initialize or retrieve a player. 17 * 18 * var myPlayer = vjs('my_video_id'); 19 * 20 * @param {String|Element} id Video element or video element ID 21 * @param {Object=} options Optional options object for config/settings 22 * @param {Function=} ready Optional ready callback 23 * @return {vjs.Player} A player instance 24 * @namespace 25 */ 26 var vjs = function(id, options, ready){ 27 var tag; // Element of ID 28 29 // Allow for element or ID to be passed in 30 // String ID 31 if (typeof id === 'string') { 32 33 // Adjust for jQuery ID syntax 34 if (id.indexOf('#') === 0) { 35 id = id.slice(1); 36 } 37 38 // If a player instance has already been created for this ID return it. 39 if (vjs.players[id]) { 40 return vjs.players[id]; 41 42 // Otherwise get element for ID 43 } else { 44 tag = vjs.el(id); 45 } 46 47 // ID is a media element 48 } else { 49 tag = id; 50 } 51 52 // Check for a useable element 53 if (!tag || !tag.nodeName) { // re: nodeName, could be a box div also 54 throw new TypeError('The element or ID supplied is not valid. (videojs)'); // Returns 55 } 56 57 // Element may have a player attr referring to an already created player instance. 58 // If not, set up a new player and return the instance. 59 return tag['player'] || new vjs.Player(tag, options, ready); 60 }; 61 62 // Extended name, also available externally, window.videojs 63 var videojs = window['videojs'] = vjs; 64 65 // CDN Version. Used to target right flash swf. 66 vjs.CDN_VERSION = '4.8'; 67 vjs.ACCESS_PROTOCOL = ('https:' == document.location.protocol ? 'https://' : 'http://'); 68 69 /** 70 * Global Player instance options, surfaced from vjs.Player.prototype.options_ 71 * vjs.options = vjs.Player.prototype.options_ 72 * All options should use string keys so they avoid 73 * renaming by closure compiler 74 * @type {Object} 75 */ 76 vjs.options = { 77 // Default order of fallback technology 78 'techOrder': ['html5','flash'], 79 // techOrder: ['flash','html5'], 80 81 'html5': {}, 82 'flash': {}, 83 84 // Default of web browser is 300x150. Should rely on source width/height. 85 'width': 300, 86 'height': 150, 87 // defaultVolume: 0.85, 88 'defaultVolume': 0.00, // The freakin seaguls are driving me crazy! 89 90 // default playback rates 91 'playbackRates': [], 92 // Add playback rate selection by adding rates 93 // 'playbackRates': [0.5, 1, 1.5, 2], 94 95 // default inactivity timeout 96 'inactivityTimeout': 2000, 97 98 // Included control sets 99 'children': { 100 'mediaLoader': {}, 101 'posterImage': {}, 102 'textTrackDisplay': {}, 103 'loadingSpinner': {}, 104 'bigPlayButton': {}, 105 'controlBar': {}, 106 'errorDisplay': {} 107 }, 108 109 'language': document.getElementsByTagName('html')[0].getAttribute('lang') || navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language || 'en', 110 111 // locales and their language translations 112 'languages': {}, 113 114 // Default message to show when a video cannot be played. 115 'notSupportedMessage': 'No compatible source was found for this video.' 116 }; 117 118 // Set CDN Version of swf 119 // The added (+) blocks the replace from changing this 4.8 string 120 if (vjs.CDN_VERSION !== 'GENERATED'+'_CDN_VSN') { 121 videojs.options['flash']['swf'] = vjs.ACCESS_PROTOCOL + 'vjs.zencdn.net/'+vjs.CDN_VERSION+'/video-js.swf'; 122 } 123 124 /** 125 * Utility function for adding languages to the default options. Useful for 126 * amending multiple language support at runtime. 127 * 128 * Example: vjs.addLanguage('es', {'Hello':'Hola'}); 129 * 130 * @param {String} code The language code or dictionary property 131 * @param {Object} data The data values to be translated 132 * @return {Object} The resulting global languages dictionary object 133 */ 134 vjs.addLanguage = function(code, data){ 135 if(vjs.options['languages'][code] !== undefined) { 136 vjs.options['languages'][code] = vjs.util.mergeOptions(vjs.options['languages'][code], data); 137 } else { 138 vjs.options['languages'][code] = data; 139 } 140 return vjs.options['languages']; 141 }; 142 143 /** 144 * Global player list 145 * @type {Object} 146 */ 147 vjs.players = {}; 148 149 /*! 150 * Custom Universal Module Definition (UMD) 151 * 152 * Video.js will never be a non-browser lib so we can simplify UMD a bunch and 153 * still support requirejs and browserify. This also needs to be closure 154 * compiler compatible, so string keys are used. 155 */ 156 if (typeof define === 'function' && define['amd']) { 157 define([], function(){ return videojs; }); 158 159 // checking that module is an object too because of umdjs/umd#35 160 } else if (typeof exports === 'object' && typeof module === 'object') { 161 module['exports'] = videojs; 162 } 163 /** 164 * Core Object/Class for objects that use inheritance + contstructors 165 * 166 * To create a class that can be subclassed itself, extend the CoreObject class. 167 * 168 * var Animal = CoreObject.extend(); 169 * var Horse = Animal.extend(); 170 * 171 * The constructor can be defined through the init property of an object argument. 172 * 173 * var Animal = CoreObject.extend({ 174 * init: function(name, sound){ 175 * this.name = name; 176 * } 177 * }); 178 * 179 * Other methods and properties can be added the same way, or directly to the 180 * prototype. 181 * 182 * var Animal = CoreObject.extend({ 183 * init: function(name){ 184 * this.name = name; 185 * }, 186 * getName: function(){ 187 * return this.name; 188 * }, 189 * sound: '...' 190 * }); 191 * 192 * Animal.prototype.makeSound = function(){ 193 * alert(this.sound); 194 * }; 195 * 196 * To create an instance of a class, use the create method. 197 * 198 * var fluffy = Animal.create('Fluffy'); 199 * fluffy.getName(); // -> Fluffy 200 * 201 * Methods and properties can be overridden in subclasses. 202 * 203 * var Horse = Animal.extend({ 204 * sound: 'Neighhhhh!' 205 * }); 206 * 207 * var horsey = Horse.create('Horsey'); 208 * horsey.getName(); // -> Horsey 209 * horsey.makeSound(); // -> Alert: Neighhhhh! 210 * 211 * @class 212 * @constructor 213 */ 214 vjs.CoreObject = vjs['CoreObject'] = function(){}; 215 // Manually exporting vjs['CoreObject'] here for Closure Compiler 216 // because of the use of the extend/create class methods 217 // If we didn't do this, those functions would get flattend to something like 218 // `a = ...` and `this.prototype` would refer to the global object instead of 219 // CoreObject 220 221 /** 222 * Create a new object that inherits from this Object 223 * 224 * var Animal = CoreObject.extend(); 225 * var Horse = Animal.extend(); 226 * 227 * @param {Object} props Functions and properties to be applied to the 228 * new object's prototype 229 * @return {vjs.CoreObject} An object that inherits from CoreObject 230 * @this {*} 231 */ 232 vjs.CoreObject.extend = function(props){ 233 var init, subObj; 234 235 props = props || {}; 236 // Set up the constructor using the supplied init method 237 // or using the init of the parent object 238 // Make sure to check the unobfuscated version for external libs 239 init = props['init'] || props.init || this.prototype['init'] || this.prototype.init || function(){}; 240 // In Resig's simple class inheritance (previously used) the constructor 241 // is a function that calls `this.init.apply(arguments)` 242 // However that would prevent us from using `ParentObject.call(this);` 243 // in a Child constuctor because the `this` in `this.init` 244 // would still refer to the Child and cause an inifinite loop. 245 // We would instead have to do 246 // `ParentObject.prototype.init.apply(this, argumnents);` 247 // Bleh. We're not creating a _super() function, so it's good to keep 248 // the parent constructor reference simple. 249 subObj = function(){ 250 init.apply(this, arguments); 251 }; 252 253 // Inherit from this object's prototype 254 subObj.prototype = vjs.obj.create(this.prototype); 255 // Reset the constructor property for subObj otherwise 256 // instances of subObj would have the constructor of the parent Object 257 subObj.prototype.constructor = subObj; 258 259 // Make the class extendable 260 subObj.extend = vjs.CoreObject.extend; 261 // Make a function for creating instances 262 subObj.create = vjs.CoreObject.create; 263 264 // Extend subObj's prototype with functions and other properties from props 265 for (var name in props) { 266 if (props.hasOwnProperty(name)) { 267 subObj.prototype[name] = props[name]; 268 } 269 } 270 271 return subObj; 272 }; 273 274 /** 275 * Create a new instace of this Object class 276 * 277 * var myAnimal = Animal.create(); 278 * 279 * @return {vjs.CoreObject} An instance of a CoreObject subclass 280 * @this {*} 281 */ 282 vjs.CoreObject.create = function(){ 283 // Create a new object that inherits from this object's prototype 284 var inst = vjs.obj.create(this.prototype); 285 286 // Apply this constructor function to the new object 287 this.apply(inst, arguments); 288 289 // Return the new object 290 return inst; 291 }; 292 /** 293 * @fileoverview Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) 294 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) 295 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as 296 * robust as jquery's, so there's probably some differences. 297 */ 298 299 /** 300 * Add an event listener to element 301 * It stores the handler function in a separate cache object 302 * and adds a generic handler to the element's event, 303 * along with a unique id (guid) to the element. 304 * @param {Element|Object} elem Element or object to bind listeners to 305 * @param {String|Array} type Type of event to bind to. 306 * @param {Function} fn Event listener. 307 * @private 308 */ 309 vjs.on = function(elem, type, fn){ 310 if (vjs.obj.isArray(type)) { 311 return _handleMultipleEvents(vjs.on, elem, type, fn); 312 } 313 314 var data = vjs.getData(elem); 315 316 // We need a place to store all our handler data 317 if (!data.handlers) data.handlers = {}; 318 319 if (!data.handlers[type]) data.handlers[type] = []; 320 321 if (!fn.guid) fn.guid = vjs.guid++; 322 323 data.handlers[type].push(fn); 324 325 if (!data.dispatcher) { 326 data.disabled = false; 327 328 data.dispatcher = function (event){ 329 330 if (data.disabled) return; 331 event = vjs.fixEvent(event); 332 333 var handlers = data.handlers[event.type]; 334 335 if (handlers) { 336 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. 337 var handlersCopy = handlers.slice(0); 338 339 for (var m = 0, n = handlersCopy.length; m < n; m++) { 340 if (event.isImmediatePropagationStopped()) { 341 break; 342 } else { 343 handlersCopy[m].call(elem, event); 344 } 345 } 346 } 347 }; 348 } 349 350 if (data.handlers[type].length == 1) { 351 if (elem.addEventListener) { 352 elem.addEventListener(type, data.dispatcher, false); 353 } else if (elem.attachEvent) { 354 elem.attachEvent('on' + type, data.dispatcher); 355 } 356 } 357 }; 358 359 /** 360 * Removes event listeners from an element 361 * @param {Element|Object} elem Object to remove listeners from 362 * @param {String|Array=} type Type of listener to remove. Don't include to remove all events from element. 363 * @param {Function} fn Specific listener to remove. Don't incldue to remove listeners for an event type. 364 * @private 365 */ 366 vjs.off = function(elem, type, fn) { 367 // Don't want to add a cache object through getData if not needed 368 if (!vjs.hasData(elem)) return; 369 370 var data = vjs.getData(elem); 371 372 // If no events exist, nothing to unbind 373 if (!data.handlers) { return; } 374 375 if (vjs.obj.isArray(type)) { 376 return _handleMultipleEvents(vjs.off, elem, type, fn); 377 } 378 379 // Utility function 380 var removeType = function(t){ 381 data.handlers[t] = []; 382 vjs.cleanUpEvents(elem,t); 383 }; 384 385 // Are we removing all bound events? 386 if (!type) { 387 for (var t in data.handlers) removeType(t); 388 return; 389 } 390 391 var handlers = data.handlers[type]; 392 393 // If no handlers exist, nothing to unbind 394 if (!handlers) return; 395 396 // If no listener was provided, remove all listeners for type 397 if (!fn) { 398 removeType(type); 399 return; 400 } 401 402 // We're only removing a single handler 403 if (fn.guid) { 404 for (var n = 0; n < handlers.length; n++) { 405 if (handlers[n].guid === fn.guid) { 406 handlers.splice(n--, 1); 407 } 408 } 409 } 410 411 vjs.cleanUpEvents(elem, type); 412 }; 413 414 /** 415 * Clean up the listener cache and dispatchers 416 * @param {Element|Object} elem Element to clean up 417 * @param {String} type Type of event to clean up 418 * @private 419 */ 420 vjs.cleanUpEvents = function(elem, type) { 421 var data = vjs.getData(elem); 422 423 // Remove the events of a particular type if there are none left 424 if (data.handlers[type].length === 0) { 425 delete data.handlers[type]; 426 // data.handlers[type] = null; 427 // Setting to null was causing an error with data.handlers 428 429 // Remove the meta-handler from the element 430 if (elem.removeEventListener) { 431 elem.removeEventListener(type, data.dispatcher, false); 432 } else if (elem.detachEvent) { 433 elem.detachEvent('on' + type, data.dispatcher); 434 } 435 } 436 437 // Remove the events object if there are no types left 438 if (vjs.isEmpty(data.handlers)) { 439 delete data.handlers; 440 delete data.dispatcher; 441 delete data.disabled; 442 443 // data.handlers = null; 444 // data.dispatcher = null; 445 // data.disabled = null; 446 } 447 448 // Finally remove the expando if there is no data left 449 if (vjs.isEmpty(data)) { 450 vjs.removeData(elem); 451 } 452 }; 453 454 /** 455 * Fix a native event to have standard property values 456 * @param {Object} event Event object to fix 457 * @return {Object} 458 * @private 459 */ 460 vjs.fixEvent = function(event) { 461 462 function returnTrue() { return true; } 463 function returnFalse() { return false; } 464 465 // Test if fixing up is needed 466 // Used to check if !event.stopPropagation instead of isPropagationStopped 467 // But native events return true for stopPropagation, but don't have 468 // other expected methods like isPropagationStopped. Seems to be a problem 469 // with the Javascript Ninja code. So we're just overriding all events now. 470 if (!event || !event.isPropagationStopped) { 471 var old = event || window.event; 472 473 event = {}; 474 // Clone the old object so that we can modify the values event = {}; 475 // IE8 Doesn't like when you mess with native event properties 476 // Firefox returns false for event.hasOwnProperty('type') and other props 477 // which makes copying more difficult. 478 // TODO: Probably best to create a whitelist of event props 479 for (var key in old) { 480 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y 481 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation 482 if (key !== 'layerX' && key !== 'layerY' && key !== 'keyboardEvent.keyLocation') { 483 // Chrome 32+ warns if you try to copy deprecated returnValue, but 484 // we still want to if preventDefault isn't supported (IE8). 485 if (!(key == 'returnValue' && old.preventDefault)) { 486 event[key] = old[key]; 487 } 488 } 489 } 490 491 // The event occurred on this element 492 if (!event.target) { 493 event.target = event.srcElement || document; 494 } 495 496 // Handle which other element the event is related to 497 event.relatedTarget = event.fromElement === event.target ? 498 event.toElement : 499 event.fromElement; 500 501 // Stop the default browser action 502 event.preventDefault = function () { 503 if (old.preventDefault) { 504 old.preventDefault(); 505 } 506 event.returnValue = false; 507 event.isDefaultPrevented = returnTrue; 508 event.defaultPrevented = true; 509 }; 510 511 event.isDefaultPrevented = returnFalse; 512 event.defaultPrevented = false; 513 514 // Stop the event from bubbling 515 event.stopPropagation = function () { 516 if (old.stopPropagation) { 517 old.stopPropagation(); 518 } 519 event.cancelBubble = true; 520 event.isPropagationStopped = returnTrue; 521 }; 522 523 event.isPropagationStopped = returnFalse; 524 525 // Stop the event from bubbling and executing other handlers 526 event.stopImmediatePropagation = function () { 527 if (old.stopImmediatePropagation) { 528 old.stopImmediatePropagation(); 529 } 530 event.isImmediatePropagationStopped = returnTrue; 531 event.stopPropagation(); 532 }; 533 534 event.isImmediatePropagationStopped = returnFalse; 535 536 // Handle mouse position 537 if (event.clientX != null) { 538 var doc = document.documentElement, body = document.body; 539 540 event.pageX = event.clientX + 541 (doc && doc.scrollLeft || body && body.scrollLeft || 0) - 542 (doc && doc.clientLeft || body && body.clientLeft || 0); 543 event.pageY = event.clientY + 544 (doc && doc.scrollTop || body && body.scrollTop || 0) - 545 (doc && doc.clientTop || body && body.clientTop || 0); 546 } 547 548 // Handle key presses 549 event.which = event.charCode || event.keyCode; 550 551 // Fix button for mouse clicks: 552 // 0 == left; 1 == middle; 2 == right 553 if (event.button != null) { 554 event.button = (event.button & 1 ? 0 : 555 (event.button & 4 ? 1 : 556 (event.button & 2 ? 2 : 0))); 557 } 558 } 559 560 // Returns fixed-up instance 561 return event; 562 }; 563 564 /** 565 * Trigger an event for an element 566 * @param {Element|Object} elem Element to trigger an event on 567 * @param {Event|Object|String} event A string (the type) or an event object with a type attribute 568 * @private 569 */ 570 vjs.trigger = function(elem, event) { 571 // Fetches element data and a reference to the parent (for bubbling). 572 // Don't want to add a data object to cache for every parent, 573 // so checking hasData first. 574 var elemData = (vjs.hasData(elem)) ? vjs.getData(elem) : {}; 575 var parent = elem.parentNode || elem.ownerDocument; 576 // type = event.type || event, 577 // handler; 578 579 // If an event name was passed as a string, creates an event out of it 580 if (typeof event === 'string') { 581 event = { type:event, target:elem }; 582 } 583 // Normalizes the event properties. 584 event = vjs.fixEvent(event); 585 586 // If the passed element has a dispatcher, executes the established handlers. 587 if (elemData.dispatcher) { 588 elemData.dispatcher.call(elem, event); 589 } 590 591 // Unless explicitly stopped or the event does not bubble (e.g. media events) 592 // recursively calls this function to bubble the event up the DOM. 593 if (parent && !event.isPropagationStopped() && event.bubbles !== false) { 594 vjs.trigger(parent, event); 595 596 // If at the top of the DOM, triggers the default action unless disabled. 597 } else if (!parent && !event.defaultPrevented) { 598 var targetData = vjs.getData(event.target); 599 600 // Checks if the target has a default action for this event. 601 if (event.target[event.type]) { 602 // Temporarily disables event dispatching on the target as we have already executed the handler. 603 targetData.disabled = true; 604 // Executes the default action. 605 if (typeof event.target[event.type] === 'function') { 606 event.target[event.type](); 607 } 608 // Re-enables event dispatching. 609 targetData.disabled = false; 610 } 611 } 612 613 // Inform the triggerer if the default was prevented by returning false 614 return !event.defaultPrevented; 615 /* Original version of js ninja events wasn't complete. 616 * We've since updated to the latest version, but keeping this around 617 * for now just in case. 618 */ 619 // // Added in attion to book. Book code was broke. 620 // event = typeof event === 'object' ? 621 // event[vjs.expando] ? 622 // event : 623 // new vjs.Event(type, event) : 624 // new vjs.Event(type); 625 626 // event.type = type; 627 // if (handler) { 628 // handler.call(elem, event); 629 // } 630 631 // // Clean up the event in case it is being reused 632 // event.result = undefined; 633 // event.target = elem; 634 }; 635 636 /** 637 * Trigger a listener only once for an event 638 * @param {Element|Object} elem Element or object to 639 * @param {String|Array} type 640 * @param {Function} fn 641 * @private 642 */ 643 vjs.one = function(elem, type, fn) { 644 if (vjs.obj.isArray(type)) { 645 return _handleMultipleEvents(vjs.one, elem, type, fn); 646 } 647 var func = function(){ 648 vjs.off(elem, type, func); 649 fn.apply(this, arguments); 650 }; 651 // copy the guid to the new function so it can removed using the original function's ID 652 func.guid = fn.guid = fn.guid || vjs.guid++; 653 vjs.on(elem, type, func); 654 }; 655 656 /** 657 * Loops through an array of event types and calls the requested method for each type. 658 * @param {Function} fn The event method we want to use. 659 * @param {Element|Object} elem Element or object to bind listeners to 660 * @param {String} type Type of event to bind to. 661 * @param {Function} callback Event listener. 662 * @private 663 */ 664 function _handleMultipleEvents(fn, elem, type, callback) { 665 vjs.arr.forEach(type, function(type) { 666 fn(elem, type, callback); //Call the event method for each one of the types 667 }); 668 } 669 var hasOwnProp = Object.prototype.hasOwnProperty; 670 671 /** 672 * Creates an element and applies properties. 673 * @param {String=} tagName Name of tag to be created. 674 * @param {Object=} properties Element properties to be applied. 675 * @return {Element} 676 * @private 677 */ 678 vjs.createEl = function(tagName, properties){ 679 var el; 680 681 tagName = tagName || 'div'; 682 properties = properties || {}; 683 684 el = document.createElement(tagName); 685 686 vjs.obj.each(properties, function(propName, val){ 687 // Not remembering why we were checking for dash 688 // but using setAttribute means you have to use getAttribute 689 690 // The check for dash checks for the aria-* attributes, like aria-label, aria-valuemin. 691 // The additional check for "role" is because the default method for adding attributes does not 692 // add the attribute "role". My guess is because it's not a valid attribute in some namespaces, although 693 // browsers handle the attribute just fine. The W3C allows for aria-* attributes to be used in pre-HTML5 docs. 694 // http://www.w3.org/TR/wai-aria-primer/#ariahtml. Using setAttribute gets around this problem. 695 if (propName.indexOf('aria-') !== -1 || propName == 'role') { 696 el.setAttribute(propName, val); 697 } else { 698 el[propName] = val; 699 } 700 }); 701 702 return el; 703 }; 704 705 /** 706 * Uppercase the first letter of a string 707 * @param {String} string String to be uppercased 708 * @return {String} 709 * @private 710 */ 711 vjs.capitalize = function(string){ 712 return string.charAt(0).toUpperCase() + string.slice(1); 713 }; 714 715 /** 716 * Object functions container 717 * @type {Object} 718 * @private 719 */ 720 vjs.obj = {}; 721 722 /** 723 * Object.create shim for prototypal inheritance 724 * 725 * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create 726 * 727 * @function 728 * @param {Object} obj Object to use as prototype 729 * @private 730 */ 731 vjs.obj.create = Object.create || function(obj){ 732 //Create a new function called 'F' which is just an empty object. 733 function F() {} 734 735 //the prototype of the 'F' function should point to the 736 //parameter of the anonymous function. 737 F.prototype = obj; 738 739 //create a new constructor function based off of the 'F' function. 740 return new F(); 741 }; 742 743 /** 744 * Loop through each property in an object and call a function 745 * whose arguments are (key,value) 746 * @param {Object} obj Object of properties 747 * @param {Function} fn Function to be called on each property. 748 * @this {*} 749 * @private 750 */ 751 vjs.obj.each = function(obj, fn, context){ 752 for (var key in obj) { 753 if (hasOwnProp.call(obj, key)) { 754 fn.call(context || this, key, obj[key]); 755 } 756 } 757 }; 758 759 /** 760 * Merge two objects together and return the original. 761 * @param {Object} obj1 762 * @param {Object} obj2 763 * @return {Object} 764 * @private 765 */ 766 vjs.obj.merge = function(obj1, obj2){ 767 if (!obj2) { return obj1; } 768 for (var key in obj2){ 769 if (hasOwnProp.call(obj2, key)) { 770 obj1[key] = obj2[key]; 771 } 772 } 773 return obj1; 774 }; 775 776 /** 777 * Merge two objects, and merge any properties that are objects 778 * instead of just overwriting one. Uses to merge options hashes 779 * where deeper default settings are important. 780 * @param {Object} obj1 Object to override 781 * @param {Object} obj2 Overriding object 782 * @return {Object} New object. Obj1 and Obj2 will be untouched. 783 * @private 784 */ 785 vjs.obj.deepMerge = function(obj1, obj2){ 786 var key, val1, val2; 787 788 // make a copy of obj1 so we're not ovewriting original values. 789 // like prototype.options_ and all sub options objects 790 obj1 = vjs.obj.copy(obj1); 791 792 for (key in obj2){ 793 if (hasOwnProp.call(obj2, key)) { 794 val1 = obj1[key]; 795 val2 = obj2[key]; 796 797 // Check if both properties are pure objects and do a deep merge if so 798 if (vjs.obj.isPlain(val1) && vjs.obj.isPlain(val2)) { 799 obj1[key] = vjs.obj.deepMerge(val1, val2); 800 } else { 801 obj1[key] = obj2[key]; 802 } 803 } 804 } 805 return obj1; 806 }; 807 808 /** 809 * Make a copy of the supplied object 810 * @param {Object} obj Object to copy 811 * @return {Object} Copy of object 812 * @private 813 */ 814 vjs.obj.copy = function(obj){ 815 return vjs.obj.merge({}, obj); 816 }; 817 818 /** 819 * Check if an object is plain, and not a dom node or any object sub-instance 820 * @param {Object} obj Object to check 821 * @return {Boolean} True if plain, false otherwise 822 * @private 823 */ 824 vjs.obj.isPlain = function(obj){ 825 return !!obj 826 && typeof obj === 'object' 827 && obj.toString() === '[object Object]' 828 && obj.constructor === Object; 829 }; 830 831 /** 832 * Check if an object is Array 833 * Since instanceof Array will not work on arrays created in another frame we need to use Array.isArray, but since IE8 does not support Array.isArray we need this shim 834 * @param {Object} obj Object to check 835 * @return {Boolean} True if plain, false otherwise 836 * @private 837 */ 838 vjs.obj.isArray = Array.isArray || function(arr) { 839 return Object.prototype.toString.call(arr) === '[object Array]'; 840 }; 841 842 /** 843 * Check to see whether the input is NaN or not. 844 * NaN is the only JavaScript construct that isn't equal to itself 845 * @param {Number} num Number to check 846 * @return {Boolean} True if NaN, false otherwise 847 * @private 848 */ 849 vjs.isNaN = function(num) { 850 return num !== num; 851 }; 852 853 /** 854 * Bind (a.k.a proxy or Context). A simple method for changing the context of a function 855 It also stores a unique id on the function so it can be easily removed from events 856 * @param {*} context The object to bind as scope 857 * @param {Function} fn The function to be bound to a scope 858 * @param {Number=} uid An optional unique ID for the function to be set 859 * @return {Function} 860 * @private 861 */ 862 vjs.bind = function(context, fn, uid) { 863 // Make sure the function has a unique ID 864 if (!fn.guid) { fn.guid = vjs.guid++; } 865 866 // Create the new function that changes the context 867 var ret = function() { 868 return fn.apply(context, arguments); 869 }; 870 871 // Allow for the ability to individualize this function 872 // Needed in the case where multiple objects might share the same prototype 873 // IF both items add an event listener with the same function, then you try to remove just one 874 // it will remove both because they both have the same guid. 875 // when using this, you need to use the bind method when you remove the listener as well. 876 // currently used in text tracks 877 ret.guid = (uid) ? uid + '_' + fn.guid : fn.guid; 878 879 return ret; 880 }; 881 882 /** 883 * Element Data Store. Allows for binding data to an element without putting it directly on the element. 884 * Ex. Event listneres are stored here. 885 * (also from jsninja.com, slightly modified and updated for closure compiler) 886 * @type {Object} 887 * @private 888 */ 889 vjs.cache = {}; 890 891 /** 892 * Unique ID for an element or function 893 * @type {Number} 894 * @private 895 */ 896 vjs.guid = 1; 897 898 /** 899 * Unique attribute name to store an element's guid in 900 * @type {String} 901 * @constant 902 * @private 903 */ 904 vjs.expando = 'vdata' + (new Date()).getTime(); 905 906 /** 907 * Returns the cache object where data for an element is stored 908 * @param {Element} el Element to store data for. 909 * @return {Object} 910 * @private 911 */ 912 vjs.getData = function(el){ 913 var id = el[vjs.expando]; 914 if (!id) { 915 id = el[vjs.expando] = vjs.guid++; 916 vjs.cache[id] = {}; 917 } 918 return vjs.cache[id]; 919 }; 920 921 /** 922 * Returns the cache object where data for an element is stored 923 * @param {Element} el Element to store data for. 924 * @return {Object} 925 * @private 926 */ 927 vjs.hasData = function(el){ 928 var id = el[vjs.expando]; 929 return !(!id || vjs.isEmpty(vjs.cache[id])); 930 }; 931 932 /** 933 * Delete data for the element from the cache and the guid attr from getElementById 934 * @param {Element} el Remove data for an element 935 * @private 936 */ 937 vjs.removeData = function(el){ 938 var id = el[vjs.expando]; 939 if (!id) { return; } 940 // Remove all stored data 941 // Changed to = null 942 // http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/ 943 // vjs.cache[id] = null; 944 delete vjs.cache[id]; 945 946 // Remove the expando property from the DOM node 947 try { 948 delete el[vjs.expando]; 949 } catch(e) { 950 if (el.removeAttribute) { 951 el.removeAttribute(vjs.expando); 952 } else { 953 // IE doesn't appear to support removeAttribute on the document element 954 el[vjs.expando] = null; 955 } 956 } 957 }; 958 959 /** 960 * Check if an object is empty 961 * @param {Object} obj The object to check for emptiness 962 * @return {Boolean} 963 * @private 964 */ 965 vjs.isEmpty = function(obj) { 966 for (var prop in obj) { 967 // Inlude null properties as empty. 968 if (obj[prop] !== null) { 969 return false; 970 } 971 } 972 return true; 973 }; 974 975 /** 976 * Add a CSS class name to an element 977 * @param {Element} element Element to add class name to 978 * @param {String} classToAdd Classname to add 979 * @private 980 */ 981 vjs.addClass = function(element, classToAdd){ 982 if ((' '+element.className+' ').indexOf(' '+classToAdd+' ') == -1) { 983 element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd; 984 } 985 }; 986 987 /** 988 * Remove a CSS class name from an element 989 * @param {Element} element Element to remove from class name 990 * @param {String} classToAdd Classname to remove 991 * @private 992 */ 993 vjs.removeClass = function(element, classToRemove){ 994 var classNames, i; 995 996 if (element.className.indexOf(classToRemove) == -1) { return; } 997 998 classNames = element.className.split(' '); 999 1000 // no arr.indexOf in ie8, and we don't want to add a big shim 1001 for (i = classNames.length - 1; i >= 0; i--) { 1002 if (classNames[i] === classToRemove) { 1003 classNames.splice(i,1); 1004 } 1005 } 1006 1007 element.className = classNames.join(' '); 1008 }; 1009 1010 /** 1011 * Element for testing browser HTML5 video capabilities 1012 * @type {Element} 1013 * @constant 1014 * @private 1015 */ 1016 vjs.TEST_VID = vjs.createEl('video'); 1017 1018 /** 1019 * Useragent for browser testing. 1020 * @type {String} 1021 * @constant 1022 * @private 1023 */ 1024 vjs.USER_AGENT = navigator.userAgent; 1025 1026 /** 1027 * Device is an iPhone 1028 * @type {Boolean} 1029 * @constant 1030 * @private 1031 */ 1032 vjs.IS_IPHONE = (/iPhone/i).test(vjs.USER_AGENT); 1033 vjs.IS_IPAD = (/iPad/i).test(vjs.USER_AGENT); 1034 vjs.IS_IPOD = (/iPod/i).test(vjs.USER_AGENT); 1035 vjs.IS_IOS = vjs.IS_IPHONE || vjs.IS_IPAD || vjs.IS_IPOD; 1036 1037 vjs.IOS_VERSION = (function(){ 1038 var match = vjs.USER_AGENT.match(/OS (\d+)_/i); 1039 if (match && match[1]) { return match[1]; } 1040 })(); 1041 1042 vjs.IS_ANDROID = (/Android/i).test(vjs.USER_AGENT); 1043 vjs.ANDROID_VERSION = (function() { 1044 // This matches Android Major.Minor.Patch versions 1045 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 1046 var match = vjs.USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i), 1047 major, 1048 minor; 1049 1050 if (!match) { 1051 return null; 1052 } 1053 1054 major = match[1] && parseFloat(match[1]); 1055 minor = match[2] && parseFloat(match[2]); 1056 1057 if (major && minor) { 1058 return parseFloat(match[1] + '.' + match[2]); 1059 } else if (major) { 1060 return major; 1061 } else { 1062 return null; 1063 } 1064 })(); 1065 // Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser 1066 vjs.IS_OLD_ANDROID = vjs.IS_ANDROID && (/webkit/i).test(vjs.USER_AGENT) && vjs.ANDROID_VERSION < 2.3; 1067 1068 vjs.IS_FIREFOX = (/Firefox/i).test(vjs.USER_AGENT); 1069 vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT); 1070 1071 vjs.TOUCH_ENABLED = !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch); 1072 1073 /** 1074 * Apply attributes to an HTML element. 1075 * @param {Element} el Target element. 1076 * @param {Object=} attributes Element attributes to be applied. 1077 * @private 1078 */ 1079 vjs.setElementAttributes = function(el, attributes){ 1080 vjs.obj.each(attributes, function(attrName, attrValue) { 1081 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { 1082 el.removeAttribute(attrName); 1083 } else { 1084 el.setAttribute(attrName, (attrValue === true ? '' : attrValue)); 1085 } 1086 }); 1087 }; 1088 1089 /** 1090 * Get an element's attribute values, as defined on the HTML tag 1091 * Attributs are not the same as properties. They're defined on the tag 1092 * or with setAttribute (which shouldn't be used with HTML) 1093 * This will return true or false for boolean attributes. 1094 * @param {Element} tag Element from which to get tag attributes 1095 * @return {Object} 1096 * @private 1097 */ 1098 vjs.getElementAttributes = function(tag){ 1099 var obj, knownBooleans, attrs, attrName, attrVal; 1100 1101 obj = {}; 1102 1103 // known boolean attributes 1104 // we can check for matching boolean properties, but older browsers 1105 // won't know about HTML5 boolean attributes that we still read from 1106 knownBooleans = ','+'autoplay,controls,loop,muted,default'+','; 1107 1108 if (tag && tag.attributes && tag.attributes.length > 0) { 1109 attrs = tag.attributes; 1110 1111 for (var i = attrs.length - 1; i >= 0; i--) { 1112 attrName = attrs[i].name; 1113 attrVal = attrs[i].value; 1114 1115 // check for known booleans 1116 // the matching element property will return a value for typeof 1117 if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(','+attrName+',') !== -1) { 1118 // the value of an included boolean attribute is typically an empty 1119 // string ('') which would equal false if we just check for a false value. 1120 // we also don't want support bad code like autoplay='false' 1121 attrVal = (attrVal !== null) ? true : false; 1122 } 1123 1124 obj[attrName] = attrVal; 1125 } 1126 } 1127 1128 return obj; 1129 }; 1130 1131 /** 1132 * Get the computed style value for an element 1133 * From http://robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/ 1134 * @param {Element} el Element to get style value for 1135 * @param {String} strCssRule Style name 1136 * @return {String} Style value 1137 * @private 1138 */ 1139 vjs.getComputedDimension = function(el, strCssRule){ 1140 var strValue = ''; 1141 if(document.defaultView && document.defaultView.getComputedStyle){ 1142 strValue = document.defaultView.getComputedStyle(el, '').getPropertyValue(strCssRule); 1143 1144 } else if(el.currentStyle){ 1145 // IE8 Width/Height support 1146 strValue = el['client'+strCssRule.substr(0,1).toUpperCase() + strCssRule.substr(1)] + 'px'; 1147 } 1148 return strValue; 1149 }; 1150 1151 /** 1152 * Insert an element as the first child node of another 1153 * @param {Element} child Element to insert 1154 * @param {[type]} parent Element to insert child into 1155 * @private 1156 */ 1157 vjs.insertFirst = function(child, parent){ 1158 if (parent.firstChild) { 1159 parent.insertBefore(child, parent.firstChild); 1160 } else { 1161 parent.appendChild(child); 1162 } 1163 }; 1164 1165 /** 1166 * Object to hold browser support information 1167 * @type {Object} 1168 * @private 1169 */ 1170 vjs.browser = {}; 1171 1172 /** 1173 * Shorthand for document.getElementById() 1174 * Also allows for CSS (jQuery) ID syntax. But nothing other than IDs. 1175 * @param {String} id Element ID 1176 * @return {Element} Element with supplied ID 1177 * @private 1178 */ 1179 vjs.el = function(id){ 1180 if (id.indexOf('#') === 0) { 1181 id = id.slice(1); 1182 } 1183 1184 return document.getElementById(id); 1185 }; 1186 1187 /** 1188 * Format seconds as a time string, H:MM:SS or M:SS 1189 * Supplying a guide (in seconds) will force a number of leading zeros 1190 * to cover the length of the guide 1191 * @param {Number} seconds Number of seconds to be turned into a string 1192 * @param {Number} guide Number (in seconds) to model the string after 1193 * @return {String} Time formatted as H:MM:SS or M:SS 1194 * @private 1195 */ 1196 vjs.formatTime = function(seconds, guide) { 1197 // Default to using seconds as guide 1198 guide = guide || seconds; 1199 var s = Math.floor(seconds % 60), 1200 m = Math.floor(seconds / 60 % 60), 1201 h = Math.floor(seconds / 3600), 1202 gm = Math.floor(guide / 60 % 60), 1203 gh = Math.floor(guide / 3600); 1204 1205 // handle invalid times 1206 if (isNaN(seconds) || seconds === Infinity) { 1207 // '-' is false for all relational operators (e.g. <, >=) so this setting 1208 // will add the minimum number of fields specified by the guide 1209 h = m = s = '-'; 1210 } 1211 1212 // Check if we need to show hours 1213 h = (h > 0 || gh > 0) ? h + ':' : ''; 1214 1215 // If hours are showing, we may need to add a leading zero. 1216 // Always show at least one digit of minutes. 1217 m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':'; 1218 1219 // Check if leading zero is need for seconds 1220 s = (s < 10) ? '0' + s : s; 1221 1222 return h + m + s; 1223 }; 1224 1225 // Attempt to block the ability to select text while dragging controls 1226 vjs.blockTextSelection = function(){ 1227 document.body.focus(); 1228 document.onselectstart = function () { return false; }; 1229 }; 1230 // Turn off text selection blocking 1231 vjs.unblockTextSelection = function(){ document.onselectstart = function () { return true; }; }; 1232 1233 /** 1234 * Trim whitespace from the ends of a string. 1235 * @param {String} string String to trim 1236 * @return {String} Trimmed string 1237 * @private 1238 */ 1239 vjs.trim = function(str){ 1240 return (str+'').replace(/^\s+|\s+$/g, ''); 1241 }; 1242 1243 /** 1244 * Should round off a number to a decimal place 1245 * @param {Number} num Number to round 1246 * @param {Number} dec Number of decimal places to round to 1247 * @return {Number} Rounded number 1248 * @private 1249 */ 1250 vjs.round = function(num, dec) { 1251 if (!dec) { dec = 0; } 1252 return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 1253 }; 1254 1255 /** 1256 * Should create a fake TimeRange object 1257 * Mimics an HTML5 time range instance, which has functions that 1258 * return the start and end times for a range 1259 * TimeRanges are returned by the buffered() method 1260 * @param {Number} start Start time in seconds 1261 * @param {Number} end End time in seconds 1262 * @return {Object} Fake TimeRange object 1263 * @private 1264 */ 1265 vjs.createTimeRange = function(start, end){ 1266 return { 1267 length: 1, 1268 start: function() { return start; }, 1269 end: function() { return end; } 1270 }; 1271 }; 1272 1273 /** 1274 * Simple http request for retrieving external files (e.g. text tracks) 1275 * @param {String} url URL of resource 1276 * @param {Function} onSuccess Success callback 1277 * @param {Function=} onError Error callback 1278 * @param {Boolean=} withCredentials Flag which allow credentials 1279 * @private 1280 */ 1281 vjs.get = function(url, onSuccess, onError, withCredentials){ 1282 var fileUrl, request, urlInfo, winLoc, crossOrigin; 1283 1284 onError = onError || function(){}; 1285 1286 if (typeof XMLHttpRequest === 'undefined') { 1287 // Shim XMLHttpRequest for older IEs 1288 window.XMLHttpRequest = function () { 1289 try { return new window.ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) {} 1290 try { return new window.ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (f) {} 1291 try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (g) {} 1292 throw new Error('This browser does not support XMLHttpRequest.'); 1293 }; 1294 } 1295 1296 request = new XMLHttpRequest(); 1297 1298 urlInfo = vjs.parseUrl(url); 1299 winLoc = window.location; 1300 // check if url is for another domain/origin 1301 // ie8 doesn't know location.origin, so we won't rely on it here 1302 crossOrigin = (urlInfo.protocol + urlInfo.host) !== (winLoc.protocol + winLoc.host); 1303 1304 // Use XDomainRequest for IE if XMLHTTPRequest2 isn't available 1305 // 'withCredentials' is only available in XMLHTTPRequest2 1306 // Also XDomainRequest has a lot of gotchas, so only use if cross domain 1307 if(crossOrigin && window.XDomainRequest && !('withCredentials' in request)) { 1308 request = new window.XDomainRequest(); 1309 request.onload = function() { 1310 onSuccess(request.responseText); 1311 }; 1312 request.onerror = onError; 1313 // these blank handlers need to be set to fix ie9 http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ 1314 request.onprogress = function() {}; 1315 request.ontimeout = onError; 1316 1317 // XMLHTTPRequest 1318 } else { 1319 fileUrl = (urlInfo.protocol == 'file:' || winLoc.protocol == 'file:'); 1320 1321 request.onreadystatechange = function() { 1322 if (request.readyState === 4) { 1323 if (request.status === 200 || fileUrl && request.status === 0) { 1324 onSuccess(request.responseText); 1325 } else { 1326 onError(request.responseText); 1327 } 1328 } 1329 }; 1330 } 1331 1332 // open the connection 1333 try { 1334 // Third arg is async, or ignored by XDomainRequest 1335 request.open('GET', url, true); 1336 // withCredentials only supported by XMLHttpRequest2 1337 if(withCredentials) { 1338 request.withCredentials = true; 1339 } 1340 } catch(e) { 1341 onError(e); 1342 return; 1343 } 1344 1345 // send the request 1346 try { 1347 request.send(); 1348 } catch(e) { 1349 onError(e); 1350 } 1351 }; 1352 1353 /** 1354 * Add to local storage (may removeable) 1355 * @private 1356 */ 1357 vjs.setLocalStorage = function(key, value){ 1358 try { 1359 // IE was throwing errors referencing the var anywhere without this 1360 var localStorage = window.localStorage || false; 1361 if (!localStorage) { return; } 1362 localStorage[key] = value; 1363 } catch(e) { 1364 if (e.code == 22 || e.code == 1014) { // Webkit == 22 / Firefox == 1014 1365 vjs.log('LocalStorage Full (VideoJS)', e); 1366 } else { 1367 if (e.code == 18) { 1368 vjs.log('LocalStorage not allowed (VideoJS)', e); 1369 } else { 1370 vjs.log('LocalStorage Error (VideoJS)', e); 1371 } 1372 } 1373 } 1374 }; 1375 1376 /** 1377 * Get abosolute version of relative URL. Used to tell flash correct URL. 1378 * http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 1379 * @param {String} url URL to make absolute 1380 * @return {String} Absolute URL 1381 * @private 1382 */ 1383 vjs.getAbsoluteURL = function(url){ 1384 1385 // Check if absolute URL 1386 if (!url.match(/^https?:\/\//)) { 1387 // Convert to absolute URL. Flash hosted off-site needs an absolute URL. 1388 url = vjs.createEl('div', { 1389 innerHTML: '<a href="'+url+'">x</a>' 1390 }).firstChild.href; 1391 } 1392 1393 return url; 1394 }; 1395 1396 1397 /** 1398 * Resolve and parse the elements of a URL 1399 * @param {String} url The url to parse 1400 * @return {Object} An object of url details 1401 */ 1402 vjs.parseUrl = function(url) { 1403 var div, a, addToBody, props, details; 1404 1405 props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; 1406 1407 // add the url to an anchor and let the browser parse the URL 1408 a = vjs.createEl('a', { href: url }); 1409 1410 // IE8 (and 9?) Fix 1411 // ie8 doesn't parse the URL correctly until the anchor is actually 1412 // added to the body, and an innerHTML is needed to trigger the parsing 1413 addToBody = (a.host === '' && a.protocol !== 'file:'); 1414 if (addToBody) { 1415 div = vjs.createEl('div'); 1416 div.innerHTML = '<a href="'+url+'"></a>'; 1417 a = div.firstChild; 1418 // prevent the div from affecting layout 1419 div.setAttribute('style', 'display:none; position:absolute;'); 1420 document.body.appendChild(div); 1421 } 1422 1423 // Copy the specific URL properties to a new object 1424 // This is also needed for IE8 because the anchor loses its 1425 // properties when it's removed from the dom 1426 details = {}; 1427 for (var i = 0; i < props.length; i++) { 1428 details[props[i]] = a[props[i]]; 1429 } 1430 1431 if (addToBody) { 1432 document.body.removeChild(div); 1433 } 1434 1435 return details; 1436 }; 1437 1438 /** 1439 * Log messags to the console and history based on the type of message 1440 * 1441 * @param {String} type The type of message, or `null` for `log` 1442 * @param {[type]} args The args to be passed to the log 1443 * @private 1444 */ 1445 function _logType(type, args){ 1446 var argsArray, noop, console; 1447 1448 // convert args to an array to get array functions 1449 argsArray = Array.prototype.slice.call(args); 1450 // if there's no console then don't try to output messages 1451 // they will still be stored in vjs.log.history 1452 // Was setting these once outside of this function, but containing them 1453 // in the function makes it easier to test cases where console doesn't exist 1454 noop = function(){}; 1455 console = window['console'] || { 1456 'log': noop, 1457 'warn': noop, 1458 'error': noop 1459 }; 1460 1461 if (type) { 1462 // add the type to the front of the message 1463 argsArray.unshift(type.toUpperCase()+':'); 1464 } else { 1465 // default to log with no prefix 1466 type = 'log'; 1467 } 1468 1469 // add to history 1470 vjs.log.history.push(argsArray); 1471 1472 // add console prefix after adding to history 1473 argsArray.unshift('VIDEOJS:'); 1474 1475 // call appropriate log function 1476 if (console[type].apply) { 1477 console[type].apply(console, argsArray); 1478 } else { 1479 // ie8 doesn't allow error.apply, but it will just join() the array anyway 1480 console[type](argsArray.join(' ')); 1481 } 1482 } 1483 1484 /** 1485 * Log plain debug messages 1486 */ 1487 vjs.log = function(){ 1488 _logType(null, arguments); 1489 }; 1490 1491 /** 1492 * Keep a history of log messages 1493 * @type {Array} 1494 */ 1495 vjs.log.history = []; 1496 1497 /** 1498 * Log error messages 1499 */ 1500 vjs.log.error = function(){ 1501 _logType('error', arguments); 1502 }; 1503 1504 /** 1505 * Log warning messages 1506 */ 1507 vjs.log.warn = function(){ 1508 _logType('warn', arguments); 1509 }; 1510 1511 // Offset Left 1512 // getBoundingClientRect technique from John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/ 1513 vjs.findPosition = function(el) { 1514 var box, docEl, body, clientLeft, scrollLeft, left, clientTop, scrollTop, top; 1515 1516 if (el.getBoundingClientRect && el.parentNode) { 1517 box = el.getBoundingClientRect(); 1518 } 1519 1520 if (!box) { 1521 return { 1522 left: 0, 1523 top: 0 1524 }; 1525 } 1526 1527 docEl = document.documentElement; 1528 body = document.body; 1529 1530 clientLeft = docEl.clientLeft || body.clientLeft || 0; 1531 scrollLeft = window.pageXOffset || body.scrollLeft; 1532 left = box.left + scrollLeft - clientLeft; 1533 1534 clientTop = docEl.clientTop || body.clientTop || 0; 1535 scrollTop = window.pageYOffset || body.scrollTop; 1536 top = box.top + scrollTop - clientTop; 1537 1538 // Android sometimes returns slightly off decimal values, so need to round 1539 return { 1540 left: vjs.round(left), 1541 top: vjs.round(top) 1542 }; 1543 }; 1544 1545 /** 1546 * Array functions container 1547 * @type {Object} 1548 * @private 1549 */ 1550 vjs.arr = {}; 1551 1552 /* 1553 * Loops through an array and runs a function for each item inside it. 1554 * @param {Array} array The array 1555 * @param {Function} callback The function to be run for each item 1556 * @param {*} thisArg The `this` binding of callback 1557 * @returns {Array} The array 1558 * @private 1559 */ 1560 vjs.arr.forEach = function(array, callback, thisArg) { 1561 if (vjs.obj.isArray(array) && callback instanceof Function) { 1562 for (var i = 0, len = array.length; i < len; ++i) { 1563 callback.call(thisArg || vjs, array[i], i, array); 1564 } 1565 } 1566 1567 return array; 1568 }; 1569 /** 1570 * Utility functions namespace 1571 * @namespace 1572 * @type {Object} 1573 */ 1574 vjs.util = {}; 1575 1576 /** 1577 * Merge two options objects, recursively merging any plain object properties as 1578 * well. Previously `deepMerge` 1579 * 1580 * @param {Object} obj1 Object to override values in 1581 * @param {Object} obj2 Overriding object 1582 * @return {Object} New object -- obj1 and obj2 will be untouched 1583 */ 1584 vjs.util.mergeOptions = function(obj1, obj2){ 1585 var key, val1, val2; 1586 1587 // make a copy of obj1 so we're not overwriting original values. 1588 // like prototype.options_ and all sub options objects 1589 obj1 = vjs.obj.copy(obj1); 1590 1591 for (key in obj2){ 1592 if (obj2.hasOwnProperty(key)) { 1593 val1 = obj1[key]; 1594 val2 = obj2[key]; 1595 1596 // Check if both properties are pure objects and do a deep merge if so 1597 if (vjs.obj.isPlain(val1) && vjs.obj.isPlain(val2)) { 1598 obj1[key] = vjs.util.mergeOptions(val1, val2); 1599 } else { 1600 obj1[key] = obj2[key]; 1601 } 1602 } 1603 } 1604 return obj1; 1605 };/** 1606 * @fileoverview Player Component - Base class for all UI objects 1607 * 1608 */ 1609 1610 /** 1611 * Base UI Component class 1612 * 1613 * Components are embeddable UI objects that are represented by both a 1614 * javascript object and an element in the DOM. They can be children of other 1615 * components, and can have many children themselves. 1616 * 1617 * // adding a button to the player 1618 * var button = player.addChild('button'); 1619 * button.el(); // -> button element 1620 * 1621 * <div class="video-js"> 1622 * <div class="vjs-button">Button</div> 1623 * </div> 1624 * 1625 * Components are also event emitters. 1626 * 1627 * button.on('click', function(){ 1628 * console.log('Button Clicked!'); 1629 * }); 1630 * 1631 * button.trigger('customevent'); 1632 * 1633 * @param {Object} player Main Player 1634 * @param {Object=} options 1635 * @class 1636 * @constructor 1637 * @extends vjs.CoreObject 1638 */ 1639 vjs.Component = vjs.CoreObject.extend({ 1640 /** 1641 * the constructor function for the class 1642 * 1643 * @constructor 1644 */ 1645 init: function(player, options, ready){ 1646 this.player_ = player; 1647 1648 // Make a copy of prototype.options_ to protect against overriding global defaults 1649 this.options_ = vjs.obj.copy(this.options_); 1650 1651 // Updated options with supplied options 1652 options = this.options(options); 1653 1654 // Get ID from options, element, or create using player ID and unique ID 1655 this.id_ = options['id'] || ((options['el'] && options['el']['id']) ? options['el']['id'] : player.id() + '_component_' + vjs.guid++ ); 1656 1657 this.name_ = options['name'] || null; 1658 1659 // Create element if one wasn't provided in options 1660 this.el_ = options['el'] || this.createEl(); 1661 1662 this.children_ = []; 1663 this.childIndex_ = {}; 1664 this.childNameIndex_ = {}; 1665 1666 // Add any child components in options 1667 this.initChildren(); 1668 1669 this.ready(ready); 1670 // Don't want to trigger ready here or it will before init is actually 1671 // finished for all children that run this constructor 1672 1673 if (options.reportTouchActivity !== false) { 1674 this.enableTouchActivity(); 1675 } 1676 } 1677 }); 1678 1679 /** 1680 * Dispose of the component and all child components 1681 */ 1682 vjs.Component.prototype.dispose = function(){ 1683 this.trigger({ type: 'dispose', 'bubbles': false }); 1684 1685 // Dispose all children. 1686 if (this.children_) { 1687 for (var i = this.children_.length - 1; i >= 0; i--) { 1688 if (this.children_[i].dispose) { 1689 this.children_[i].dispose(); 1690 } 1691 } 1692 } 1693 1694 // Delete child references 1695 this.children_ = null; 1696 this.childIndex_ = null; 1697 this.childNameIndex_ = null; 1698 1699 // Remove all event listeners. 1700 this.off(); 1701 1702 // Remove element from DOM 1703 if (this.el_.parentNode) { 1704 this.el_.parentNode.removeChild(this.el_); 1705 } 1706 1707 vjs.removeData(this.el_); 1708 this.el_ = null; 1709 }; 1710 1711 /** 1712 * Reference to main player instance 1713 * 1714 * @type {vjs.Player} 1715 * @private 1716 */ 1717 vjs.Component.prototype.player_ = true; 1718 1719 /** 1720 * Return the component's player 1721 * 1722 * @return {vjs.Player} 1723 */ 1724 vjs.Component.prototype.player = function(){ 1725 return this.player_; 1726 }; 1727 1728 /** 1729 * The component's options object 1730 * 1731 * @type {Object} 1732 * @private 1733 */ 1734 vjs.Component.prototype.options_; 1735 1736 /** 1737 * Deep merge of options objects 1738 * 1739 * Whenever a property is an object on both options objects 1740 * the two properties will be merged using vjs.obj.deepMerge. 1741 * 1742 * This is used for merging options for child components. We 1743 * want it to be easy to override individual options on a child 1744 * component without having to rewrite all the other default options. 1745 * 1746 * Parent.prototype.options_ = { 1747 * children: { 1748 * 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, 1749 * 'childTwo': {}, 1750 * 'childThree': {} 1751 * } 1752 * } 1753 * newOptions = { 1754 * children: { 1755 * 'childOne': { 'foo': 'baz', 'abc': '123' } 1756 * 'childTwo': null, 1757 * 'childFour': {} 1758 * } 1759 * } 1760 * 1761 * this.options(newOptions); 1762 * 1763 * RESULT 1764 * 1765 * { 1766 * children: { 1767 * 'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' }, 1768 * 'childTwo': null, // Disabled. Won't be initialized. 1769 * 'childThree': {}, 1770 * 'childFour': {} 1771 * } 1772 * } 1773 * 1774 * @param {Object} obj Object of new option values 1775 * @return {Object} A NEW object of this.options_ and obj merged 1776 */ 1777 vjs.Component.prototype.options = function(obj){ 1778 if (obj === undefined) return this.options_; 1779 1780 return this.options_ = vjs.util.mergeOptions(this.options_, obj); 1781 }; 1782 1783 /** 1784 * The DOM element for the component 1785 * 1786 * @type {Element} 1787 * @private 1788 */ 1789 vjs.Component.prototype.el_; 1790 1791 /** 1792 * Create the component's DOM element 1793 * 1794 * @param {String=} tagName Element's node type. e.g. 'div' 1795 * @param {Object=} attributes An object of element attributes that should be set on the element 1796 * @return {Element} 1797 */ 1798 vjs.Component.prototype.createEl = function(tagName, attributes){ 1799 return vjs.createEl(tagName, attributes); 1800 }; 1801 1802 vjs.Component.prototype.localize = function(string){ 1803 var lang = this.player_.language(), 1804 languages = this.player_.languages(); 1805 if (languages && languages[lang] && languages[lang][string]) { 1806 return languages[lang][string]; 1807 } 1808 return string; 1809 }; 1810 1811 /** 1812 * Get the component's DOM element 1813 * 1814 * var domEl = myComponent.el(); 1815 * 1816 * @return {Element} 1817 */ 1818 vjs.Component.prototype.el = function(){ 1819 return this.el_; 1820 }; 1821 1822 /** 1823 * An optional element where, if defined, children will be inserted instead of 1824 * directly in `el_` 1825 * 1826 * @type {Element} 1827 * @private 1828 */ 1829 vjs.Component.prototype.contentEl_; 1830 1831 /** 1832 * Return the component's DOM element for embedding content. 1833 * Will either be el_ or a new element defined in createEl. 1834 * 1835 * @return {Element} 1836 */ 1837 vjs.Component.prototype.contentEl = function(){ 1838 return this.contentEl_ || this.el_; 1839 }; 1840 1841 /** 1842 * The ID for the component 1843 * 1844 * @type {String} 1845 * @private 1846 */ 1847 vjs.Component.prototype.id_; 1848 1849 /** 1850 * Get the component's ID 1851 * 1852 * var id = myComponent.id(); 1853 * 1854 * @return {String} 1855 */ 1856 vjs.Component.prototype.id = function(){ 1857 return this.id_; 1858 }; 1859 1860 /** 1861 * The name for the component. Often used to reference the component. 1862 * 1863 * @type {String} 1864 * @private 1865 */ 1866 vjs.Component.prototype.name_; 1867 1868 /** 1869 * Get the component's name. The name is often used to reference the component. 1870 * 1871 * var name = myComponent.name(); 1872 * 1873 * @return {String} 1874 */ 1875 vjs.Component.prototype.name = function(){ 1876 return this.name_; 1877 }; 1878 1879 /** 1880 * Array of child components 1881 * 1882 * @type {Array} 1883 * @private 1884 */ 1885 vjs.Component.prototype.children_; 1886 1887 /** 1888 * Get an array of all child components 1889 * 1890 * var kids = myComponent.children(); 1891 * 1892 * @return {Array} The children 1893 */ 1894 vjs.Component.prototype.children = function(){ 1895 return this.children_; 1896 }; 1897 1898 /** 1899 * Object of child components by ID 1900 * 1901 * @type {Object} 1902 * @private 1903 */ 1904 vjs.Component.prototype.childIndex_; 1905 1906 /** 1907 * Returns a child component with the provided ID 1908 * 1909 * @return {vjs.Component} 1910 */ 1911 vjs.Component.prototype.getChildById = function(id){ 1912 return this.childIndex_[id]; 1913 }; 1914 1915 /** 1916 * Object of child components by name 1917 * 1918 * @type {Object} 1919 * @private 1920 */ 1921 vjs.Component.prototype.childNameIndex_; 1922 1923 /** 1924 * Returns a child component with the provided name 1925 * 1926 * @return {vjs.Component} 1927 */ 1928 vjs.Component.prototype.getChild = function(name){ 1929 return this.childNameIndex_[name]; 1930 }; 1931 1932 /** 1933 * Adds a child component inside this component 1934 * 1935 * myComponent.el(); 1936 * // -> <div class='my-component'></div> 1937 * myComonent.children(); 1938 * // [empty array] 1939 * 1940 * var myButton = myComponent.addChild('MyButton'); 1941 * // -> <div class='my-component'><div class="my-button">myButton<div></div> 1942 * // -> myButton === myComonent.children()[0]; 1943 * 1944 * Pass in options for child constructors and options for children of the child 1945 * 1946 * var myButton = myComponent.addChild('MyButton', { 1947 * text: 'Press Me', 1948 * children: { 1949 * buttonChildExample: { 1950 * buttonChildOption: true 1951 * } 1952 * } 1953 * }); 1954 * 1955 * @param {String|vjs.Component} child The class name or instance of a child to add 1956 * @param {Object=} options Options, including options to be passed to children of the child. 1957 * @return {vjs.Component} The child component (created by this process if a string was used) 1958 * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility} 1959 */ 1960 vjs.Component.prototype.addChild = function(child, options){ 1961 var component, componentClass, componentName, componentId; 1962 1963 // If string, create new component with options 1964 if (typeof child === 'string') { 1965 1966 componentName = child; 1967 1968 // Make sure options is at least an empty object to protect against errors 1969 options = options || {}; 1970 1971 // Assume name of set is a lowercased name of the UI Class (PlayButton, etc.) 1972 componentClass = options['componentClass'] || vjs.capitalize(componentName); 1973 1974 // Set name through options 1975 options['name'] = componentName; 1976 1977 // Create a new object & element for this controls set 1978 // If there's no .player_, this is a player 1979 // Closure Compiler throws an 'incomplete alias' warning if we use the vjs variable directly. 1980 // Every class should be exported, so this should never be a problem here. 1981 component = new window['videojs'][componentClass](this.player_ || this, options); 1982 1983 // child is a component instance 1984 } else { 1985 component = child; 1986 } 1987 1988 this.children_.push(component); 1989 1990 if (typeof component.id === 'function') { 1991 this.childIndex_[component.id()] = component; 1992 } 1993 1994 // If a name wasn't used to create the component, check if we can use the 1995 // name function of the component 1996 componentName = componentName || (component.name && component.name()); 1997 1998 if (componentName) { 1999 this.childNameIndex_[componentName] = component; 2000 } 2001 2002 // Add the UI object's element to the container div (box) 2003 // Having an element is not required 2004 if (typeof component['el'] === 'function' && component['el']()) { 2005 this.contentEl().appendChild(component['el']()); 2006 } 2007 2008 // Return so it can stored on parent object if desired. 2009 return component; 2010 }; 2011 2012 /** 2013 * Remove a child component from this component's list of children, and the 2014 * child component's element from this component's element 2015 * 2016 * @param {vjs.Component} component Component to remove 2017 */ 2018 vjs.Component.prototype.removeChild = function(component){ 2019 if (typeof component === 'string') { 2020 component = this.getChild(component); 2021 } 2022 2023 if (!component || !this.children_) return; 2024 2025 var childFound = false; 2026 for (var i = this.children_.length - 1; i >= 0; i--) { 2027 if (this.children_[i] === component) { 2028 childFound = true; 2029 this.children_.splice(i,1); 2030 break; 2031 } 2032 } 2033 2034 if (!childFound) return; 2035 2036 this.childIndex_[component.id] = null; 2037 this.childNameIndex_[component.name] = null; 2038 2039 var compEl = component.el(); 2040 if (compEl && compEl.parentNode === this.contentEl()) { 2041 this.contentEl().removeChild(component.el()); 2042 } 2043 }; 2044 2045 /** 2046 * Add and initialize default child components from options 2047 * 2048 * // when an instance of MyComponent is created, all children in options 2049 * // will be added to the instance by their name strings and options 2050 * MyComponent.prototype.options_.children = { 2051 * myChildComponent: { 2052 * myChildOption: true 2053 * } 2054 * } 2055 * 2056 * // Or when creating the component 2057 * var myComp = new MyComponent(player, { 2058 * children: { 2059 * myChildComponent: { 2060 * myChildOption: true 2061 * } 2062 * } 2063 * }); 2064 * 2065 * The children option can also be an Array of child names or 2066 * child options objects (that also include a 'name' key). 2067 * 2068 * var myComp = new MyComponent(player, { 2069 * children: [ 2070 * 'button', 2071 * { 2072 * name: 'button', 2073 * someOtherOption: true 2074 * } 2075 * ] 2076 * }); 2077 * 2078 */ 2079 vjs.Component.prototype.initChildren = function(){ 2080 var parent, children, child, name, opts; 2081 2082 parent = this; 2083 children = this.options()['children']; 2084 2085 if (children) { 2086 // Allow for an array of children details to passed in the options 2087 if (vjs.obj.isArray(children)) { 2088 for (var i = 0; i < children.length; i++) { 2089 child = children[i]; 2090 2091 if (typeof child == 'string') { 2092 name = child; 2093 opts = {}; 2094 } else { 2095 name = child.name; 2096 opts = child; 2097 } 2098 2099 parent[name] = parent.addChild(name, opts); 2100 } 2101 } else { 2102 vjs.obj.each(children, function(name, opts){ 2103 // Allow for disabling default components 2104 // e.g. vjs.options['children']['posterImage'] = false 2105 if (opts === false) return; 2106 2107 // Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy. 2108 parent[name] = parent.addChild(name, opts); 2109 }); 2110 } 2111 } 2112 }; 2113 2114 /** 2115 * Allows sub components to stack CSS class names 2116 * 2117 * @return {String} The constructed class name 2118 */ 2119 vjs.Component.prototype.buildCSSClass = function(){ 2120 // Child classes can include a function that does: 2121 // return 'CLASS NAME' + this._super(); 2122 return ''; 2123 }; 2124 2125 /* Events 2126 ============================================================================= */ 2127 2128 /** 2129 * Add an event listener to this component's element 2130 * 2131 * var myFunc = function(){ 2132 * var myPlayer = this; 2133 * // Do something when the event is fired 2134 * }; 2135 * 2136 * myPlayer.on("eventName", myFunc); 2137 * 2138 * The context will be the component. 2139 * 2140 * @param {String} type The event type e.g. 'click' 2141 * @param {Function} fn The event listener 2142 * @return {vjs.Component} self 2143 */ 2144 vjs.Component.prototype.on = function(type, fn){ 2145 vjs.on(this.el_, type, vjs.bind(this, fn)); 2146 return this; 2147 }; 2148 2149 /** 2150 * Remove an event listener from the component's element 2151 * 2152 * myComponent.off("eventName", myFunc); 2153 * 2154 * @param {String=} type Event type. Without type it will remove all listeners. 2155 * @param {Function=} fn Event listener. Without fn it will remove all listeners for a type. 2156 * @return {vjs.Component} 2157 */ 2158 vjs.Component.prototype.off = function(type, fn){ 2159 vjs.off(this.el_, type, fn); 2160 return this; 2161 }; 2162 2163 /** 2164 * Add an event listener to be triggered only once and then removed 2165 * 2166 * @param {String} type Event type 2167 * @param {Function} fn Event listener 2168 * @return {vjs.Component} 2169 */ 2170 vjs.Component.prototype.one = function(type, fn) { 2171 vjs.one(this.el_, type, vjs.bind(this, fn)); 2172 return this; 2173 }; 2174 2175 /** 2176 * Trigger an event on an element 2177 * 2178 * myComponent.trigger('eventName'); 2179 * myComponent.trigger({'type':'eventName'}); 2180 * 2181 * @param {Event|Object|String} event A string (the type) or an event object with a type attribute 2182 * @return {vjs.Component} self 2183 */ 2184 vjs.Component.prototype.trigger = function(event){ 2185 vjs.trigger(this.el_, event); 2186 return this; 2187 }; 2188 2189 /* Ready 2190 ================================================================================ */ 2191 /** 2192 * Is the component loaded 2193 * This can mean different things depending on the component. 2194 * 2195 * @private 2196 * @type {Boolean} 2197 */ 2198 vjs.Component.prototype.isReady_; 2199 2200 /** 2201 * Trigger ready as soon as initialization is finished 2202 * 2203 * Allows for delaying ready. Override on a sub class prototype. 2204 * If you set this.isReadyOnInitFinish_ it will affect all components. 2205 * Specially used when waiting for the Flash player to asynchrnously load. 2206 * 2207 * @type {Boolean} 2208 * @private 2209 */ 2210 vjs.Component.prototype.isReadyOnInitFinish_ = true; 2211 2212 /** 2213 * List of ready listeners 2214 * 2215 * @type {Array} 2216 * @private 2217 */ 2218 vjs.Component.prototype.readyQueue_; 2219 2220 /** 2221 * Bind a listener to the component's ready state 2222 * 2223 * Different from event listeners in that if the ready event has already happend 2224 * it will trigger the function immediately. 2225 * 2226 * @param {Function} fn Ready listener 2227 * @return {vjs.Component} 2228 */ 2229 vjs.Component.prototype.ready = function(fn){ 2230 if (fn) { 2231 if (this.isReady_) { 2232 fn.call(this); 2233 } else { 2234 if (this.readyQueue_ === undefined) { 2235 this.readyQueue_ = []; 2236 } 2237 this.readyQueue_.push(fn); 2238 } 2239 } 2240 return this; 2241 }; 2242 2243 /** 2244 * Trigger the ready listeners 2245 * 2246 * @return {vjs.Component} 2247 */ 2248 vjs.Component.prototype.triggerReady = function(){ 2249 this.isReady_ = true; 2250 2251 var readyQueue = this.readyQueue_; 2252 2253 if (readyQueue && readyQueue.length > 0) { 2254 2255 for (var i = 0, j = readyQueue.length; i < j; i++) { 2256 readyQueue[i].call(this); 2257 } 2258 2259 // Reset Ready Queue 2260 this.readyQueue_ = []; 2261 2262 // Allow for using event listeners also, in case you want to do something everytime a source is ready. 2263 this.trigger('ready'); 2264 } 2265 }; 2266 2267 /* Display 2268 ============================================================================= */ 2269 2270 /** 2271 * Add a CSS class name to the component's element 2272 * 2273 * @param {String} classToAdd Classname to add 2274 * @return {vjs.Component} 2275 */ 2276 vjs.Component.prototype.addClass = function(classToAdd){ 2277 vjs.addClass(this.el_, classToAdd); 2278 return this; 2279 }; 2280 2281 /** 2282 * Remove a CSS class name from the component's element 2283 * 2284 * @param {String} classToRemove Classname to remove 2285 * @return {vjs.Component} 2286 */ 2287 vjs.Component.prototype.removeClass = function(classToRemove){ 2288 vjs.removeClass(this.el_, classToRemove); 2289 return this; 2290 }; 2291 2292 /** 2293 * Show the component element if hidden 2294 * 2295 * @return {vjs.Component} 2296 */ 2297 vjs.Component.prototype.show = function(){ 2298 this.el_.style.display = 'block'; 2299 return this; 2300 }; 2301 2302 /** 2303 * Hide the component element if currently showing 2304 * 2305 * @return {vjs.Component} 2306 */ 2307 vjs.Component.prototype.hide = function(){ 2308 this.el_.style.display = 'none'; 2309 return this; 2310 }; 2311 2312 /** 2313 * Lock an item in its visible state 2314 * To be used with fadeIn/fadeOut. 2315 * 2316 * @return {vjs.Component} 2317 * @private 2318 */ 2319 vjs.Component.prototype.lockShowing = function(){ 2320 this.addClass('vjs-lock-showing'); 2321 return this; 2322 }; 2323 2324 /** 2325 * Unlock an item to be hidden 2326 * To be used with fadeIn/fadeOut. 2327 * 2328 * @return {vjs.Component} 2329 * @private 2330 */ 2331 vjs.Component.prototype.unlockShowing = function(){ 2332 this.removeClass('vjs-lock-showing'); 2333 return this; 2334 }; 2335 2336 /** 2337 * Disable component by making it unshowable 2338 * 2339 * Currently private because we're movign towards more css-based states. 2340 * @private 2341 */ 2342 vjs.Component.prototype.disable = function(){ 2343 this.hide(); 2344 this.show = function(){}; 2345 }; 2346 2347 /** 2348 * Set or get the width of the component (CSS values) 2349 * 2350 * Setting the video tag dimension values only works with values in pixels. 2351 * Percent values will not work. 2352 * Some percents can be used, but width()/height() will return the number + %, 2353 * not the actual computed width/height. 2354 * 2355 * @param {Number|String=} num Optional width number 2356 * @param {Boolean} skipListeners Skip the 'resize' event trigger 2357 * @return {vjs.Component} This component, when setting the width 2358 * @return {Number|String} The width, when getting 2359 */ 2360 vjs.Component.prototype.width = function(num, skipListeners){ 2361 return this.dimension('width', num, skipListeners); 2362 }; 2363 2364 /** 2365 * Get or set the height of the component (CSS values) 2366 * 2367 * Setting the video tag dimension values only works with values in pixels. 2368 * Percent values will not work. 2369 * Some percents can be used, but width()/height() will return the number + %, 2370 * not the actual computed width/height. 2371 * 2372 * @param {Number|String=} num New component height 2373 * @param {Boolean=} skipListeners Skip the resize event trigger 2374 * @return {vjs.Component} This component, when setting the height 2375 * @return {Number|String} The height, when getting 2376 */ 2377 vjs.Component.prototype.height = function(num, skipListeners){ 2378 return this.dimension('height', num, skipListeners); 2379 }; 2380 2381 /** 2382 * Set both width and height at the same time 2383 * 2384 * @param {Number|String} width 2385 * @param {Number|String} height 2386 * @return {vjs.Component} The component 2387 */ 2388 vjs.Component.prototype.dimensions = function(width, height){ 2389 // Skip resize listeners on width for optimization 2390 return this.width(width, true).height(height); 2391 }; 2392 2393 /** 2394 * Get or set width or height 2395 * 2396 * This is the shared code for the width() and height() methods. 2397 * All for an integer, integer + 'px' or integer + '%'; 2398 * 2399 * Known issue: Hidden elements officially have a width of 0. We're defaulting 2400 * to the style.width value and falling back to computedStyle which has the 2401 * hidden element issue. Info, but probably not an efficient fix: 2402 * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/ 2403 * 2404 * @param {String} widthOrHeight 'width' or 'height' 2405 * @param {Number|String=} num New dimension 2406 * @param {Boolean=} skipListeners Skip resize event trigger 2407 * @return {vjs.Component} The component if a dimension was set 2408 * @return {Number|String} The dimension if nothing was set 2409 * @private 2410 */ 2411 vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ 2412 if (num !== undefined) { 2413 if (num === null || vjs.isNaN(num)) { 2414 num = 0; 2415 } 2416 2417 // Check if using css width/height (% or px) and adjust 2418 if ((''+num).indexOf('%') !== -1 || (''+num).indexOf('px') !== -1) { 2419 this.el_.style[widthOrHeight] = num; 2420 } else if (num === 'auto') { 2421 this.el_.style[widthOrHeight] = ''; 2422 } else { 2423 this.el_.style[widthOrHeight] = num+'px'; 2424 } 2425 2426 // skipListeners allows us to avoid triggering the resize event when setting both width and height 2427 if (!skipListeners) { this.trigger('resize'); } 2428 2429 // Return component 2430 return this; 2431 } 2432 2433 // Not setting a value, so getting it 2434 // Make sure element exists 2435 if (!this.el_) return 0; 2436 2437 // Get dimension value from style 2438 var val = this.el_.style[widthOrHeight]; 2439 var pxIndex = val.indexOf('px'); 2440 if (pxIndex !== -1) { 2441 // Return the pixel value with no 'px' 2442 return parseInt(val.slice(0,pxIndex), 10); 2443 2444 // No px so using % or no style was set, so falling back to offsetWidth/height 2445 // If component has display:none, offset will return 0 2446 // TODO: handle display:none and no dimension style using px 2447 } else { 2448 2449 return parseInt(this.el_['offset'+vjs.capitalize(widthOrHeight)], 10); 2450 2451 // ComputedStyle version. 2452 // Only difference is if the element is hidden it will return 2453 // the percent value (e.g. '100%'') 2454 // instead of zero like offsetWidth returns. 2455 // var val = vjs.getComputedStyleValue(this.el_, widthOrHeight); 2456 // var pxIndex = val.indexOf('px'); 2457 2458 // if (pxIndex !== -1) { 2459 // return val.slice(0, pxIndex); 2460 // } else { 2461 // return val; 2462 // } 2463 } 2464 }; 2465 2466 /** 2467 * Fired when the width and/or height of the component changes 2468 * @event resize 2469 */ 2470 vjs.Component.prototype.onResize; 2471 2472 /** 2473 * Emit 'tap' events when touch events are supported 2474 * 2475 * This is used to support toggling the controls through a tap on the video. 2476 * 2477 * We're requireing them to be enabled because otherwise every component would 2478 * have this extra overhead unnecessarily, on mobile devices where extra 2479 * overhead is especially bad. 2480 * @private 2481 */ 2482 vjs.Component.prototype.emitTapEvents = function(){ 2483 var touchStart, firstTouch, touchTime, couldBeTap, noTap, 2484 xdiff, ydiff, touchDistance, tapMovementThreshold; 2485 2486 // Track the start time so we can determine how long the touch lasted 2487 touchStart = 0; 2488 firstTouch = null; 2489 2490 // Maximum movement allowed during a touch event to still be considered a tap 2491 tapMovementThreshold = 22; 2492 2493 this.on('touchstart', function(event) { 2494 // If more than one finger, don't consider treating this as a click 2495 if (event.touches.length === 1) { 2496 firstTouch = event.touches[0]; 2497 // Record start time so we can detect a tap vs. "touch and hold" 2498 touchStart = new Date().getTime(); 2499 // Reset couldBeTap tracking 2500 couldBeTap = true; 2501 } 2502 }); 2503 2504 this.on('touchmove', function(event) { 2505 // If more than one finger, don't consider treating this as a click 2506 if (event.touches.length > 1) { 2507 couldBeTap = false; 2508 } else if (firstTouch) { 2509 // Some devices will throw touchmoves for all but the slightest of taps. 2510 // So, if we moved only a small distance, this could still be a tap 2511 xdiff = event.touches[0].pageX - firstTouch.pageX; 2512 ydiff = event.touches[0].pageY - firstTouch.pageY; 2513 touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); 2514 if (touchDistance > tapMovementThreshold) { 2515 couldBeTap = false; 2516 } 2517 } 2518 }); 2519 2520 noTap = function(){ 2521 couldBeTap = false; 2522 }; 2523 // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s 2524 this.on('touchleave', noTap); 2525 this.on('touchcancel', noTap); 2526 2527 // When the touch ends, measure how long it took and trigger the appropriate 2528 // event 2529 this.on('touchend', function(event) { 2530 firstTouch = null; 2531 // Proceed only if the touchmove/leave/cancel event didn't happen 2532 if (couldBeTap === true) { 2533 // Measure how long the touch lasted 2534 touchTime = new Date().getTime() - touchStart; 2535 // The touch needs to be quick in order to consider it a tap 2536 if (touchTime < 250) { 2537 event.preventDefault(); // Don't let browser turn this into a click 2538 this.trigger('tap'); 2539 // It may be good to copy the touchend event object and change the 2540 // type to tap, if the other event properties aren't exact after 2541 // vjs.fixEvent runs (e.g. event.target) 2542 } 2543 } 2544 }); 2545 }; 2546 2547 /** 2548 * Report user touch activity when touch events occur 2549 * 2550 * User activity is used to determine when controls should show/hide. It's 2551 * relatively simple when it comes to mouse events, because any mouse event 2552 * should show the controls. So we capture mouse events that bubble up to the 2553 * player and report activity when that happens. 2554 * 2555 * With touch events it isn't as easy. We can't rely on touch events at the 2556 * player level, because a tap (touchstart + touchend) on the video itself on 2557 * mobile devices is meant to turn controls off (and on). User activity is 2558 * checked asynchronously, so what could happen is a tap event on the video 2559 * turns the controls off, then the touchend event bubbles up to the player, 2560 * which if it reported user activity, would turn the controls right back on. 2561 * (We also don't want to completely block touch events from bubbling up) 2562 * 2563 * Also a touchmove, touch+hold, and anything other than a tap is not supposed 2564 * to turn the controls back on on a mobile device. 2565 * 2566 * Here we're setting the default component behavior to report user activity 2567 * whenever touch events happen, and this can be turned off by components that 2568 * want touch events to act differently. 2569 */ 2570 vjs.Component.prototype.enableTouchActivity = function() { 2571 var report, touchHolding, touchEnd; 2572 2573 // listener for reporting that the user is active 2574 report = vjs.bind(this.player(), this.player().reportUserActivity); 2575 2576 this.on('touchstart', function() { 2577 report(); 2578 // For as long as the they are touching the device or have their mouse down, 2579 // we consider them active even if they're not moving their finger or mouse. 2580 // So we want to continue to update that they are active 2581 clearInterval(touchHolding); 2582 // report at the same interval as activityCheck 2583 touchHolding = setInterval(report, 250); 2584 }); 2585 2586 touchEnd = function(event) { 2587 report(); 2588 // stop the interval that maintains activity if the touch is holding 2589 clearInterval(touchHolding); 2590 }; 2591 2592 this.on('touchmove', report); 2593 this.on('touchend', touchEnd); 2594 this.on('touchcancel', touchEnd); 2595 }; 2596 2597 /* Button - Base class for all buttons 2598 ================================================================================ */ 2599 /** 2600 * Base class for all buttons 2601 * @param {vjs.Player|Object} player 2602 * @param {Object=} options 2603 * @class 2604 * @constructor 2605 */ 2606 vjs.Button = vjs.Component.extend({ 2607 /** 2608 * @constructor 2609 * @inheritDoc 2610 */ 2611 init: function(player, options){ 2612 vjs.Component.call(this, player, options); 2613 2614 this.emitTapEvents(); 2615 2616 this.on('tap', this.onClick); 2617 this.on('click', this.onClick); 2618 this.on('focus', this.onFocus); 2619 this.on('blur', this.onBlur); 2620 } 2621 }); 2622 2623 vjs.Button.prototype.createEl = function(type, props){ 2624 var el; 2625 2626 // Add standard Aria and Tabindex info 2627 props = vjs.obj.merge({ 2628 className: this.buildCSSClass(), 2629 'role': 'button', 2630 'aria-live': 'polite', // let the screen reader user know that the text of the button may change 2631 tabIndex: 0 2632 }, props); 2633 2634 el = vjs.Component.prototype.createEl.call(this, type, props); 2635 2636 // if innerHTML hasn't been overridden (bigPlayButton), add content elements 2637 if (!props.innerHTML) { 2638 this.contentEl_ = vjs.createEl('div', { 2639 className: 'vjs-control-content' 2640 }); 2641 2642 this.controlText_ = vjs.createEl('span', { 2643 className: 'vjs-control-text', 2644 innerHTML: this.localize(this.buttonText) || 'Need Text' 2645 }); 2646 2647 this.contentEl_.appendChild(this.controlText_); 2648 el.appendChild(this.contentEl_); 2649 } 2650 2651 return el; 2652 }; 2653 2654 vjs.Button.prototype.buildCSSClass = function(){ 2655 // TODO: Change vjs-control to vjs-button? 2656 return 'vjs-control ' + vjs.Component.prototype.buildCSSClass.call(this); 2657 }; 2658 2659 // Click - Override with specific functionality for button 2660 vjs.Button.prototype.onClick = function(){}; 2661 2662 // Focus - Add keyboard functionality to element 2663 vjs.Button.prototype.onFocus = function(){ 2664 vjs.on(document, 'keydown', vjs.bind(this, this.onKeyPress)); 2665 }; 2666 2667 // KeyPress (document level) - Trigger click when keys are pressed 2668 vjs.Button.prototype.onKeyPress = function(event){ 2669 // Check for space bar (32) or enter (13) keys 2670 if (event.which == 32 || event.which == 13) { 2671 event.preventDefault(); 2672 this.onClick(); 2673 } 2674 }; 2675 2676 // Blur - Remove keyboard triggers 2677 vjs.Button.prototype.onBlur = function(){ 2678 vjs.off(document, 'keydown', vjs.bind(this, this.onKeyPress)); 2679 }; 2680 /* Slider 2681 ================================================================================ */ 2682 /** 2683 * The base functionality for sliders like the volume bar and seek bar 2684 * 2685 * @param {vjs.Player|Object} player 2686 * @param {Object=} options 2687 * @constructor 2688 */ 2689 vjs.Slider = vjs.Component.extend({ 2690 /** @constructor */ 2691 init: function(player, options){ 2692 vjs.Component.call(this, player, options); 2693 2694 // Set property names to bar and handle to match with the child Slider class is looking for 2695 this.bar = this.getChild(this.options_['barName']); 2696 this.handle = this.getChild(this.options_['handleName']); 2697 2698 this.on('mousedown', this.onMouseDown); 2699 this.on('touchstart', this.onMouseDown); 2700 this.on('focus', this.onFocus); 2701 this.on('blur', this.onBlur); 2702 this.on('click', this.onClick); 2703 2704 this.player_.on('controlsvisible', vjs.bind(this, this.update)); 2705 2706 player.on(this.playerEvent, vjs.bind(this, this.update)); 2707 2708 this.boundEvents = {}; 2709 2710 2711 this.boundEvents.move = vjs.bind(this, this.onMouseMove); 2712 this.boundEvents.end = vjs.bind(this, this.onMouseUp); 2713 } 2714 }); 2715 2716 vjs.Slider.prototype.createEl = function(type, props) { 2717 props = props || {}; 2718 // Add the slider element class to all sub classes 2719 props.className = props.className + ' vjs-slider'; 2720 props = vjs.obj.merge({ 2721 'role': 'slider', 2722 'aria-valuenow': 0, 2723 'aria-valuemin': 0, 2724 'aria-valuemax': 100, 2725 tabIndex: 0 2726 }, props); 2727 2728 return vjs.Component.prototype.createEl.call(this, type, props); 2729 }; 2730 2731 vjs.Slider.prototype.onMouseDown = function(event){ 2732 event.preventDefault(); 2733 vjs.blockTextSelection(); 2734 this.addClass('vjs-sliding'); 2735 2736 vjs.on(document, 'mousemove', this.boundEvents.move); 2737 vjs.on(document, 'mouseup', this.boundEvents.end); 2738 vjs.on(document, 'touchmove', this.boundEvents.move); 2739 vjs.on(document, 'touchend', this.boundEvents.end); 2740 2741 this.onMouseMove(event); 2742 }; 2743 2744 // To be overridden by a subclass 2745 vjs.Slider.prototype.onMouseMove = function(){}; 2746 2747 vjs.Slider.prototype.onMouseUp = function() { 2748 vjs.unblockTextSelection(); 2749 this.removeClass('vjs-sliding'); 2750 2751 vjs.off(document, 'mousemove', this.boundEvents.move, false); 2752 vjs.off(document, 'mouseup', this.boundEvents.end, false); 2753 vjs.off(document, 'touchmove', this.boundEvents.move, false); 2754 vjs.off(document, 'touchend', this.boundEvents.end, false); 2755 2756 this.update(); 2757 }; 2758 2759 vjs.Slider.prototype.update = function(){ 2760 // In VolumeBar init we have a setTimeout for update that pops and update to the end of the 2761 // execution stack. The player is destroyed before then update will cause an error 2762 if (!this.el_) return; 2763 2764 // If scrubbing, we could use a cached value to make the handle keep up with the user's mouse. 2765 // On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later. 2766 // var progress = (this.player_.scrubbing) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); 2767 2768 var barProgress, 2769 progress = this.getPercent(), 2770 handle = this.handle, 2771 bar = this.bar; 2772 2773 // Protect against no duration and other division issues 2774 if (isNaN(progress)) { progress = 0; } 2775 2776 barProgress = progress; 2777 2778 // If there is a handle, we need to account for the handle in our calculation for progress bar 2779 // so that it doesn't fall short of or extend past the handle. 2780 if (handle) { 2781 2782 var box = this.el_, 2783 boxWidth = box.offsetWidth, 2784 2785 handleWidth = handle.el().offsetWidth, 2786 2787 // The width of the handle in percent of the containing box 2788 // In IE, widths may not be ready yet causing NaN 2789 handlePercent = (handleWidth) ? handleWidth / boxWidth : 0, 2790 2791 // Get the adjusted size of the box, considering that the handle's center never touches the left or right side. 2792 // There is a margin of half the handle's width on both sides. 2793 boxAdjustedPercent = 1 - handlePercent, 2794 2795 // Adjust the progress that we'll use to set widths to the new adjusted box width 2796 adjustedProgress = progress * boxAdjustedPercent; 2797 2798 // The bar does reach the left side, so we need to account for this in the bar's width 2799 barProgress = adjustedProgress + (handlePercent / 2); 2800 2801 // Move the handle from the left based on the adjected progress 2802 handle.el().style.left = vjs.round(adjustedProgress * 100, 2) + '%'; 2803 } 2804 2805 // Set the new bar width 2806 if (bar) { 2807 bar.el().style.width = vjs.round(barProgress * 100, 2) + '%'; 2808 } 2809 }; 2810 2811 vjs.Slider.prototype.calculateDistance = function(event){ 2812 var el, box, boxX, boxY, boxW, boxH, handle, pageX, pageY; 2813 2814 el = this.el_; 2815 box = vjs.findPosition(el); 2816 boxW = boxH = el.offsetWidth; 2817 handle = this.handle; 2818 2819 if (this.options()['vertical']) { 2820 boxY = box.top; 2821 2822 if (event.changedTouches) { 2823 pageY = event.changedTouches[0].pageY; 2824 } else { 2825 pageY = event.pageY; 2826 } 2827 2828 if (handle) { 2829 var handleH = handle.el().offsetHeight; 2830 // Adjusted X and Width, so handle doesn't go outside the bar 2831 boxY = boxY + (handleH / 2); 2832 boxH = boxH - handleH; 2833 } 2834 2835 // Percent that the click is through the adjusted area 2836 return Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); 2837 2838 } else { 2839 boxX = box.left; 2840 2841 if (event.changedTouches) { 2842 pageX = event.changedTouches[0].pageX; 2843 } else { 2844 pageX = event.pageX; 2845 } 2846 2847 if (handle) { 2848 var handleW = handle.el().offsetWidth; 2849 2850 // Adjusted X and Width, so handle doesn't go outside the bar 2851 boxX = boxX + (handleW / 2); 2852 boxW = boxW - handleW; 2853 } 2854 2855 // Percent that the click is through the adjusted area 2856 return Math.max(0, Math.min(1, (pageX - boxX) / boxW)); 2857 } 2858 }; 2859 2860 vjs.Slider.prototype.onFocus = function(){ 2861 vjs.on(document, 'keyup', vjs.bind(this, this.onKeyPress)); 2862 }; 2863 2864 vjs.Slider.prototype.onKeyPress = function(event){ 2865 if (event.which == 37 || event.which == 40) { // Left and Down Arrows 2866 event.preventDefault(); 2867 this.stepBack(); 2868 } else if (event.which == 38 || event.which == 39) { // Up and Right Arrows 2869 event.preventDefault(); 2870 this.stepForward(); 2871 } 2872 }; 2873 2874 vjs.Slider.prototype.onBlur = function(){ 2875 vjs.off(document, 'keyup', vjs.bind(this, this.onKeyPress)); 2876 }; 2877 2878 /** 2879 * Listener for click events on slider, used to prevent clicks 2880 * from bubbling up to parent elements like button menus. 2881 * @param {Object} event Event object 2882 */ 2883 vjs.Slider.prototype.onClick = function(event){ 2884 event.stopImmediatePropagation(); 2885 event.preventDefault(); 2886 }; 2887 2888 /** 2889 * SeekBar Behavior includes play progress bar, and seek handle 2890 * Needed so it can determine seek position based on handle position/size 2891 * @param {vjs.Player|Object} player 2892 * @param {Object=} options 2893 * @constructor 2894 */ 2895 vjs.SliderHandle = vjs.Component.extend(); 2896 2897 /** 2898 * Default value of the slider 2899 * 2900 * @type {Number} 2901 * @private 2902 */ 2903 vjs.SliderHandle.prototype.defaultValue = 0; 2904 2905 /** @inheritDoc */ 2906 vjs.SliderHandle.prototype.createEl = function(type, props) { 2907 props = props || {}; 2908 // Add the slider element class to all sub classes 2909 props.className = props.className + ' vjs-slider-handle'; 2910 props = vjs.obj.merge({ 2911 innerHTML: '<span class="vjs-control-text">'+this.defaultValue+'</span>' 2912 }, props); 2913 2914 return vjs.Component.prototype.createEl.call(this, 'div', props); 2915 }; 2916 /* Menu 2917 ================================================================================ */ 2918 /** 2919 * The Menu component is used to build pop up menus, including subtitle and 2920 * captions selection menus. 2921 * 2922 * @param {vjs.Player|Object} player 2923 * @param {Object=} options 2924 * @class 2925 * @constructor 2926 */ 2927 vjs.Menu = vjs.Component.extend(); 2928 2929 /** 2930 * Add a menu item to the menu 2931 * @param {Object|String} component Component or component type to add 2932 */ 2933 vjs.Menu.prototype.addItem = function(component){ 2934 this.addChild(component); 2935 component.on('click', vjs.bind(this, function(){ 2936 this.unlockShowing(); 2937 })); 2938 }; 2939 2940 /** @inheritDoc */ 2941 vjs.Menu.prototype.createEl = function(){ 2942 var contentElType = this.options().contentElType || 'ul'; 2943 this.contentEl_ = vjs.createEl(contentElType, { 2944 className: 'vjs-menu-content' 2945 }); 2946 var el = vjs.Component.prototype.createEl.call(this, 'div', { 2947 append: this.contentEl_, 2948 className: 'vjs-menu' 2949 }); 2950 el.appendChild(this.contentEl_); 2951 2952 // Prevent clicks from bubbling up. Needed for Menu Buttons, 2953 // where a click on the parent is significant 2954 vjs.on(el, 'click', function(event){ 2955 event.preventDefault(); 2956 event.stopImmediatePropagation(); 2957 }); 2958 2959 return el; 2960 }; 2961 2962 /** 2963 * The component for a menu item. `<li>` 2964 * 2965 * @param {vjs.Player|Object} player 2966 * @param {Object=} options 2967 * @class 2968 * @constructor 2969 */ 2970 vjs.MenuItem = vjs.Button.extend({ 2971 /** @constructor */ 2972 init: function(player, options){ 2973 vjs.Button.call(this, player, options); 2974 this.selected(options['selected']); 2975 } 2976 }); 2977 2978 /** @inheritDoc */ 2979 vjs.MenuItem.prototype.createEl = function(type, props){ 2980 return vjs.Button.prototype.createEl.call(this, 'li', vjs.obj.merge({ 2981 className: 'vjs-menu-item', 2982 innerHTML: this.options_['label'] 2983 }, props)); 2984 }; 2985 2986 /** 2987 * Handle a click on the menu item, and set it to selected 2988 */ 2989 vjs.MenuItem.prototype.onClick = function(){ 2990 this.selected(true); 2991 }; 2992 2993 /** 2994 * Set this menu item as selected or not 2995 * @param {Boolean} selected 2996 */ 2997 vjs.MenuItem.prototype.selected = function(selected){ 2998 if (selected) { 2999 this.addClass('vjs-selected'); 3000 this.el_.setAttribute('aria-selected',true); 3001 } else { 3002 this.removeClass('vjs-selected'); 3003 this.el_.setAttribute('aria-selected',false); 3004 } 3005 }; 3006 3007 3008 /** 3009 * A button class with a popup menu 3010 * @param {vjs.Player|Object} player 3011 * @param {Object=} options 3012 * @constructor 3013 */ 3014 vjs.MenuButton = vjs.Button.extend({ 3015 /** @constructor */ 3016 init: function(player, options){ 3017 vjs.Button.call(this, player, options); 3018 3019 this.menu = this.createMenu(); 3020 3021 // Add list to element 3022 this.addChild(this.menu); 3023 3024 // Automatically hide empty menu buttons 3025 if (this.items && this.items.length === 0) { 3026 this.hide(); 3027 } 3028 3029 this.on('keyup', this.onKeyPress); 3030 this.el_.setAttribute('aria-haspopup', true); 3031 this.el_.setAttribute('role', 'button'); 3032 } 3033 }); 3034 3035 /** 3036 * Track the state of the menu button 3037 * @type {Boolean} 3038 * @private 3039 */ 3040 vjs.MenuButton.prototype.buttonPressed_ = false; 3041 3042 vjs.MenuButton.prototype.createMenu = function(){ 3043 var menu = new vjs.Menu(this.player_); 3044 3045 // Add a title list item to the top 3046 if (this.options().title) { 3047 menu.contentEl().appendChild(vjs.createEl('li', { 3048 className: 'vjs-menu-title', 3049 innerHTML: vjs.capitalize(this.options().title), 3050 tabindex: -1 3051 })); 3052 } 3053 3054 this.items = this['createItems'](); 3055 3056 if (this.items) { 3057 // Add menu items to the menu 3058 for (var i = 0; i < this.items.length; i++) { 3059 menu.addItem(this.items[i]); 3060 } 3061 } 3062 3063 return menu; 3064 }; 3065 3066 /** 3067 * Create the list of menu items. Specific to each subclass. 3068 */ 3069 vjs.MenuButton.prototype.createItems = function(){}; 3070 3071 /** @inheritDoc */ 3072 vjs.MenuButton.prototype.buildCSSClass = function(){ 3073 return this.className + ' vjs-menu-button ' + vjs.Button.prototype.buildCSSClass.call(this); 3074 }; 3075 3076 // Focus - Add keyboard functionality to element 3077 // This function is not needed anymore. Instead, the keyboard functionality is handled by 3078 // treating the button as triggering a submenu. When the button is pressed, the submenu 3079 // appears. Pressing the button again makes the submenu disappear. 3080 vjs.MenuButton.prototype.onFocus = function(){}; 3081 // Can't turn off list display that we turned on with focus, because list would go away. 3082 vjs.MenuButton.prototype.onBlur = function(){}; 3083 3084 vjs.MenuButton.prototype.onClick = function(){ 3085 // When you click the button it adds focus, which will show the menu indefinitely. 3086 // So we'll remove focus when the mouse leaves the button. 3087 // Focus is needed for tab navigation. 3088 this.one('mouseout', vjs.bind(this, function(){ 3089 this.menu.unlockShowing(); 3090 this.el_.blur(); 3091 })); 3092 if (this.buttonPressed_){ 3093 this.unpressButton(); 3094 } else { 3095 this.pressButton(); 3096 } 3097 }; 3098 3099 vjs.MenuButton.prototype.onKeyPress = function(event){ 3100 event.preventDefault(); 3101 3102 // Check for space bar (32) or enter (13) keys 3103 if (event.which == 32 || event.which == 13) { 3104 if (this.buttonPressed_){ 3105 this.unpressButton(); 3106 } else { 3107 this.pressButton(); 3108 } 3109 // Check for escape (27) key 3110 } else if (event.which == 27){ 3111 if (this.buttonPressed_){ 3112 this.unpressButton(); 3113 } 3114 } 3115 }; 3116 3117 vjs.MenuButton.prototype.pressButton = function(){ 3118 this.buttonPressed_ = true; 3119 this.menu.lockShowing(); 3120 this.el_.setAttribute('aria-pressed', true); 3121 if (this.items && this.items.length > 0) { 3122 this.items[0].el().focus(); // set the focus to the title of the submenu 3123 } 3124 }; 3125 3126 vjs.MenuButton.prototype.unpressButton = function(){ 3127 this.buttonPressed_ = false; 3128 this.menu.unlockShowing(); 3129 this.el_.setAttribute('aria-pressed', false); 3130 }; 3131 3132 /** 3133 * Custom MediaError to mimic the HTML5 MediaError 3134 * @param {Number} code The media error code 3135 */ 3136 vjs.MediaError = function(code){ 3137 if (typeof code === 'number') { 3138 this.code = code; 3139 } else if (typeof code === 'string') { 3140 // default code is zero, so this is a custom error 3141 this.message = code; 3142 } else if (typeof code === 'object') { // object 3143 vjs.obj.merge(this, code); 3144 } 3145 3146 if (!this.message) { 3147 this.message = vjs.MediaError.defaultMessages[this.code] || ''; 3148 } 3149 }; 3150 3151 /** 3152 * The error code that refers two one of the defined 3153 * MediaError types 3154 * @type {Number} 3155 */ 3156 vjs.MediaError.prototype.code = 0; 3157 3158 /** 3159 * An optional message to be shown with the error. 3160 * Message is not part of the HTML5 video spec 3161 * but allows for more informative custom errors. 3162 * @type {String} 3163 */ 3164 vjs.MediaError.prototype.message = ''; 3165 3166 /** 3167 * An optional status code that can be set by plugins 3168 * to allow even more detail about the error. 3169 * For example the HLS plugin might provide the specific 3170 * HTTP status code that was returned when the error 3171 * occurred, then allowing a custom error overlay 3172 * to display more information. 3173 * @type {[type]} 3174 */ 3175 vjs.MediaError.prototype.status = null; 3176 3177 vjs.MediaError.errorTypes = [ 3178 'MEDIA_ERR_CUSTOM', // = 0 3179 'MEDIA_ERR_ABORTED', // = 1 3180 'MEDIA_ERR_NETWORK', // = 2 3181 'MEDIA_ERR_DECODE', // = 3 3182 'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4 3183 'MEDIA_ERR_ENCRYPTED' // = 5 3184 ]; 3185 3186 vjs.MediaError.defaultMessages = { 3187 1: 'You aborted the video playback', 3188 2: 'A network error caused the video download to fail part-way.', 3189 3: 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support.', 3190 4: 'The video could not be loaded, either because the server or network failed or because the format is not supported.', 3191 5: 'The video is encrypted and we do not have the keys to decrypt it.' 3192 }; 3193 3194 // Add types as properties on MediaError 3195 // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 3196 for (var errNum = 0; errNum < vjs.MediaError.errorTypes.length; errNum++) { 3197 vjs.MediaError[vjs.MediaError.errorTypes[errNum]] = errNum; 3198 // values should be accessible on both the class and instance 3199 vjs.MediaError.prototype[vjs.MediaError.errorTypes[errNum]] = errNum; 3200 } 3201 (function(){ 3202 var apiMap, specApi, browserApi, i; 3203 3204 /** 3205 * Store the browser-specifc methods for the fullscreen API 3206 * @type {Object|undefined} 3207 * @private 3208 */ 3209 vjs.browser.fullscreenAPI; 3210 3211 // browser API methods 3212 // map approach from Screenful.js - https://github.com/sindresorhus/screenfull.js 3213 apiMap = [ 3214 // Spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html 3215 [ 3216 'requestFullscreen', 3217 'exitFullscreen', 3218 'fullscreenElement', 3219 'fullscreenEnabled', 3220 'fullscreenchange', 3221 'fullscreenerror' 3222 ], 3223 // WebKit 3224 [ 3225 'webkitRequestFullscreen', 3226 'webkitExitFullscreen', 3227 'webkitFullscreenElement', 3228 'webkitFullscreenEnabled', 3229 'webkitfullscreenchange', 3230 'webkitfullscreenerror' 3231 ], 3232 // Old WebKit (Safari 5.1) 3233 [ 3234 'webkitRequestFullScreen', 3235 'webkitCancelFullScreen', 3236 'webkitCurrentFullScreenElement', 3237 'webkitCancelFullScreen', 3238 'webkitfullscreenchange', 3239 'webkitfullscreenerror' 3240 ], 3241 // Mozilla 3242 [ 3243 'mozRequestFullScreen', 3244 'mozCancelFullScreen', 3245 'mozFullScreenElement', 3246 'mozFullScreenEnabled', 3247 'mozfullscreenchange', 3248 'mozfullscreenerror' 3249 ], 3250 // Microsoft 3251 [ 3252 'msRequestFullscreen', 3253 'msExitFullscreen', 3254 'msFullscreenElement', 3255 'msFullscreenEnabled', 3256 'MSFullscreenChange', 3257 'MSFullscreenError' 3258 ] 3259 ]; 3260 3261 specApi = apiMap[0]; 3262 3263 // determine the supported set of functions 3264 for (i=0; i<apiMap.length; i++) { 3265 // check for exitFullscreen function 3266 if (apiMap[i][1] in document) { 3267 browserApi = apiMap[i]; 3268 break; 3269 } 3270 } 3271 3272 // map the browser API names to the spec API names 3273 // or leave vjs.browser.fullscreenAPI undefined 3274 if (browserApi) { 3275 vjs.browser.fullscreenAPI = {}; 3276 3277 for (i=0; i<browserApi.length; i++) { 3278 vjs.browser.fullscreenAPI[specApi[i]] = browserApi[i]; 3279 } 3280 } 3281 3282 })(); 3283 /** 3284 * An instance of the `vjs.Player` class is created when any of the Video.js setup methods are used to initialize a video. 3285 * 3286 * ```js 3287 * var myPlayer = videojs('example_video_1'); 3288 * ``` 3289 * 3290 * In the following example, the `data-setup` attribute tells the Video.js library to create a player instance when the library is ready. 3291 * 3292 * ```html 3293 * <video id="example_video_1" data-setup='{}' controls> 3294 * <source src="my-source.mp4" type="video/mp4"> 3295 * </video> 3296 * ``` 3297 * 3298 * After an instance has been created it can be accessed globally using `Video('example_video_1')`. 3299 * 3300 * @class 3301 * @extends vjs.Component 3302 */ 3303 vjs.Player = vjs.Component.extend({ 3304 3305 /** 3306 * player's constructor function 3307 * 3308 * @constructs 3309 * @method init 3310 * @param {Element} tag The original video tag used for configuring options 3311 * @param {Object=} options Player options 3312 * @param {Function=} ready Ready callback function 3313 */ 3314 init: function(tag, options, ready){ 3315 this.tag = tag; // Store the original tag used to set options 3316 3317 // Make sure tag ID exists 3318 tag.id = tag.id || 'vjs_video_' + vjs.guid++; 3319 3320 // Store the tag attributes used to restore html5 element 3321 this.tagAttributes = tag && vjs.getElementAttributes(tag); 3322 3323 // Set Options 3324 // The options argument overrides options set in the video tag 3325 // which overrides globally set options. 3326 // This latter part coincides with the load order 3327 // (tag must exist before Player) 3328 options = vjs.obj.merge(this.getTagSettings(tag), options); 3329 3330 // Update Current Language 3331 this.language_ = options['language'] || vjs.options['language']; 3332 3333 // Update Supported Languages 3334 this.languages_ = options['languages'] || vjs.options['languages']; 3335 3336 // Cache for video property values. 3337 this.cache_ = {}; 3338 3339 // Set poster 3340 this.poster_ = options['poster']; 3341 // Set controls 3342 this.controls_ = options['controls']; 3343 // Original tag settings stored in options 3344 // now remove immediately so native controls don't flash. 3345 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 3346 tag.controls = false; 3347 3348 // we don't want the player to report touch activity on itself 3349 // see enableTouchActivity in Component 3350 options.reportTouchActivity = false; 3351 3352 // Run base component initializing with new options. 3353 // Builds the element through createEl() 3354 // Inits and embeds any child components in opts 3355 vjs.Component.call(this, this, options, ready); 3356 3357 // Update controls className. Can't do this when the controls are initially 3358 // set because the element doesn't exist yet. 3359 if (this.controls()) { 3360 this.addClass('vjs-controls-enabled'); 3361 } else { 3362 this.addClass('vjs-controls-disabled'); 3363 } 3364 3365 // TODO: Make this smarter. Toggle user state between touching/mousing 3366 // using events, since devices can have both touch and mouse events. 3367 // if (vjs.TOUCH_ENABLED) { 3368 // this.addClass('vjs-touch-enabled'); 3369 // } 3370 3371 // Make player easily findable by ID 3372 vjs.players[this.id_] = this; 3373 3374 if (options['plugins']) { 3375 vjs.obj.each(options['plugins'], function(key, val){ 3376 this[key](val); 3377 }, this); 3378 } 3379 3380 this.listenForUserActivity(); 3381 } 3382 }); 3383 3384 /** 3385 * The players's stored language code 3386 * 3387 * @type {String} 3388 * @private 3389 */ 3390 vjs.Player.prototype.language_; 3391 3392 /** 3393 * The player's language code 3394 * @param {String} languageCode The locale string 3395 * @return {String} The locale string when getting 3396 * @return {vjs.Player} self, when setting 3397 */ 3398 vjs.Player.prototype.language = function (languageCode) { 3399 if (languageCode === undefined) { 3400 return this.language_; 3401 } 3402 3403 this.language_ = languageCode; 3404 return this; 3405 }; 3406 3407 /** 3408 * The players's stored language dictionary 3409 * 3410 * @type {Object} 3411 * @private 3412 */ 3413 vjs.Player.prototype.languages_; 3414 3415 vjs.Player.prototype.languages = function(){ 3416 return this.languages_; 3417 }; 3418 3419 /** 3420 * Player instance options, surfaced using vjs.options 3421 * vjs.options = vjs.Player.prototype.options_ 3422 * Make changes in vjs.options, not here. 3423 * All options should use string keys so they avoid 3424 * renaming by closure compiler 3425 * @type {Object} 3426 * @private 3427 */ 3428 vjs.Player.prototype.options_ = vjs.options; 3429 3430 /** 3431 * Destroys the video player and does any necessary cleanup 3432 * 3433 * myPlayer.dispose(); 3434 * 3435 * This is especially helpful if you are dynamically adding and removing videos 3436 * to/from the DOM. 3437 */ 3438 vjs.Player.prototype.dispose = function(){ 3439 this.trigger('dispose'); 3440 // prevent dispose from being called twice 3441 this.off('dispose'); 3442 3443 // Kill reference to this player 3444 vjs.players[this.id_] = null; 3445 if (this.tag && this.tag['player']) { this.tag['player'] = null; } 3446 if (this.el_ && this.el_['player']) { this.el_['player'] = null; } 3447 3448 if (this.tech) { this.tech.dispose(); } 3449 3450 // Component dispose 3451 vjs.Component.prototype.dispose.call(this); 3452 }; 3453 3454 vjs.Player.prototype.getTagSettings = function(tag){ 3455 var options = { 3456 'sources': [], 3457 'tracks': [] 3458 }; 3459 3460 vjs.obj.merge(options, vjs.getElementAttributes(tag)); 3461 3462 // Get tag children settings 3463 if (tag.hasChildNodes()) { 3464 var children, child, childName, i, j; 3465 3466 children = tag.childNodes; 3467 3468 for (i=0,j=children.length; i<j; i++) { 3469 child = children[i]; 3470 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 3471 childName = child.nodeName.toLowerCase(); 3472 if (childName === 'source') { 3473 options['sources'].push(vjs.getElementAttributes(child)); 3474 } else if (childName === 'track') { 3475 options['tracks'].push(vjs.getElementAttributes(child)); 3476 } 3477 } 3478 } 3479 3480 return options; 3481 }; 3482 3483 vjs.Player.prototype.createEl = function(){ 3484 var 3485 el = this.el_ = vjs.Component.prototype.createEl.call(this, 'div'), 3486 tag = this.tag, 3487 attrs; 3488 3489 // Remove width/height attrs from tag so CSS can make it 100% width/height 3490 tag.removeAttribute('width'); 3491 tag.removeAttribute('height'); 3492 // Empty video tag tracks so the built-in player doesn't use them also. 3493 // This may not be fast enough to stop HTML5 browsers from reading the tags 3494 // so we'll need to turn off any default tracks if we're manually doing 3495 // captions and subtitles. videoElement.textTracks 3496 if (tag.hasChildNodes()) { 3497 var nodes, nodesLength, i, node, nodeName, removeNodes; 3498 3499 nodes = tag.childNodes; 3500 nodesLength = nodes.length; 3501 removeNodes = []; 3502 3503 while (nodesLength--) { 3504 node = nodes[nodesLength]; 3505 nodeName = node.nodeName.toLowerCase(); 3506 if (nodeName === 'track') { 3507 removeNodes.push(node); 3508 } 3509 } 3510 3511 for (i=0; i<removeNodes.length; i++) { 3512 tag.removeChild(removeNodes[i]); 3513 } 3514 } 3515 3516 // Copy over all the attributes from the tag, including ID and class 3517 // ID will now reference player box, not the video tag 3518 attrs = vjs.getElementAttributes(tag); 3519 vjs.obj.each(attrs, function(attr) { 3520 el.setAttribute(attr, attrs[attr]); 3521 }); 3522 3523 // Update tag id/class for use as HTML5 playback tech 3524 // Might think we should do this after embedding in container so .vjs-tech class 3525 // doesn't flash 100% width/height, but class only applies with .video-js parent 3526 tag.id += '_html5_api'; 3527 tag.className = 'vjs-tech'; 3528 3529 // Make player findable on elements 3530 tag['player'] = el['player'] = this; 3531 // Default state of video is paused 3532 this.addClass('vjs-paused'); 3533 3534 // Make box use width/height of tag, or rely on default implementation 3535 // Enforce with CSS since width/height attrs don't work on divs 3536 this.width(this.options_['width'], true); // (true) Skip resize listener on load 3537 this.height(this.options_['height'], true); 3538 3539 // Wrap video tag in div (el/box) container 3540 if (tag.parentNode) { 3541 tag.parentNode.insertBefore(el, tag); 3542 } 3543 vjs.insertFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup. 3544 3545 // The event listeners need to be added before the children are added 3546 // in the component init because the tech (loaded with mediaLoader) may 3547 // fire events, like loadstart, that these events need to capture. 3548 // Long term it might be better to expose a way to do this in component.init 3549 // like component.initEventListeners() that runs between el creation and 3550 // adding children 3551 this.el_ = el; 3552 this.on('loadstart', this.onLoadStart); 3553 this.on('waiting', this.onWaiting); 3554 this.on(['canplay', 'canplaythrough', 'playing', 'ended'], this.onWaitEnd); 3555 this.on('seeking', this.onSeeking); 3556 this.on('seeked', this.onSeeked); 3557 this.on('ended', this.onEnded); 3558 this.on('play', this.onPlay); 3559 this.on('firstplay', this.onFirstPlay); 3560 this.on('pause', this.onPause); 3561 this.on('progress', this.onProgress); 3562 this.on('durationchange', this.onDurationChange); 3563 this.on('fullscreenchange', this.onFullscreenChange); 3564 3565 return el; 3566 }; 3567 3568 // /* Media Technology (tech) 3569 // ================================================================================ */ 3570 // Load/Create an instance of playback technlogy including element and API methods 3571 // And append playback element in player div. 3572 vjs.Player.prototype.loadTech = function(techName, source){ 3573 3574 // Pause and remove current playback technology 3575 if (this.tech) { 3576 this.unloadTech(); 3577 } 3578 3579 // get rid of the HTML5 video tag as soon as we are using another tech 3580 if (techName !== 'Html5' && this.tag) { 3581 vjs.Html5.disposeMediaElement(this.tag); 3582 this.tag = null; 3583 } 3584 3585 this.techName = techName; 3586 3587 // Turn off API access because we're loading a new tech that might load asynchronously 3588 this.isReady_ = false; 3589 3590 var techReady = function(){ 3591 this.player_.triggerReady(); 3592 }; 3593 3594 // Grab tech-specific options from player options and add source and parent element to use. 3595 var techOptions = vjs.obj.merge({ 'source': source, 'parentEl': this.el_ }, this.options_[techName.toLowerCase()]); 3596 3597 if (source) { 3598 this.currentType_ = source.type; 3599 if (source.src == this.cache_.src && this.cache_.currentTime > 0) { 3600 techOptions['startTime'] = this.cache_.currentTime; 3601 } 3602 3603 this.cache_.src = source.src; 3604 } 3605 3606 // Initialize tech instance 3607 this.tech = new window['videojs'][techName](this, techOptions); 3608 3609 this.tech.ready(techReady); 3610 }; 3611 3612 vjs.Player.prototype.unloadTech = function(){ 3613 this.isReady_ = false; 3614 3615 this.tech.dispose(); 3616 3617 this.tech = false; 3618 }; 3619 3620 // There's many issues around changing the size of a Flash (or other plugin) object. 3621 // First is a plugin reload issue in Firefox that has been around for 11 years: https://bugzilla.mozilla.org/show_bug.cgi?id=90268 3622 // Then with the new fullscreen API, Mozilla and webkit browsers will reload the flash object after going to fullscreen. 3623 // To get around this, we're unloading the tech, caching source and currentTime values, and reloading the tech once the plugin is resized. 3624 // reloadTech: function(betweenFn){ 3625 // vjs.log('unloadingTech') 3626 // this.unloadTech(); 3627 // vjs.log('unloadedTech') 3628 // if (betweenFn) { betweenFn.call(); } 3629 // vjs.log('LoadingTech') 3630 // this.loadTech(this.techName, { src: this.cache_.src }) 3631 // vjs.log('loadedTech') 3632 // }, 3633 3634 // /* Player event handlers (how the player reacts to certain events) 3635 // ================================================================================ */ 3636 3637 /** 3638 * Fired when the user agent begins looking for media data 3639 * @event loadstart 3640 */ 3641 vjs.Player.prototype.onLoadStart = function() { 3642 // TODO: Update to use `emptied` event instead. See #1277. 3643 3644 // reset the error state 3645 this.error(null); 3646 3647 // If it's already playing we want to trigger a firstplay event now. 3648 // The firstplay event relies on both the play and loadstart events 3649 // which can happen in any order for a new source 3650 if (!this.paused()) { 3651 this.trigger('firstplay'); 3652 } else { 3653 // reset the hasStarted state 3654 this.hasStarted(false); 3655 this.one('play', function(){ 3656 this.hasStarted(true); 3657 }); 3658 } 3659 }; 3660 3661 vjs.Player.prototype.hasStarted_ = false; 3662 3663 vjs.Player.prototype.hasStarted = function(hasStarted){ 3664 if (hasStarted !== undefined) { 3665 // only update if this is a new value 3666 if (this.hasStarted_ !== hasStarted) { 3667 this.hasStarted_ = hasStarted; 3668 if (hasStarted) { 3669 this.addClass('vjs-has-started'); 3670 // trigger the firstplay event if this newly has played 3671 this.trigger('firstplay'); 3672 } else { 3673 this.removeClass('vjs-has-started'); 3674 } 3675 } 3676 return this; 3677 } 3678 return this.hasStarted_; 3679 }; 3680 3681 /** 3682 * Fired when the player has initial duration and dimension information 3683 * @event loadedmetadata 3684 */ 3685 vjs.Player.prototype.onLoadedMetaData; 3686 3687 /** 3688 * Fired when the player has downloaded data at the current playback position 3689 * @event loadeddata 3690 */ 3691 vjs.Player.prototype.onLoadedData; 3692 3693 /** 3694 * Fired when the player has finished downloading the source data 3695 * @event loadedalldata 3696 */ 3697 vjs.Player.prototype.onLoadedAllData; 3698 3699 /** 3700 * Fired whenever the media begins or resumes playback 3701 * @event play 3702 */ 3703 vjs.Player.prototype.onPlay = function(){ 3704 this.removeClass('vjs-paused'); 3705 this.addClass('vjs-playing'); 3706 }; 3707 3708 /** 3709 * Fired whenever the media begins wating 3710 * @event waiting 3711 */ 3712 vjs.Player.prototype.onWaiting = function(){ 3713 this.addClass('vjs-waiting'); 3714 }; 3715 3716 /** 3717 * A handler for events that signal that waiting has eneded 3718 * which is not consistent between browsers. See #1351 3719 */ 3720 vjs.Player.prototype.onWaitEnd = function(){ 3721 this.removeClass('vjs-waiting'); 3722 }; 3723 3724 /** 3725 * Fired whenever the player is jumping to a new time 3726 * @event seeking 3727 */ 3728 vjs.Player.prototype.onSeeking = function(){ 3729 this.addClass('vjs-seeking'); 3730 }; 3731 3732 /** 3733 * Fired when the player has finished jumping to a new time 3734 * @event seeked 3735 */ 3736 vjs.Player.prototype.onSeeked = function(){ 3737 this.removeClass('vjs-seeking'); 3738 }; 3739 3740 /** 3741 * Fired the first time a video is played 3742 * 3743 * Not part of the HLS spec, and we're not sure if this is the best 3744 * implementation yet, so use sparingly. If you don't have a reason to 3745 * prevent playback, use `myPlayer.one('play');` instead. 3746 * 3747 * @event firstplay 3748 */ 3749 vjs.Player.prototype.onFirstPlay = function(){ 3750 //If the first starttime attribute is specified 3751 //then we will start at the given offset in seconds 3752 if(this.options_['starttime']){ 3753 this.currentTime(this.options_['starttime']); 3754 } 3755 3756 this.addClass('vjs-has-started'); 3757 }; 3758 3759 /** 3760 * Fired whenever the media has been paused 3761 * @event pause 3762 */ 3763 vjs.Player.prototype.onPause = function(){ 3764 this.removeClass('vjs-playing'); 3765 this.addClass('vjs-paused'); 3766 }; 3767 3768 /** 3769 * Fired when the current playback position has changed 3770 * 3771 * During playback this is fired every 15-250 milliseconds, depnding on the 3772 * playback technology in use. 3773 * @event timeupdate 3774 */ 3775 vjs.Player.prototype.onTimeUpdate; 3776 3777 /** 3778 * Fired while the user agent is downloading media data 3779 * @event progress 3780 */ 3781 vjs.Player.prototype.onProgress = function(){ 3782 // Add custom event for when source is finished downloading. 3783 if (this.bufferedPercent() == 1) { 3784 this.trigger('loadedalldata'); 3785 } 3786 }; 3787 3788 /** 3789 * Fired when the end of the media resource is reached (currentTime == duration) 3790 * @event ended 3791 */ 3792 vjs.Player.prototype.onEnded = function(){ 3793 if (this.options_['loop']) { 3794 this.currentTime(0); 3795 this.play(); 3796 } else if (!this.paused()) { 3797 this.pause(); 3798 } 3799 }; 3800 3801 /** 3802 * Fired when the duration of the media resource is first known or changed 3803 * @event durationchange 3804 */ 3805 vjs.Player.prototype.onDurationChange = function(){ 3806 // Allows for cacheing value instead of asking player each time. 3807 // We need to get the techGet response and check for a value so we don't 3808 // accidentally cause the stack to blow up. 3809 var duration = this.techGet('duration'); 3810 if (duration) { 3811 if (duration < 0) { 3812 duration = Infinity; 3813 } 3814 this.duration(duration); 3815 // Determine if the stream is live and propagate styles down to UI. 3816 if (duration === Infinity) { 3817 this.addClass('vjs-live'); 3818 } else { 3819 this.removeClass('vjs-live'); 3820 } 3821 } 3822 }; 3823 3824 /** 3825 * Fired when the volume changes 3826 * @event volumechange 3827 */ 3828 vjs.Player.prototype.onVolumeChange; 3829 3830 /** 3831 * Fired when the player switches in or out of fullscreen mode 3832 * @event fullscreenchange 3833 */ 3834 vjs.Player.prototype.onFullscreenChange = function() { 3835 if (this.isFullscreen()) { 3836 this.addClass('vjs-fullscreen'); 3837 } else { 3838 this.removeClass('vjs-fullscreen'); 3839 } 3840 }; 3841 3842 // /* Player API 3843 // ================================================================================ */ 3844 3845 /** 3846 * Object for cached values. 3847 * @private 3848 */ 3849 vjs.Player.prototype.cache_; 3850 3851 vjs.Player.prototype.getCache = function(){ 3852 return this.cache_; 3853 }; 3854 3855 // Pass values to the playback tech 3856 vjs.Player.prototype.techCall = function(method, arg){ 3857 // If it's not ready yet, call method when it is 3858 if (this.tech && !this.tech.isReady_) { 3859 this.tech.ready(function(){ 3860 this[method](arg); 3861 }); 3862 3863 // Otherwise call method now 3864 } else { 3865 try { 3866 this.tech[method](arg); 3867 } catch(e) { 3868 vjs.log(e); 3869 throw e; 3870 } 3871 } 3872 }; 3873 3874 // Get calls can't wait for the tech, and sometimes don't need to. 3875 vjs.Player.prototype.techGet = function(method){ 3876 if (this.tech && this.tech.isReady_) { 3877 3878 // Flash likes to die and reload when you hide or reposition it. 3879 // In these cases the object methods go away and we get errors. 3880 // When that happens we'll catch the errors and inform tech that it's not ready any more. 3881 try { 3882 return this.tech[method](); 3883 } catch(e) { 3884 // When building additional tech libs, an expected method may not be defined yet 3885 if (this.tech[method] === undefined) { 3886 vjs.log('Video.js: ' + method + ' method not defined for '+this.techName+' playback technology.', e); 3887 } else { 3888 // When a method isn't available on the object it throws a TypeError 3889 if (e.name == 'TypeError') { 3890 vjs.log('Video.js: ' + method + ' unavailable on '+this.techName+' playback technology element.', e); 3891 this.tech.isReady_ = false; 3892 } else { 3893 vjs.log(e); 3894 } 3895 } 3896 throw e; 3897 } 3898 } 3899 3900 return; 3901 }; 3902 3903 /** 3904 * start media playback 3905 * 3906 * myPlayer.play(); 3907 * 3908 * @return {vjs.Player} self 3909 */ 3910 vjs.Player.prototype.play = function(){ 3911 this.techCall('play'); 3912 return this; 3913 }; 3914 3915 /** 3916 * Pause the video playback 3917 * 3918 * myPlayer.pause(); 3919 * 3920 * @return {vjs.Player} self 3921 */ 3922 vjs.Player.prototype.pause = function(){ 3923 this.techCall('pause'); 3924 return this; 3925 }; 3926 3927 /** 3928 * Check if the player is paused 3929 * 3930 * var isPaused = myPlayer.paused(); 3931 * var isPlaying = !myPlayer.paused(); 3932 * 3933 * @return {Boolean} false if the media is currently playing, or true otherwise 3934 */ 3935 vjs.Player.prototype.paused = function(){ 3936 // The initial state of paused should be true (in Safari it's actually false) 3937 return (this.techGet('paused') === false) ? false : true; 3938 }; 3939 3940 /** 3941 * Get or set the current time (in seconds) 3942 * 3943 * // get 3944 * var whereYouAt = myPlayer.currentTime(); 3945 * 3946 * // set 3947 * myPlayer.currentTime(120); // 2 minutes into the video 3948 * 3949 * @param {Number|String=} seconds The time to seek to 3950 * @return {Number} The time in seconds, when not setting 3951 * @return {vjs.Player} self, when the current time is set 3952 */ 3953 vjs.Player.prototype.currentTime = function(seconds){ 3954 if (seconds !== undefined) { 3955 3956 this.techCall('setCurrentTime', seconds); 3957 3958 return this; 3959 } 3960 3961 // cache last currentTime and return. default to 0 seconds 3962 // 3963 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's 3964 // currentTime when scrubbing, but may not provide much performace benefit afterall. 3965 // Should be tested. Also something has to read the actual current time or the cache will 3966 // never get updated. 3967 return this.cache_.currentTime = (this.techGet('currentTime') || 0); 3968 }; 3969 3970 /** 3971 * Get the length in time of the video in seconds 3972 * 3973 * var lengthOfVideo = myPlayer.duration(); 3974 * 3975 * **NOTE**: The video must have started loading before the duration can be 3976 * known, and in the case of Flash, may not be known until the video starts 3977 * playing. 3978 * 3979 * @return {Number} The duration of the video in seconds 3980 */ 3981 vjs.Player.prototype.duration = function(seconds){ 3982 if (seconds !== undefined) { 3983 3984 // cache the last set value for optimiized scrubbing (esp. Flash) 3985 this.cache_.duration = parseFloat(seconds); 3986 3987 return this; 3988 } 3989 3990 if (this.cache_.duration === undefined) { 3991 this.onDurationChange(); 3992 } 3993 3994 return this.cache_.duration || 0; 3995 }; 3996 3997 // Calculates how much time is left. Not in spec, but useful. 3998 vjs.Player.prototype.remainingTime = function(){ 3999 return this.duration() - this.currentTime(); 4000 }; 4001 4002 // http://dev.w3.org/html5/spec/video.html#dom-media-buffered 4003 // Buffered returns a timerange object. 4004 // Kind of like an array of portions of the video that have been downloaded. 4005 4006 /** 4007 * Get a TimeRange object with the times of the video that have been downloaded 4008 * 4009 * If you just want the percent of the video that's been downloaded, 4010 * use bufferedPercent. 4011 * 4012 * // Number of different ranges of time have been buffered. Usually 1. 4013 * numberOfRanges = bufferedTimeRange.length, 4014 * 4015 * // Time in seconds when the first range starts. Usually 0. 4016 * firstRangeStart = bufferedTimeRange.start(0), 4017 * 4018 * // Time in seconds when the first range ends 4019 * firstRangeEnd = bufferedTimeRange.end(0), 4020 * 4021 * // Length in seconds of the first time range 4022 * firstRangeLength = firstRangeEnd - firstRangeStart; 4023 * 4024 * @return {Object} A mock TimeRange object (following HTML spec) 4025 */ 4026 vjs.Player.prototype.buffered = function(){ 4027 var buffered = this.techGet('buffered'); 4028 4029 if (!buffered || !buffered.length) { 4030 buffered = vjs.createTimeRange(0,0); 4031 } 4032 4033 return buffered; 4034 }; 4035 4036 /** 4037 * Get the percent (as a decimal) of the video that's been downloaded 4038 * 4039 * var howMuchIsDownloaded = myPlayer.bufferedPercent(); 4040 * 4041 * 0 means none, 1 means all. 4042 * (This method isn't in the HTML5 spec, but it's very convenient) 4043 * 4044 * @return {Number} A decimal between 0 and 1 representing the percent 4045 */ 4046 vjs.Player.prototype.bufferedPercent = function(){ 4047 var duration = this.duration(), 4048 buffered = this.buffered(), 4049 bufferedDuration = 0, 4050 start, end; 4051 4052 if (!duration) { 4053 return 0; 4054 } 4055 4056 for (var i=0; i<buffered.length; i++){ 4057 start = buffered.start(i); 4058 end = buffered.end(i); 4059 4060 // buffered end can be bigger than duration by a very small fraction 4061 if (end > duration) { 4062 end = duration; 4063 } 4064 4065 bufferedDuration += end - start; 4066 } 4067 4068 return bufferedDuration / duration; 4069 }; 4070 4071 /** 4072 * Get the ending time of the last buffered time range 4073 * 4074 * This is used in the progress bar to encapsulate all time ranges. 4075 * @return {Number} The end of the last buffered time range 4076 */ 4077 vjs.Player.prototype.bufferedEnd = function(){ 4078 var buffered = this.buffered(), 4079 duration = this.duration(), 4080 end = buffered.end(buffered.length-1); 4081 4082 if (end > duration) { 4083 end = duration; 4084 } 4085 4086 return end; 4087 }; 4088 4089 /** 4090 * Get or set the current volume of the media 4091 * 4092 * // get 4093 * var howLoudIsIt = myPlayer.volume(); 4094 * 4095 * // set 4096 * myPlayer.volume(0.5); // Set volume to half 4097 * 4098 * 0 is off (muted), 1.0 is all the way up, 0.5 is half way. 4099 * 4100 * @param {Number} percentAsDecimal The new volume as a decimal percent 4101 * @return {Number} The current volume, when getting 4102 * @return {vjs.Player} self, when setting 4103 */ 4104 vjs.Player.prototype.volume = function(percentAsDecimal){ 4105 var vol; 4106 4107 if (percentAsDecimal !== undefined) { 4108 vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1 4109 this.cache_.volume = vol; 4110 this.techCall('setVolume', vol); 4111 vjs.setLocalStorage('volume', vol); 4112 return this; 4113 } 4114 4115 // Default to 1 when returning current volume. 4116 vol = parseFloat(this.techGet('volume')); 4117 return (isNaN(vol)) ? 1 : vol; 4118 }; 4119 4120 4121 /** 4122 * Get the current muted state, or turn mute on or off 4123 * 4124 * // get 4125 * var isVolumeMuted = myPlayer.muted(); 4126 * 4127 * // set 4128 * myPlayer.muted(true); // mute the volume 4129 * 4130 * @param {Boolean=} muted True to mute, false to unmute 4131 * @return {Boolean} True if mute is on, false if not, when getting 4132 * @return {vjs.Player} self, when setting mute 4133 */ 4134 vjs.Player.prototype.muted = function(muted){ 4135 if (muted !== undefined) { 4136 this.techCall('setMuted', muted); 4137 return this; 4138 } 4139 return this.techGet('muted') || false; // Default to false 4140 }; 4141 4142 // Check if current tech can support native fullscreen 4143 // (e.g. with built in controls lik iOS, so not our flash swf) 4144 vjs.Player.prototype.supportsFullScreen = function(){ 4145 return this.techGet('supportsFullScreen') || false; 4146 }; 4147 4148 /** 4149 * is the player in fullscreen 4150 * @type {Boolean} 4151 * @private 4152 */ 4153 vjs.Player.prototype.isFullscreen_ = false; 4154 4155 /** 4156 * Check if the player is in fullscreen mode 4157 * 4158 * // get 4159 * var fullscreenOrNot = myPlayer.isFullscreen(); 4160 * 4161 * // set 4162 * myPlayer.isFullscreen(true); // tell the player it's in fullscreen 4163 * 4164 * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 4165 * property and instead document.fullscreenElement is used. But isFullscreen is 4166 * still a valuable property for internal player workings. 4167 * 4168 * @param {Boolean=} isFS Update the player's fullscreen state 4169 * @return {Boolean} true if fullscreen, false if not 4170 * @return {vjs.Player} self, when setting 4171 */ 4172 vjs.Player.prototype.isFullscreen = function(isFS){ 4173 if (isFS !== undefined) { 4174 this.isFullscreen_ = !!isFS; 4175 return this; 4176 } 4177 return this.isFullscreen_; 4178 }; 4179 4180 /** 4181 * Old naming for isFullscreen() 4182 * @deprecated for lowercase 's' version 4183 */ 4184 vjs.Player.prototype.isFullScreen = function(isFS){ 4185 vjs.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")'); 4186 return this.isFullscreen(isFS); 4187 }; 4188 4189 /** 4190 * Increase the size of the video to full screen 4191 * 4192 * myPlayer.requestFullscreen(); 4193 * 4194 * In some browsers, full screen is not supported natively, so it enters 4195 * "full window mode", where the video fills the browser window. 4196 * In browsers and devices that support native full screen, sometimes the 4197 * browser's default controls will be shown, and not the Video.js custom skin. 4198 * This includes most mobile devices (iOS, Android) and older versions of 4199 * Safari. 4200 * 4201 * @return {vjs.Player} self 4202 */ 4203 vjs.Player.prototype.requestFullscreen = function(){ 4204 var fsApi = vjs.browser.fullscreenAPI; 4205 4206 this.isFullscreen(true); 4207 4208 if (fsApi) { 4209 // the browser supports going fullscreen at the element level so we can 4210 // take the controls fullscreen as well as the video 4211 4212 // Trigger fullscreenchange event after change 4213 // We have to specifically add this each time, and remove 4214 // when cancelling fullscreen. Otherwise if there's multiple 4215 // players on a page, they would all be reacting to the same fullscreen 4216 // events 4217 vjs.on(document, fsApi['fullscreenchange'], vjs.bind(this, function(e){ 4218 this.isFullscreen(document[fsApi.fullscreenElement]); 4219 4220 // If cancelling fullscreen, remove event listener. 4221 if (this.isFullscreen() === false) { 4222 vjs.off(document, fsApi['fullscreenchange'], arguments.callee); 4223 } 4224 4225 this.trigger('fullscreenchange'); 4226 })); 4227 4228 this.el_[fsApi.requestFullscreen](); 4229 4230 } else if (this.tech.supportsFullScreen()) { 4231 // we can't take the video.js controls fullscreen but we can go fullscreen 4232 // with native controls 4233 this.techCall('enterFullScreen'); 4234 } else { 4235 // fullscreen isn't supported so we'll just stretch the video element to 4236 // fill the viewport 4237 this.enterFullWindow(); 4238 this.trigger('fullscreenchange'); 4239 } 4240 4241 return this; 4242 }; 4243 4244 /** 4245 * Old naming for requestFullscreen 4246 * @deprecated for lower case 's' version 4247 */ 4248 vjs.Player.prototype.requestFullScreen = function(){ 4249 vjs.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")'); 4250 return this.requestFullscreen(); 4251 }; 4252 4253 4254 /** 4255 * Return the video to its normal size after having been in full screen mode 4256 * 4257 * myPlayer.exitFullscreen(); 4258 * 4259 * @return {vjs.Player} self 4260 */ 4261 vjs.Player.prototype.exitFullscreen = function(){ 4262 var fsApi = vjs.browser.fullscreenAPI; 4263 this.isFullscreen(false); 4264 4265 // Check for browser element fullscreen support 4266 if (fsApi) { 4267 document[fsApi.exitFullscreen](); 4268 } else if (this.tech.supportsFullScreen()) { 4269 this.techCall('exitFullScreen'); 4270 } else { 4271 this.exitFullWindow(); 4272 this.trigger('fullscreenchange'); 4273 } 4274 4275 return this; 4276 }; 4277 4278 /** 4279 * Old naming for exitFullscreen 4280 * @deprecated for exitFullscreen 4281 */ 4282 vjs.Player.prototype.cancelFullScreen = function(){ 4283 vjs.log.warn('player.cancelFullScreen() has been deprecated, use player.exitFullscreen()'); 4284 return this.exitFullscreen(); 4285 }; 4286 4287 // When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us. 4288 vjs.Player.prototype.enterFullWindow = function(){ 4289 this.isFullWindow = true; 4290 4291 // Storing original doc overflow value to return to when fullscreen is off 4292 this.docOrigOverflow = document.documentElement.style.overflow; 4293 4294 // Add listener for esc key to exit fullscreen 4295 vjs.on(document, 'keydown', vjs.bind(this, this.fullWindowOnEscKey)); 4296 4297 // Hide any scroll bars 4298 document.documentElement.style.overflow = 'hidden'; 4299 4300 // Apply fullscreen styles 4301 vjs.addClass(document.body, 'vjs-full-window'); 4302 4303 this.trigger('enterFullWindow'); 4304 }; 4305 vjs.Player.prototype.fullWindowOnEscKey = function(event){ 4306 if (event.keyCode === 27) { 4307 if (this.isFullscreen() === true) { 4308 this.exitFullscreen(); 4309 } else { 4310 this.exitFullWindow(); 4311 } 4312 } 4313 }; 4314 4315 vjs.Player.prototype.exitFullWindow = function(){ 4316 this.isFullWindow = false; 4317 vjs.off(document, 'keydown', this.fullWindowOnEscKey); 4318 4319 // Unhide scroll bars. 4320 document.documentElement.style.overflow = this.docOrigOverflow; 4321 4322 // Remove fullscreen styles 4323 vjs.removeClass(document.body, 'vjs-full-window'); 4324 4325 // Resize the box, controller, and poster to original sizes 4326 // this.positionAll(); 4327 this.trigger('exitFullWindow'); 4328 }; 4329 4330 vjs.Player.prototype.selectSource = function(sources){ 4331 4332 // Loop through each playback technology in the options order 4333 for (var i=0,j=this.options_['techOrder'];i<j.length;i++) { 4334 var techName = vjs.capitalize(j[i]), 4335 tech = window['videojs'][techName]; 4336 4337 // Check if the current tech is defined before continuing 4338 if (!tech) { 4339 vjs.log.error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); 4340 continue; 4341 } 4342 4343 // Check if the browser supports this technology 4344 if (tech.isSupported()) { 4345 // Loop through each source object 4346 for (var a=0,b=sources;a<b.length;a++) { 4347 var source = b[a]; 4348 4349 // Check if source can be played with this technology 4350 if (tech['canPlaySource'](source)) { 4351 return { source: source, tech: techName }; 4352 } 4353 } 4354 } 4355 } 4356 4357 return false; 4358 }; 4359 4360 /** 4361 * The source function updates the video source 4362 * 4363 * There are three types of variables you can pass as the argument. 4364 * 4365 * **URL String**: A URL to the the video file. Use this method if you are sure 4366 * the current playback technology (HTML5/Flash) can support the source you 4367 * provide. Currently only MP4 files can be used in both HTML5 and Flash. 4368 * 4369 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 4370 * 4371 * **Source Object (or element):** A javascript object containing information 4372 * about the source file. Use this method if you want the player to determine if 4373 * it can support the file using the type information. 4374 * 4375 * myPlayer.src({ type: "video/mp4", src: "http://www.example.com/path/to/video.mp4" }); 4376 * 4377 * **Array of Source Objects:** To provide multiple versions of the source so 4378 * that it can be played using HTML5 across browsers you can use an array of 4379 * source objects. Video.js will detect which version is supported and load that 4380 * file. 4381 * 4382 * myPlayer.src([ 4383 * { type: "video/mp4", src: "http://www.example.com/path/to/video.mp4" }, 4384 * { type: "video/webm", src: "http://www.example.com/path/to/video.webm" }, 4385 * { type: "video/ogg", src: "http://www.example.com/path/to/video.ogv" } 4386 * ]); 4387 * 4388 * @param {String|Object|Array=} source The source URL, object, or array of sources 4389 * @return {String} The current video source when getting 4390 * @return {String} The player when setting 4391 */ 4392 vjs.Player.prototype.src = function(source){ 4393 if (source === undefined) { 4394 return this.techGet('src'); 4395 } 4396 4397 // case: Array of source objects to choose from and pick the best to play 4398 if (vjs.obj.isArray(source)) { 4399 this.sourceList_(source); 4400 4401 // case: URL String (http://myvideo...) 4402 } else if (typeof source === 'string') { 4403 // create a source object from the string 4404 this.src({ src: source }); 4405 4406 // case: Source object { src: '', type: '' ... } 4407 } else if (source instanceof Object) { 4408 // check if the source has a type and the loaded tech cannot play the source 4409 // if there's no type we'll just try the current tech 4410 if (source.type && !window['videojs'][this.techName]['canPlaySource'](source)) { 4411 // create a source list with the current source and send through 4412 // the tech loop to check for a compatible technology 4413 this.sourceList_([source]); 4414 } else { 4415 this.cache_.src = source.src; 4416 this.currentType_ = source.type || ''; 4417 4418 // wait until the tech is ready to set the source 4419 this.ready(function(){ 4420 this.techCall('src', source.src); 4421 4422 if (this.options_['preload'] == 'auto') { 4423 this.load(); 4424 } 4425 4426 if (this.options_['autoplay']) { 4427 this.play(); 4428 } 4429 }); 4430 } 4431 } 4432 4433 return this; 4434 }; 4435 4436 /** 4437 * Handle an array of source objects 4438 * @param {[type]} sources Array of source objects 4439 * @private 4440 */ 4441 vjs.Player.prototype.sourceList_ = function(sources){ 4442 var sourceTech = this.selectSource(sources), 4443 errorTimeout; 4444 4445 if (sourceTech) { 4446 if (sourceTech.tech === this.techName) { 4447 // if this technology is already loaded, set the source 4448 this.src(sourceTech.source); 4449 } else { 4450 // load this technology with the chosen source 4451 this.loadTech(sourceTech.tech, sourceTech.source); 4452 } 4453 } else { 4454 // We need to wrap this in a timeout to give folks a chance to add error event handlers 4455 errorTimeout = setTimeout(vjs.bind(this, function() { 4456 this.error({ code: 4, message: this.localize(this.options()['notSupportedMessage']) }); 4457 }), 0); 4458 4459 // we could not find an appropriate tech, but let's still notify the delegate that this is it 4460 // this needs a better comment about why this is needed 4461 this.triggerReady(); 4462 4463 this.on('dispose', function() { 4464 clearTimeout(errorTimeout); 4465 }); 4466 } 4467 }; 4468 4469 // Begin loading the src data 4470 // http://dev.w3.org/html5/spec/video.html#dom-media-load 4471 vjs.Player.prototype.load = function(){ 4472 this.techCall('load'); 4473 return this; 4474 }; 4475 4476 // http://dev.w3.org/html5/spec/video.html#dom-media-currentsrc 4477 vjs.Player.prototype.currentSrc = function(){ 4478 return this.techGet('currentSrc') || this.cache_.src || ''; 4479 }; 4480 4481 /** 4482 * Get the current source type e.g. video/mp4 4483 * This can allow you rebuild the current source object so that you could load the same 4484 * source and tech later 4485 * @return {String} The source MIME type 4486 */ 4487 vjs.Player.prototype.currentType = function(){ 4488 return this.currentType_ || ''; 4489 }; 4490 4491 // Attributes/Options 4492 vjs.Player.prototype.preload = function(value){ 4493 if (value !== undefined) { 4494 this.techCall('setPreload', value); 4495 this.options_['preload'] = value; 4496 return this; 4497 } 4498 return this.techGet('preload'); 4499 }; 4500 vjs.Player.prototype.autoplay = function(value){ 4501 if (value !== undefined) { 4502 this.techCall('setAutoplay', value); 4503 this.options_['autoplay'] = value; 4504 return this; 4505 } 4506 return this.techGet('autoplay', value); 4507 }; 4508 vjs.Player.prototype.loop = function(value){ 4509 if (value !== undefined) { 4510 this.techCall('setLoop', value); 4511 this.options_['loop'] = value; 4512 return this; 4513 } 4514 return this.techGet('loop'); 4515 }; 4516 4517 /** 4518 * the url of the poster image source 4519 * @type {String} 4520 * @private 4521 */ 4522 vjs.Player.prototype.poster_; 4523 4524 /** 4525 * get or set the poster image source url 4526 * 4527 * ##### EXAMPLE: 4528 * 4529 * // getting 4530 * var currentPoster = myPlayer.poster(); 4531 * 4532 * // setting 4533 * myPlayer.poster('http://example.com/myImage.jpg'); 4534 * 4535 * @param {String=} [src] Poster image source URL 4536 * @return {String} poster URL when getting 4537 * @return {vjs.Player} self when setting 4538 */ 4539 vjs.Player.prototype.poster = function(src){ 4540 if (src === undefined) { 4541 return this.poster_; 4542 } 4543 4544 // update the internal poster variable 4545 this.poster_ = src; 4546 4547 // update the tech's poster 4548 this.techCall('setPoster', src); 4549 4550 // alert components that the poster has been set 4551 this.trigger('posterchange'); 4552 }; 4553 4554 /** 4555 * Whether or not the controls are showing 4556 * @type {Boolean} 4557 * @private 4558 */ 4559 vjs.Player.prototype.controls_; 4560 4561 /** 4562 * Get or set whether or not the controls are showing. 4563 * @param {Boolean} controls Set controls to showing or not 4564 * @return {Boolean} Controls are showing 4565 */ 4566 vjs.Player.prototype.controls = function(bool){ 4567 if (bool !== undefined) { 4568 bool = !!bool; // force boolean 4569 // Don't trigger a change event unless it actually changed 4570 if (this.controls_ !== bool) { 4571 this.controls_ = bool; 4572 if (bool) { 4573 this.removeClass('vjs-controls-disabled'); 4574 this.addClass('vjs-controls-enabled'); 4575 this.trigger('controlsenabled'); 4576 } else { 4577 this.removeClass('vjs-controls-enabled'); 4578 this.addClass('vjs-controls-disabled'); 4579 this.trigger('controlsdisabled'); 4580 } 4581 } 4582 return this; 4583 } 4584 return this.controls_; 4585 }; 4586 4587 vjs.Player.prototype.usingNativeControls_; 4588 4589 /** 4590 * Toggle native controls on/off. Native controls are the controls built into 4591 * devices (e.g. default iPhone controls), Flash, or other techs 4592 * (e.g. Vimeo Controls) 4593 * 4594 * **This should only be set by the current tech, because only the tech knows 4595 * if it can support native controls** 4596 * 4597 * @param {Boolean} bool True signals that native controls are on 4598 * @return {vjs.Player} Returns the player 4599 * @private 4600 */ 4601 vjs.Player.prototype.usingNativeControls = function(bool){ 4602 if (bool !== undefined) { 4603 bool = !!bool; // force boolean 4604 // Don't trigger a change event unless it actually changed 4605 if (this.usingNativeControls_ !== bool) { 4606 this.usingNativeControls_ = bool; 4607 if (bool) { 4608 this.addClass('vjs-using-native-controls'); 4609 4610 /** 4611 * player is using the native device controls 4612 * 4613 * @event usingnativecontrols 4614 * @memberof vjs.Player 4615 * @instance 4616 * @private 4617 */ 4618 this.trigger('usingnativecontrols'); 4619 } else { 4620 this.removeClass('vjs-using-native-controls'); 4621 4622 /** 4623 * player is using the custom HTML controls 4624 * 4625 * @event usingcustomcontrols 4626 * @memberof vjs.Player 4627 * @instance 4628 * @private 4629 */ 4630 this.trigger('usingcustomcontrols'); 4631 } 4632 } 4633 return this; 4634 } 4635 return this.usingNativeControls_; 4636 }; 4637 4638 /** 4639 * Store the current media error 4640 * @type {Object} 4641 * @private 4642 */ 4643 vjs.Player.prototype.error_ = null; 4644 4645 /** 4646 * Set or get the current MediaError 4647 * @param {*} err A MediaError or a String/Number to be turned into a MediaError 4648 * @return {vjs.MediaError|null} when getting 4649 * @return {vjs.Player} when setting 4650 */ 4651 vjs.Player.prototype.error = function(err){ 4652 if (err === undefined) { 4653 return this.error_; 4654 } 4655 4656 // restoring to default 4657 if (err === null) { 4658 this.error_ = err; 4659 this.removeClass('vjs-error'); 4660 return this; 4661 } 4662 4663 // error instance 4664 if (err instanceof vjs.MediaError) { 4665 this.error_ = err; 4666 } else { 4667 this.error_ = new vjs.MediaError(err); 4668 } 4669 4670 // fire an error event on the player 4671 this.trigger('error'); 4672 4673 // add the vjs-error classname to the player 4674 this.addClass('vjs-error'); 4675 4676 // log the name of the error type and any message 4677 // ie8 just logs "[object object]" if you just log the error object 4678 vjs.log.error('(CODE:'+this.error_.code+' '+vjs.MediaError.errorTypes[this.error_.code]+')', this.error_.message, this.error_); 4679 4680 return this; 4681 }; 4682 4683 vjs.Player.prototype.ended = function(){ return this.techGet('ended'); }; 4684 vjs.Player.prototype.seeking = function(){ return this.techGet('seeking'); }; 4685 4686 // When the player is first initialized, trigger activity so components 4687 // like the control bar show themselves if needed 4688 vjs.Player.prototype.userActivity_ = true; 4689 vjs.Player.prototype.reportUserActivity = function(event){ 4690 this.userActivity_ = true; 4691 }; 4692 4693 vjs.Player.prototype.userActive_ = true; 4694 vjs.Player.prototype.userActive = function(bool){ 4695 if (bool !== undefined) { 4696 bool = !!bool; 4697 if (bool !== this.userActive_) { 4698 this.userActive_ = bool; 4699 if (bool) { 4700 // If the user was inactive and is now active we want to reset the 4701 // inactivity timer 4702 this.userActivity_ = true; 4703 this.removeClass('vjs-user-inactive'); 4704 this.addClass('vjs-user-active'); 4705 this.trigger('useractive'); 4706 } else { 4707 // We're switching the state to inactive manually, so erase any other 4708 // activity 4709 this.userActivity_ = false; 4710 4711 // Chrome/Safari/IE have bugs where when you change the cursor it can 4712 // trigger a mousemove event. This causes an issue when you're hiding 4713 // the cursor when the user is inactive, and a mousemove signals user 4714 // activity. Making it impossible to go into inactive mode. Specifically 4715 // this happens in fullscreen when we really need to hide the cursor. 4716 // 4717 // When this gets resolved in ALL browsers it can be removed 4718 // https://code.google.com/p/chromium/issues/detail?id=103041 4719 if(this.tech) { 4720 this.tech.one('mousemove', function(e){ 4721 e.stopPropagation(); 4722 e.preventDefault(); 4723 }); 4724 } 4725 4726 this.removeClass('vjs-user-active'); 4727 this.addClass('vjs-user-inactive'); 4728 this.trigger('userinactive'); 4729 } 4730 } 4731 return this; 4732 } 4733 return this.userActive_; 4734 }; 4735 4736 vjs.Player.prototype.listenForUserActivity = function(){ 4737 var onActivity, onMouseMove, onMouseDown, mouseInProgress, onMouseUp, 4738 activityCheck, inactivityTimeout, lastMoveX, lastMoveY; 4739 4740 onActivity = vjs.bind(this, this.reportUserActivity); 4741 4742 onMouseMove = function(e) { 4743 // #1068 - Prevent mousemove spamming 4744 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 4745 if(e.screenX != lastMoveX || e.screenY != lastMoveY) { 4746 lastMoveX = e.screenX; 4747 lastMoveY = e.screenY; 4748 onActivity(); 4749 } 4750 }; 4751 4752 onMouseDown = function() { 4753 onActivity(); 4754 // For as long as the they are touching the device or have their mouse down, 4755 // we consider them active even if they're not moving their finger or mouse. 4756 // So we want to continue to update that they are active 4757 clearInterval(mouseInProgress); 4758 // Setting userActivity=true now and setting the interval to the same time 4759 // as the activityCheck interval (250) should ensure we never miss the 4760 // next activityCheck 4761 mouseInProgress = setInterval(onActivity, 250); 4762 }; 4763 4764 onMouseUp = function(event) { 4765 onActivity(); 4766 // Stop the interval that maintains activity if the mouse/touch is down 4767 clearInterval(mouseInProgress); 4768 }; 4769 4770 // Any mouse movement will be considered user activity 4771 this.on('mousedown', onMouseDown); 4772 this.on('mousemove', onMouseMove); 4773 this.on('mouseup', onMouseUp); 4774 4775 // Listen for keyboard navigation 4776 // Shouldn't need to use inProgress interval because of key repeat 4777 this.on('keydown', onActivity); 4778 this.on('keyup', onActivity); 4779 4780 // Run an interval every 250 milliseconds instead of stuffing everything into 4781 // the mousemove/touchmove function itself, to prevent performance degradation. 4782 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 4783 // then gets picked up by this loop 4784 // http://ejohn.org/blog/learning-from-twitter/ 4785 activityCheck = setInterval(vjs.bind(this, function() { 4786 // Check to see if mouse/touch activity has happened 4787 if (this.userActivity_) { 4788 // Reset the activity tracker 4789 this.userActivity_ = false; 4790 4791 // If the user state was inactive, set the state to active 4792 this.userActive(true); 4793 4794 // Clear any existing inactivity timeout to start the timer over 4795 clearTimeout(inactivityTimeout); 4796 4797 var timeout = this.options()['inactivityTimeout']; 4798 if (timeout > 0) { 4799 // In <timeout> milliseconds, if no more activity has occurred the 4800 // user will be considered inactive 4801 inactivityTimeout = setTimeout(vjs.bind(this, function () { 4802 // Protect against the case where the inactivityTimeout can trigger just 4803 // before the next user activity is picked up by the activityCheck loop 4804 // causing a flicker 4805 if (!this.userActivity_) { 4806 this.userActive(false); 4807 } 4808 }), timeout); 4809 } 4810 } 4811 }), 250); 4812 4813 // Clean up the intervals when we kill the player 4814 this.on('dispose', function(){ 4815 clearInterval(activityCheck); 4816 clearTimeout(inactivityTimeout); 4817 }); 4818 }; 4819 4820 vjs.Player.prototype.playbackRate = function(rate) { 4821 if (rate !== undefined) { 4822 this.techCall('setPlaybackRate', rate); 4823 return this; 4824 } 4825 4826 if (this.tech && this.tech['featuresPlaybackRate']) { 4827 return this.techGet('playbackRate'); 4828 } else { 4829 return 1.0; 4830 } 4831 4832 }; 4833 4834 // Methods to add support for 4835 // networkState: function(){ return this.techCall('networkState'); }, 4836 // readyState: function(){ return this.techCall('readyState'); }, 4837 // initialTime: function(){ return this.techCall('initialTime'); }, 4838 // startOffsetTime: function(){ return this.techCall('startOffsetTime'); }, 4839 // played: function(){ return this.techCall('played'); }, 4840 // seekable: function(){ return this.techCall('seekable'); }, 4841 // videoTracks: function(){ return this.techCall('videoTracks'); }, 4842 // audioTracks: function(){ return this.techCall('audioTracks'); }, 4843 // videoWidth: function(){ return this.techCall('videoWidth'); }, 4844 // videoHeight: function(){ return this.techCall('videoHeight'); }, 4845 // defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); }, 4846 // mediaGroup: function(){ return this.techCall('mediaGroup'); }, 4847 // controller: function(){ return this.techCall('controller'); }, 4848 // defaultMuted: function(){ return this.techCall('defaultMuted'); } 4849 4850 // TODO 4851 // currentSrcList: the array of sources including other formats and bitrates 4852 // playList: array of source lists in order of playback 4853 /** 4854 * Container of main controls 4855 * @param {vjs.Player|Object} player 4856 * @param {Object=} options 4857 * @class 4858 * @constructor 4859 * @extends vjs.Component 4860 */ 4861 vjs.ControlBar = vjs.Component.extend(); 4862 4863 vjs.ControlBar.prototype.options_ = { 4864 loadEvent: 'play', 4865 children: { 4866 'playToggle': {}, 4867 'currentTimeDisplay': {}, 4868 'timeDivider': {}, 4869 'durationDisplay': {}, 4870 'remainingTimeDisplay': {}, 4871 'liveDisplay': {}, 4872 'progressControl': {}, 4873 'fullscreenToggle': {}, 4874 'volumeControl': {}, 4875 'muteToggle': {}, 4876 // 'volumeMenuButton': {}, 4877 'playbackRateMenuButton': {} 4878 } 4879 }; 4880 4881 vjs.ControlBar.prototype.createEl = function(){ 4882 return vjs.createEl('div', { 4883 className: 'vjs-control-bar' 4884 }); 4885 }; 4886 /** 4887 * Displays the live indicator 4888 * TODO - Future make it click to snap to live 4889 * @param {vjs.Player|Object} player 4890 * @param {Object=} options 4891 * @constructor 4892 */ 4893 vjs.LiveDisplay = vjs.Component.extend({ 4894 init: function(player, options){ 4895 vjs.Component.call(this, player, options); 4896 } 4897 }); 4898 4899 vjs.LiveDisplay.prototype.createEl = function(){ 4900 var el = vjs.Component.prototype.createEl.call(this, 'div', { 4901 className: 'vjs-live-controls vjs-control' 4902 }); 4903 4904 this.contentEl_ = vjs.createEl('div', { 4905 className: 'vjs-live-display', 4906 innerHTML: '<span class="vjs-control-text">' + this.localize('Stream Type') + '</span>' + this.localize('LIVE'), 4907 'aria-live': 'off' 4908 }); 4909 4910 el.appendChild(this.contentEl_); 4911 4912 return el; 4913 }; 4914 /** 4915 * Button to toggle between play and pause 4916 * @param {vjs.Player|Object} player 4917 * @param {Object=} options 4918 * @class 4919 * @constructor 4920 */ 4921 vjs.PlayToggle = vjs.Button.extend({ 4922 /** @constructor */ 4923 init: function(player, options){ 4924 vjs.Button.call(this, player, options); 4925 4926 player.on('play', vjs.bind(this, this.onPlay)); 4927 player.on('pause', vjs.bind(this, this.onPause)); 4928 } 4929 }); 4930 4931 vjs.PlayToggle.prototype.buttonText = 'Play'; 4932 4933 vjs.PlayToggle.prototype.buildCSSClass = function(){ 4934 return 'vjs-play-control ' + vjs.Button.prototype.buildCSSClass.call(this); 4935 }; 4936 4937 // OnClick - Toggle between play and pause 4938 vjs.PlayToggle.prototype.onClick = function(){ 4939 if (this.player_.paused()) { 4940 this.player_.play(); 4941 } else { 4942 this.player_.pause(); 4943 } 4944 }; 4945 4946 // OnPlay - Add the vjs-playing class to the element so it can change appearance 4947 vjs.PlayToggle.prototype.onPlay = function(){ 4948 vjs.removeClass(this.el_, 'vjs-paused'); 4949 vjs.addClass(this.el_, 'vjs-playing'); 4950 this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" 4951 }; 4952 4953 // OnPause - Add the vjs-paused class to the element so it can change appearance 4954 vjs.PlayToggle.prototype.onPause = function(){ 4955 vjs.removeClass(this.el_, 'vjs-playing'); 4956 vjs.addClass(this.el_, 'vjs-paused'); 4957 this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" 4958 }; 4959 /** 4960 * Displays the current time 4961 * @param {vjs.Player|Object} player 4962 * @param {Object=} options 4963 * @constructor 4964 */ 4965 vjs.CurrentTimeDisplay = vjs.Component.extend({ 4966 /** @constructor */ 4967 init: function(player, options){ 4968 vjs.Component.call(this, player, options); 4969 4970 player.on('timeupdate', vjs.bind(this, this.updateContent)); 4971 } 4972 }); 4973 4974 vjs.CurrentTimeDisplay.prototype.createEl = function(){ 4975 var el = vjs.Component.prototype.createEl.call(this, 'div', { 4976 className: 'vjs-current-time vjs-time-controls vjs-control' 4977 }); 4978 4979 this.contentEl_ = vjs.createEl('div', { 4980 className: 'vjs-current-time-display', 4981 innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00', // label the current time for screen reader users 4982 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes 4983 }); 4984 4985 el.appendChild(this.contentEl_); 4986 return el; 4987 }; 4988 4989 vjs.CurrentTimeDisplay.prototype.updateContent = function(){ 4990 // Allows for smooth scrubbing, when player can't keep up. 4991 var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 4992 this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Current Time') + '</span> ' + vjs.formatTime(time, this.player_.duration()); 4993 }; 4994 4995 /** 4996 * Displays the duration 4997 * @param {vjs.Player|Object} player 4998 * @param {Object=} options 4999 * @constructor 5000 */ 5001 vjs.DurationDisplay = vjs.Component.extend({ 5002 /** @constructor */ 5003 init: function(player, options){ 5004 vjs.Component.call(this, player, options); 5005 5006 // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, 5007 // however the durationchange event fires before this.player_.duration() is set, 5008 // so the value cannot be written out using this method. 5009 // Once the order of durationchange and this.player_.duration() being set is figured out, 5010 // this can be updated. 5011 player.on('timeupdate', vjs.bind(this, this.updateContent)); 5012 } 5013 }); 5014 5015 vjs.DurationDisplay.prototype.createEl = function(){ 5016 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5017 className: 'vjs-duration vjs-time-controls vjs-control' 5018 }); 5019 5020 this.contentEl_ = vjs.createEl('div', { 5021 className: 'vjs-duration-display', 5022 innerHTML: '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + '0:00', // label the duration time for screen reader users 5023 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes 5024 }); 5025 5026 el.appendChild(this.contentEl_); 5027 return el; 5028 }; 5029 5030 vjs.DurationDisplay.prototype.updateContent = function(){ 5031 var duration = this.player_.duration(); 5032 if (duration) { 5033 this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + vjs.formatTime(duration); // label the duration time for screen reader users 5034 } 5035 }; 5036 5037 /** 5038 * The separator between the current time and duration 5039 * 5040 * Can be hidden if it's not needed in the design. 5041 * 5042 * @param {vjs.Player|Object} player 5043 * @param {Object=} options 5044 * @constructor 5045 */ 5046 vjs.TimeDivider = vjs.Component.extend({ 5047 /** @constructor */ 5048 init: function(player, options){ 5049 vjs.Component.call(this, player, options); 5050 } 5051 }); 5052 5053 vjs.TimeDivider.prototype.createEl = function(){ 5054 return vjs.Component.prototype.createEl.call(this, 'div', { 5055 className: 'vjs-time-divider', 5056 innerHTML: '<div><span>/</span></div>' 5057 }); 5058 }; 5059 5060 /** 5061 * Displays the time left in the video 5062 * @param {vjs.Player|Object} player 5063 * @param {Object=} options 5064 * @constructor 5065 */ 5066 vjs.RemainingTimeDisplay = vjs.Component.extend({ 5067 /** @constructor */ 5068 init: function(player, options){ 5069 vjs.Component.call(this, player, options); 5070 5071 player.on('timeupdate', vjs.bind(this, this.updateContent)); 5072 } 5073 }); 5074 5075 vjs.RemainingTimeDisplay.prototype.createEl = function(){ 5076 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5077 className: 'vjs-remaining-time vjs-time-controls vjs-control' 5078 }); 5079 5080 this.contentEl_ = vjs.createEl('div', { 5081 className: 'vjs-remaining-time-display', 5082 innerHTML: '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-0:00', // label the remaining time for screen reader users 5083 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes 5084 }); 5085 5086 el.appendChild(this.contentEl_); 5087 return el; 5088 }; 5089 5090 vjs.RemainingTimeDisplay.prototype.updateContent = function(){ 5091 if (this.player_.duration()) { 5092 this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-'+ vjs.formatTime(this.player_.remainingTime()); 5093 } 5094 5095 // Allows for smooth scrubbing, when player can't keep up. 5096 // var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5097 // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); 5098 }; 5099 /** 5100 * Toggle fullscreen video 5101 * @param {vjs.Player|Object} player 5102 * @param {Object=} options 5103 * @class 5104 * @extends vjs.Button 5105 */ 5106 vjs.FullscreenToggle = vjs.Button.extend({ 5107 /** 5108 * @constructor 5109 * @memberof vjs.FullscreenToggle 5110 * @instance 5111 */ 5112 init: function(player, options){ 5113 vjs.Button.call(this, player, options); 5114 } 5115 }); 5116 5117 vjs.FullscreenToggle.prototype.buttonText = 'Fullscreen'; 5118 5119 vjs.FullscreenToggle.prototype.buildCSSClass = function(){ 5120 return 'vjs-fullscreen-control ' + vjs.Button.prototype.buildCSSClass.call(this); 5121 }; 5122 5123 vjs.FullscreenToggle.prototype.onClick = function(){ 5124 if (!this.player_.isFullscreen()) { 5125 this.player_.requestFullscreen(); 5126 this.controlText_.innerHTML = this.localize('Non-Fullscreen'); 5127 } else { 5128 this.player_.exitFullscreen(); 5129 this.controlText_.innerHTML = this.localize('Fullscreen'); 5130 } 5131 }; 5132 /** 5133 * The Progress Control component contains the seek bar, load progress, 5134 * and play progress 5135 * 5136 * @param {vjs.Player|Object} player 5137 * @param {Object=} options 5138 * @constructor 5139 */ 5140 vjs.ProgressControl = vjs.Component.extend({ 5141 /** @constructor */ 5142 init: function(player, options){ 5143 vjs.Component.call(this, player, options); 5144 } 5145 }); 5146 5147 vjs.ProgressControl.prototype.options_ = { 5148 children: { 5149 'seekBar': {} 5150 } 5151 }; 5152 5153 vjs.ProgressControl.prototype.createEl = function(){ 5154 return vjs.Component.prototype.createEl.call(this, 'div', { 5155 className: 'vjs-progress-control vjs-control' 5156 }); 5157 }; 5158 5159 /** 5160 * Seek Bar and holder for the progress bars 5161 * 5162 * @param {vjs.Player|Object} player 5163 * @param {Object=} options 5164 * @constructor 5165 */ 5166 vjs.SeekBar = vjs.Slider.extend({ 5167 /** @constructor */ 5168 init: function(player, options){ 5169 vjs.Slider.call(this, player, options); 5170 player.on('timeupdate', vjs.bind(this, this.updateARIAAttributes)); 5171 player.ready(vjs.bind(this, this.updateARIAAttributes)); 5172 } 5173 }); 5174 5175 vjs.SeekBar.prototype.options_ = { 5176 children: { 5177 'loadProgressBar': {}, 5178 'playProgressBar': {}, 5179 'seekHandle': {} 5180 }, 5181 'barName': 'playProgressBar', 5182 'handleName': 'seekHandle' 5183 }; 5184 5185 vjs.SeekBar.prototype.playerEvent = 'timeupdate'; 5186 5187 vjs.SeekBar.prototype.createEl = function(){ 5188 return vjs.Slider.prototype.createEl.call(this, 'div', { 5189 className: 'vjs-progress-holder', 5190 'aria-label': 'video progress bar' 5191 }); 5192 }; 5193 5194 vjs.SeekBar.prototype.updateARIAAttributes = function(){ 5195 // Allows for smooth scrubbing, when player can't keep up. 5196 var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5197 this.el_.setAttribute('aria-valuenow',vjs.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete) 5198 this.el_.setAttribute('aria-valuetext',vjs.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) 5199 }; 5200 5201 vjs.SeekBar.prototype.getPercent = function(){ 5202 return this.player_.currentTime() / this.player_.duration(); 5203 }; 5204 5205 vjs.SeekBar.prototype.onMouseDown = function(event){ 5206 vjs.Slider.prototype.onMouseDown.call(this, event); 5207 5208 this.player_.scrubbing = true; 5209 5210 this.videoWasPlaying = !this.player_.paused(); 5211 this.player_.pause(); 5212 }; 5213 5214 vjs.SeekBar.prototype.onMouseMove = function(event){ 5215 var newTime = this.calculateDistance(event) * this.player_.duration(); 5216 5217 // Don't let video end while scrubbing. 5218 if (newTime == this.player_.duration()) { newTime = newTime - 0.1; } 5219 5220 // Set new time (tell player to seek to new time) 5221 this.player_.currentTime(newTime); 5222 }; 5223 5224 vjs.SeekBar.prototype.onMouseUp = function(event){ 5225 vjs.Slider.prototype.onMouseUp.call(this, event); 5226 5227 this.player_.scrubbing = false; 5228 if (this.videoWasPlaying) { 5229 this.player_.play(); 5230 } 5231 }; 5232 5233 vjs.SeekBar.prototype.stepForward = function(){ 5234 this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users 5235 }; 5236 5237 vjs.SeekBar.prototype.stepBack = function(){ 5238 this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users 5239 }; 5240 5241 /** 5242 * Shows load progress 5243 * 5244 * @param {vjs.Player|Object} player 5245 * @param {Object=} options 5246 * @constructor 5247 */ 5248 vjs.LoadProgressBar = vjs.Component.extend({ 5249 /** @constructor */ 5250 init: function(player, options){ 5251 vjs.Component.call(this, player, options); 5252 player.on('progress', vjs.bind(this, this.update)); 5253 } 5254 }); 5255 5256 vjs.LoadProgressBar.prototype.createEl = function(){ 5257 return vjs.Component.prototype.createEl.call(this, 'div', { 5258 className: 'vjs-load-progress', 5259 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>' 5260 }); 5261 }; 5262 5263 vjs.LoadProgressBar.prototype.update = function(){ 5264 var i, start, end, part, 5265 buffered = this.player_.buffered(), 5266 duration = this.player_.duration(), 5267 bufferedEnd = this.player_.bufferedEnd(), 5268 children = this.el_.children, 5269 // get the percent width of a time compared to the total end 5270 percentify = function (time, end){ 5271 var percent = (time / end) || 0; // no NaN 5272 return (percent * 100) + '%'; 5273 }; 5274 5275 // update the width of the progress bar 5276 this.el_.style.width = percentify(bufferedEnd, duration); 5277 5278 // add child elements to represent the individual buffered time ranges 5279 for (i = 0; i < buffered.length; i++) { 5280 start = buffered.start(i), 5281 end = buffered.end(i), 5282 part = children[i]; 5283 5284 if (!part) { 5285 part = this.el_.appendChild(vjs.createEl()) 5286 }; 5287 5288 // set the percent based on the width of the progress bar (bufferedEnd) 5289 part.style.left = percentify(start, bufferedEnd); 5290 part.style.width = percentify(end - start, bufferedEnd); 5291 }; 5292 5293 // remove unused buffered range elements 5294 for (i = children.length; i > buffered.length; i--) { 5295 this.el_.removeChild(children[i-1]); 5296 } 5297 }; 5298 5299 /** 5300 * Shows play progress 5301 * 5302 * @param {vjs.Player|Object} player 5303 * @param {Object=} options 5304 * @constructor 5305 */ 5306 vjs.PlayProgressBar = vjs.Component.extend({ 5307 /** @constructor */ 5308 init: function(player, options){ 5309 vjs.Component.call(this, player, options); 5310 } 5311 }); 5312 5313 vjs.PlayProgressBar.prototype.createEl = function(){ 5314 return vjs.Component.prototype.createEl.call(this, 'div', { 5315 className: 'vjs-play-progress', 5316 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>' 5317 }); 5318 }; 5319 5320 /** 5321 * The Seek Handle shows the current position of the playhead during playback, 5322 * and can be dragged to adjust the playhead. 5323 * 5324 * @param {vjs.Player|Object} player 5325 * @param {Object=} options 5326 * @constructor 5327 */ 5328 vjs.SeekHandle = vjs.SliderHandle.extend({ 5329 init: function(player, options) { 5330 vjs.SliderHandle.call(this, player, options); 5331 player.on('timeupdate', vjs.bind(this, this.updateContent)); 5332 } 5333 }); 5334 5335 /** 5336 * The default value for the handle content, which may be read by screen readers 5337 * 5338 * @type {String} 5339 * @private 5340 */ 5341 vjs.SeekHandle.prototype.defaultValue = '00:00'; 5342 5343 /** @inheritDoc */ 5344 vjs.SeekHandle.prototype.createEl = function() { 5345 return vjs.SliderHandle.prototype.createEl.call(this, 'div', { 5346 className: 'vjs-seek-handle', 5347 'aria-live': 'off' 5348 }); 5349 }; 5350 5351 vjs.SeekHandle.prototype.updateContent = function() { 5352 var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5353 this.el_.innerHTML = '<span class="vjs-control-text">' + vjs.formatTime(time, this.player_.duration()) + '</span>'; 5354 }; 5355 /** 5356 * The component for controlling the volume level 5357 * 5358 * @param {vjs.Player|Object} player 5359 * @param {Object=} options 5360 * @constructor 5361 */ 5362 vjs.VolumeControl = vjs.Component.extend({ 5363 /** @constructor */ 5364 init: function(player, options){ 5365 vjs.Component.call(this, player, options); 5366 5367 // hide volume controls when they're not supported by the current tech 5368 if (player.tech && player.tech['featuresVolumeControl'] === false) { 5369 this.addClass('vjs-hidden'); 5370 } 5371 player.on('loadstart', vjs.bind(this, function(){ 5372 if (player.tech['featuresVolumeControl'] === false) { 5373 this.addClass('vjs-hidden'); 5374 } else { 5375 this.removeClass('vjs-hidden'); 5376 } 5377 })); 5378 } 5379 }); 5380 5381 vjs.VolumeControl.prototype.options_ = { 5382 children: { 5383 'volumeBar': {} 5384 } 5385 }; 5386 5387 vjs.VolumeControl.prototype.createEl = function(){ 5388 return vjs.Component.prototype.createEl.call(this, 'div', { 5389 className: 'vjs-volume-control vjs-control' 5390 }); 5391 }; 5392 5393 /** 5394 * The bar that contains the volume level and can be clicked on to adjust the level 5395 * 5396 * @param {vjs.Player|Object} player 5397 * @param {Object=} options 5398 * @constructor 5399 */ 5400 vjs.VolumeBar = vjs.Slider.extend({ 5401 /** @constructor */ 5402 init: function(player, options){ 5403 vjs.Slider.call(this, player, options); 5404 player.on('volumechange', vjs.bind(this, this.updateARIAAttributes)); 5405 player.ready(vjs.bind(this, this.updateARIAAttributes)); 5406 } 5407 }); 5408 5409 vjs.VolumeBar.prototype.updateARIAAttributes = function(){ 5410 // Current value of volume bar as a percentage 5411 this.el_.setAttribute('aria-valuenow',vjs.round(this.player_.volume()*100, 2)); 5412 this.el_.setAttribute('aria-valuetext',vjs.round(this.player_.volume()*100, 2)+'%'); 5413 }; 5414 5415 vjs.VolumeBar.prototype.options_ = { 5416 children: { 5417 'volumeLevel': {}, 5418 'volumeHandle': {} 5419 }, 5420 'barName': 'volumeLevel', 5421 'handleName': 'volumeHandle' 5422 }; 5423 5424 vjs.VolumeBar.prototype.playerEvent = 'volumechange'; 5425 5426 vjs.VolumeBar.prototype.createEl = function(){ 5427 return vjs.Slider.prototype.createEl.call(this, 'div', { 5428 className: 'vjs-volume-bar', 5429 'aria-label': 'volume level' 5430 }); 5431 }; 5432 5433 vjs.VolumeBar.prototype.onMouseMove = function(event) { 5434 if (this.player_.muted()) { 5435 this.player_.muted(false); 5436 } 5437 5438 this.player_.volume(this.calculateDistance(event)); 5439 }; 5440 5441 vjs.VolumeBar.prototype.getPercent = function(){ 5442 if (this.player_.muted()) { 5443 return 0; 5444 } else { 5445 return this.player_.volume(); 5446 } 5447 }; 5448 5449 vjs.VolumeBar.prototype.stepForward = function(){ 5450 this.player_.volume(this.player_.volume() + 0.1); 5451 }; 5452 5453 vjs.VolumeBar.prototype.stepBack = function(){ 5454 this.player_.volume(this.player_.volume() - 0.1); 5455 }; 5456 5457 /** 5458 * Shows volume level 5459 * 5460 * @param {vjs.Player|Object} player 5461 * @param {Object=} options 5462 * @constructor 5463 */ 5464 vjs.VolumeLevel = vjs.Component.extend({ 5465 /** @constructor */ 5466 init: function(player, options){ 5467 vjs.Component.call(this, player, options); 5468 } 5469 }); 5470 5471 vjs.VolumeLevel.prototype.createEl = function(){ 5472 return vjs.Component.prototype.createEl.call(this, 'div', { 5473 className: 'vjs-volume-level', 5474 innerHTML: '<span class="vjs-control-text"></span>' 5475 }); 5476 }; 5477 5478 /** 5479 * The volume handle can be dragged to adjust the volume level 5480 * 5481 * @param {vjs.Player|Object} player 5482 * @param {Object=} options 5483 * @constructor 5484 */ 5485 vjs.VolumeHandle = vjs.SliderHandle.extend(); 5486 5487 vjs.VolumeHandle.prototype.defaultValue = '00:00'; 5488 5489 /** @inheritDoc */ 5490 vjs.VolumeHandle.prototype.createEl = function(){ 5491 return vjs.SliderHandle.prototype.createEl.call(this, 'div', { 5492 className: 'vjs-volume-handle' 5493 }); 5494 }; 5495 /** 5496 * A button component for muting the audio 5497 * 5498 * @param {vjs.Player|Object} player 5499 * @param {Object=} options 5500 * @constructor 5501 */ 5502 vjs.MuteToggle = vjs.Button.extend({ 5503 /** @constructor */ 5504 init: function(player, options){ 5505 vjs.Button.call(this, player, options); 5506 5507 player.on('volumechange', vjs.bind(this, this.update)); 5508 5509 // hide mute toggle if the current tech doesn't support volume control 5510 if (player.tech && player.tech['featuresVolumeControl'] === false) { 5511 this.addClass('vjs-hidden'); 5512 } 5513 player.on('loadstart', vjs.bind(this, function(){ 5514 if (player.tech['featuresVolumeControl'] === false) { 5515 this.addClass('vjs-hidden'); 5516 } else { 5517 this.removeClass('vjs-hidden'); 5518 } 5519 })); 5520 } 5521 }); 5522 5523 vjs.MuteToggle.prototype.createEl = function(){ 5524 return vjs.Button.prototype.createEl.call(this, 'div', { 5525 className: 'vjs-mute-control vjs-control', 5526 innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>' 5527 }); 5528 }; 5529 5530 vjs.MuteToggle.prototype.onClick = function(){ 5531 this.player_.muted( this.player_.muted() ? false : true ); 5532 }; 5533 5534 vjs.MuteToggle.prototype.update = function(){ 5535 var vol = this.player_.volume(), 5536 level = 3; 5537 5538 if (vol === 0 || this.player_.muted()) { 5539 level = 0; 5540 } else if (vol < 0.33) { 5541 level = 1; 5542 } else if (vol < 0.67) { 5543 level = 2; 5544 } 5545 5546 // Don't rewrite the button text if the actual text doesn't change. 5547 // This causes unnecessary and confusing information for screen reader users. 5548 // This check is needed because this function gets called every time the volume level is changed. 5549 if(this.player_.muted()){ 5550 if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ 5551 this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" 5552 } 5553 } else { 5554 if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ 5555 this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" 5556 } 5557 } 5558 5559 /* TODO improve muted icon classes */ 5560 for (var i = 0; i < 4; i++) { 5561 vjs.removeClass(this.el_, 'vjs-vol-'+i); 5562 } 5563 vjs.addClass(this.el_, 'vjs-vol-'+level); 5564 }; 5565 /** 5566 * Menu button with a popup for showing the volume slider. 5567 * @constructor 5568 */ 5569 vjs.VolumeMenuButton = vjs.MenuButton.extend({ 5570 /** @constructor */ 5571 init: function(player, options){ 5572 vjs.MenuButton.call(this, player, options); 5573 5574 // Same listeners as MuteToggle 5575 player.on('volumechange', vjs.bind(this, this.update)); 5576 5577 // hide mute toggle if the current tech doesn't support volume control 5578 if (player.tech && player.tech['featuresVolumeControl'] === false) { 5579 this.addClass('vjs-hidden'); 5580 } 5581 player.on('loadstart', vjs.bind(this, function(){ 5582 if (player.tech['featuresVolumeControl'] === false) { 5583 this.addClass('vjs-hidden'); 5584 } else { 5585 this.removeClass('vjs-hidden'); 5586 } 5587 })); 5588 this.addClass('vjs-menu-button'); 5589 } 5590 }); 5591 5592 vjs.VolumeMenuButton.prototype.createMenu = function(){ 5593 var menu = new vjs.Menu(this.player_, { 5594 contentElType: 'div' 5595 }); 5596 var vc = new vjs.VolumeBar(this.player_, vjs.obj.merge({'vertical': true}, this.options_.volumeBar)); 5597 menu.addChild(vc); 5598 return menu; 5599 }; 5600 5601 vjs.VolumeMenuButton.prototype.onClick = function(){ 5602 vjs.MuteToggle.prototype.onClick.call(this); 5603 vjs.MenuButton.prototype.onClick.call(this); 5604 }; 5605 5606 vjs.VolumeMenuButton.prototype.createEl = function(){ 5607 return vjs.Button.prototype.createEl.call(this, 'div', { 5608 className: 'vjs-volume-menu-button vjs-menu-button vjs-control', 5609 innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>' 5610 }); 5611 }; 5612 vjs.VolumeMenuButton.prototype.update = vjs.MuteToggle.prototype.update; 5613 /** 5614 * The component for controlling the playback rate 5615 * 5616 * @param {vjs.Player|Object} player 5617 * @param {Object=} options 5618 * @constructor 5619 */ 5620 vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({ 5621 /** @constructor */ 5622 init: function(player, options){ 5623 vjs.MenuButton.call(this, player, options); 5624 5625 this.updateVisibility(); 5626 this.updateLabel(); 5627 5628 player.on('loadstart', vjs.bind(this, this.updateVisibility)); 5629 player.on('ratechange', vjs.bind(this, this.updateLabel)); 5630 } 5631 }); 5632 5633 vjs.PlaybackRateMenuButton.prototype.createEl = function(){ 5634 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5635 className: 'vjs-playback-rate vjs-menu-button vjs-control', 5636 innerHTML: '<div class="vjs-control-content"><span class="vjs-control-text">' + this.localize('Playback Rate') + '</span></div>' 5637 }); 5638 5639 this.labelEl_ = vjs.createEl('div', { 5640 className: 'vjs-playback-rate-value', 5641 innerHTML: 1.0 5642 }); 5643 5644 el.appendChild(this.labelEl_); 5645 5646 return el; 5647 }; 5648 5649 // Menu creation 5650 vjs.PlaybackRateMenuButton.prototype.createMenu = function(){ 5651 var menu = new vjs.Menu(this.player()); 5652 var rates = this.player().options()['playbackRates']; 5653 5654 if (rates) { 5655 for (var i = rates.length - 1; i >= 0; i--) { 5656 menu.addChild( 5657 new vjs.PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'}) 5658 ); 5659 }; 5660 } 5661 5662 return menu; 5663 }; 5664 5665 vjs.PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){ 5666 // Current playback rate 5667 this.el().setAttribute('aria-valuenow', this.player().playbackRate()); 5668 }; 5669 5670 vjs.PlaybackRateMenuButton.prototype.onClick = function(){ 5671 // select next rate option 5672 var currentRate = this.player().playbackRate(); 5673 var rates = this.player().options()['playbackRates']; 5674 // this will select first one if the last one currently selected 5675 var newRate = rates[0]; 5676 for (var i = 0; i <rates.length ; i++) { 5677 if (rates[i] > currentRate) { 5678 newRate = rates[i]; 5679 break; 5680 } 5681 }; 5682 this.player().playbackRate(newRate); 5683 }; 5684 5685 vjs.PlaybackRateMenuButton.prototype.playbackRateSupported = function(){ 5686 return this.player().tech 5687 && this.player().tech['featuresPlaybackRate'] 5688 && this.player().options()['playbackRates'] 5689 && this.player().options()['playbackRates'].length > 0 5690 ; 5691 }; 5692 5693 /** 5694 * Hide playback rate controls when they're no playback rate options to select 5695 */ 5696 vjs.PlaybackRateMenuButton.prototype.updateVisibility = function(){ 5697 if (this.playbackRateSupported()) { 5698 this.removeClass('vjs-hidden'); 5699 } else { 5700 this.addClass('vjs-hidden'); 5701 } 5702 }; 5703 5704 /** 5705 * Update button label when rate changed 5706 */ 5707 vjs.PlaybackRateMenuButton.prototype.updateLabel = function(){ 5708 if (this.playbackRateSupported()) { 5709 this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; 5710 } 5711 }; 5712 5713 /** 5714 * The specific menu item type for selecting a playback rate 5715 * 5716 * @constructor 5717 */ 5718 vjs.PlaybackRateMenuItem = vjs.MenuItem.extend({ 5719 contentElType: 'button', 5720 /** @constructor */ 5721 init: function(player, options){ 5722 var label = this.label = options['rate']; 5723 var rate = this.rate = parseFloat(label, 10); 5724 5725 // Modify options for parent MenuItem class's init. 5726 options['label'] = label; 5727 options['selected'] = rate === 1; 5728 vjs.MenuItem.call(this, player, options); 5729 5730 this.player().on('ratechange', vjs.bind(this, this.update)); 5731 } 5732 }); 5733 5734 vjs.PlaybackRateMenuItem.prototype.onClick = function(){ 5735 vjs.MenuItem.prototype.onClick.call(this); 5736 this.player().playbackRate(this.rate); 5737 }; 5738 5739 vjs.PlaybackRateMenuItem.prototype.update = function(){ 5740 this.selected(this.player().playbackRate() == this.rate); 5741 }; 5742 /* Poster Image 5743 ================================================================================ */ 5744 /** 5745 * The component that handles showing the poster image. 5746 * 5747 * @param {vjs.Player|Object} player 5748 * @param {Object=} options 5749 * @constructor 5750 */ 5751 vjs.PosterImage = vjs.Button.extend({ 5752 /** @constructor */ 5753 init: function(player, options){ 5754 vjs.Button.call(this, player, options); 5755 5756 if (player.poster()) { 5757 this.src(player.poster()); 5758 } 5759 5760 if (!player.poster() || !player.controls()) { 5761 this.hide(); 5762 } 5763 5764 player.on('posterchange', vjs.bind(this, function(){ 5765 this.src(player.poster()); 5766 })); 5767 5768 player.on('play', vjs.bind(this, this.hide)); 5769 } 5770 }); 5771 5772 // use the test el to check for backgroundSize style support 5773 var _backgroundSizeSupported = 'backgroundSize' in vjs.TEST_VID.style; 5774 5775 vjs.PosterImage.prototype.createEl = function(){ 5776 var el = vjs.createEl('div', { 5777 className: 'vjs-poster', 5778 5779 // Don't want poster to be tabbable. 5780 tabIndex: -1 5781 }); 5782 5783 if (!_backgroundSizeSupported) { 5784 // setup an img element as a fallback for IE8 5785 el.appendChild(vjs.createEl('img')); 5786 } 5787 5788 return el; 5789 }; 5790 5791 vjs.PosterImage.prototype.src = function(url){ 5792 var el = this.el(); 5793 5794 // getter 5795 // can't think of a need for a getter here 5796 // see #838 if on is needed in the future 5797 // still don't want a getter to set src as undefined 5798 if (url === undefined) { 5799 return; 5800 } 5801 5802 // setter 5803 // To ensure the poster image resizes while maintaining its original aspect 5804 // ratio, use a div with `background-size` when available. For browsers that 5805 // do not support `background-size` (e.g. IE8), fall back on using a regular 5806 // img element. 5807 if (_backgroundSizeSupported) { 5808 el.style.backgroundImage = 'url("' + url + '")'; 5809 } else { 5810 el.firstChild.src = url; 5811 } 5812 }; 5813 5814 vjs.PosterImage.prototype.onClick = function(){ 5815 // Only accept clicks when controls are enabled 5816 if (this.player().controls()) { 5817 this.player_.play(); 5818 } 5819 }; 5820 /* Loading Spinner 5821 ================================================================================ */ 5822 /** 5823 * Loading spinner for waiting events 5824 * @param {vjs.Player|Object} player 5825 * @param {Object=} options 5826 * @class 5827 * @constructor 5828 */ 5829 vjs.LoadingSpinner = vjs.Component.extend({ 5830 /** @constructor */ 5831 init: function(player, options){ 5832 vjs.Component.call(this, player, options); 5833 5834 // MOVING DISPLAY HANDLING TO CSS 5835 5836 // player.on('canplay', vjs.bind(this, this.hide)); 5837 // player.on('canplaythrough', vjs.bind(this, this.hide)); 5838 // player.on('playing', vjs.bind(this, this.hide)); 5839 // player.on('seeking', vjs.bind(this, this.show)); 5840 5841 // in some browsers seeking does not trigger the 'playing' event, 5842 // so we also need to trap 'seeked' if we are going to set a 5843 // 'seeking' event 5844 // player.on('seeked', vjs.bind(this, this.hide)); 5845 5846 // player.on('ended', vjs.bind(this, this.hide)); 5847 5848 // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner. 5849 // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing 5850 // player.on('stalled', vjs.bind(this, this.show)); 5851 5852 // player.on('waiting', vjs.bind(this, this.show)); 5853 } 5854 }); 5855 5856 vjs.LoadingSpinner.prototype.createEl = function(){ 5857 return vjs.Component.prototype.createEl.call(this, 'div', { 5858 className: 'vjs-loading-spinner' 5859 }); 5860 }; 5861 /* Big Play Button 5862 ================================================================================ */ 5863 /** 5864 * Initial play button. Shows before the video has played. The hiding of the 5865 * big play button is done via CSS and player states. 5866 * @param {vjs.Player|Object} player 5867 * @param {Object=} options 5868 * @class 5869 * @constructor 5870 */ 5871 vjs.BigPlayButton = vjs.Button.extend(); 5872 5873 vjs.BigPlayButton.prototype.createEl = function(){ 5874 return vjs.Button.prototype.createEl.call(this, 'div', { 5875 className: 'vjs-big-play-button', 5876 innerHTML: '<span aria-hidden="true"></span>', 5877 'aria-label': 'play video' 5878 }); 5879 }; 5880 5881 vjs.BigPlayButton.prototype.onClick = function(){ 5882 this.player_.play(); 5883 }; 5884 /** 5885 * Display that an error has occurred making the video unplayable 5886 * @param {vjs.Player|Object} player 5887 * @param {Object=} options 5888 * @constructor 5889 */ 5890 vjs.ErrorDisplay = vjs.Component.extend({ 5891 init: function(player, options){ 5892 vjs.Component.call(this, player, options); 5893 5894 this.update(); 5895 player.on('error', vjs.bind(this, this.update)); 5896 } 5897 }); 5898 5899 vjs.ErrorDisplay.prototype.createEl = function(){ 5900 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5901 className: 'vjs-error-display' 5902 }); 5903 5904 this.contentEl_ = vjs.createEl('div'); 5905 el.appendChild(this.contentEl_); 5906 5907 return el; 5908 }; 5909 5910 vjs.ErrorDisplay.prototype.update = function(){ 5911 if (this.player().error()) { 5912 this.contentEl_.innerHTML = this.localize(this.player().error().message); 5913 } 5914 }; 5915 /** 5916 * @fileoverview Media Technology Controller - Base class for media playback 5917 * technology controllers like Flash and HTML5 5918 */ 5919 5920 /** 5921 * Base class for media (HTML5 Video, Flash) controllers 5922 * @param {vjs.Player|Object} player Central player instance 5923 * @param {Object=} options Options object 5924 * @constructor 5925 */ 5926 vjs.MediaTechController = vjs.Component.extend({ 5927 /** @constructor */ 5928 init: function(player, options, ready){ 5929 options = options || {}; 5930 // we don't want the tech to report user activity automatically. 5931 // This is done manually in addControlsListeners 5932 options.reportTouchActivity = false; 5933 vjs.Component.call(this, player, options, ready); 5934 5935 // Manually track progress in cases where the browser/flash player doesn't report it. 5936 if (!this['featuresProgressEvents']) { 5937 this.manualProgressOn(); 5938 } 5939 5940 // Manually track timeudpates in cases where the browser/flash player doesn't report it. 5941 if (!this['featuresTimeupdateEvents']) { 5942 this.manualTimeUpdatesOn(); 5943 } 5944 5945 this.initControlsListeners(); 5946 } 5947 }); 5948 5949 /** 5950 * Set up click and touch listeners for the playback element 5951 * On desktops, a click on the video itself will toggle playback, 5952 * on a mobile device a click on the video toggles controls. 5953 * (toggling controls is done by toggling the user state between active and 5954 * inactive) 5955 * 5956 * A tap can signal that a user has become active, or has become inactive 5957 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 5958 * quick tap should hide them again (signaling the user is in an inactive 5959 * viewing state) 5960 * 5961 * In addition to this, we still want the user to be considered inactive after 5962 * a few seconds of inactivity. 5963 * 5964 * Note: the only part of iOS interaction we can't mimic with this setup 5965 * is a touch and hold on the video element counting as activity in order to 5966 * keep the controls showing, but that shouldn't be an issue. A touch and hold on 5967 * any controls will still keep the user active 5968 */ 5969 vjs.MediaTechController.prototype.initControlsListeners = function(){ 5970 var player, tech, activateControls, deactivateControls; 5971 5972 tech = this; 5973 player = this.player(); 5974 5975 var activateControls = function(){ 5976 if (player.controls() && !player.usingNativeControls()) { 5977 tech.addControlsListeners(); 5978 } 5979 }; 5980 5981 deactivateControls = vjs.bind(tech, tech.removeControlsListeners); 5982 5983 // Set up event listeners once the tech is ready and has an element to apply 5984 // listeners to 5985 this.ready(activateControls); 5986 player.on('controlsenabled', activateControls); 5987 player.on('controlsdisabled', deactivateControls); 5988 5989 // if we're loading the playback object after it has started loading or playing the 5990 // video (often with autoplay on) then the loadstart event has already fired and we 5991 // need to fire it manually because many things rely on it. 5992 // Long term we might consider how we would do this for other events like 'canplay' 5993 // that may also have fired. 5994 this.ready(function(){ 5995 if (this.networkState && this.networkState() > 0) { 5996 this.player().trigger('loadstart'); 5997 } 5998 }); 5999 }; 6000 6001 vjs.MediaTechController.prototype.addControlsListeners = function(){ 6002 var userWasActive; 6003 6004 // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do 6005 // trigger mousedown/up. 6006 // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object 6007 // Any touch events are set to block the mousedown event from happening 6008 this.on('mousedown', this.onClick); 6009 6010 // If the controls were hidden we don't want that to change without a tap event 6011 // so we'll check if the controls were already showing before reporting user 6012 // activity 6013 this.on('touchstart', function(event) { 6014 userWasActive = this.player_.userActive(); 6015 }); 6016 6017 this.on('touchmove', function(event) { 6018 if (userWasActive){ 6019 this.player().reportUserActivity(); 6020 } 6021 }); 6022 6023 this.on('touchend', function(event) { 6024 // Stop the mouse events from also happening 6025 event.preventDefault(); 6026 }); 6027 6028 // Turn on component tap events 6029 this.emitTapEvents(); 6030 6031 // The tap listener needs to come after the touchend listener because the tap 6032 // listener cancels out any reportedUserActivity when setting userActive(false) 6033 this.on('tap', this.onTap); 6034 }; 6035 6036 /** 6037 * Remove the listeners used for click and tap controls. This is needed for 6038 * toggling to controls disabled, where a tap/touch should do nothing. 6039 */ 6040 vjs.MediaTechController.prototype.removeControlsListeners = function(){ 6041 // We don't want to just use `this.off()` because there might be other needed 6042 // listeners added by techs that extend this. 6043 this.off('tap'); 6044 this.off('touchstart'); 6045 this.off('touchmove'); 6046 this.off('touchleave'); 6047 this.off('touchcancel'); 6048 this.off('touchend'); 6049 this.off('click'); 6050 this.off('mousedown'); 6051 }; 6052 6053 /** 6054 * Handle a click on the media element. By default will play/pause the media. 6055 */ 6056 vjs.MediaTechController.prototype.onClick = function(event){ 6057 // We're using mousedown to detect clicks thanks to Flash, but mousedown 6058 // will also be triggered with right-clicks, so we need to prevent that 6059 if (event.button !== 0) return; 6060 6061 // When controls are disabled a click should not toggle playback because 6062 // the click is considered a control 6063 if (this.player().controls()) { 6064 if (this.player().paused()) { 6065 this.player().play(); 6066 } else { 6067 this.player().pause(); 6068 } 6069 } 6070 }; 6071 6072 /** 6073 * Handle a tap on the media element. By default it will toggle the user 6074 * activity state, which hides and shows the controls. 6075 */ 6076 vjs.MediaTechController.prototype.onTap = function(){ 6077 this.player().userActive(!this.player().userActive()); 6078 }; 6079 6080 /* Fallbacks for unsupported event types 6081 ================================================================================ */ 6082 // Manually trigger progress events based on changes to the buffered amount 6083 // Many flash players and older HTML5 browsers don't send progress or progress-like events 6084 vjs.MediaTechController.prototype.manualProgressOn = function(){ 6085 this.manualProgress = true; 6086 6087 // Trigger progress watching when a source begins loading 6088 this.trackProgress(); 6089 }; 6090 6091 vjs.MediaTechController.prototype.manualProgressOff = function(){ 6092 this.manualProgress = false; 6093 this.stopTrackingProgress(); 6094 }; 6095 6096 vjs.MediaTechController.prototype.trackProgress = function(){ 6097 6098 this.progressInterval = setInterval(vjs.bind(this, function(){ 6099 // Don't trigger unless buffered amount is greater than last time 6100 6101 var bufferedPercent = this.player().bufferedPercent(); 6102 6103 if (this.bufferedPercent_ != bufferedPercent) { 6104 this.player().trigger('progress'); 6105 } 6106 6107 this.bufferedPercent_ = bufferedPercent; 6108 6109 if (bufferedPercent === 1) { 6110 this.stopTrackingProgress(); 6111 } 6112 }), 500); 6113 }; 6114 vjs.MediaTechController.prototype.stopTrackingProgress = function(){ clearInterval(this.progressInterval); }; 6115 6116 /*! Time Tracking -------------------------------------------------------------- */ 6117 vjs.MediaTechController.prototype.manualTimeUpdatesOn = function(){ 6118 this.manualTimeUpdates = true; 6119 6120 this.player().on('play', vjs.bind(this, this.trackCurrentTime)); 6121 this.player().on('pause', vjs.bind(this, this.stopTrackingCurrentTime)); 6122 // timeupdate is also called by .currentTime whenever current time is set 6123 6124 // Watch for native timeupdate event 6125 this.one('timeupdate', function(){ 6126 // Update known progress support for this playback technology 6127 this['featuresTimeupdateEvents'] = true; 6128 // Turn off manual progress tracking 6129 this.manualTimeUpdatesOff(); 6130 }); 6131 }; 6132 6133 vjs.MediaTechController.prototype.manualTimeUpdatesOff = function(){ 6134 this.manualTimeUpdates = false; 6135 this.stopTrackingCurrentTime(); 6136 this.off('play', this.trackCurrentTime); 6137 this.off('pause', this.stopTrackingCurrentTime); 6138 }; 6139 6140 vjs.MediaTechController.prototype.trackCurrentTime = function(){ 6141 if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); } 6142 this.currentTimeInterval = setInterval(vjs.bind(this, function(){ 6143 this.player().trigger('timeupdate'); 6144 }), 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 6145 }; 6146 6147 // Turn off play progress tracking (when paused or dragging) 6148 vjs.MediaTechController.prototype.stopTrackingCurrentTime = function(){ 6149 clearInterval(this.currentTimeInterval); 6150 6151 // #1002 - if the video ends right before the next timeupdate would happen, 6152 // the progress bar won't make it all the way to the end 6153 this.player().trigger('timeupdate'); 6154 }; 6155 6156 vjs.MediaTechController.prototype.dispose = function() { 6157 // Turn off any manual progress or timeupdate tracking 6158 if (this.manualProgress) { this.manualProgressOff(); } 6159 6160 if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); } 6161 6162 vjs.Component.prototype.dispose.call(this); 6163 }; 6164 6165 vjs.MediaTechController.prototype.setCurrentTime = function() { 6166 // improve the accuracy of manual timeupdates 6167 if (this.manualTimeUpdates) { this.player().trigger('timeupdate'); } 6168 }; 6169 6170 /** 6171 * Provide a default setPoster method for techs 6172 * 6173 * Poster support for techs should be optional, so we don't want techs to 6174 * break if they don't have a way to set a poster. 6175 */ 6176 vjs.MediaTechController.prototype.setPoster = function(){}; 6177 6178 vjs.MediaTechController.prototype['featuresVolumeControl'] = true; 6179 6180 // Resizing plugins using request fullscreen reloads the plugin 6181 vjs.MediaTechController.prototype['featuresFullscreenResize'] = false; 6182 vjs.MediaTechController.prototype['featuresPlaybackRate'] = false; 6183 6184 // Optional events that we can manually mimic with timers 6185 // currently not triggered by video-js-swf 6186 vjs.MediaTechController.prototype['featuresProgressEvents'] = false; 6187 vjs.MediaTechController.prototype['featuresTimeupdateEvents'] = false; 6188 6189 vjs.media = {}; 6190 /** 6191 * @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API 6192 */ 6193 6194 /** 6195 * HTML5 Media Controller - Wrapper for HTML5 Media API 6196 * @param {vjs.Player|Object} player 6197 * @param {Object=} options 6198 * @param {Function=} ready 6199 * @constructor 6200 */ 6201 vjs.Html5 = vjs.MediaTechController.extend({ 6202 /** @constructor */ 6203 init: function(player, options, ready){ 6204 // volume cannot be changed from 1 on iOS 6205 this['featuresVolumeControl'] = vjs.Html5.canControlVolume(); 6206 6207 // just in case; or is it excessively... 6208 this['featuresPlaybackRate'] = vjs.Html5.canControlPlaybackRate(); 6209 6210 // In iOS, if you move a video element in the DOM, it breaks video playback. 6211 this['movingMediaElementInDOM'] = !vjs.IS_IOS; 6212 6213 // HTML video is able to automatically resize when going to fullscreen 6214 this['featuresFullscreenResize'] = true; 6215 6216 // HTML video supports progress events 6217 this['featuresProgressEvents'] = true; 6218 6219 vjs.MediaTechController.call(this, player, options, ready); 6220 this.setupTriggers(); 6221 6222 var source = options['source']; 6223 6224 // set the source if one was provided 6225 if (source && this.el_.currentSrc !== source.src) { 6226 this.el_.src = source.src; 6227 } 6228 6229 // Determine if native controls should be used 6230 // Our goal should be to get the custom controls on mobile solid everywhere 6231 // so we can remove this all together. Right now this will block custom 6232 // controls on touch enabled laptops like the Chrome Pixel 6233 if (vjs.TOUCH_ENABLED && player.options()['nativeControlsForTouch'] !== false) { 6234 this.useNativeControls(); 6235 } 6236 6237 // Chrome and Safari both have issues with autoplay. 6238 // In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work. 6239 // In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays) 6240 // This fixes both issues. Need to wait for API, so it updates displays correctly 6241 player.ready(function(){ 6242 if (this.tag && this.options_['autoplay'] && this.paused()) { 6243 delete this.tag['poster']; // Chrome Fix. Fixed in Chrome v16. 6244 this.play(); 6245 } 6246 }); 6247 6248 this.triggerReady(); 6249 } 6250 }); 6251 6252 vjs.Html5.prototype.dispose = function(){ 6253 vjs.Html5.disposeMediaElement(this.el_); 6254 vjs.MediaTechController.prototype.dispose.call(this); 6255 }; 6256 6257 vjs.Html5.prototype.createEl = function(){ 6258 var player = this.player_, 6259 // If possible, reuse original tag for HTML5 playback technology element 6260 el = player.tag, 6261 newEl, 6262 clone; 6263 6264 // Check if this browser supports moving the element into the box. 6265 // On the iPhone video will break if you move the element, 6266 // So we have to create a brand new element. 6267 if (!el || this['movingMediaElementInDOM'] === false) { 6268 6269 // If the original tag is still there, clone and remove it. 6270 if (el) { 6271 clone = el.cloneNode(false); 6272 vjs.Html5.disposeMediaElement(el); 6273 el = clone; 6274 player.tag = null; 6275 } else { 6276 el = vjs.createEl('video'); 6277 vjs.setElementAttributes(el, 6278 vjs.obj.merge(player.tagAttributes || {}, { 6279 id:player.id() + '_html5_api', 6280 'class':'vjs-tech' 6281 }) 6282 ); 6283 } 6284 // associate the player with the new tag 6285 el['player'] = player; 6286 6287 vjs.insertFirst(el, player.el()); 6288 } 6289 6290 // Update specific tag settings, in case they were overridden 6291 var settingsAttrs = ['autoplay','preload','loop','muted']; 6292 for (var i = settingsAttrs.length - 1; i >= 0; i--) { 6293 var attr = settingsAttrs[i]; 6294 var overwriteAttrs = {}; 6295 if (typeof player.options_[attr] !== 'undefined') { 6296 overwriteAttrs[attr] = player.options_[attr]; 6297 } 6298 vjs.setElementAttributes(el, overwriteAttrs); 6299 } 6300 6301 return el; 6302 // jenniisawesome = true; 6303 }; 6304 6305 // Make video events trigger player events 6306 // May seem verbose here, but makes other APIs possible. 6307 // Triggers removed using this.off when disposed 6308 vjs.Html5.prototype.setupTriggers = function(){ 6309 for (var i = vjs.Html5.Events.length - 1; i >= 0; i--) { 6310 vjs.on(this.el_, vjs.Html5.Events[i], vjs.bind(this, this.eventHandler)); 6311 } 6312 }; 6313 6314 vjs.Html5.prototype.eventHandler = function(evt){ 6315 // In the case of an error on the video element, set the error prop 6316 // on the player and let the player handle triggering the event. On 6317 // some platforms, error events fire that do not cause the error 6318 // property on the video element to be set. See #1465 for an example. 6319 if (evt.type == 'error' && this.error()) { 6320 this.player().error(this.error().code); 6321 6322 // in some cases we pass the event directly to the player 6323 } else { 6324 // No need for media events to bubble up. 6325 evt.bubbles = false; 6326 6327 this.player().trigger(evt); 6328 } 6329 }; 6330 6331 vjs.Html5.prototype.useNativeControls = function(){ 6332 var tech, player, controlsOn, controlsOff, cleanUp; 6333 6334 tech = this; 6335 player = this.player(); 6336 6337 // If the player controls are enabled turn on the native controls 6338 tech.setControls(player.controls()); 6339 6340 // Update the native controls when player controls state is updated 6341 controlsOn = function(){ 6342 tech.setControls(true); 6343 }; 6344 controlsOff = function(){ 6345 tech.setControls(false); 6346 }; 6347 player.on('controlsenabled', controlsOn); 6348 player.on('controlsdisabled', controlsOff); 6349 6350 // Clean up when not using native controls anymore 6351 cleanUp = function(){ 6352 player.off('controlsenabled', controlsOn); 6353 player.off('controlsdisabled', controlsOff); 6354 }; 6355 tech.on('dispose', cleanUp); 6356 player.on('usingcustomcontrols', cleanUp); 6357 6358 // Update the state of the player to using native controls 6359 player.usingNativeControls(true); 6360 }; 6361 6362 6363 vjs.Html5.prototype.play = function(){ this.el_.play(); }; 6364 vjs.Html5.prototype.pause = function(){ this.el_.pause(); }; 6365 vjs.Html5.prototype.paused = function(){ return this.el_.paused; }; 6366 6367 vjs.Html5.prototype.currentTime = function(){ return this.el_.currentTime; }; 6368 vjs.Html5.prototype.setCurrentTime = function(seconds){ 6369 try { 6370 this.el_.currentTime = seconds; 6371 } catch(e) { 6372 vjs.log(e, 'Video is not ready. (Video.js)'); 6373 // this.warning(VideoJS.warnings.videoNotReady); 6374 } 6375 }; 6376 6377 vjs.Html5.prototype.duration = function(){ return this.el_.duration || 0; }; 6378 vjs.Html5.prototype.buffered = function(){ return this.el_.buffered; }; 6379 6380 vjs.Html5.prototype.volume = function(){ return this.el_.volume; }; 6381 vjs.Html5.prototype.setVolume = function(percentAsDecimal){ this.el_.volume = percentAsDecimal; }; 6382 vjs.Html5.prototype.muted = function(){ return this.el_.muted; }; 6383 vjs.Html5.prototype.setMuted = function(muted){ this.el_.muted = muted; }; 6384 6385 vjs.Html5.prototype.width = function(){ return this.el_.offsetWidth; }; 6386 vjs.Html5.prototype.height = function(){ return this.el_.offsetHeight; }; 6387 6388 vjs.Html5.prototype.supportsFullScreen = function(){ 6389 if (typeof this.el_.webkitEnterFullScreen == 'function') { 6390 6391 // Seems to be broken in Chromium/Chrome && Safari in Leopard 6392 if (/Android/.test(vjs.USER_AGENT) || !/Chrome|Mac OS X 10.5/.test(vjs.USER_AGENT)) { 6393 return true; 6394 } 6395 } 6396 return false; 6397 }; 6398 6399 vjs.Html5.prototype.enterFullScreen = function(){ 6400 var video = this.el_; 6401 6402 if ('webkitDisplayingFullscreen' in video) { 6403 this.one('webkitbeginfullscreen', vjs.bind(this, function(e) { 6404 this.player_.isFullscreen(true); 6405 6406 this.one('webkitendfullscreen', vjs.bind(this, function(e) { 6407 this.player_.isFullscreen(false); 6408 this.player_.trigger('fullscreenchange'); 6409 })); 6410 6411 this.player_.trigger('fullscreenchange'); 6412 })); 6413 } 6414 6415 if (video.paused && video.networkState <= video.HAVE_METADATA) { 6416 // attempt to prime the video element for programmatic access 6417 // this isn't necessary on the desktop but shouldn't hurt 6418 this.el_.play(); 6419 6420 // playing and pausing synchronously during the transition to fullscreen 6421 // can get iOS ~6.1 devices into a play/pause loop 6422 setTimeout(function(){ 6423 video.pause(); 6424 video.webkitEnterFullScreen(); 6425 }, 0); 6426 } else { 6427 video.webkitEnterFullScreen(); 6428 } 6429 }; 6430 vjs.Html5.prototype.exitFullScreen = function(){ 6431 this.el_.webkitExitFullScreen(); 6432 }; 6433 vjs.Html5.prototype.src = function(src) { 6434 if (src === undefined) { 6435 return this.el_.src; 6436 } else { 6437 this.el_.src = src; 6438 } 6439 }; 6440 vjs.Html5.prototype.load = function(){ this.el_.load(); }; 6441 vjs.Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; }; 6442 6443 vjs.Html5.prototype.poster = function(){ return this.el_.poster; }; 6444 vjs.Html5.prototype.setPoster = function(val){ this.el_.poster = val; }; 6445 6446 vjs.Html5.prototype.preload = function(){ return this.el_.preload; }; 6447 vjs.Html5.prototype.setPreload = function(val){ this.el_.preload = val; }; 6448 6449 vjs.Html5.prototype.autoplay = function(){ return this.el_.autoplay; }; 6450 vjs.Html5.prototype.setAutoplay = function(val){ this.el_.autoplay = val; }; 6451 6452 vjs.Html5.prototype.controls = function(){ return this.el_.controls; }; 6453 vjs.Html5.prototype.setControls = function(val){ this.el_.controls = !!val; }; 6454 6455 vjs.Html5.prototype.loop = function(){ return this.el_.loop; }; 6456 vjs.Html5.prototype.setLoop = function(val){ this.el_.loop = val; }; 6457 6458 vjs.Html5.prototype.error = function(){ return this.el_.error; }; 6459 vjs.Html5.prototype.seeking = function(){ return this.el_.seeking; }; 6460 vjs.Html5.prototype.ended = function(){ return this.el_.ended; }; 6461 vjs.Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; }; 6462 6463 vjs.Html5.prototype.playbackRate = function(){ return this.el_.playbackRate; }; 6464 vjs.Html5.prototype.setPlaybackRate = function(val){ this.el_.playbackRate = val; }; 6465 6466 vjs.Html5.prototype.networkState = function(){ return this.el_.networkState; }; 6467 6468 /* HTML5 Support Testing ---------------------------------------------------- */ 6469 6470 vjs.Html5.isSupported = function(){ 6471 // ie9 with no Media Player is a LIAR! (#984) 6472 try { 6473 vjs.TEST_VID['volume'] = 0.5; 6474 } catch (e) { 6475 return false; 6476 } 6477 6478 return !!vjs.TEST_VID.canPlayType; 6479 }; 6480 6481 vjs.Html5.canPlaySource = function(srcObj){ 6482 // IE9 on Windows 7 without MediaPlayer throws an error here 6483 // https://github.com/videojs/video.js/issues/519 6484 try { 6485 return !!vjs.TEST_VID.canPlayType(srcObj.type); 6486 } catch(e) { 6487 return ''; 6488 } 6489 // TODO: Check Type 6490 // If no Type, check ext 6491 // Check Media Type 6492 }; 6493 6494 vjs.Html5.canControlVolume = function(){ 6495 var volume = vjs.TEST_VID.volume; 6496 vjs.TEST_VID.volume = (volume / 2) + 0.1; 6497 return volume !== vjs.TEST_VID.volume; 6498 }; 6499 6500 vjs.Html5.canControlPlaybackRate = function(){ 6501 var playbackRate = vjs.TEST_VID.playbackRate; 6502 vjs.TEST_VID.playbackRate = (playbackRate / 2) + 0.1; 6503 return playbackRate !== vjs.TEST_VID.playbackRate; 6504 }; 6505 6506 // HTML5 Feature detection and Device Fixes --------------------------------- // 6507 (function() { 6508 var canPlayType, 6509 mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i, 6510 mp4RE = /^video\/mp4/i; 6511 6512 vjs.Html5.patchCanPlayType = function() { 6513 // Android 4.0 and above can play HLS to some extent but it reports being unable to do so 6514 if (vjs.ANDROID_VERSION >= 4.0) { 6515 if (!canPlayType) { 6516 canPlayType = vjs.TEST_VID.constructor.prototype.canPlayType; 6517 } 6518 6519 vjs.TEST_VID.constructor.prototype.canPlayType = function(type) { 6520 if (type && mpegurlRE.test(type)) { 6521 return 'maybe'; 6522 } 6523 return canPlayType.call(this, type); 6524 }; 6525 } 6526 6527 // Override Android 2.2 and less canPlayType method which is broken 6528 if (vjs.IS_OLD_ANDROID) { 6529 if (!canPlayType) { 6530 canPlayType = vjs.TEST_VID.constructor.prototype.canPlayType; 6531 } 6532 6533 vjs.TEST_VID.constructor.prototype.canPlayType = function(type){ 6534 if (type && mp4RE.test(type)) { 6535 return 'maybe'; 6536 } 6537 return canPlayType.call(this, type); 6538 }; 6539 } 6540 }; 6541 6542 vjs.Html5.unpatchCanPlayType = function() { 6543 var r = vjs.TEST_VID.constructor.prototype.canPlayType; 6544 vjs.TEST_VID.constructor.prototype.canPlayType = canPlayType; 6545 canPlayType = null; 6546 return r; 6547 }; 6548 6549 // by default, patch the video element 6550 vjs.Html5.patchCanPlayType(); 6551 })(); 6552 6553 // List of all HTML5 events (various uses). 6554 vjs.Html5.Events = 'loadstart,suspend,abort,error,emptied,stalled,loadedmetadata,loadeddata,canplay,canplaythrough,playing,waiting,seeking,seeked,ended,durationchange,timeupdate,progress,play,pause,ratechange,volumechange'.split(','); 6555 6556 vjs.Html5.disposeMediaElement = function(el){ 6557 if (!el) { return; } 6558 6559 el['player'] = null; 6560 6561 if (el.parentNode) { 6562 el.parentNode.removeChild(el); 6563 } 6564 6565 // remove any child track or source nodes to prevent their loading 6566 while(el.hasChildNodes()) { 6567 el.removeChild(el.firstChild); 6568 } 6569 6570 // remove any src reference. not setting `src=''` because that causes a warning 6571 // in firefox 6572 el.removeAttribute('src'); 6573 6574 // force the media element to update its loading state by calling load() 6575 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 6576 if (typeof el.load === 'function') { 6577 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 6578 (function() { 6579 try { 6580 el.load(); 6581 } catch (e) { 6582 // not supported 6583 } 6584 })(); 6585 } 6586 }; 6587 /** 6588 * @fileoverview VideoJS-SWF - Custom Flash Player with HTML5-ish API 6589 * https://github.com/zencoder/video-js-swf 6590 * Not using setupTriggers. Using global onEvent func to distribute events 6591 */ 6592 6593 /** 6594 * Flash Media Controller - Wrapper for fallback SWF API 6595 * 6596 * @param {vjs.Player} player 6597 * @param {Object=} options 6598 * @param {Function=} ready 6599 * @constructor 6600 */ 6601 vjs.Flash = vjs.MediaTechController.extend({ 6602 /** @constructor */ 6603 init: function(player, options, ready){ 6604 vjs.MediaTechController.call(this, player, options, ready); 6605 6606 var source = options['source'], 6607 6608 // Which element to embed in 6609 parentEl = options['parentEl'], 6610 6611 // Create a temporary element to be replaced by swf object 6612 placeHolder = this.el_ = vjs.createEl('div', { id: player.id() + '_temp_flash' }), 6613 6614 // Generate ID for swf object 6615 objId = player.id()+'_flash_api', 6616 6617 // Store player options in local var for optimization 6618 // TODO: switch to using player methods instead of options 6619 // e.g. player.autoplay(); 6620 playerOptions = player.options_, 6621 6622 // Merge default flashvars with ones passed in to init 6623 flashVars = vjs.obj.merge({ 6624 6625 // SWF Callback Functions 6626 'readyFunction': 'videojs.Flash.onReady', 6627 'eventProxyFunction': 'videojs.Flash.onEvent', 6628 'errorEventProxyFunction': 'videojs.Flash.onError', 6629 6630 // Player Settings 6631 'autoplay': playerOptions.autoplay, 6632 'preload': playerOptions.preload, 6633 'loop': playerOptions.loop, 6634 'muted': playerOptions.muted 6635 6636 }, options['flashVars']), 6637 6638 // Merge default parames with ones passed in 6639 params = vjs.obj.merge({ 6640 'wmode': 'opaque', // Opaque is needed to overlay controls, but can affect playback performance 6641 'bgcolor': '#000000' // Using bgcolor prevents a white flash when the object is loading 6642 }, options['params']), 6643 6644 // Merge default attributes with ones passed in 6645 attributes = vjs.obj.merge({ 6646 'id': objId, 6647 'name': objId, // Both ID and Name needed or swf to identifty itself 6648 'class': 'vjs-tech' 6649 }, options['attributes']) 6650 ; 6651 6652 // If source was supplied pass as a flash var. 6653 if (source) { 6654 if (source.type && vjs.Flash.isStreamingType(source.type)) { 6655 var parts = vjs.Flash.streamToParts(source.src); 6656 flashVars['rtmpConnection'] = encodeURIComponent(parts.connection); 6657 flashVars['rtmpStream'] = encodeURIComponent(parts.stream); 6658 } 6659 else { 6660 flashVars['src'] = encodeURIComponent(vjs.getAbsoluteURL(source.src)); 6661 } 6662 } 6663 6664 // Add placeholder to player div 6665 vjs.insertFirst(placeHolder, parentEl); 6666 6667 // Having issues with Flash reloading on certain page actions (hide/resize/fullscreen) in certain browsers 6668 // This allows resetting the playhead when we catch the reload 6669 if (options['startTime']) { 6670 this.ready(function(){ 6671 this.load(); 6672 this.play(); 6673 this['currentTime'](options['startTime']); 6674 }); 6675 } 6676 6677 // firefox doesn't bubble mousemove events to parent. videojs/video-js-swf#37 6678 // bugzilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=836786 6679 if (vjs.IS_FIREFOX) { 6680 this.ready(function(){ 6681 vjs.on(this.el(), 'mousemove', vjs.bind(this, function(){ 6682 // since it's a custom event, don't bubble higher than the player 6683 this.player().trigger({ 'type':'mousemove', 'bubbles': false }); 6684 })); 6685 }); 6686 } 6687 6688 // native click events on the SWF aren't triggered on IE11, Win8.1RT 6689 // use stageclick events triggered from inside the SWF instead 6690 player.on('stageclick', player.reportUserActivity); 6691 6692 this.el_ = vjs.Flash.embed(options['swf'], placeHolder, flashVars, params, attributes); 6693 } 6694 }); 6695 6696 vjs.Flash.prototype.dispose = function(){ 6697 vjs.MediaTechController.prototype.dispose.call(this); 6698 }; 6699 6700 vjs.Flash.prototype.play = function(){ 6701 this.el_.vjs_play(); 6702 }; 6703 6704 vjs.Flash.prototype.pause = function(){ 6705 this.el_.vjs_pause(); 6706 }; 6707 6708 vjs.Flash.prototype.src = function(src){ 6709 if (src === undefined) { 6710 return this['currentSrc'](); 6711 } 6712 6713 if (vjs.Flash.isStreamingSrc(src)) { 6714 src = vjs.Flash.streamToParts(src); 6715 this.setRtmpConnection(src.connection); 6716 this.setRtmpStream(src.stream); 6717 } else { 6718 // Make sure source URL is abosolute. 6719 src = vjs.getAbsoluteURL(src); 6720 this.el_.vjs_src(src); 6721 } 6722 6723 // Currently the SWF doesn't autoplay if you load a source later. 6724 // e.g. Load player w/ no source, wait 2s, set src. 6725 if (this.player_.autoplay()) { 6726 var tech = this; 6727 setTimeout(function(){ tech.play(); }, 0); 6728 } 6729 }; 6730 6731 vjs.Flash.prototype['setCurrentTime'] = function(time){ 6732 this.lastSeekTarget_ = time; 6733 this.el_.vjs_setProperty('currentTime', time); 6734 vjs.MediaTechController.prototype.setCurrentTime.call(this); 6735 }; 6736 6737 vjs.Flash.prototype['currentTime'] = function(time){ 6738 // when seeking make the reported time keep up with the requested time 6739 // by reading the time we're seeking to 6740 if (this.seeking()) { 6741 return this.lastSeekTarget_ || 0; 6742 } 6743 return this.el_.vjs_getProperty('currentTime'); 6744 }; 6745 6746 vjs.Flash.prototype['currentSrc'] = function(){ 6747 var src = this.el_.vjs_getProperty('currentSrc'); 6748 // no src, check and see if RTMP 6749 if (src == null) { 6750 var connection = this['rtmpConnection'](), 6751 stream = this['rtmpStream'](); 6752 6753 if (connection && stream) { 6754 src = vjs.Flash.streamFromParts(connection, stream); 6755 } 6756 } 6757 return src; 6758 }; 6759 6760 vjs.Flash.prototype.load = function(){ 6761 this.el_.vjs_load(); 6762 }; 6763 6764 vjs.Flash.prototype.poster = function(){ 6765 this.el_.vjs_getProperty('poster'); 6766 }; 6767 vjs.Flash.prototype['setPoster'] = function(){ 6768 // poster images are not handled by the Flash tech so make this a no-op 6769 }; 6770 6771 vjs.Flash.prototype.buffered = function(){ 6772 return vjs.createTimeRange(0, this.el_.vjs_getProperty('buffered')); 6773 }; 6774 6775 vjs.Flash.prototype.supportsFullScreen = function(){ 6776 return false; // Flash does not allow fullscreen through javascript 6777 }; 6778 6779 vjs.Flash.prototype.enterFullScreen = function(){ 6780 return false; 6781 }; 6782 6783 (function(){ 6784 // Create setters and getters for attributes 6785 var api = vjs.Flash.prototype, 6786 readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','), 6787 readOnly = 'error,networkState,readyState,seeking,initialTime,duration,startOffsetTime,paused,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks'.split(','), 6788 // Overridden: buffered, currentTime, currentSrc 6789 i; 6790 6791 function createSetter(attr){ 6792 var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1); 6793 api['set'+attrUpper] = function(val){ return this.el_.vjs_setProperty(attr, val); }; 6794 }; 6795 function createGetter(attr) { 6796 api[attr] = function(){ return this.el_.vjs_getProperty(attr); }; 6797 }; 6798 6799 // Create getter and setters for all read/write attributes 6800 for (i = 0; i < readWrite.length; i++) { 6801 createGetter(readWrite[i]); 6802 createSetter(readWrite[i]); 6803 } 6804 6805 // Create getters for read-only attributes 6806 for (i = 0; i < readOnly.length; i++) { 6807 createGetter(readOnly[i]); 6808 } 6809 })(); 6810 6811 /* Flash Support Testing -------------------------------------------------------- */ 6812 6813 vjs.Flash.isSupported = function(){ 6814 return vjs.Flash.version()[0] >= 10; 6815 // return swfobject.hasFlashPlayerVersion('10'); 6816 }; 6817 6818 vjs.Flash.canPlaySource = function(srcObj){ 6819 var type; 6820 6821 if (!srcObj.type) { 6822 return ''; 6823 } 6824 6825 type = srcObj.type.replace(/;.*/,'').toLowerCase(); 6826 if (type in vjs.Flash.formats || type in vjs.Flash.streamingFormats) { 6827 return 'maybe'; 6828 } 6829 }; 6830 6831 vjs.Flash.formats = { 6832 'video/flv': 'FLV', 6833 'video/x-flv': 'FLV', 6834 'video/mp4': 'MP4', 6835 'video/m4v': 'MP4' 6836 }; 6837 6838 vjs.Flash.streamingFormats = { 6839 'rtmp/mp4': 'MP4', 6840 'rtmp/flv': 'FLV' 6841 }; 6842 6843 vjs.Flash['onReady'] = function(currSwf){ 6844 var el, player; 6845 6846 el = vjs.el(currSwf); 6847 6848 // get player from the player div property 6849 player = el && el.parentNode && el.parentNode['player']; 6850 6851 // if there is no el or player then the tech has been disposed 6852 // and the tech element was removed from the player div 6853 if (player) { 6854 // reference player on tech element 6855 el['player'] = player; 6856 // check that the flash object is really ready 6857 vjs.Flash['checkReady'](player.tech); 6858 } 6859 }; 6860 6861 // The SWF isn't always ready when it says it is. Sometimes the API functions still need to be added to the object. 6862 // If it's not ready, we set a timeout to check again shortly. 6863 vjs.Flash['checkReady'] = function(tech){ 6864 // stop worrying if the tech has been disposed 6865 if (!tech.el()) { 6866 return; 6867 } 6868 6869 // check if API property exists 6870 if (tech.el().vjs_getProperty) { 6871 // tell tech it's ready 6872 tech.triggerReady(); 6873 } else { 6874 // wait longer 6875 setTimeout(function(){ 6876 vjs.Flash['checkReady'](tech); 6877 }, 50); 6878 } 6879 }; 6880 6881 // Trigger events from the swf on the player 6882 vjs.Flash['onEvent'] = function(swfID, eventName){ 6883 var player = vjs.el(swfID)['player']; 6884 player.trigger(eventName); 6885 }; 6886 6887 // Log errors from the swf 6888 vjs.Flash['onError'] = function(swfID, err){ 6889 var player = vjs.el(swfID)['player']; 6890 var msg = 'FLASH: '+err; 6891 6892 if (err == 'srcnotfound') { 6893 player.error({ code: 4, message: msg }); 6894 6895 // errors we haven't categorized into the media errors 6896 } else { 6897 player.error(msg); 6898 } 6899 }; 6900 6901 // Flash Version Check 6902 vjs.Flash.version = function(){ 6903 var version = '0,0,0'; 6904 6905 // IE 6906 try { 6907 version = new window.ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; 6908 6909 // other browsers 6910 } catch(e) { 6911 try { 6912 if (navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin){ 6913 version = (navigator.plugins['Shockwave Flash 2.0'] || navigator.plugins['Shockwave Flash']).description.replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; 6914 } 6915 } catch(err) {} 6916 } 6917 return version.split(','); 6918 }; 6919 6920 // Flash embedding method. Only used in non-iframe mode 6921 vjs.Flash.embed = function(swf, placeHolder, flashVars, params, attributes){ 6922 var code = vjs.Flash.getEmbedCode(swf, flashVars, params, attributes), 6923 6924 // Get element by embedding code and retrieving created element 6925 obj = vjs.createEl('div', { innerHTML: code }).childNodes[0], 6926 6927 par = placeHolder.parentNode 6928 ; 6929 6930 placeHolder.parentNode.replaceChild(obj, placeHolder); 6931 6932 // IE6 seems to have an issue where it won't initialize the swf object after injecting it. 6933 // This is a dumb fix 6934 var newObj = par.childNodes[0]; 6935 setTimeout(function(){ 6936 newObj.style.display = 'block'; 6937 }, 1000); 6938 6939 return obj; 6940 6941 }; 6942 6943 vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ 6944 6945 var objTag = '<object type="application/x-shockwave-flash"', 6946 flashVarsString = '', 6947 paramsString = '', 6948 attrsString = ''; 6949 6950 // Convert flash vars to string 6951 if (flashVars) { 6952 vjs.obj.each(flashVars, function(key, val){ 6953 flashVarsString += (key + '=' + val + '&'); 6954 }); 6955 } 6956 6957 // Add swf, flashVars, and other default params 6958 params = vjs.obj.merge({ 6959 'movie': swf, 6960 'flashvars': flashVarsString, 6961 'allowScriptAccess': 'always', // Required to talk to swf 6962 'allowNetworking': 'all' // All should be default, but having security issues. 6963 }, params); 6964 6965 // Create param tags string 6966 vjs.obj.each(params, function(key, val){ 6967 paramsString += '<param name="'+key+'" value="'+val+'" />'; 6968 }); 6969 6970 attributes = vjs.obj.merge({ 6971 // Add swf to attributes (need both for IE and Others to work) 6972 'data': swf, 6973 6974 // Default to 100% width/height 6975 'width': '100%', 6976 'height': '100%' 6977 6978 }, attributes); 6979 6980 // Create Attributes string 6981 vjs.obj.each(attributes, function(key, val){ 6982 attrsString += (key + '="' + val + '" '); 6983 }); 6984 6985 return objTag + attrsString + '>' + paramsString + '</object>'; 6986 }; 6987 6988 vjs.Flash.streamFromParts = function(connection, stream) { 6989 return connection + '&' + stream; 6990 }; 6991 6992 vjs.Flash.streamToParts = function(src) { 6993 var parts = { 6994 connection: '', 6995 stream: '' 6996 }; 6997 6998 if (! src) { 6999 return parts; 7000 } 7001 7002 // Look for the normal URL separator we expect, '&'. 7003 // If found, we split the URL into two pieces around the 7004 // first '&'. 7005 var connEnd = src.indexOf('&'); 7006 var streamBegin; 7007 if (connEnd !== -1) { 7008 streamBegin = connEnd + 1; 7009 } 7010 else { 7011 // If there's not a '&', we use the last '/' as the delimiter. 7012 connEnd = streamBegin = src.lastIndexOf('/') + 1; 7013 if (connEnd === 0) { 7014 // really, there's not a '/'? 7015 connEnd = streamBegin = src.length; 7016 } 7017 } 7018 parts.connection = src.substring(0, connEnd); 7019 parts.stream = src.substring(streamBegin, src.length); 7020 7021 return parts; 7022 }; 7023 7024 vjs.Flash.isStreamingType = function(srcType) { 7025 return srcType in vjs.Flash.streamingFormats; 7026 }; 7027 7028 // RTMP has four variations, any string starting 7029 // with one of these protocols should be valid 7030 vjs.Flash.RTMP_RE = /^rtmp[set]?:\/\//i; 7031 7032 vjs.Flash.isStreamingSrc = function(src) { 7033 return vjs.Flash.RTMP_RE.test(src); 7034 }; 7035 /** 7036 * The Media Loader is the component that decides which playback technology to load 7037 * when the player is initialized. 7038 * 7039 * @constructor 7040 */ 7041 vjs.MediaLoader = vjs.Component.extend({ 7042 /** @constructor */ 7043 init: function(player, options, ready){ 7044 vjs.Component.call(this, player, options, ready); 7045 7046 // If there are no sources when the player is initialized, 7047 // load the first supported playback technology. 7048 if (!player.options_['sources'] || player.options_['sources'].length === 0) { 7049 for (var i=0,j=player.options_['techOrder']; i<j.length; i++) { 7050 var techName = vjs.capitalize(j[i]), 7051 tech = window['videojs'][techName]; 7052 7053 // Check if the browser supports this technology 7054 if (tech && tech.isSupported()) { 7055 player.loadTech(techName); 7056 break; 7057 } 7058 } 7059 } else { 7060 // // Loop through playback technologies (HTML5, Flash) and check for support. 7061 // // Then load the best source. 7062 // // A few assumptions here: 7063 // // All playback technologies respect preload false. 7064 player.src(player.options_['sources']); 7065 } 7066 } 7067 }); 7068 /** 7069 * @fileoverview Text Tracks 7070 * Text tracks are tracks of timed text events. 7071 * Captions - text displayed over the video for the hearing impared 7072 * Subtitles - text displayed over the video for those who don't understand langauge in the video 7073 * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video 7074 * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device 7075 */ 7076 7077 // Player Additions - Functions add to the player object for easier access to tracks 7078 7079 /** 7080 * List of associated text tracks 7081 * @type {Array} 7082 * @private 7083 */ 7084 vjs.Player.prototype.textTracks_; 7085 7086 /** 7087 * Get an array of associated text tracks. captions, subtitles, chapters, descriptions 7088 * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 7089 * @return {Array} Array of track objects 7090 * @private 7091 */ 7092 vjs.Player.prototype.textTracks = function(){ 7093 this.textTracks_ = this.textTracks_ || []; 7094 return this.textTracks_; 7095 }; 7096 7097 /** 7098 * Add a text track 7099 * In addition to the W3C settings we allow adding additional info through options. 7100 * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 7101 * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata 7102 * @param {String=} label Optional label 7103 * @param {String=} language Optional language 7104 * @param {Object=} options Additional track options, like src 7105 * @private 7106 */ 7107 vjs.Player.prototype.addTextTrack = function(kind, label, language, options){ 7108 var tracks = this.textTracks_ = this.textTracks_ || []; 7109 options = options || {}; 7110 7111 options['kind'] = kind; 7112 options['label'] = label; 7113 options['language'] = language; 7114 7115 // HTML5 Spec says default to subtitles. 7116 // Uppercase first letter to match class names 7117 var Kind = vjs.capitalize(kind || 'subtitles'); 7118 7119 // Create correct texttrack class. CaptionsTrack, etc. 7120 var track = new window['videojs'][Kind + 'Track'](this, options); 7121 7122 tracks.push(track); 7123 7124 // If track.dflt() is set, start showing immediately 7125 // TODO: Add a process to deterime the best track to show for the specific kind 7126 // Incase there are mulitple defaulted tracks of the same kind 7127 // Or the user has a set preference of a specific language that should override the default 7128 // Note: The setTimeout is a workaround because with the html5 tech, the player is 'ready' 7129 // before it's child components (including the textTrackDisplay) have finished loading. 7130 if (track.dflt()) { 7131 this.ready(function(){ 7132 setTimeout(function(){ 7133 track.player().showTextTrack(track.id()); 7134 }, 0); 7135 }); 7136 } 7137 7138 return track; 7139 }; 7140 7141 /** 7142 * Add an array of text tracks. captions, subtitles, chapters, descriptions 7143 * Track objects will be stored in the player.textTracks() array 7144 * @param {Array} trackList Array of track elements or objects (fake track elements) 7145 * @private 7146 */ 7147 vjs.Player.prototype.addTextTracks = function(trackList){ 7148 var trackObj; 7149 7150 for (var i = 0; i < trackList.length; i++) { 7151 trackObj = trackList[i]; 7152 this.addTextTrack(trackObj['kind'], trackObj['label'], trackObj['language'], trackObj); 7153 } 7154 7155 return this; 7156 }; 7157 7158 // Show a text track 7159 // disableSameKind: disable all other tracks of the same kind. Value should be a track kind (captions, etc.) 7160 vjs.Player.prototype.showTextTrack = function(id, disableSameKind){ 7161 var tracks = this.textTracks_, 7162 i = 0, 7163 j = tracks.length, 7164 track, showTrack, kind; 7165 7166 // Find Track with same ID 7167 for (;i<j;i++) { 7168 track = tracks[i]; 7169 if (track.id() === id) { 7170 track.show(); 7171 showTrack = track; 7172 7173 // Disable tracks of the same kind 7174 } else if (disableSameKind && track.kind() == disableSameKind && track.mode() > 0) { 7175 track.disable(); 7176 } 7177 } 7178 7179 // Get track kind from shown track or disableSameKind 7180 kind = (showTrack) ? showTrack.kind() : ((disableSameKind) ? disableSameKind : false); 7181 7182 // Trigger trackchange event, captionstrackchange, subtitlestrackchange, etc. 7183 if (kind) { 7184 this.trigger(kind+'trackchange'); 7185 } 7186 7187 return this; 7188 }; 7189 7190 /** 7191 * The base class for all text tracks 7192 * 7193 * Handles the parsing, hiding, and showing of text track cues 7194 * 7195 * @param {vjs.Player|Object} player 7196 * @param {Object=} options 7197 * @constructor 7198 */ 7199 vjs.TextTrack = vjs.Component.extend({ 7200 /** @constructor */ 7201 init: function(player, options){ 7202 vjs.Component.call(this, player, options); 7203 7204 // Apply track info to track object 7205 // Options will often be a track element 7206 7207 // Build ID if one doesn't exist 7208 this.id_ = options['id'] || ('vjs_' + options['kind'] + '_' + options['language'] + '_' + vjs.guid++); 7209 this.src_ = options['src']; 7210 // 'default' is a reserved keyword in js so we use an abbreviated version 7211 this.dflt_ = options['default'] || options['dflt']; 7212 this.title_ = options['title']; 7213 this.language_ = options['srclang']; 7214 this.label_ = options['label']; 7215 this.cues_ = []; 7216 this.activeCues_ = []; 7217 this.readyState_ = 0; 7218 this.mode_ = 0; 7219 7220 this.player_.on('fullscreenchange', vjs.bind(this, this.adjustFontSize)); 7221 } 7222 }); 7223 7224 /** 7225 * Track kind value. Captions, subtitles, etc. 7226 * @private 7227 */ 7228 vjs.TextTrack.prototype.kind_; 7229 7230 /** 7231 * Get the track kind value 7232 * @return {String} 7233 */ 7234 vjs.TextTrack.prototype.kind = function(){ 7235 return this.kind_; 7236 }; 7237 7238 /** 7239 * Track src value 7240 * @private 7241 */ 7242 vjs.TextTrack.prototype.src_; 7243 7244 /** 7245 * Get the track src value 7246 * @return {String} 7247 */ 7248 vjs.TextTrack.prototype.src = function(){ 7249 return this.src_; 7250 }; 7251 7252 /** 7253 * Track default value 7254 * If default is used, subtitles/captions to start showing 7255 * @private 7256 */ 7257 vjs.TextTrack.prototype.dflt_; 7258 7259 /** 7260 * Get the track default value. ('default' is a reserved keyword) 7261 * @return {Boolean} 7262 */ 7263 vjs.TextTrack.prototype.dflt = function(){ 7264 return this.dflt_; 7265 }; 7266 7267 /** 7268 * Track title value 7269 * @private 7270 */ 7271 vjs.TextTrack.prototype.title_; 7272 7273 /** 7274 * Get the track title value 7275 * @return {String} 7276 */ 7277 vjs.TextTrack.prototype.title = function(){ 7278 return this.title_; 7279 }; 7280 7281 /** 7282 * Language - two letter string to represent track language, e.g. 'en' for English 7283 * Spec def: readonly attribute DOMString language; 7284 * @private 7285 */ 7286 vjs.TextTrack.prototype.language_; 7287 7288 /** 7289 * Get the track language value 7290 * @return {String} 7291 */ 7292 vjs.TextTrack.prototype.language = function(){ 7293 return this.language_; 7294 }; 7295 7296 /** 7297 * Track label e.g. 'English' 7298 * Spec def: readonly attribute DOMString label; 7299 * @private 7300 */ 7301 vjs.TextTrack.prototype.label_; 7302 7303 /** 7304 * Get the track label value 7305 * @return {String} 7306 */ 7307 vjs.TextTrack.prototype.label = function(){ 7308 return this.label_; 7309 }; 7310 7311 /** 7312 * All cues of the track. Cues have a startTime, endTime, text, and other properties. 7313 * Spec def: readonly attribute TextTrackCueList cues; 7314 * @private 7315 */ 7316 vjs.TextTrack.prototype.cues_; 7317 7318 /** 7319 * Get the track cues 7320 * @return {Array} 7321 */ 7322 vjs.TextTrack.prototype.cues = function(){ 7323 return this.cues_; 7324 }; 7325 7326 /** 7327 * ActiveCues is all cues that are currently showing 7328 * Spec def: readonly attribute TextTrackCueList activeCues; 7329 * @private 7330 */ 7331 vjs.TextTrack.prototype.activeCues_; 7332 7333 /** 7334 * Get the track active cues 7335 * @return {Array} 7336 */ 7337 vjs.TextTrack.prototype.activeCues = function(){ 7338 return this.activeCues_; 7339 }; 7340 7341 /** 7342 * ReadyState describes if the text file has been loaded 7343 * const unsigned short NONE = 0; 7344 * const unsigned short LOADING = 1; 7345 * const unsigned short LOADED = 2; 7346 * const unsigned short ERROR = 3; 7347 * readonly attribute unsigned short readyState; 7348 * @private 7349 */ 7350 vjs.TextTrack.prototype.readyState_; 7351 7352 /** 7353 * Get the track readyState 7354 * @return {Number} 7355 */ 7356 vjs.TextTrack.prototype.readyState = function(){ 7357 return this.readyState_; 7358 }; 7359 7360 /** 7361 * Mode describes if the track is showing, hidden, or disabled 7362 * const unsigned short OFF = 0; 7363 * const unsigned short HIDDEN = 1; (still triggering cuechange events, but not visible) 7364 * const unsigned short SHOWING = 2; 7365 * attribute unsigned short mode; 7366 * @private 7367 */ 7368 vjs.TextTrack.prototype.mode_; 7369 7370 /** 7371 * Get the track mode 7372 * @return {Number} 7373 */ 7374 vjs.TextTrack.prototype.mode = function(){ 7375 return this.mode_; 7376 }; 7377 7378 /** 7379 * Change the font size of the text track to make it larger when playing in fullscreen mode 7380 * and restore it to its normal size when not in fullscreen mode. 7381 */ 7382 vjs.TextTrack.prototype.adjustFontSize = function(){ 7383 if (this.player_.isFullscreen()) { 7384 // Scale the font by the same factor as increasing the video width to the full screen window width. 7385 // Additionally, multiply that factor by 1.4, which is the default font size for 7386 // the caption track (from the CSS) 7387 this.el_.style.fontSize = screen.width / this.player_.width() * 1.4 * 100 + '%'; 7388 } else { 7389 // Change the font size of the text track back to its original non-fullscreen size 7390 this.el_.style.fontSize = ''; 7391 } 7392 }; 7393 7394 /** 7395 * Create basic div to hold cue text 7396 * @return {Element} 7397 */ 7398 vjs.TextTrack.prototype.createEl = function(){ 7399 return vjs.Component.prototype.createEl.call(this, 'div', { 7400 className: 'vjs-' + this.kind_ + ' vjs-text-track' 7401 }); 7402 }; 7403 7404 /** 7405 * Show: Mode Showing (2) 7406 * Indicates that the text track is active. If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily. 7407 * The user agent is maintaining a list of which cues are active, and events are being fired accordingly. 7408 * In addition, for text tracks whose kind is subtitles or captions, the cues are being displayed over the video as appropriate; 7409 * for text tracks whose kind is descriptions, the user agent is making the cues available to the user in a non-visual fashion; 7410 * and for text tracks whose kind is chapters, the user agent is making available to the user a mechanism by which the user can navigate to any point in the media resource by selecting a cue. 7411 * The showing by default state is used in conjunction with the default attribute on track elements to indicate that the text track was enabled due to that attribute. 7412 * This allows the user agent to override the state if a later track is discovered that is more appropriate per the user's preferences. 7413 */ 7414 vjs.TextTrack.prototype.show = function(){ 7415 this.activate(); 7416 7417 this.mode_ = 2; 7418 7419 // Show element. 7420 vjs.Component.prototype.show.call(this); 7421 }; 7422 7423 /** 7424 * Hide: Mode Hidden (1) 7425 * Indicates that the text track is active, but that the user agent is not actively displaying the cues. 7426 * If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily. 7427 * The user agent is maintaining a list of which cues are active, and events are being fired accordingly. 7428 */ 7429 vjs.TextTrack.prototype.hide = function(){ 7430 // When hidden, cues are still triggered. Disable to stop triggering. 7431 this.activate(); 7432 7433 this.mode_ = 1; 7434 7435 // Hide element. 7436 vjs.Component.prototype.hide.call(this); 7437 }; 7438 7439 /** 7440 * Disable: Mode Off/Disable (0) 7441 * Indicates that the text track is not active. Other than for the purposes of exposing the track in the DOM, the user agent is ignoring the text track. 7442 * No cues are active, no events are fired, and the user agent will not attempt to obtain the track's cues. 7443 */ 7444 vjs.TextTrack.prototype.disable = function(){ 7445 // If showing, hide. 7446 if (this.mode_ == 2) { this.hide(); } 7447 7448 // Stop triggering cues 7449 this.deactivate(); 7450 7451 // Switch Mode to Off 7452 this.mode_ = 0; 7453 }; 7454 7455 /** 7456 * Turn on cue tracking. Tracks that are showing OR hidden are active. 7457 */ 7458 vjs.TextTrack.prototype.activate = function(){ 7459 // Load text file if it hasn't been yet. 7460 if (this.readyState_ === 0) { this.load(); } 7461 7462 // Only activate if not already active. 7463 if (this.mode_ === 0) { 7464 // Update current cue on timeupdate 7465 // Using unique ID for bind function so other tracks don't remove listener 7466 this.player_.on('timeupdate', vjs.bind(this, this.update, this.id_)); 7467 7468 // Reset cue time on media end 7469 this.player_.on('ended', vjs.bind(this, this.reset, this.id_)); 7470 7471 // Add to display 7472 if (this.kind_ === 'captions' || this.kind_ === 'subtitles') { 7473 this.player_.getChild('textTrackDisplay').addChild(this); 7474 } 7475 } 7476 }; 7477 7478 /** 7479 * Turn off cue tracking. 7480 */ 7481 vjs.TextTrack.prototype.deactivate = function(){ 7482 // Using unique ID for bind function so other tracks don't remove listener 7483 this.player_.off('timeupdate', vjs.bind(this, this.update, this.id_)); 7484 this.player_.off('ended', vjs.bind(this, this.reset, this.id_)); 7485 this.reset(); // Reset 7486 7487 // Remove from display 7488 this.player_.getChild('textTrackDisplay').removeChild(this); 7489 }; 7490 7491 // A readiness state 7492 // One of the following: 7493 // 7494 // Not loaded 7495 // Indicates that the text track is known to exist (e.g. it has been declared with a track element), but its cues have not been obtained. 7496 // 7497 // Loading 7498 // Indicates that the text track is loading and there have been no fatal errors encountered so far. Further cues might still be added to the track. 7499 // 7500 // Loaded 7501 // Indicates that the text track has been loaded with no fatal errors. No new cues will be added to the track except if the text track corresponds to a MutableTextTrack object. 7502 // 7503 // Failed to load 7504 // Indicates that the text track was enabled, but when the user agent attempted to obtain it, this failed in some way (e.g. URL could not be resolved, network error, unknown text track format). Some or all of the cues are likely missing and will not be obtained. 7505 vjs.TextTrack.prototype.load = function(){ 7506 7507 // Only load if not loaded yet. 7508 if (this.readyState_ === 0) { 7509 this.readyState_ = 1; 7510 vjs.get(this.src_, vjs.bind(this, this.parseCues), vjs.bind(this, this.onError)); 7511 } 7512 7513 }; 7514 7515 vjs.TextTrack.prototype.onError = function(err){ 7516 this.error = err; 7517 this.readyState_ = 3; 7518 this.trigger('error'); 7519 }; 7520 7521 // Parse the WebVTT text format for cue times. 7522 // TODO: Separate parser into own class so alternative timed text formats can be used. (TTML, DFXP) 7523 vjs.TextTrack.prototype.parseCues = function(srcContent) { 7524 var cue, time, text, 7525 lines = srcContent.split('\n'), 7526 line = '', id; 7527 7528 for (var i=1, j=lines.length; i<j; i++) { 7529 // Line 0 should be 'WEBVTT', so skipping i=0 7530 7531 line = vjs.trim(lines[i]); // Trim whitespace and linebreaks 7532 7533 if (line) { // Loop until a line with content 7534 7535 // First line could be an optional cue ID 7536 // Check if line has the time separator 7537 if (line.indexOf('-->') == -1) { 7538 id = line; 7539 // Advance to next line for timing. 7540 line = vjs.trim(lines[++i]); 7541 } else { 7542 id = this.cues_.length; 7543 } 7544 7545 // First line - Number 7546 cue = { 7547 id: id, // Cue Number 7548 index: this.cues_.length // Position in Array 7549 }; 7550 7551 // Timing line 7552 time = line.split(/[\t ]+/); 7553 cue.startTime = this.parseCueTime(time[0]); 7554 cue.endTime = this.parseCueTime(time[2]); 7555 7556 // Additional lines - Cue Text 7557 text = []; 7558 7559 // Loop until a blank line or end of lines 7560 // Assumeing trim('') returns false for blank lines 7561 while (lines[++i] && (line = vjs.trim(lines[i]))) { 7562 text.push(line); 7563 } 7564 7565 cue.text = text.join('<br/>'); 7566 7567 // Add this cue 7568 this.cues_.push(cue); 7569 } 7570 } 7571 7572 this.readyState_ = 2; 7573 this.trigger('loaded'); 7574 }; 7575 7576 7577 vjs.TextTrack.prototype.parseCueTime = function(timeText) { 7578 var parts = timeText.split(':'), 7579 time = 0, 7580 hours, minutes, other, seconds, ms; 7581 7582 // Check if optional hours place is included 7583 // 00:00:00.000 vs. 00:00.000 7584 if (parts.length == 3) { 7585 hours = parts[0]; 7586 minutes = parts[1]; 7587 other = parts[2]; 7588 } else { 7589 hours = 0; 7590 minutes = parts[0]; 7591 other = parts[1]; 7592 } 7593 7594 // Break other (seconds, milliseconds, and flags) by spaces 7595 // TODO: Make additional cue layout settings work with flags 7596 other = other.split(/\s+/); 7597 // Remove seconds. Seconds is the first part before any spaces. 7598 seconds = other.splice(0,1)[0]; 7599 // Could use either . or , for decimal 7600 seconds = seconds.split(/\.|,/); 7601 // Get milliseconds 7602 ms = parseFloat(seconds[1]); 7603 seconds = seconds[0]; 7604 7605 // hours => seconds 7606 time += parseFloat(hours) * 3600; 7607 // minutes => seconds 7608 time += parseFloat(minutes) * 60; 7609 // Add seconds 7610 time += parseFloat(seconds); 7611 // Add milliseconds 7612 if (ms) { time += ms/1000; } 7613 7614 return time; 7615 }; 7616 7617 // Update active cues whenever timeupdate events are triggered on the player. 7618 vjs.TextTrack.prototype.update = function(){ 7619 if (this.cues_.length > 0) { 7620 7621 // Get current player time, adjust for track offset 7622 var offset = this.player_.options()['trackTimeOffset'] || 0; 7623 var time = this.player_.currentTime() + offset; 7624 7625 // Check if the new time is outside the time box created by the the last update. 7626 if (this.prevChange === undefined || time < this.prevChange || this.nextChange <= time) { 7627 var cues = this.cues_, 7628 7629 // Create a new time box for this state. 7630 newNextChange = this.player_.duration(), // Start at beginning of the timeline 7631 newPrevChange = 0, // Start at end 7632 7633 reverse = false, // Set the direction of the loop through the cues. Optimized the cue check. 7634 newCues = [], // Store new active cues. 7635 7636 // Store where in the loop the current active cues are, to provide a smart starting point for the next loop. 7637 firstActiveIndex, lastActiveIndex, 7638 cue, i; // Loop vars 7639 7640 // Check if time is going forwards or backwards (scrubbing/rewinding) 7641 // If we know the direction we can optimize the starting position and direction of the loop through the cues array. 7642 if (time >= this.nextChange || this.nextChange === undefined) { // NextChange should happen 7643 // Forwards, so start at the index of the first active cue and loop forward 7644 i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0; 7645 } else { 7646 // Backwards, so start at the index of the last active cue and loop backward 7647 reverse = true; 7648 i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length - 1; 7649 } 7650 7651 while (true) { // Loop until broken 7652 cue = cues[i]; 7653 7654 // Cue ended at this point 7655 if (cue.endTime <= time) { 7656 newPrevChange = Math.max(newPrevChange, cue.endTime); 7657 7658 if (cue.active) { 7659 cue.active = false; 7660 } 7661 7662 // No earlier cues should have an active start time. 7663 // Nevermind. Assume first cue could have a duration the same as the video. 7664 // In that case we need to loop all the way back to the beginning. 7665 // if (reverse && cue.startTime) { break; } 7666 7667 // Cue hasn't started 7668 } else if (time < cue.startTime) { 7669 newNextChange = Math.min(newNextChange, cue.startTime); 7670 7671 if (cue.active) { 7672 cue.active = false; 7673 } 7674 7675 // No later cues should have an active start time. 7676 if (!reverse) { break; } 7677 7678 // Cue is current 7679 } else { 7680 7681 if (reverse) { 7682 // Add cue to front of array to keep in time order 7683 newCues.splice(0,0,cue); 7684 7685 // If in reverse, the first current cue is our lastActiveCue 7686 if (lastActiveIndex === undefined) { lastActiveIndex = i; } 7687 firstActiveIndex = i; 7688 } else { 7689 // Add cue to end of array 7690 newCues.push(cue); 7691 7692 // If forward, the first current cue is our firstActiveIndex 7693 if (firstActiveIndex === undefined) { firstActiveIndex = i; } 7694 lastActiveIndex = i; 7695 } 7696 7697 newNextChange = Math.min(newNextChange, cue.endTime); 7698 newPrevChange = Math.max(newPrevChange, cue.startTime); 7699 7700 cue.active = true; 7701 } 7702 7703 if (reverse) { 7704 // Reverse down the array of cues, break if at first 7705 if (i === 0) { break; } else { i--; } 7706 } else { 7707 // Walk up the array fo cues, break if at last 7708 if (i === cues.length - 1) { break; } else { i++; } 7709 } 7710 7711 } 7712 7713 this.activeCues_ = newCues; 7714 this.nextChange = newNextChange; 7715 this.prevChange = newPrevChange; 7716 this.firstActiveIndex = firstActiveIndex; 7717 this.lastActiveIndex = lastActiveIndex; 7718 7719 this.updateDisplay(); 7720 7721 this.trigger('cuechange'); 7722 } 7723 } 7724 }; 7725 7726 // Add cue HTML to display 7727 vjs.TextTrack.prototype.updateDisplay = function(){ 7728 var cues = this.activeCues_, 7729 html = '', 7730 i=0,j=cues.length; 7731 7732 for (;i<j;i++) { 7733 html += '<span class="vjs-tt-cue">'+cues[i].text+'</span>'; 7734 } 7735 7736 this.el_.innerHTML = html; 7737 }; 7738 7739 // Set all loop helper values back 7740 vjs.TextTrack.prototype.reset = function(){ 7741 this.nextChange = 0; 7742 this.prevChange = this.player_.duration(); 7743 this.firstActiveIndex = 0; 7744 this.lastActiveIndex = 0; 7745 }; 7746 7747 // Create specific track types 7748 /** 7749 * The track component for managing the hiding and showing of captions 7750 * 7751 * @constructor 7752 */ 7753 vjs.CaptionsTrack = vjs.TextTrack.extend(); 7754 vjs.CaptionsTrack.prototype.kind_ = 'captions'; 7755 // Exporting here because Track creation requires the track kind 7756 // to be available on global object. e.g. new window['videojs'][Kind + 'Track'] 7757 7758 /** 7759 * The track component for managing the hiding and showing of subtitles 7760 * 7761 * @constructor 7762 */ 7763 vjs.SubtitlesTrack = vjs.TextTrack.extend(); 7764 vjs.SubtitlesTrack.prototype.kind_ = 'subtitles'; 7765 7766 /** 7767 * The track component for managing the hiding and showing of chapters 7768 * 7769 * @constructor 7770 */ 7771 vjs.ChaptersTrack = vjs.TextTrack.extend(); 7772 vjs.ChaptersTrack.prototype.kind_ = 'chapters'; 7773 7774 7775 /* Text Track Display 7776 ============================================================================= */ 7777 // Global container for both subtitle and captions text. Simple div container. 7778 7779 /** 7780 * The component for displaying text track cues 7781 * 7782 * @constructor 7783 */ 7784 vjs.TextTrackDisplay = vjs.Component.extend({ 7785 /** @constructor */ 7786 init: function(player, options, ready){ 7787 vjs.Component.call(this, player, options, ready); 7788 7789 // This used to be called during player init, but was causing an error 7790 // if a track should show by default and the display hadn't loaded yet. 7791 // Should probably be moved to an external track loader when we support 7792 // tracks that don't need a display. 7793 if (player.options_['tracks'] && player.options_['tracks'].length > 0) { 7794 this.player_.addTextTracks(player.options_['tracks']); 7795 } 7796 } 7797 }); 7798 7799 vjs.TextTrackDisplay.prototype.createEl = function(){ 7800 return vjs.Component.prototype.createEl.call(this, 'div', { 7801 className: 'vjs-text-track-display' 7802 }); 7803 }; 7804 7805 7806 /** 7807 * The specific menu item type for selecting a language within a text track kind 7808 * 7809 * @constructor 7810 */ 7811 vjs.TextTrackMenuItem = vjs.MenuItem.extend({ 7812 /** @constructor */ 7813 init: function(player, options){ 7814 var track = this.track = options['track']; 7815 7816 // Modify options for parent MenuItem class's init. 7817 options['label'] = track.label(); 7818 options['selected'] = track.dflt(); 7819 vjs.MenuItem.call(this, player, options); 7820 7821 this.player_.on(track.kind() + 'trackchange', vjs.bind(this, this.update)); 7822 } 7823 }); 7824 7825 vjs.TextTrackMenuItem.prototype.onClick = function(){ 7826 vjs.MenuItem.prototype.onClick.call(this); 7827 this.player_.showTextTrack(this.track.id_, this.track.kind()); 7828 }; 7829 7830 vjs.TextTrackMenuItem.prototype.update = function(){ 7831 this.selected(this.track.mode() == 2); 7832 }; 7833 7834 /** 7835 * A special menu item for turning of a specific type of text track 7836 * 7837 * @constructor 7838 */ 7839 vjs.OffTextTrackMenuItem = vjs.TextTrackMenuItem.extend({ 7840 /** @constructor */ 7841 init: function(player, options){ 7842 // Create pseudo track info 7843 // Requires options['kind'] 7844 options['track'] = { 7845 kind: function() { return options['kind']; }, 7846 player: player, 7847 label: function(){ return options['kind'] + ' off'; }, 7848 dflt: function(){ return false; }, 7849 mode: function(){ return false; } 7850 }; 7851 vjs.TextTrackMenuItem.call(this, player, options); 7852 this.selected(true); 7853 } 7854 }); 7855 7856 vjs.OffTextTrackMenuItem.prototype.onClick = function(){ 7857 vjs.TextTrackMenuItem.prototype.onClick.call(this); 7858 this.player_.showTextTrack(this.track.id_, this.track.kind()); 7859 }; 7860 7861 vjs.OffTextTrackMenuItem.prototype.update = function(){ 7862 var tracks = this.player_.textTracks(), 7863 i=0, j=tracks.length, track, 7864 off = true; 7865 7866 for (;i<j;i++) { 7867 track = tracks[i]; 7868 if (track.kind() == this.track.kind() && track.mode() == 2) { 7869 off = false; 7870 } 7871 } 7872 7873 this.selected(off); 7874 }; 7875 7876 /** 7877 * The base class for buttons that toggle specific text track types (e.g. subtitles) 7878 * 7879 * @constructor 7880 */ 7881 vjs.TextTrackButton = vjs.MenuButton.extend({ 7882 /** @constructor */ 7883 init: function(player, options){ 7884 vjs.MenuButton.call(this, player, options); 7885 7886 if (this.items.length <= 1) { 7887 this.hide(); 7888 } 7889 } 7890 }); 7891 7892 // vjs.TextTrackButton.prototype.buttonPressed = false; 7893 7894 // vjs.TextTrackButton.prototype.createMenu = function(){ 7895 // var menu = new vjs.Menu(this.player_); 7896 7897 // // Add a title list item to the top 7898 // // menu.el().appendChild(vjs.createEl('li', { 7899 // // className: 'vjs-menu-title', 7900 // // innerHTML: vjs.capitalize(this.kind_), 7901 // // tabindex: -1 7902 // // })); 7903 7904 // this.items = this.createItems(); 7905 7906 // // Add menu items to the menu 7907 // for (var i = 0; i < this.items.length; i++) { 7908 // menu.addItem(this.items[i]); 7909 // } 7910 7911 // // Add list to element 7912 // this.addChild(menu); 7913 7914 // return menu; 7915 // }; 7916 7917 // Create a menu item for each text track 7918 vjs.TextTrackButton.prototype.createItems = function(){ 7919 var items = [], track; 7920 7921 // Add an OFF menu item to turn all tracks off 7922 items.push(new vjs.OffTextTrackMenuItem(this.player_, { 'kind': this.kind_ })); 7923 7924 for (var i = 0; i < this.player_.textTracks().length; i++) { 7925 track = this.player_.textTracks()[i]; 7926 if (track.kind() === this.kind_) { 7927 items.push(new vjs.TextTrackMenuItem(this.player_, { 7928 'track': track 7929 })); 7930 } 7931 } 7932 7933 return items; 7934 }; 7935 7936 /** 7937 * The button component for toggling and selecting captions 7938 * 7939 * @constructor 7940 */ 7941 vjs.CaptionsButton = vjs.TextTrackButton.extend({ 7942 /** @constructor */ 7943 init: function(player, options, ready){ 7944 vjs.TextTrackButton.call(this, player, options, ready); 7945 this.el_.setAttribute('aria-label','Captions Menu'); 7946 } 7947 }); 7948 vjs.CaptionsButton.prototype.kind_ = 'captions'; 7949 vjs.CaptionsButton.prototype.buttonText = 'Captions'; 7950 vjs.CaptionsButton.prototype.className = 'vjs-captions-button'; 7951 7952 /** 7953 * The button component for toggling and selecting subtitles 7954 * 7955 * @constructor 7956 */ 7957 vjs.SubtitlesButton = vjs.TextTrackButton.extend({ 7958 /** @constructor */ 7959 init: function(player, options, ready){ 7960 vjs.TextTrackButton.call(this, player, options, ready); 7961 this.el_.setAttribute('aria-label','Subtitles Menu'); 7962 } 7963 }); 7964 vjs.SubtitlesButton.prototype.kind_ = 'subtitles'; 7965 vjs.SubtitlesButton.prototype.buttonText = 'Subtitles'; 7966 vjs.SubtitlesButton.prototype.className = 'vjs-subtitles-button'; 7967 7968 // Chapters act much differently than other text tracks 7969 // Cues are navigation vs. other tracks of alternative languages 7970 /** 7971 * The button component for toggling and selecting chapters 7972 * 7973 * @constructor 7974 */ 7975 vjs.ChaptersButton = vjs.TextTrackButton.extend({ 7976 /** @constructor */ 7977 init: function(player, options, ready){ 7978 vjs.TextTrackButton.call(this, player, options, ready); 7979 this.el_.setAttribute('aria-label','Chapters Menu'); 7980 } 7981 }); 7982 vjs.ChaptersButton.prototype.kind_ = 'chapters'; 7983 vjs.ChaptersButton.prototype.buttonText = 'Chapters'; 7984 vjs.ChaptersButton.prototype.className = 'vjs-chapters-button'; 7985 7986 // Create a menu item for each text track 7987 vjs.ChaptersButton.prototype.createItems = function(){ 7988 var items = [], track; 7989 7990 for (var i = 0; i < this.player_.textTracks().length; i++) { 7991 track = this.player_.textTracks()[i]; 7992 if (track.kind() === this.kind_) { 7993 items.push(new vjs.TextTrackMenuItem(this.player_, { 7994 'track': track 7995 })); 7996 } 7997 } 7998 7999 return items; 8000 }; 8001 8002 vjs.ChaptersButton.prototype.createMenu = function(){ 8003 var tracks = this.player_.textTracks(), 8004 i = 0, 8005 j = tracks.length, 8006 track, chaptersTrack, 8007 items = this.items = []; 8008 8009 for (;i<j;i++) { 8010 track = tracks[i]; 8011 if (track.kind() == this.kind_) { 8012 if (track.readyState() === 0) { 8013 track.load(); 8014 track.on('loaded', vjs.bind(this, this.createMenu)); 8015 } else { 8016 chaptersTrack = track; 8017 break; 8018 } 8019 } 8020 } 8021 8022 var menu = this.menu; 8023 if (menu === undefined) { 8024 menu = new vjs.Menu(this.player_); 8025 menu.contentEl().appendChild(vjs.createEl('li', { 8026 className: 'vjs-menu-title', 8027 innerHTML: vjs.capitalize(this.kind_), 8028 tabindex: -1 8029 })); 8030 } 8031 8032 if (chaptersTrack) { 8033 var cues = chaptersTrack.cues_, cue, mi; 8034 i = 0; 8035 j = cues.length; 8036 8037 for (;i<j;i++) { 8038 cue = cues[i]; 8039 8040 mi = new vjs.ChaptersTrackMenuItem(this.player_, { 8041 'track': chaptersTrack, 8042 'cue': cue 8043 }); 8044 8045 items.push(mi); 8046 8047 menu.addChild(mi); 8048 } 8049 this.addChild(menu); 8050 } 8051 8052 if (this.items.length > 0) { 8053 this.show(); 8054 } 8055 8056 return menu; 8057 }; 8058 8059 8060 /** 8061 * @constructor 8062 */ 8063 vjs.ChaptersTrackMenuItem = vjs.MenuItem.extend({ 8064 /** @constructor */ 8065 init: function(player, options){ 8066 var track = this.track = options['track'], 8067 cue = this.cue = options['cue'], 8068 currentTime = player.currentTime(); 8069 8070 // Modify options for parent MenuItem class's init. 8071 options['label'] = cue.text; 8072 options['selected'] = (cue.startTime <= currentTime && currentTime < cue.endTime); 8073 vjs.MenuItem.call(this, player, options); 8074 8075 track.on('cuechange', vjs.bind(this, this.update)); 8076 } 8077 }); 8078 8079 vjs.ChaptersTrackMenuItem.prototype.onClick = function(){ 8080 vjs.MenuItem.prototype.onClick.call(this); 8081 this.player_.currentTime(this.cue.startTime); 8082 this.update(this.cue.startTime); 8083 }; 8084 8085 vjs.ChaptersTrackMenuItem.prototype.update = function(){ 8086 var cue = this.cue, 8087 currentTime = this.player_.currentTime(); 8088 8089 // vjs.log(currentTime, cue.startTime); 8090 this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); 8091 }; 8092 8093 // Add Buttons to controlBar 8094 vjs.obj.merge(vjs.ControlBar.prototype.options_['children'], { 8095 'subtitlesButton': {}, 8096 'captionsButton': {}, 8097 'chaptersButton': {} 8098 }); 8099 8100 // vjs.Cue = vjs.Component.extend({ 8101 // /** @constructor */ 8102 // init: function(player, options){ 8103 // vjs.Component.call(this, player, options); 8104 // } 8105 // }); 8106 /** 8107 * @fileoverview Add JSON support 8108 * @suppress {undefinedVars} 8109 * (Compiler doesn't like JSON not being declared) 8110 */ 8111 8112 /** 8113 * Javascript JSON implementation 8114 * (Parse Method Only) 8115 * https://github.com/douglascrockford/JSON-js/blob/master/json2.js 8116 * Only using for parse method when parsing data-setup attribute JSON. 8117 * @suppress {undefinedVars} 8118 * @namespace 8119 * @private 8120 */ 8121 vjs.JSON; 8122 8123 if (typeof window.JSON !== 'undefined' && window.JSON.parse === 'function') { 8124 vjs.JSON = window.JSON; 8125 8126 } else { 8127 vjs.JSON = {}; 8128 8129 var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 8130 8131 /** 8132 * parse the json 8133 * 8134 * @memberof vjs.JSON 8135 * @param {String} text The JSON string to parse 8136 * @param {Function=} [reviver] Optional function that can transform the results 8137 * @return {Object|Array} The parsed JSON 8138 */ 8139 vjs.JSON.parse = function (text, reviver) { 8140 var j; 8141 8142 function walk(holder, key) { 8143 var k, v, value = holder[key]; 8144 if (value && typeof value === 'object') { 8145 for (k in value) { 8146 if (Object.prototype.hasOwnProperty.call(value, k)) { 8147 v = walk(value, k); 8148 if (v !== undefined) { 8149 value[k] = v; 8150 } else { 8151 delete value[k]; 8152 } 8153 } 8154 } 8155 } 8156 return reviver.call(holder, key, value); 8157 } 8158 text = String(text); 8159 cx.lastIndex = 0; 8160 if (cx.test(text)) { 8161 text = text.replace(cx, function (a) { 8162 return '\\u' + 8163 ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 8164 }); 8165 } 8166 8167 if (/^[\],:{}\s]*$/ 8168 .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 8169 .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 8170 .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 8171 8172 j = eval('(' + text + ')'); 8173 8174 return typeof reviver === 'function' ? 8175 walk({'': j}, '') : j; 8176 } 8177 8178 throw new SyntaxError('JSON.parse(): invalid or malformed JSON data'); 8179 }; 8180 } 8181 /** 8182 * @fileoverview Functions for automatically setting up a player 8183 * based on the data-setup attribute of the video tag 8184 */ 8185 8186 // Automatically set up any tags that have a data-setup attribute 8187 vjs.autoSetup = function(){ 8188 var options, vid, player, 8189 vids = document.getElementsByTagName('video'); 8190 8191 // Check if any media elements exist 8192 if (vids && vids.length > 0) { 8193 8194 for (var i=0,j=vids.length; i<j; i++) { 8195 vid = vids[i]; 8196 8197 // Check if element exists, has getAttribute func. 8198 // IE seems to consider typeof el.getAttribute == 'object' instead of 'function' like expected, at least when loading the player immediately. 8199 if (vid && vid.getAttribute) { 8200 8201 // Make sure this player hasn't already been set up. 8202 if (vid['player'] === undefined) { 8203 options = vid.getAttribute('data-setup'); 8204 8205 // Check if data-setup attr exists. 8206 // We only auto-setup if they've added the data-setup attr. 8207 if (options !== null) { 8208 8209 // Parse options JSON 8210 // If empty string, make it a parsable json object. 8211 options = vjs.JSON.parse(options || '{}'); 8212 8213 // Create new video.js instance. 8214 player = videojs(vid, options); 8215 } 8216 } 8217 8218 // If getAttribute isn't defined, we need to wait for the DOM. 8219 } else { 8220 vjs.autoSetupTimeout(1); 8221 break; 8222 } 8223 } 8224 8225 // No videos were found, so keep looping unless page is finished loading. 8226 } else if (!vjs.windowLoaded) { 8227 vjs.autoSetupTimeout(1); 8228 } 8229 }; 8230 8231 // Pause to let the DOM keep processing 8232 vjs.autoSetupTimeout = function(wait){ 8233 setTimeout(vjs.autoSetup, wait); 8234 }; 8235 8236 if (document.readyState === 'complete') { 8237 vjs.windowLoaded = true; 8238 } else { 8239 vjs.one(window, 'load', function(){ 8240 vjs.windowLoaded = true; 8241 }); 8242 } 8243 8244 // Run Auto-load players 8245 // You have to wait at least once in case this script is loaded after your video in the DOM (weird behavior only with minified version) 8246 vjs.autoSetupTimeout(1); 8247 /** 8248 * the method for registering a video.js plugin 8249 * 8250 * @param {String} name The name of the plugin 8251 * @param {Function} init The function that is run when the player inits 8252 */ 8253 vjs.plugin = function(name, init){ 8254 vjs.Player.prototype[name] = init; 8255 };