/** * @fileoverview Main function src. */ // HTML5 Shiv. Must be in to support older browsers. document.createElement('video'); document.createElement('audio'); document.createElement('track'); /** * Doubles as the main function for users to create a player instance and also * the main library object. * * **ALIASES** videojs, _V_ (deprecated) * * The `vjs` function can be used to initialize or retrieve a player. * * var myPlayer = vjs('my_video_id'); * * @param {String|Element} id Video element or video element ID * @param {Object=} options Optional options object for config/settings * @param {Function=} ready Optional ready callback * @return {vjs.Player} A player instance * @namespace */ var vjs = function(id, options, ready){ var tag; // Element of ID // Allow for element or ID to be passed in // String ID if (typeof id === 'string') { // Adjust for jQuery ID syntax if (id.indexOf('#') === 0) { id = id.slice(1); } // If a player instance has already been created for this ID return it. if (vjs.players[id]) { // If options or ready funtion are passed, warn if (options) { vjs.log.warn ('Player "' + id + '" is already initialised. Options will not be applied.'); } if (ready) { vjs.players[id].ready(ready); } return vjs.players[id]; // Otherwise get element for ID } else { tag = vjs.el(id); } // ID is a media element } else { tag = id; } // Check for a useable element if (!tag || !tag.nodeName) { // re: nodeName, could be a box div also throw new TypeError('The element or ID supplied is not valid. (videojs)'); // Returns } // Element may have a player attr referring to an already created player instance. // If not, set up a new player and return the instance. return tag['player'] || new vjs.Player(tag, options, ready); }; // Extended name, also available externally, window.videojs var videojs = window['videojs'] = vjs; // CDN Version. Used to target right flash swf. vjs.CDN_VERSION = '4.12'; vjs.ACCESS_PROTOCOL = ('https:' == document.location.protocol ? 'https://' : 'http://'); /** * Full player version * @type {string} */ vjs['VERSION'] = '4.12.15'; /** * Global Player instance options, surfaced from vjs.Player.prototype.options_ * vjs.options = vjs.Player.prototype.options_ * All options should use string keys so they avoid * renaming by closure compiler * @type {Object} */ vjs.options = { // Default order of fallback technology 'techOrder': ['html5','flash'], // techOrder: ['flash','html5'], 'html5': {}, 'flash': {}, // Default of web browser is 300x150. Should rely on source width/height. 'width': 300, 'height': 150, // defaultVolume: 0.85, 'defaultVolume': 0.00, // The freakin seaguls are driving me crazy! // default playback rates 'playbackRates': [], // Add playback rate selection by adding rates // 'playbackRates': [0.5, 1, 1.5, 2], // default inactivity timeout 'inactivityTimeout': 2000, // Included control sets 'children': { 'mediaLoader': {}, 'posterImage': {}, 'loadingSpinner': {}, 'textTrackDisplay': {}, 'bigPlayButton': {}, 'controlBar': {}, 'errorDisplay': {}, 'textTrackSettings': {} }, 'language': document.getElementsByTagName('html')[0].getAttribute('lang') || navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language || 'en', // locales and their language translations 'languages': {}, // Default message to show when a video cannot be played. 'notSupportedMessage': 'No compatible source was found for this video.' }; // Set CDN Version of swf // The added (+) blocks the replace from changing this 4.12 string if (vjs.CDN_VERSION !== 'GENERATED'+'_CDN_VSN') { videojs.options['flash']['swf'] = "<%= asset_path('video-js.swf') %>"; } /** * Utility function for adding languages to the default options. Useful for * amending multiple language support at runtime. * * Example: vjs.addLanguage('es', {'Hello':'Hola'}); * * @param {String} code The language code or dictionary property * @param {Object} data The data values to be translated * @return {Object} The resulting global languages dictionary object */ vjs.addLanguage = function(code, data){ if(vjs.options['languages'][code] !== undefined) { vjs.options['languages'][code] = vjs.util.mergeOptions(vjs.options['languages'][code], data); } else { vjs.options['languages'][code] = data; } return vjs.options['languages']; }; /** * Global player list * @type {Object} */ vjs.players = {}; /*! * Custom Universal Module Definition (UMD) * * Video.js will never be a non-browser lib so we can simplify UMD a bunch and * still support requirejs and browserify. This also needs to be closure * compiler compatible, so string keys are used. */ if (typeof define === 'function' && define['amd']) { define('videojs', [], function(){ return videojs; }); // checking that module is an object too because of umdjs/umd#35 } else if (typeof exports === 'object' && typeof module === 'object') { module['exports'] = videojs; } /** * Core Object/Class for objects that use inheritance + constructors * * To create a class that can be subclassed itself, extend the CoreObject class. * * var Animal = CoreObject.extend(); * var Horse = Animal.extend(); * * The constructor can be defined through the init property of an object argument. * * var Animal = CoreObject.extend({ * init: function(name, sound){ * this.name = name; * } * }); * * Other methods and properties can be added the same way, or directly to the * prototype. * * var Animal = CoreObject.extend({ * init: function(name){ * this.name = name; * }, * getName: function(){ * return this.name; * }, * sound: '...' * }); * * Animal.prototype.makeSound = function(){ * alert(this.sound); * }; * * To create an instance of a class, use the create method. * * var fluffy = Animal.create('Fluffy'); * fluffy.getName(); // -> Fluffy * * Methods and properties can be overridden in subclasses. * * var Horse = Animal.extend({ * sound: 'Neighhhhh!' * }); * * var horsey = Horse.create('Horsey'); * horsey.getName(); // -> Horsey * horsey.makeSound(); // -> Alert: Neighhhhh! * * @class * @constructor */ vjs.CoreObject = vjs['CoreObject'] = function(){}; // Manually exporting vjs['CoreObject'] here for Closure Compiler // because of the use of the extend/create class methods // If we didn't do this, those functions would get flattened to something like // `a = ...` and `this.prototype` would refer to the global object instead of // CoreObject /** * Create a new object that inherits from this Object * * var Animal = CoreObject.extend(); * var Horse = Animal.extend(); * * @param {Object} props Functions and properties to be applied to the * new object's prototype * @return {vjs.CoreObject} An object that inherits from CoreObject * @this {*} */ vjs.CoreObject.extend = function(props){ var init, subObj; props = props || {}; // Set up the constructor using the supplied init method // or using the init of the parent object // Make sure to check the unobfuscated version for external libs init = props['init'] || props.init || this.prototype['init'] || this.prototype.init || function(){}; // In Resig's simple class inheritance (previously used) the constructor // is a function that calls `this.init.apply(arguments)` // However that would prevent us from using `ParentObject.call(this);` // in a Child constructor because the `this` in `this.init` // would still refer to the Child and cause an infinite loop. // We would instead have to do // `ParentObject.prototype.init.apply(this, arguments);` // Bleh. We're not creating a _super() function, so it's good to keep // the parent constructor reference simple. subObj = function(){ init.apply(this, arguments); }; // Inherit from this object's prototype subObj.prototype = vjs.obj.create(this.prototype); // Reset the constructor property for subObj otherwise // instances of subObj would have the constructor of the parent Object subObj.prototype.constructor = subObj; // Make the class extendable subObj.extend = vjs.CoreObject.extend; // Make a function for creating instances subObj.create = vjs.CoreObject.create; // Extend subObj's prototype with functions and other properties from props for (var name in props) { if (props.hasOwnProperty(name)) { subObj.prototype[name] = props[name]; } } return subObj; }; /** * Create a new instance of this Object class * * var myAnimal = Animal.create(); * * @return {vjs.CoreObject} An instance of a CoreObject subclass * @this {*} */ vjs.CoreObject.create = function(){ // Create a new object that inherits from this object's prototype var inst = vjs.obj.create(this.prototype); // Apply this constructor function to the new object this.apply(inst, arguments); // Return the new object return inst; }; /** * @fileoverview Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) * This should work very similarly to jQuery's events, however it's based off the book version which isn't as * robust as jquery's, so there's probably some differences. */ /** * Add an event listener to element * It stores the handler function in a separate cache object * and adds a generic handler to the element's event, * along with a unique id (guid) to the element. * @param {Element|Object} elem Element or object to bind listeners to * @param {String|Array} type Type of event to bind to. * @param {Function} fn Event listener. * @private */ vjs.on = function(elem, type, fn){ if (vjs.obj.isArray(type)) { return _handleMultipleEvents(vjs.on, elem, type, fn); } var data = vjs.getData(elem); // We need a place to store all our handler data if (!data.handlers) data.handlers = {}; if (!data.handlers[type]) data.handlers[type] = []; if (!fn.guid) fn.guid = vjs.guid++; data.handlers[type].push(fn); if (!data.dispatcher) { data.disabled = false; data.dispatcher = function (event){ if (data.disabled) return; event = vjs.fixEvent(event); var handlers = data.handlers[event.type]; if (handlers) { // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. var handlersCopy = handlers.slice(0); for (var m = 0, n = handlersCopy.length; m < n; m++) { if (event.isImmediatePropagationStopped()) { break; } else { handlersCopy[m].call(elem, event); } } } }; } if (data.handlers[type].length == 1) { if (elem.addEventListener) { elem.addEventListener(type, data.dispatcher, false); } else if (elem.attachEvent) { elem.attachEvent('on' + type, data.dispatcher); } } }; /** * Removes event listeners from an element * @param {Element|Object} elem Object to remove listeners from * @param {String|Array=} type Type of listener to remove. Don't include to remove all events from element. * @param {Function} fn Specific listener to remove. Don't include to remove listeners for an event type. * @private */ vjs.off = function(elem, type, fn) { // Don't want to add a cache object through getData if not needed if (!vjs.hasData(elem)) return; var data = vjs.getData(elem); // If no events exist, nothing to unbind if (!data.handlers) { return; } if (vjs.obj.isArray(type)) { return _handleMultipleEvents(vjs.off, elem, type, fn); } // Utility function var removeType = function(t){ data.handlers[t] = []; vjs.cleanUpEvents(elem,t); }; // Are we removing all bound events? if (!type) { for (var t in data.handlers) removeType(t); return; } var handlers = data.handlers[type]; // If no handlers exist, nothing to unbind if (!handlers) return; // If no listener was provided, remove all listeners for type if (!fn) { removeType(type); return; } // We're only removing a single handler if (fn.guid) { for (var n = 0; n < handlers.length; n++) { if (handlers[n].guid === fn.guid) { handlers.splice(n--, 1); } } } vjs.cleanUpEvents(elem, type); }; /** * Clean up the listener cache and dispatchers * @param {Element|Object} elem Element to clean up * @param {String} type Type of event to clean up * @private */ vjs.cleanUpEvents = function(elem, type) { var data = vjs.getData(elem); // Remove the events of a particular type if there are none left if (data.handlers[type].length === 0) { delete data.handlers[type]; // data.handlers[type] = null; // Setting to null was causing an error with data.handlers // Remove the meta-handler from the element if (elem.removeEventListener) { elem.removeEventListener(type, data.dispatcher, false); } else if (elem.detachEvent) { elem.detachEvent('on' + type, data.dispatcher); } } // Remove the events object if there are no types left if (vjs.isEmpty(data.handlers)) { delete data.handlers; delete data.dispatcher; delete data.disabled; // data.handlers = null; // data.dispatcher = null; // data.disabled = null; } // Finally remove the expando if there is no data left if (vjs.isEmpty(data)) { vjs.removeData(elem); } }; /** * Fix a native event to have standard property values * @param {Object} event Event object to fix * @return {Object} * @private */ vjs.fixEvent = function(event) { function returnTrue() { return true; } function returnFalse() { return false; } // Test if fixing up is needed // Used to check if !event.stopPropagation instead of isPropagationStopped // But native events return true for stopPropagation, but don't have // other expected methods like isPropagationStopped. Seems to be a problem // with the Javascript Ninja code. So we're just overriding all events now. if (!event || !event.isPropagationStopped) { var old = event || window.event; event = {}; // Clone the old object so that we can modify the values event = {}; // IE8 Doesn't like when you mess with native event properties // Firefox returns false for event.hasOwnProperty('type') and other props // which makes copying more difficult. // TODO: Probably best to create a whitelist of event props for (var key in old) { // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation') { // Chrome 32+ warns if you try to copy deprecated returnValue, but // we still want to if preventDefault isn't supported (IE8). if (!(key == 'returnValue' && old.preventDefault)) { event[key] = old[key]; } } } // The event occurred on this element if (!event.target) { event.target = event.srcElement || document; } // Handle which other element the event is related to event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; // Stop the default browser action event.preventDefault = function () { if (old.preventDefault) { old.preventDefault(); } event.returnValue = false; event.isDefaultPrevented = returnTrue; event.defaultPrevented = true; }; event.isDefaultPrevented = returnFalse; event.defaultPrevented = false; // Stop the event from bubbling event.stopPropagation = function () { if (old.stopPropagation) { old.stopPropagation(); } event.cancelBubble = true; event.isPropagationStopped = returnTrue; }; event.isPropagationStopped = returnFalse; // Stop the event from bubbling and executing other handlers event.stopImmediatePropagation = function () { if (old.stopImmediatePropagation) { old.stopImmediatePropagation(); } event.isImmediatePropagationStopped = returnTrue; event.stopPropagation(); }; event.isImmediatePropagationStopped = returnFalse; // Handle mouse position if (event.clientX != null) { var doc = document.documentElement, body = document.body; event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } // Handle key presses event.which = event.charCode || event.keyCode; // Fix button for mouse clicks: // 0 == left; 1 == middle; 2 == right if (event.button != null) { event.button = (event.button & 1 ? 0 : (event.button & 4 ? 1 : (event.button & 2 ? 2 : 0))); } } // Returns fixed-up instance return event; }; /** * Trigger an event for an element * @param {Element|Object} elem Element to trigger an event on * @param {Event|Object|String} event A string (the type) or an event object with a type attribute * @private */ vjs.trigger = function(elem, event) { // Fetches element data and a reference to the parent (for bubbling). // Don't want to add a data object to cache for every parent, // so checking hasData first. var elemData = (vjs.hasData(elem)) ? vjs.getData(elem) : {}; var parent = elem.parentNode || elem.ownerDocument; // type = event.type || event, // handler; // If an event name was passed as a string, creates an event out of it if (typeof event === 'string') { event = { type:event, target:elem }; } // Normalizes the event properties. event = vjs.fixEvent(event); // If the passed element has a dispatcher, executes the established handlers. if (elemData.dispatcher) { elemData.dispatcher.call(elem, event); } // Unless explicitly stopped or the event does not bubble (e.g. media events) // recursively calls this function to bubble the event up the DOM. if (parent && !event.isPropagationStopped() && event.bubbles !== false) { vjs.trigger(parent, event); // If at the top of the DOM, triggers the default action unless disabled. } else if (!parent && !event.defaultPrevented) { var targetData = vjs.getData(event.target); // Checks if the target has a default action for this event. if (event.target[event.type]) { // Temporarily disables event dispatching on the target as we have already executed the handler. targetData.disabled = true; // Executes the default action. if (typeof event.target[event.type] === 'function') { event.target[event.type](); } // Re-enables event dispatching. targetData.disabled = false; } } // Inform the triggerer if the default was prevented by returning false return !event.defaultPrevented; /* Original version of js ninja events wasn't complete. * We've since updated to the latest version, but keeping this around * for now just in case. */ // // Added in addition to book. Book code was broke. // event = typeof event === 'object' ? // event[vjs.expando] ? // event : // new vjs.Event(type, event) : // new vjs.Event(type); // event.type = type; // if (handler) { // handler.call(elem, event); // } // // Clean up the event in case it is being reused // event.result = undefined; // event.target = elem; }; /** * Trigger a listener only once for an event * @param {Element|Object} elem Element or object to * @param {String|Array} type * @param {Function} fn * @private */ vjs.one = function(elem, type, fn) { if (vjs.obj.isArray(type)) { return _handleMultipleEvents(vjs.one, elem, type, fn); } var func = function(){ vjs.off(elem, type, func); fn.apply(this, arguments); }; // copy the guid to the new function so it can removed using the original function's ID func.guid = fn.guid = fn.guid || vjs.guid++; vjs.on(elem, type, func); }; /** * Loops through an array of event types and calls the requested method for each type. * @param {Function} fn The event method we want to use. * @param {Element|Object} elem Element or object to bind listeners to * @param {String} type Type of event to bind to. * @param {Function} callback Event listener. * @private */ function _handleMultipleEvents(fn, elem, type, callback) { vjs.arr.forEach(type, function(type) { fn(elem, type, callback); //Call the event method for each one of the types }); } var hasOwnProp = Object.prototype.hasOwnProperty; /** * Creates an element and applies properties. * @param {String=} tagName Name of tag to be created. * @param {Object=} properties Element properties to be applied. * @return {Element} * @private */ vjs.createEl = function(tagName, properties){ var el; tagName = tagName || 'div'; properties = properties || {}; el = document.createElement(tagName); vjs.obj.each(properties, function(propName, val){ // Not remembering why we were checking for dash // but using setAttribute means you have to use getAttribute // The check for dash checks for the aria-* attributes, like aria-label, aria-valuemin. // The additional check for "role" is because the default method for adding attributes does not // add the attribute "role". My guess is because it's not a valid attribute in some namespaces, although // browsers handle the attribute just fine. The W3C allows for aria-* attributes to be used in pre-HTML5 docs. // http://www.w3.org/TR/wai-aria-primer/#ariahtml. Using setAttribute gets around this problem. if (propName.indexOf('aria-') !== -1 || propName == 'role') { el.setAttribute(propName, val); } else { el[propName] = val; } }); return el; }; /** * Uppercase the first letter of a string * @param {String} string String to be uppercased * @return {String} * @private */ vjs.capitalize = function(string){ return string.charAt(0).toUpperCase() + string.slice(1); }; /** * Object functions container * @type {Object} * @private */ vjs.obj = {}; /** * Object.create shim for prototypal inheritance * * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create * * @function * @param {Object} obj Object to use as prototype * @private */ vjs.obj.create = Object.create || function(obj){ //Create a new function called 'F' which is just an empty object. function F() {} //the prototype of the 'F' function should point to the //parameter of the anonymous function. F.prototype = obj; //create a new constructor function based off of the 'F' function. return new F(); }; /** * Loop through each property in an object and call a function * whose arguments are (key,value) * @param {Object} obj Object of properties * @param {Function} fn Function to be called on each property. * @this {*} * @private */ vjs.obj.each = function(obj, fn, context){ for (var key in obj) { if (hasOwnProp.call(obj, key)) { fn.call(context || this, key, obj[key]); } } }; /** * Merge two objects together and return the original. * @param {Object} obj1 * @param {Object} obj2 * @return {Object} * @private */ vjs.obj.merge = function(obj1, obj2){ if (!obj2) { return obj1; } for (var key in obj2){ if (hasOwnProp.call(obj2, key)) { obj1[key] = obj2[key]; } } return obj1; }; /** * Merge two objects, and merge any properties that are objects * instead of just overwriting one. Uses to merge options hashes * where deeper default settings are important. * @param {Object} obj1 Object to override * @param {Object} obj2 Overriding object * @return {Object} New object. Obj1 and Obj2 will be untouched. * @private */ vjs.obj.deepMerge = function(obj1, obj2){ var key, val1, val2; // make a copy of obj1 so we're not overwriting original values. // like prototype.options_ and all sub options objects obj1 = vjs.obj.copy(obj1); for (key in obj2){ if (hasOwnProp.call(obj2, key)) { val1 = obj1[key]; val2 = obj2[key]; // Check if both properties are pure objects and do a deep merge if so if (vjs.obj.isPlain(val1) && vjs.obj.isPlain(val2)) { obj1[key] = vjs.obj.deepMerge(val1, val2); } else { obj1[key] = obj2[key]; } } } return obj1; }; /** * Make a copy of the supplied object * @param {Object} obj Object to copy * @return {Object} Copy of object * @private */ vjs.obj.copy = function(obj){ return vjs.obj.merge({}, obj); }; /** * Check if an object is plain, and not a dom node or any object sub-instance * @param {Object} obj Object to check * @return {Boolean} True if plain, false otherwise * @private */ vjs.obj.isPlain = function(obj){ return !!obj && typeof obj === 'object' && obj.toString() === '[object Object]' && obj.constructor === Object; }; /** * Check if an object is Array * 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 * @param {Object} obj Object to check * @return {Boolean} True if plain, false otherwise * @private */ vjs.obj.isArray = Array.isArray || function(arr) { return Object.prototype.toString.call(arr) === '[object Array]'; }; /** * Check to see whether the input is NaN or not. * NaN is the only JavaScript construct that isn't equal to itself * @param {Number} num Number to check * @return {Boolean} True if NaN, false otherwise * @private */ vjs.isNaN = function(num) { return num !== num; }; /** * Bind (a.k.a proxy or Context). A simple method for changing the context of a function It also stores a unique id on the function so it can be easily removed from events * @param {*} context The object to bind as scope * @param {Function} fn The function to be bound to a scope * @param {Number=} uid An optional unique ID for the function to be set * @return {Function} * @private */ vjs.bind = function(context, fn, uid) { // Make sure the function has a unique ID if (!fn.guid) { fn.guid = vjs.guid++; } // Create the new function that changes the context var ret = function() { return fn.apply(context, arguments); }; // Allow for the ability to individualize this function // Needed in the case where multiple objects might share the same prototype // IF both items add an event listener with the same function, then you try to remove just one // it will remove both because they both have the same guid. // when using this, you need to use the bind method when you remove the listener as well. // currently used in text tracks ret.guid = (uid) ? uid + '_' + fn.guid : fn.guid; return ret; }; /** * Element Data Store. Allows for binding data to an element without putting it directly on the element. * Ex. Event listeners are stored here. * (also from jsninja.com, slightly modified and updated for closure compiler) * @type {Object} * @private */ vjs.cache = {}; /** * Unique ID for an element or function * @type {Number} * @private */ vjs.guid = 1; /** * Unique attribute name to store an element's guid in * @type {String} * @constant * @private */ vjs.expando = 'vdata' + (new Date()).getTime(); /** * Returns the cache object where data for an element is stored * @param {Element} el Element to store data for. * @return {Object} * @private */ vjs.getData = function(el){ var id = el[vjs.expando]; if (!id) { id = el[vjs.expando] = vjs.guid++; } if (!vjs.cache[id]) { vjs.cache[id] = {}; } return vjs.cache[id]; }; /** * Returns the cache object where data for an element is stored * @param {Element} el Element to store data for. * @return {Object} * @private */ vjs.hasData = function(el){ var id = el[vjs.expando]; return !(!id || vjs.isEmpty(vjs.cache[id])); }; /** * Delete data for the element from the cache and the guid attr from getElementById * @param {Element} el Remove data for an element * @private */ vjs.removeData = function(el){ var id = el[vjs.expando]; if (!id) { return; } // Remove all stored data // Changed to = null // http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/ // vjs.cache[id] = null; delete vjs.cache[id]; // Remove the expando property from the DOM node try { delete el[vjs.expando]; } catch(e) { if (el.removeAttribute) { el.removeAttribute(vjs.expando); } else { // IE doesn't appear to support removeAttribute on the document element el[vjs.expando] = null; } } }; /** * Check if an object is empty * @param {Object} obj The object to check for emptiness * @return {Boolean} * @private */ vjs.isEmpty = function(obj) { for (var prop in obj) { // Inlude null properties as empty. if (obj[prop] !== null) { return false; } } return true; }; /** * Check if an element has a CSS class * @param {Element} element Element to check * @param {String} classToCheck Classname to check * @private */ vjs.hasClass = function(element, classToCheck){ return ((' ' + element.className + ' ').indexOf(' ' + classToCheck + ' ') !== -1); }; /** * Add a CSS class name to an element * @param {Element} element Element to add class name to * @param {String} classToAdd Classname to add * @private */ vjs.addClass = function(element, classToAdd){ if (!vjs.hasClass(element, classToAdd)) { element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd; } }; /** * Remove a CSS class name from an element * @param {Element} element Element to remove from class name * @param {String} classToAdd Classname to remove * @private */ vjs.removeClass = function(element, classToRemove){ var classNames, i; if (!vjs.hasClass(element, classToRemove)) {return;} classNames = element.className.split(' '); // no arr.indexOf in ie8, and we don't want to add a big shim for (i = classNames.length - 1; i >= 0; i--) { if (classNames[i] === classToRemove) { classNames.splice(i,1); } } element.className = classNames.join(' '); }; /** * Element for testing browser HTML5 video capabilities * @type {Element} * @constant * @private */ vjs.TEST_VID = vjs.createEl('video'); (function() { var track = document.createElement('track'); track.kind = 'captions'; track.srclang = 'en'; track.label = 'English'; vjs.TEST_VID.appendChild(track); })(); /** * Useragent for browser testing. * @type {String} * @constant * @private */ vjs.USER_AGENT = navigator.userAgent; /** * Device is an iPhone * @type {Boolean} * @constant * @private */ vjs.IS_IPHONE = (/iPhone/i).test(vjs.USER_AGENT); vjs.IS_IPAD = (/iPad/i).test(vjs.USER_AGENT); vjs.IS_IPOD = (/iPod/i).test(vjs.USER_AGENT); vjs.IS_IOS = vjs.IS_IPHONE || vjs.IS_IPAD || vjs.IS_IPOD; vjs.IOS_VERSION = (function(){ var match = vjs.USER_AGENT.match(/OS (\d+)_/i); if (match && match[1]) { return match[1]; } })(); vjs.IS_ANDROID = (/Android/i).test(vjs.USER_AGENT); vjs.ANDROID_VERSION = (function() { // This matches Android Major.Minor.Patch versions // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned var match = vjs.USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i), major, minor; if (!match) { return null; } major = match[1] && parseFloat(match[1]); minor = match[2] && parseFloat(match[2]); if (major && minor) { return parseFloat(match[1] + '.' + match[2]); } else if (major) { return major; } else { return null; } })(); // Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser vjs.IS_OLD_ANDROID = vjs.IS_ANDROID && (/webkit/i).test(vjs.USER_AGENT) && vjs.ANDROID_VERSION < 2.3; vjs.IS_FIREFOX = (/Firefox/i).test(vjs.USER_AGENT); vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT); vjs.IS_IE8 = (/MSIE\s8\.0/).test(vjs.USER_AGENT); vjs.TOUCH_ENABLED = !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch); vjs.BACKGROUND_SIZE_SUPPORTED = 'backgroundSize' in vjs.TEST_VID.style; /** * Apply attributes to an HTML element. * @param {Element} el Target element. * @param {Object=} attributes Element attributes to be applied. * @private */ vjs.setElementAttributes = function(el, attributes){ vjs.obj.each(attributes, function(attrName, attrValue) { if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { el.removeAttribute(attrName); } else { el.setAttribute(attrName, (attrValue === true ? '' : attrValue)); } }); }; /** * Get an element's attribute values, as defined on the HTML tag * Attributes are not the same as properties. They're defined on the tag * or with setAttribute (which shouldn't be used with HTML) * This will return true or false for boolean attributes. * @param {Element} tag Element from which to get tag attributes * @return {Object} * @private */ vjs.getElementAttributes = function(tag){ var obj, knownBooleans, attrs, attrName, attrVal; obj = {}; // known boolean attributes // we can check for matching boolean properties, but older browsers // won't know about HTML5 boolean attributes that we still read from knownBooleans = ','+'autoplay,controls,loop,muted,default'+','; if (tag && tag.attributes && tag.attributes.length > 0) { attrs = tag.attributes; for (var i = attrs.length - 1; i >= 0; i--) { attrName = attrs[i].name; attrVal = attrs[i].value; // check for known booleans // the matching element property will return a value for typeof if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(','+attrName+',') !== -1) { // the value of an included boolean attribute is typically an empty // string ('') which would equal false if we just check for a false value. // we also don't want support bad code like autoplay='false' attrVal = (attrVal !== null) ? true : false; } obj[attrName] = attrVal; } } return obj; }; /** * Get the computed style value for an element * From http://robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/ * @param {Element} el Element to get style value for * @param {String} strCssRule Style name * @return {String} Style value * @private */ vjs.getComputedDimension = function(el, strCssRule){ var strValue = ''; if(document.defaultView && document.defaultView.getComputedStyle){ strValue = document.defaultView.getComputedStyle(el, '').getPropertyValue(strCssRule); } else if(el.currentStyle){ // IE8 Width/Height support strValue = el['client'+strCssRule.substr(0,1).toUpperCase() + strCssRule.substr(1)] + 'px'; } return strValue; }; /** * Insert an element as the first child node of another * @param {Element} child Element to insert * @param {[type]} parent Element to insert child into * @private */ vjs.insertFirst = function(child, parent){ if (parent.firstChild) { parent.insertBefore(child, parent.firstChild); } else { parent.appendChild(child); } }; /** * Object to hold browser support information * @type {Object} * @private */ vjs.browser = {}; /** * Shorthand for document.getElementById() * Also allows for CSS (jQuery) ID syntax. But nothing other than IDs. * @param {String} id Element ID * @return {Element} Element with supplied ID * @private */ vjs.el = function(id){ if (id.indexOf('#') === 0) { id = id.slice(1); } return document.getElementById(id); }; /** * Format seconds as a time string, H:MM:SS or M:SS * Supplying a guide (in seconds) will force a number of leading zeros * to cover the length of the guide * @param {Number} seconds Number of seconds to be turned into a string * @param {Number} guide Number (in seconds) to model the string after * @return {String} Time formatted as H:MM:SS or M:SS * @private */ vjs.formatTime = function(seconds, guide) { // Default to using seconds as guide guide = guide || seconds; var s = Math.floor(seconds % 60), m = Math.floor(seconds / 60 % 60), h = Math.floor(seconds / 3600), gm = Math.floor(guide / 60 % 60), gh = Math.floor(guide / 3600); // handle invalid times if (isNaN(seconds) || seconds === Infinity) { // '-' is false for all relational operators (e.g. <, >=) so this setting // will add the minimum number of fields specified by the guide h = m = s = '-'; } // Check if we need to show hours h = (h > 0 || gh > 0) ? h + ':' : ''; // If hours are showing, we may need to add a leading zero. // Always show at least one digit of minutes. m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':'; // Check if leading zero is need for seconds s = (s < 10) ? '0' + s : s; return h + m + s; }; // Attempt to block the ability to select text while dragging controls vjs.blockTextSelection = function(){ document.body.focus(); document.onselectstart = function () { return false; }; }; // Turn off text selection blocking vjs.unblockTextSelection = function(){ document.onselectstart = function () { return true; }; }; /** * Trim whitespace from the ends of a string. * @param {String} string String to trim * @return {String} Trimmed string * @private */ vjs.trim = function(str){ return (str+'').replace(/^\s+|\s+$/g, ''); }; /** * Should round off a number to a decimal place * @param {Number} num Number to round * @param {Number} dec Number of decimal places to round to * @return {Number} Rounded number * @private */ vjs.round = function(num, dec) { if (!dec) { dec = 0; } return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); }; /** * Should create a fake TimeRange object * Mimics an HTML5 time range instance, which has functions that * return the start and end times for a range * TimeRanges are returned by the buffered() method * @param {Number} start Start time in seconds * @param {Number} end End time in seconds * @return {Object} Fake TimeRange object * @private */ vjs.createTimeRange = function(start, end){ if (start === undefined && end === undefined) { return { length: 0, start: function() { throw new Error('This TimeRanges object is empty'); }, end: function() { throw new Error('This TimeRanges object is empty'); } }; } return { length: 1, start: function() { return start; }, end: function() { return end; } }; }; /** * Add to local storage (may removable) * @private */ vjs.setLocalStorage = function(key, value){ try { // IE was throwing errors referencing the var anywhere without this var localStorage = window.localStorage || false; if (!localStorage) { return; } localStorage[key] = value; } catch(e) { if (e.code == 22 || e.code == 1014) { // Webkit == 22 / Firefox == 1014 vjs.log('LocalStorage Full (VideoJS)', e); } else { if (e.code == 18) { vjs.log('LocalStorage not allowed (VideoJS)', e); } else { vjs.log('LocalStorage Error (VideoJS)', e); } } } }; /** * Get absolute version of relative URL. Used to tell flash correct URL. * http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue * @param {String} url URL to make absolute * @return {String} Absolute URL * @private */ vjs.getAbsoluteURL = function(url){ // Check if absolute URL if (!url.match(/^https?:\/\//)) { // Convert to absolute URL. Flash hosted off-site needs an absolute URL. url = vjs.createEl('div', { innerHTML: 'x' }).firstChild.href; } return url; }; /** * Resolve and parse the elements of a URL * @param {String} url The url to parse * @return {Object} An object of url details */ vjs.parseUrl = function(url) { var div, a, addToBody, props, details; props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; // add the url to an anchor and let the browser parse the URL a = vjs.createEl('a', { href: url }); // IE8 (and 9?) Fix // ie8 doesn't parse the URL correctly until the anchor is actually // added to the body, and an innerHTML is needed to trigger the parsing addToBody = (a.host === '' && a.protocol !== 'file:'); if (addToBody) { div = vjs.createEl('div'); div.innerHTML = ''; a = div.firstChild; // prevent the div from affecting layout div.setAttribute('style', 'display:none; position:absolute;'); document.body.appendChild(div); } // Copy the specific URL properties to a new object // This is also needed for IE8 because the anchor loses its // properties when it's removed from the dom details = {}; for (var i = 0; i < props.length; i++) { details[props[i]] = a[props[i]]; } // IE9 adds the port to the host property unlike everyone else. If // a port identifier is added for standard ports, strip it. if (details.protocol === 'http:') { details.host = details.host.replace(/:80$/, ''); } if (details.protocol === 'https:') { details.host = details.host.replace(/:443$/, ''); } if (addToBody) { document.body.removeChild(div); } return details; }; /** * Log messages to the console and history based on the type of message * * @param {String} type The type of message, or `null` for `log` * @param {[type]} args The args to be passed to the log * @private */ function _logType(type, args){ var argsArray, noop, console; // convert args to an array to get array functions argsArray = Array.prototype.slice.call(args); // if there's no console then don't try to output messages // they will still be stored in vjs.log.history // Was setting these once outside of this function, but containing them // in the function makes it easier to test cases where console doesn't exist noop = function(){}; console = window['console'] || { 'log': noop, 'warn': noop, 'error': noop }; if (type) { // add the type to the front of the message argsArray.unshift(type.toUpperCase()+':'); } else { // default to log with no prefix type = 'log'; } // add to history vjs.log.history.push(argsArray); // add console prefix after adding to history argsArray.unshift('VIDEOJS:'); // call appropriate log function if (console[type].apply) { console[type].apply(console, argsArray); } else { // ie8 doesn't allow error.apply, but it will just join() the array anyway console[type](argsArray.join(' ')); } } /** * Log plain debug messages */ vjs.log = function(){ _logType(null, arguments); }; /** * Keep a history of log messages * @type {Array} */ vjs.log.history = []; /** * Log error messages */ vjs.log.error = function(){ _logType('error', arguments); }; /** * Log warning messages */ vjs.log.warn = function(){ _logType('warn', arguments); }; // Offset Left // getBoundingClientRect technique from John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/ vjs.findPosition = function(el) { var box, docEl, body, clientLeft, scrollLeft, left, clientTop, scrollTop, top; if (el.getBoundingClientRect && el.parentNode) { box = el.getBoundingClientRect(); } if (!box) { return { left: 0, top: 0 }; } docEl = document.documentElement; body = document.body; clientLeft = docEl.clientLeft || body.clientLeft || 0; scrollLeft = window.pageXOffset || body.scrollLeft; left = box.left + scrollLeft - clientLeft; clientTop = docEl.clientTop || body.clientTop || 0; scrollTop = window.pageYOffset || body.scrollTop; top = box.top + scrollTop - clientTop; // Android sometimes returns slightly off decimal values, so need to round return { left: vjs.round(left), top: vjs.round(top) }; }; /** * Array functions container * @type {Object} * @private */ vjs.arr = {}; /* * Loops through an array and runs a function for each item inside it. * @param {Array} array The array * @param {Function} callback The function to be run for each item * @param {*} thisArg The `this` binding of callback * @returns {Array} The array * @private */ vjs.arr.forEach = function(array, callback, thisArg) { if (vjs.obj.isArray(array) && callback instanceof Function) { for (var i = 0, len = array.length; i < len; ++i) { callback.call(thisArg || vjs, array[i], i, array); } } return array; }; /** * Simple http request for retrieving external files (e.g. text tracks) * * ##### Example * * // using url string * videojs.xhr('http://example.com/myfile.vtt', function(error, response, responseBody){}); * * // or options block * videojs.xhr({ * uri: 'http://example.com/myfile.vtt', * method: 'GET', * responseType: 'text' * }, function(error, response, responseBody){ * if (error) { * // log the error * } else { * // successful, do something with the response * } * }); * * * API is modeled after the Raynos/xhr, which we hope to use after * getting browserify implemented. * https://github.com/Raynos/xhr/blob/master/index.js * * @param {Object|String} options Options block or URL string * @param {Function} callback The callback function * @returns {Object} The request */ vjs.xhr = function(options, callback){ var XHR, request, urlInfo, winLoc, fileUrl, crossOrigin, abortTimeout, successHandler, errorHandler; // If options is a string it's the url if (typeof options === 'string') { options = { uri: options }; } // Merge with default options videojs.util.mergeOptions({ method: 'GET', timeout: 45 * 1000 }, options); callback = callback || function(){}; successHandler = function(){ window.clearTimeout(abortTimeout); callback(null, request, request.response || request.responseText); }; errorHandler = function(err){ window.clearTimeout(abortTimeout); if (!err || typeof err === 'string') { err = new Error(err); } callback(err, request); }; XHR = window.XMLHttpRequest; if (typeof XHR === 'undefined') { // Shim XMLHttpRequest for older IEs XHR = function () { try { return new window.ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) {} try { return new window.ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (f) {} try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (g) {} throw new Error('This browser does not support XMLHttpRequest.'); }; } request = new XHR(); // Store a reference to the url on the request instance request.uri = options.uri; urlInfo = vjs.parseUrl(options.uri); winLoc = window.location; // Check if url is for another domain/origin // IE8 doesn't know location.origin, so we won't rely on it here crossOrigin = (urlInfo.protocol + urlInfo.host) !== (winLoc.protocol + winLoc.host); // XDomainRequest -- Use for IE if XMLHTTPRequest2 isn't available // 'withCredentials' is only available in XMLHTTPRequest2 // Also XDomainRequest has a lot of gotchas, so only use if cross domain if (crossOrigin && window.XDomainRequest && !('withCredentials' in request)) { request = new window.XDomainRequest(); request.onload = successHandler; request.onerror = errorHandler; // These blank handlers need to be set to fix ie9 // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ request.onprogress = function(){}; request.ontimeout = function(){}; // XMLHTTPRequest } else { fileUrl = (urlInfo.protocol == 'file:' || winLoc.protocol == 'file:'); request.onreadystatechange = function() { if (request.readyState === 4) { if (request.timedout) { return errorHandler('timeout'); } if (request.status === 200 || fileUrl && request.status === 0) { successHandler(); } else { errorHandler(); } } }; if (options.timeout) { abortTimeout = window.setTimeout(function() { if (request.readyState !== 4) { request.timedout = true; request.abort(); } }, options.timeout); } } // open the connection try { // Third arg is async, or ignored by XDomainRequest request.open(options.method || 'GET', options.uri, true); } catch(err) { return errorHandler(err); } // withCredentials only supported by XMLHttpRequest2 if(options.withCredentials) { request.withCredentials = true; } if (options.responseType) { request.responseType = options.responseType; } // send the request try { request.send(); } catch(err) { return errorHandler(err); } return request; }; /** * Utility functions namespace * @namespace * @type {Object} */ vjs.util = {}; /** * Merge two options objects, recursively merging any plain object properties as * well. Previously `deepMerge` * * @param {Object} obj1 Object to override values in * @param {Object} obj2 Overriding object * @return {Object} New object -- obj1 and obj2 will be untouched */ vjs.util.mergeOptions = function(obj1, obj2){ var key, val1, val2; // make a copy of obj1 so we're not overwriting original values. // like prototype.options_ and all sub options objects obj1 = vjs.obj.copy(obj1); for (key in obj2){ if (obj2.hasOwnProperty(key)) { val1 = obj1[key]; val2 = obj2[key]; // Check if both properties are pure objects and do a deep merge if so if (vjs.obj.isPlain(val1) && vjs.obj.isPlain(val2)) { obj1[key] = vjs.util.mergeOptions(val1, val2); } else { obj1[key] = obj2[key]; } } } return obj1; };vjs.EventEmitter = function() { }; vjs.EventEmitter.prototype.allowedEvents_ = { }; vjs.EventEmitter.prototype.on = function(type, fn) { // Remove the addEventListener alias before calling vjs.on // so we don't get into an infinite type loop var ael = this.addEventListener; this.addEventListener = Function.prototype; vjs.on(this, type, fn); this.addEventListener = ael; }; vjs.EventEmitter.prototype.addEventListener = vjs.EventEmitter.prototype.on; vjs.EventEmitter.prototype.off = function(type, fn) { vjs.off(this, type, fn); }; vjs.EventEmitter.prototype.removeEventListener = vjs.EventEmitter.prototype.off; vjs.EventEmitter.prototype.one = function(type, fn) { vjs.one(this, type, fn); }; vjs.EventEmitter.prototype.trigger = function(event) { var type = event.type || event; if (typeof event === 'string') { event = { type: type }; } event = vjs.fixEvent(event); if (this.allowedEvents_[type] && this['on' + type]) { this['on' + type](event); } vjs.trigger(this, event); }; // The standard DOM EventTarget.dispatchEvent() is aliased to trigger() vjs.EventEmitter.prototype.dispatchEvent = vjs.EventEmitter.prototype.trigger; /** * @fileoverview Player Component - Base class for all UI objects * */ /** * Base UI Component class * * Components are embeddable UI objects that are represented by both a * javascript object and an element in the DOM. They can be children of other * components, and can have many children themselves. * * // adding a button to the player * var button = player.addChild('button'); * button.el(); // -> button element * *
*
Button
*
* * Components are also event emitters. * * button.on('click', function(){ * console.log('Button Clicked!'); * }); * * button.trigger('customevent'); * * @param {Object} player Main Player * @param {Object=} options * @class * @constructor * @extends vjs.CoreObject */ vjs.Component = vjs.CoreObject.extend({ /** * the constructor function for the class * * @constructor */ init: function(player, options, ready){ this.player_ = player; // Make a copy of prototype.options_ to protect against overriding global defaults this.options_ = vjs.obj.copy(this.options_); // Updated options with supplied options options = this.options(options); // Get ID from options or options element if one is supplied this.id_ = options['id'] || (options['el'] && options['el']['id']); // If there was no ID from the options, generate one if (!this.id_) { // Don't require the player ID function in the case of mock players this.id_ = ((player.id && player.id()) || 'no_player') + '_component_' + vjs.guid++; } this.name_ = options['name'] || null; // Create element if one wasn't provided in options this.el_ = options['el'] || this.createEl(); this.children_ = []; this.childIndex_ = {}; this.childNameIndex_ = {}; // Add any child components in options this.initChildren(); this.ready(ready); // Don't want to trigger ready here or it will before init is actually // finished for all children that run this constructor if (options.reportTouchActivity !== false) { this.enableTouchActivity(); } } }); /** * Dispose of the component and all child components */ vjs.Component.prototype.dispose = function(){ this.trigger({ type: 'dispose', 'bubbles': false }); // Dispose all children. if (this.children_) { for (var i = this.children_.length - 1; i >= 0; i--) { if (this.children_[i].dispose) { this.children_[i].dispose(); } } } // Delete child references this.children_ = null; this.childIndex_ = null; this.childNameIndex_ = null; // Remove all event listeners. this.off(); // Remove element from DOM if (this.el_.parentNode) { this.el_.parentNode.removeChild(this.el_); } vjs.removeData(this.el_); this.el_ = null; }; /** * Reference to main player instance * * @type {vjs.Player} * @private */ vjs.Component.prototype.player_ = true; /** * Return the component's player * * @return {vjs.Player} */ vjs.Component.prototype.player = function(){ return this.player_; }; /** * The component's options object * * @type {Object} * @private */ vjs.Component.prototype.options_; /** * Deep merge of options objects * * Whenever a property is an object on both options objects * the two properties will be merged using vjs.obj.deepMerge. * * This is used for merging options for child components. We * want it to be easy to override individual options on a child * component without having to rewrite all the other default options. * * Parent.prototype.options_ = { * children: { * 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, * 'childTwo': {}, * 'childThree': {} * } * } * newOptions = { * children: { * 'childOne': { 'foo': 'baz', 'abc': '123' } * 'childTwo': null, * 'childFour': {} * } * } * * this.options(newOptions); * * RESULT * * { * children: { * 'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' }, * 'childTwo': null, // Disabled. Won't be initialized. * 'childThree': {}, * 'childFour': {} * } * } * * @param {Object} obj Object of new option values * @return {Object} A NEW object of this.options_ and obj merged */ vjs.Component.prototype.options = function(obj){ if (obj === undefined) return this.options_; return this.options_ = vjs.util.mergeOptions(this.options_, obj); }; /** * The DOM element for the component * * @type {Element} * @private */ vjs.Component.prototype.el_; /** * Create the component's DOM element * * @param {String=} tagName Element's node type. e.g. 'div' * @param {Object=} attributes An object of element attributes that should be set on the element * @return {Element} */ vjs.Component.prototype.createEl = function(tagName, attributes){ return vjs.createEl(tagName, attributes); }; vjs.Component.prototype.localize = function(string){ var lang = this.player_.language(), languages = this.player_.languages(); if (languages && languages[lang] && languages[lang][string]) { return languages[lang][string]; } return string; }; /** * Get the component's DOM element * * var domEl = myComponent.el(); * * @return {Element} */ vjs.Component.prototype.el = function(){ return this.el_; }; /** * An optional element where, if defined, children will be inserted instead of * directly in `el_` * * @type {Element} * @private */ vjs.Component.prototype.contentEl_; /** * Return the component's DOM element for embedding content. * Will either be el_ or a new element defined in createEl. * * @return {Element} */ vjs.Component.prototype.contentEl = function(){ return this.contentEl_ || this.el_; }; /** * The ID for the component * * @type {String} * @private */ vjs.Component.prototype.id_; /** * Get the component's ID * * var id = myComponent.id(); * * @return {String} */ vjs.Component.prototype.id = function(){ return this.id_; }; /** * The name for the component. Often used to reference the component. * * @type {String} * @private */ vjs.Component.prototype.name_; /** * Get the component's name. The name is often used to reference the component. * * var name = myComponent.name(); * * @return {String} */ vjs.Component.prototype.name = function(){ return this.name_; }; /** * Array of child components * * @type {Array} * @private */ vjs.Component.prototype.children_; /** * Get an array of all child components * * var kids = myComponent.children(); * * @return {Array} The children */ vjs.Component.prototype.children = function(){ return this.children_; }; /** * Object of child components by ID * * @type {Object} * @private */ vjs.Component.prototype.childIndex_; /** * Returns a child component with the provided ID * * @return {vjs.Component} */ vjs.Component.prototype.getChildById = function(id){ return this.childIndex_[id]; }; /** * Object of child components by name * * @type {Object} * @private */ vjs.Component.prototype.childNameIndex_; /** * Returns a child component with the provided name * * @return {vjs.Component} */ vjs.Component.prototype.getChild = function(name){ return this.childNameIndex_[name]; }; /** * Adds a child component inside this component * * myComponent.el(); * // ->
* myComonent.children(); * // [empty array] * * var myButton = myComponent.addChild('MyButton'); * // ->
myButton
* // -> myButton === myComonent.children()[0]; * * Pass in options for child constructors and options for children of the child * * var myButton = myComponent.addChild('MyButton', { * text: 'Press Me', * children: { * buttonChildExample: { * buttonChildOption: true * } * } * }); * * @param {String|vjs.Component} child The class name or instance of a child to add * @param {Object=} options Options, including options to be passed to children of the child. * @return {vjs.Component} The child component (created by this process if a string was used) * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility} */ vjs.Component.prototype.addChild = function(child, options){ var component, componentClass, componentName; // If child is a string, create new component with options if (typeof child === 'string') { componentName = child; // Make sure options is at least an empty object to protect against errors options = options || {}; // If no componentClass in options, assume componentClass is the name lowercased // (e.g. playButton) componentClass = options['componentClass'] || vjs.capitalize(componentName); // Set name through options options['name'] = componentName; // Create a new object & element for this controls set // If there's no .player_, this is a player // Closure Compiler throws an 'incomplete alias' warning if we use the vjs variable directly. // Every class should be exported, so this should never be a problem here. component = new window['videojs'][componentClass](this.player_ || this, options); // child is a component instance } else { component = child; } this.children_.push(component); if (typeof component.id === 'function') { this.childIndex_[component.id()] = component; } // If a name wasn't used to create the component, check if we can use the // name function of the component componentName = componentName || (component.name && component.name()); if (componentName) { this.childNameIndex_[componentName] = component; } // Add the UI object's element to the container div (box) // Having an element is not required if (typeof component['el'] === 'function' && component['el']()) { this.contentEl().appendChild(component['el']()); } // Return so it can stored on parent object if desired. return component; }; /** * Remove a child component from this component's list of children, and the * child component's element from this component's element * * @param {vjs.Component} component Component to remove */ vjs.Component.prototype.removeChild = function(component){ if (typeof component === 'string') { component = this.getChild(component); } if (!component || !this.children_) return; var childFound = false; for (var i = this.children_.length - 1; i >= 0; i--) { if (this.children_[i] === component) { childFound = true; this.children_.splice(i,1); break; } } if (!childFound) return; this.childIndex_[component.id()] = null; this.childNameIndex_[component.name()] = null; var compEl = component.el(); if (compEl && compEl.parentNode === this.contentEl()) { this.contentEl().removeChild(component.el()); } }; /** * Add and initialize default child components from options * * // when an instance of MyComponent is created, all children in options * // will be added to the instance by their name strings and options * MyComponent.prototype.options_.children = { * myChildComponent: { * myChildOption: true * } * } * * // Or when creating the component * var myComp = new MyComponent(player, { * children: { * myChildComponent: { * myChildOption: true * } * } * }); * * The children option can also be an Array of child names or * child options objects (that also include a 'name' key). * * var myComp = new MyComponent(player, { * children: [ * 'button', * { * name: 'button', * someOtherOption: true * } * ] * }); * */ vjs.Component.prototype.initChildren = function(){ var parent, parentOptions, children, child, name, opts, handleAdd; parent = this; parentOptions = parent.options(); children = parentOptions['children']; if (children) { handleAdd = function(name, opts){ // Allow options for children to be set at the parent options // e.g. videojs(id, { controlBar: false }); // instead of videojs(id, { children: { controlBar: false }); if (parentOptions[name] !== undefined) { opts = parentOptions[name]; } // Allow for disabling default components // e.g. vjs.options['children']['posterImage'] = false if (opts === false) return; // Create and add the child component. // Add a direct reference to the child by name on the parent instance. // If two of the same component are used, different names should be supplied // for each parent[name] = parent.addChild(name, opts); }; // Allow for an array of children details to passed in the options if (vjs.obj.isArray(children)) { for (var i = 0; i < children.length; i++) { child = children[i]; if (typeof child == 'string') { // ['myComponent'] name = child; opts = {}; } else { // [{ name: 'myComponent', otherOption: true }] name = child.name; opts = child; } handleAdd(name, opts); } } else { vjs.obj.each(children, handleAdd); } } }; /** * Allows sub components to stack CSS class names * * @return {String} The constructed class name */ vjs.Component.prototype.buildCSSClass = function(){ // Child classes can include a function that does: // return 'CLASS NAME' + this._super(); return ''; }; /* Events ============================================================================= */ /** * Add an event listener to this component's element * * var myFunc = function(){ * var myComponent = this; * // Do something when the event is fired * }; * * myComponent.on('eventType', myFunc); * * The context of myFunc will be myComponent unless previously bound. * * Alternatively, you can add a listener to another element or component. * * myComponent.on(otherElement, 'eventName', myFunc); * myComponent.on(otherComponent, 'eventName', myFunc); * * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)` * and `otherComponent.on('eventName', myFunc)` is that this way the listeners * will be automatically cleaned up when either component is disposed. * It will also bind myComponent as the context of myFunc. * * **NOTE**: When using this on elements in the page other than window * and document (both permanent), if you remove the element from the DOM * you need to call `vjs.trigger(el, 'dispose')` on it to clean up * references to it and allow the browser to garbage collect it. * * @param {String|vjs.Component} first The event type or other component * @param {Function|String} second The event handler or event type * @param {Function} third The event handler * @return {vjs.Component} self */ vjs.Component.prototype.on = function(first, second, third){ var target, type, fn, removeOnDispose, cleanRemover, thisComponent; if (typeof first === 'string' || vjs.obj.isArray(first)) { vjs.on(this.el_, first, vjs.bind(this, second)); // Targeting another component or element } else { target = first; type = second; fn = vjs.bind(this, third); thisComponent = this; // When this component is disposed, remove the listener from the other component removeOnDispose = function(){ thisComponent.off(target, type, fn); }; // Use the same function ID so we can remove it later it using the ID // of the original listener removeOnDispose.guid = fn.guid; this.on('dispose', removeOnDispose); // If the other component is disposed first we need to clean the reference // to the other component in this component's removeOnDispose listener // Otherwise we create a memory leak. cleanRemover = function(){ thisComponent.off('dispose', removeOnDispose); }; // Add the same function ID so we can easily remove it later cleanRemover.guid = fn.guid; // Check if this is a DOM node if (first.nodeName) { // Add the listener to the other element vjs.on(target, type, fn); vjs.on(target, 'dispose', cleanRemover); // Should be a component // Not using `instanceof vjs.Component` because it makes mock players difficult } else if (typeof first.on === 'function') { // Add the listener to the other component target.on(type, fn); target.on('dispose', cleanRemover); } } return this; }; /** * Remove an event listener from this component's element * * myComponent.off('eventType', myFunc); * * If myFunc is excluded, ALL listeners for the event type will be removed. * If eventType is excluded, ALL listeners will be removed from the component. * * Alternatively you can use `off` to remove listeners that were added to other * elements or components using `myComponent.on(otherComponent...`. * In this case both the event type and listener function are REQUIRED. * * myComponent.off(otherElement, 'eventType', myFunc); * myComponent.off(otherComponent, 'eventType', myFunc); * * @param {String=|vjs.Component} first The event type or other component * @param {Function=|String} second The listener function or event type * @param {Function=} third The listener for other component * @return {vjs.Component} */ vjs.Component.prototype.off = function(first, second, third){ var target, otherComponent, type, fn, otherEl; if (!first || typeof first === 'string' || vjs.obj.isArray(first)) { vjs.off(this.el_, first, second); } else { target = first; type = second; // Ensure there's at least a guid, even if the function hasn't been used fn = vjs.bind(this, third); // Remove the dispose listener on this component, // which was given the same guid as the event listener this.off('dispose', fn); if (first.nodeName) { // Remove the listener vjs.off(target, type, fn); // Remove the listener for cleaning the dispose listener vjs.off(target, 'dispose', fn); } else { target.off(type, fn); target.off('dispose', fn); } } return this; }; /** * Add an event listener to be triggered only once and then removed * * myComponent.one('eventName', myFunc); * * Alternatively you can add a listener to another element or component * that will be triggered only once. * * myComponent.one(otherElement, 'eventName', myFunc); * myComponent.one(otherComponent, 'eventName', myFunc); * * @param {String|vjs.Component} first The event type or other component * @param {Function|String} second The listener function or event type * @param {Function=} third The listener function for other component * @return {vjs.Component} */ vjs.Component.prototype.one = function(first, second, third) { var target, type, fn, thisComponent, newFunc; if (typeof first === 'string' || vjs.obj.isArray(first)) { vjs.one(this.el_, first, vjs.bind(this, second)); } else { target = first; type = second; fn = vjs.bind(this, third); thisComponent = this; newFunc = function(){ thisComponent.off(target, type, newFunc); fn.apply(this, arguments); }; // Keep the same function ID so we can remove it later newFunc.guid = fn.guid; this.on(target, type, newFunc); } return this; }; /** * Trigger an event on an element * * myComponent.trigger('eventName'); * myComponent.trigger({'type':'eventName'}); * * @param {Event|Object|String} event A string (the type) or an event object with a type attribute * @return {vjs.Component} self */ vjs.Component.prototype.trigger = function(event){ vjs.trigger(this.el_, event); return this; }; /* Ready ================================================================================ */ /** * Is the component loaded * This can mean different things depending on the component. * * @private * @type {Boolean} */ vjs.Component.prototype.isReady_; /** * Trigger ready as soon as initialization is finished * * Allows for delaying ready. Override on a sub class prototype. * If you set this.isReadyOnInitFinish_ it will affect all components. * Specially used when waiting for the Flash player to asynchronously load. * * @type {Boolean} * @private */ vjs.Component.prototype.isReadyOnInitFinish_ = true; /** * List of ready listeners * * @type {Array} * @private */ vjs.Component.prototype.readyQueue_; /** * Bind a listener to the component's ready state * * Different from event listeners in that if the ready event has already happened * it will trigger the function immediately. * * @param {Function} fn Ready listener * @return {vjs.Component} */ vjs.Component.prototype.ready = function(fn){ if (fn) { if (this.isReady_) { fn.call(this); } else { if (this.readyQueue_ === undefined) { this.readyQueue_ = []; } this.readyQueue_.push(fn); } } return this; }; /** * Trigger the ready listeners * * @return {vjs.Component} */ vjs.Component.prototype.triggerReady = function(){ this.isReady_ = true; var readyQueue = this.readyQueue_; // Reset Ready Queue this.readyQueue_ = []; if (readyQueue && readyQueue.length > 0) { for (var i = 0, j = readyQueue.length; i < j; i++) { readyQueue[i].call(this); } // Allow for using event listeners also, in case you want to do something everytime a source is ready. this.trigger('ready'); } }; /* Display ============================================================================= */ /** * Check if a component's element has a CSS class name * * @param {String} classToCheck Classname to check * @return {vjs.Component} */ vjs.Component.prototype.hasClass = function(classToCheck){ return vjs.hasClass(this.el_, classToCheck); }; /** * Add a CSS class name to the component's element * * @param {String} classToAdd Classname to add * @return {vjs.Component} */ vjs.Component.prototype.addClass = function(classToAdd){ vjs.addClass(this.el_, classToAdd); return this; }; /** * Remove a CSS class name from the component's element * * @param {String} classToRemove Classname to remove * @return {vjs.Component} */ vjs.Component.prototype.removeClass = function(classToRemove){ vjs.removeClass(this.el_, classToRemove); return this; }; /** * Show the component element if hidden * * @return {vjs.Component} */ vjs.Component.prototype.show = function(){ this.removeClass('vjs-hidden'); return this; }; /** * Hide the component element if currently showing * * @return {vjs.Component} */ vjs.Component.prototype.hide = function(){ this.addClass('vjs-hidden'); return this; }; /** * Lock an item in its visible state * To be used with fadeIn/fadeOut. * * @return {vjs.Component} * @private */ vjs.Component.prototype.lockShowing = function(){ this.addClass('vjs-lock-showing'); return this; }; /** * Unlock an item to be hidden * To be used with fadeIn/fadeOut. * * @return {vjs.Component} * @private */ vjs.Component.prototype.unlockShowing = function(){ this.removeClass('vjs-lock-showing'); return this; }; /** * Disable component by making it unshowable * * Currently private because we're moving towards more css-based states. * @private */ vjs.Component.prototype.disable = function(){ this.hide(); this.show = function(){}; }; /** * Set or get the width of the component (CSS values) * * Setting the video tag dimension values only works with values in pixels. * Percent values will not work. * Some percents can be used, but width()/height() will return the number + %, * not the actual computed width/height. * * @param {Number|String=} num Optional width number * @param {Boolean} skipListeners Skip the 'resize' event trigger * @return {vjs.Component} This component, when setting the width * @return {Number|String} The width, when getting */ vjs.Component.prototype.width = function(num, skipListeners){ return this.dimension('width', num, skipListeners); }; /** * Get or set the height of the component (CSS values) * * Setting the video tag dimension values only works with values in pixels. * Percent values will not work. * Some percents can be used, but width()/height() will return the number + %, * not the actual computed width/height. * * @param {Number|String=} num New component height * @param {Boolean=} skipListeners Skip the resize event trigger * @return {vjs.Component} This component, when setting the height * @return {Number|String} The height, when getting */ vjs.Component.prototype.height = function(num, skipListeners){ return this.dimension('height', num, skipListeners); }; /** * Set both width and height at the same time * * @param {Number|String} width * @param {Number|String} height * @return {vjs.Component} The component */ vjs.Component.prototype.dimensions = function(width, height){ // Skip resize listeners on width for optimization return this.width(width, true).height(height); }; /** * Get or set width or height * * This is the shared code for the width() and height() methods. * All for an integer, integer + 'px' or integer + '%'; * * Known issue: Hidden elements officially have a width of 0. We're defaulting * to the style.width value and falling back to computedStyle which has the * hidden element issue. Info, but probably not an efficient fix: * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/ * * @param {String} widthOrHeight 'width' or 'height' * @param {Number|String=} num New dimension * @param {Boolean=} skipListeners Skip resize event trigger * @return {vjs.Component} The component if a dimension was set * @return {Number|String} The dimension if nothing was set * @private */ vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ if (num !== undefined) { if (num === null || vjs.isNaN(num)) { num = 0; } // Check if using css width/height (% or px) and adjust if ((''+num).indexOf('%') !== -1 || (''+num).indexOf('px') !== -1) { this.el_.style[widthOrHeight] = num; } else if (num === 'auto') { this.el_.style[widthOrHeight] = ''; } else { this.el_.style[widthOrHeight] = num+'px'; } // skipListeners allows us to avoid triggering the resize event when setting both width and height if (!skipListeners) { this.trigger('resize'); } // Return component return this; } // Not setting a value, so getting it // Make sure element exists if (!this.el_) return 0; // Get dimension value from style var val = this.el_.style[widthOrHeight]; var pxIndex = val.indexOf('px'); if (pxIndex !== -1) { // Return the pixel value with no 'px' return parseInt(val.slice(0,pxIndex), 10); // No px so using % or no style was set, so falling back to offsetWidth/height // If component has display:none, offset will return 0 // TODO: handle display:none and no dimension style using px } else { return parseInt(this.el_['offset'+vjs.capitalize(widthOrHeight)], 10); // ComputedStyle version. // Only difference is if the element is hidden it will return // the percent value (e.g. '100%'') // instead of zero like offsetWidth returns. // var val = vjs.getComputedStyleValue(this.el_, widthOrHeight); // var pxIndex = val.indexOf('px'); // if (pxIndex !== -1) { // return val.slice(0, pxIndex); // } else { // return val; // } } }; /** * Fired when the width and/or height of the component changes * @event resize */ vjs.Component.prototype.onResize; /** * Emit 'tap' events when touch events are supported * * This is used to support toggling the controls through a tap on the video. * * We're requiring them to be enabled because otherwise every component would * have this extra overhead unnecessarily, on mobile devices where extra * overhead is especially bad. * @private */ vjs.Component.prototype.emitTapEvents = function(){ var touchStart, firstTouch, touchTime, couldBeTap, noTap, xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold; // Track the start time so we can determine how long the touch lasted touchStart = 0; firstTouch = null; // Maximum movement allowed during a touch event to still be considered a tap // Other popular libs use anywhere from 2 (hammer.js) to 15, so 10 seems like a nice, round number. tapMovementThreshold = 10; // The maximum length a touch can be while still being considered a tap touchTimeThreshold = 200; this.on('touchstart', function(event) { // If more than one finger, don't consider treating this as a click if (event.touches.length === 1) { firstTouch = vjs.obj.copy(event.touches[0]); // Record start time so we can detect a tap vs. "touch and hold" touchStart = new Date().getTime(); // Reset couldBeTap tracking couldBeTap = true; } }); this.on('touchmove', function(event) { // If more than one finger, don't consider treating this as a click if (event.touches.length > 1) { couldBeTap = false; } else if (firstTouch) { // Some devices will throw touchmoves for all but the slightest of taps. // So, if we moved only a small distance, this could still be a tap xdiff = event.touches[0].pageX - firstTouch.pageX; ydiff = event.touches[0].pageY - firstTouch.pageY; touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); if (touchDistance > tapMovementThreshold) { couldBeTap = false; } } }); noTap = function(){ couldBeTap = false; }; // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s this.on('touchleave', noTap); this.on('touchcancel', noTap); // When the touch ends, measure how long it took and trigger the appropriate // event this.on('touchend', function(event) { firstTouch = null; // Proceed only if the touchmove/leave/cancel event didn't happen if (couldBeTap === true) { // Measure how long the touch lasted touchTime = new Date().getTime() - touchStart; // Make sure the touch was less than the threshold to be considered a tap if (touchTime < touchTimeThreshold) { event.preventDefault(); // Don't let browser turn this into a click this.trigger('tap'); // It may be good to copy the touchend event object and change the // type to tap, if the other event properties aren't exact after // vjs.fixEvent runs (e.g. event.target) } } }); }; /** * Report user touch activity when touch events occur * * User activity is used to determine when controls should show/hide. It's * relatively simple when it comes to mouse events, because any mouse event * should show the controls. So we capture mouse events that bubble up to the * player and report activity when that happens. * * With touch events it isn't as easy. We can't rely on touch events at the * player level, because a tap (touchstart + touchend) on the video itself on * mobile devices is meant to turn controls off (and on). User activity is * checked asynchronously, so what could happen is a tap event on the video * turns the controls off, then the touchend event bubbles up to the player, * which if it reported user activity, would turn the controls right back on. * (We also don't want to completely block touch events from bubbling up) * * Also a touchmove, touch+hold, and anything other than a tap is not supposed * to turn the controls back on on a mobile device. * * Here we're setting the default component behavior to report user activity * whenever touch events happen, and this can be turned off by components that * want touch events to act differently. */ vjs.Component.prototype.enableTouchActivity = function() { var report, touchHolding, touchEnd; // Don't continue if the root player doesn't support reporting user activity if (!this.player().reportUserActivity) { return; } // listener for reporting that the user is active report = vjs.bind(this.player(), this.player().reportUserActivity); this.on('touchstart', function() { report(); // For as long as the they are touching the device or have their mouse down, // we consider them active even if they're not moving their finger or mouse. // So we want to continue to update that they are active this.clearInterval(touchHolding); // report at the same interval as activityCheck touchHolding = this.setInterval(report, 250); }); touchEnd = function(event) { report(); // stop the interval that maintains activity if the touch is holding this.clearInterval(touchHolding); }; this.on('touchmove', report); this.on('touchend', touchEnd); this.on('touchcancel', touchEnd); }; /** * Creates timeout and sets up disposal automatically. * @param {Function} fn The function to run after the timeout. * @param {Number} timeout Number of ms to delay before executing specified function. * @return {Number} Returns the timeout ID */ vjs.Component.prototype.setTimeout = function(fn, timeout) { fn = vjs.bind(this, fn); // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't. var timeoutId = setTimeout(fn, timeout); var disposeFn = function() { this.clearTimeout(timeoutId); }; disposeFn.guid = 'vjs-timeout-'+ timeoutId; this.on('dispose', disposeFn); return timeoutId; }; /** * Clears a timeout and removes the associated dispose listener * @param {Number} timeoutId The id of the timeout to clear * @return {Number} Returns the timeout ID */ vjs.Component.prototype.clearTimeout = function(timeoutId) { clearTimeout(timeoutId); var disposeFn = function(){}; disposeFn.guid = 'vjs-timeout-'+ timeoutId; this.off('dispose', disposeFn); return timeoutId; }; /** * Creates an interval and sets up disposal automatically. * @param {Function} fn The function to run every N seconds. * @param {Number} interval Number of ms to delay before executing specified function. * @return {Number} Returns the interval ID */ vjs.Component.prototype.setInterval = function(fn, interval) { fn = vjs.bind(this, fn); var intervalId = setInterval(fn, interval); var disposeFn = function() { this.clearInterval(intervalId); }; disposeFn.guid = 'vjs-interval-'+ intervalId; this.on('dispose', disposeFn); return intervalId; }; /** * Clears an interval and removes the associated dispose listener * @param {Number} intervalId The id of the interval to clear * @return {Number} Returns the interval ID */ vjs.Component.prototype.clearInterval = function(intervalId) { clearInterval(intervalId); var disposeFn = function(){}; disposeFn.guid = 'vjs-interval-'+ intervalId; this.off('dispose', disposeFn); return intervalId; }; /* Button - Base class for all buttons ================================================================================ */ /** * Base class for all buttons * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor */ vjs.Button = vjs.Component.extend({ /** * @constructor * @inheritDoc */ init: function(player, options){ vjs.Component.call(this, player, options); this.emitTapEvents(); this.on('tap', this.onClick); this.on('click', this.onClick); this.on('focus', this.onFocus); this.on('blur', this.onBlur); } }); vjs.Button.prototype.createEl = function(type, props){ var el; // Add standard Aria and Tabindex info props = vjs.obj.merge({ className: this.buildCSSClass(), 'role': 'button', 'aria-live': 'polite', // let the screen reader user know that the text of the button may change tabIndex: 0 }, props); el = vjs.Component.prototype.createEl.call(this, type, props); // if innerHTML hasn't been overridden (bigPlayButton), add content elements if (!props.innerHTML) { this.contentEl_ = vjs.createEl('div', { className: 'vjs-control-content' }); this.controlText_ = vjs.createEl('span', { className: 'vjs-control-text', innerHTML: this.localize(this.buttonText) || 'Need Text' }); this.contentEl_.appendChild(this.controlText_); el.appendChild(this.contentEl_); } return el; }; vjs.Button.prototype.buildCSSClass = function(){ // TODO: Change vjs-control to vjs-button? return 'vjs-control ' + vjs.Component.prototype.buildCSSClass.call(this); }; // Click - Override with specific functionality for button vjs.Button.prototype.onClick = function(){}; // Focus - Add keyboard functionality to element vjs.Button.prototype.onFocus = function(){ vjs.on(document, 'keydown', vjs.bind(this, this.onKeyPress)); }; // KeyPress (document level) - Trigger click when keys are pressed vjs.Button.prototype.onKeyPress = function(event){ // Check for space bar (32) or enter (13) keys if (event.which == 32 || event.which == 13) { event.preventDefault(); this.onClick(); } }; // Blur - Remove keyboard triggers vjs.Button.prototype.onBlur = function(){ vjs.off(document, 'keydown', vjs.bind(this, this.onKeyPress)); }; /* Slider ================================================================================ */ /** * The base functionality for sliders like the volume bar and seek bar * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.Slider = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); // Set property names to bar and handle to match with the child Slider class is looking for this.bar = this.getChild(this.options_['barName']); this.handle = this.getChild(this.options_['handleName']); this.on('mousedown', this.onMouseDown); this.on('touchstart', this.onMouseDown); this.on('focus', this.onFocus); this.on('blur', this.onBlur); this.on('click', this.onClick); this.on(player, 'controlsvisible', this.update); this.on(player, this.playerEvent, this.update); } }); vjs.Slider.prototype.createEl = function(type, props) { props = props || {}; // Add the slider element class to all sub classes props.className = props.className + ' vjs-slider'; props = vjs.obj.merge({ 'role': 'slider', 'aria-valuenow': 0, 'aria-valuemin': 0, 'aria-valuemax': 100, tabIndex: 0 }, props); return vjs.Component.prototype.createEl.call(this, type, props); }; vjs.Slider.prototype.onMouseDown = function(event){ event.preventDefault(); vjs.blockTextSelection(); this.addClass('vjs-sliding'); this.on(document, 'mousemove', this.onMouseMove); this.on(document, 'mouseup', this.onMouseUp); this.on(document, 'touchmove', this.onMouseMove); this.on(document, 'touchend', this.onMouseUp); this.onMouseMove(event); }; // To be overridden by a subclass vjs.Slider.prototype.onMouseMove = function(){}; vjs.Slider.prototype.onMouseUp = function() { vjs.unblockTextSelection(); this.removeClass('vjs-sliding'); this.off(document, 'mousemove', this.onMouseMove); this.off(document, 'mouseup', this.onMouseUp); this.off(document, 'touchmove', this.onMouseMove); this.off(document, 'touchend', this.onMouseUp); this.update(); }; vjs.Slider.prototype.update = function(){ // In VolumeBar init we have a setTimeout for update that pops and update to the end of the // execution stack. The player is destroyed before then update will cause an error if (!this.el_) return; // If scrubbing, we could use a cached value to make the handle keep up with the user's mouse. // On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later. // var progress = (this.player_.scrubbing) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); var barProgress, progress = this.getPercent(), handle = this.handle, bar = this.bar; // Protect against no duration and other division issues if (typeof progress !== 'number' || progress !== progress || progress < 0 || progress === Infinity) { progress = 0; } barProgress = progress; // If there is a handle, we need to account for the handle in our calculation for progress bar // so that it doesn't fall short of or extend past the handle. if (handle) { var box = this.el_, boxWidth = box.offsetWidth, handleWidth = handle.el().offsetWidth, // The width of the handle in percent of the containing box // In IE, widths may not be ready yet causing NaN handlePercent = (handleWidth) ? handleWidth / boxWidth : 0, // Get the adjusted size of the box, considering that the handle's center never touches the left or right side. // There is a margin of half the handle's width on both sides. boxAdjustedPercent = 1 - handlePercent, // Adjust the progress that we'll use to set widths to the new adjusted box width adjustedProgress = progress * boxAdjustedPercent; // The bar does reach the left side, so we need to account for this in the bar's width barProgress = adjustedProgress + (handlePercent / 2); // Move the handle from the left based on the adjected progress handle.el().style.left = vjs.round(adjustedProgress * 100, 2) + '%'; } // Set the new bar width if (bar) { bar.el().style.width = vjs.round(barProgress * 100, 2) + '%'; } }; vjs.Slider.prototype.calculateDistance = function(event){ var el, box, boxX, boxY, boxW, boxH, handle, pageX, pageY; el = this.el_; box = vjs.findPosition(el); boxW = boxH = el.offsetWidth; handle = this.handle; if (this.options()['vertical']) { boxY = box.top; if (event.changedTouches) { pageY = event.changedTouches[0].pageY; } else { pageY = event.pageY; } if (handle) { var handleH = handle.el().offsetHeight; // Adjusted X and Width, so handle doesn't go outside the bar boxY = boxY + (handleH / 2); boxH = boxH - handleH; } // Percent that the click is through the adjusted area return Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); } else { boxX = box.left; if (event.changedTouches) { pageX = event.changedTouches[0].pageX; } else { pageX = event.pageX; } if (handle) { var handleW = handle.el().offsetWidth; // Adjusted X and Width, so handle doesn't go outside the bar boxX = boxX + (handleW / 2); boxW = boxW - handleW; } // Percent that the click is through the adjusted area return Math.max(0, Math.min(1, (pageX - boxX) / boxW)); } }; vjs.Slider.prototype.onFocus = function(){ this.on(document, 'keydown', this.onKeyPress); }; vjs.Slider.prototype.onKeyPress = function(event){ if (event.which == 37 || event.which == 40) { // Left and Down Arrows event.preventDefault(); this.stepBack(); } else if (event.which == 38 || event.which == 39) { // Up and Right Arrows event.preventDefault(); this.stepForward(); } }; vjs.Slider.prototype.onBlur = function(){ this.off(document, 'keydown', this.onKeyPress); }; /** * Listener for click events on slider, used to prevent clicks * from bubbling up to parent elements like button menus. * @param {Object} event Event object */ vjs.Slider.prototype.onClick = function(event){ event.stopImmediatePropagation(); event.preventDefault(); }; /** * SeekBar Behavior includes play progress bar, and seek handle * Needed so it can determine seek position based on handle position/size * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.SliderHandle = vjs.Component.extend(); /** * Default value of the slider * * @type {Number} * @private */ vjs.SliderHandle.prototype.defaultValue = 0; /** @inheritDoc */ vjs.SliderHandle.prototype.createEl = function(type, props) { props = props || {}; // Add the slider element class to all sub classes props.className = props.className + ' vjs-slider-handle'; props = vjs.obj.merge({ innerHTML: ''+this.defaultValue+'' }, props); return vjs.Component.prototype.createEl.call(this, 'div', props); }; /* Menu ================================================================================ */ /** * The Menu component is used to build pop up menus, including subtitle and * captions selection menus. * * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor */ vjs.Menu = vjs.Component.extend(); /** * Add a menu item to the menu * @param {Object|String} component Component or component type to add */ vjs.Menu.prototype.addItem = function(component){ this.addChild(component); component.on('click', vjs.bind(this, function(){ this.unlockShowing(); })); }; /** @inheritDoc */ vjs.Menu.prototype.createEl = function(){ var contentElType = this.options().contentElType || 'ul'; this.contentEl_ = vjs.createEl(contentElType, { className: 'vjs-menu-content' }); var el = vjs.Component.prototype.createEl.call(this, 'div', { append: this.contentEl_, className: 'vjs-menu' }); el.appendChild(this.contentEl_); // Prevent clicks from bubbling up. Needed for Menu Buttons, // where a click on the parent is significant vjs.on(el, 'click', function(event){ event.preventDefault(); event.stopImmediatePropagation(); }); return el; }; /** * The component for a menu item. `
  • ` * * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor */ vjs.MenuItem = vjs.Button.extend({ /** @constructor */ init: function(player, options){ vjs.Button.call(this, player, options); this.selected(options['selected']); } }); /** @inheritDoc */ vjs.MenuItem.prototype.createEl = function(type, props){ return vjs.Button.prototype.createEl.call(this, 'li', vjs.obj.merge({ className: 'vjs-menu-item', innerHTML: this.localize(this.options_['label']) }, props)); }; /** * Handle a click on the menu item, and set it to selected */ vjs.MenuItem.prototype.onClick = function(){ this.selected(true); }; /** * Set this menu item as selected or not * @param {Boolean} selected */ vjs.MenuItem.prototype.selected = function(selected){ if (selected) { this.addClass('vjs-selected'); this.el_.setAttribute('aria-selected',true); } else { this.removeClass('vjs-selected'); this.el_.setAttribute('aria-selected',false); } }; /** * A button class with a popup menu * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.MenuButton = vjs.Button.extend({ /** @constructor */ init: function(player, options){ vjs.Button.call(this, player, options); this.update(); this.on('keydown', this.onKeyPress); this.el_.setAttribute('aria-haspopup', true); this.el_.setAttribute('role', 'button'); } }); vjs.MenuButton.prototype.update = function() { var menu = this.createMenu(); if (this.menu) { this.removeChild(this.menu); } this.menu = menu; this.addChild(menu); if (this.items && this.items.length === 0) { this.hide(); } else if (this.items && this.items.length > 1) { this.show(); } }; /** * Track the state of the menu button * @type {Boolean} * @private */ vjs.MenuButton.prototype.buttonPressed_ = false; vjs.MenuButton.prototype.createMenu = function(){ var menu = new vjs.Menu(this.player_); // Add a title list item to the top if (this.options().title) { menu.contentEl().appendChild(vjs.createEl('li', { className: 'vjs-menu-title', innerHTML: vjs.capitalize(this.options().title), tabindex: -1 })); } this.items = this['createItems'](); if (this.items) { // Add menu items to the menu for (var i = 0; i < this.items.length; i++) { menu.addItem(this.items[i]); } } return menu; }; /** * Create the list of menu items. Specific to each subclass. */ vjs.MenuButton.prototype.createItems = function(){}; /** @inheritDoc */ vjs.MenuButton.prototype.buildCSSClass = function(){ return this.className + ' vjs-menu-button ' + vjs.Button.prototype.buildCSSClass.call(this); }; // Focus - Add keyboard functionality to element // This function is not needed anymore. Instead, the keyboard functionality is handled by // treating the button as triggering a submenu. When the button is pressed, the submenu // appears. Pressing the button again makes the submenu disappear. vjs.MenuButton.prototype.onFocus = function(){}; // Can't turn off list display that we turned on with focus, because list would go away. vjs.MenuButton.prototype.onBlur = function(){}; vjs.MenuButton.prototype.onClick = function(){ // When you click the button it adds focus, which will show the menu indefinitely. // So we'll remove focus when the mouse leaves the button. // Focus is needed for tab navigation. this.one('mouseout', vjs.bind(this, function(){ this.menu.unlockShowing(); this.el_.blur(); })); if (this.buttonPressed_){ this.unpressButton(); } else { this.pressButton(); } }; vjs.MenuButton.prototype.onKeyPress = function(event){ // Check for space bar (32) or enter (13) keys if (event.which == 32 || event.which == 13) { if (this.buttonPressed_){ this.unpressButton(); } else { this.pressButton(); } event.preventDefault(); // Check for escape (27) key } else if (event.which == 27){ if (this.buttonPressed_){ this.unpressButton(); } event.preventDefault(); } }; vjs.MenuButton.prototype.pressButton = function(){ this.buttonPressed_ = true; this.menu.lockShowing(); this.el_.setAttribute('aria-pressed', true); if (this.items && this.items.length > 0) { this.items[0].el().focus(); // set the focus to the title of the submenu } }; vjs.MenuButton.prototype.unpressButton = function(){ this.buttonPressed_ = false; this.menu.unlockShowing(); this.el_.setAttribute('aria-pressed', false); }; /** * Custom MediaError to mimic the HTML5 MediaError * @param {Number} code The media error code */ vjs.MediaError = function(code){ if (typeof code === 'number') { this.code = code; } else if (typeof code === 'string') { // default code is zero, so this is a custom error this.message = code; } else if (typeof code === 'object') { // object vjs.obj.merge(this, code); } if (!this.message) { this.message = vjs.MediaError.defaultMessages[this.code] || ''; } }; /** * The error code that refers two one of the defined * MediaError types * @type {Number} */ vjs.MediaError.prototype.code = 0; /** * An optional message to be shown with the error. * Message is not part of the HTML5 video spec * but allows for more informative custom errors. * @type {String} */ vjs.MediaError.prototype.message = ''; /** * An optional status code that can be set by plugins * to allow even more detail about the error. * For example the HLS plugin might provide the specific * HTTP status code that was returned when the error * occurred, then allowing a custom error overlay * to display more information. * @type {[type]} */ vjs.MediaError.prototype.status = null; vjs.MediaError.errorTypes = [ 'MEDIA_ERR_CUSTOM', // = 0 'MEDIA_ERR_ABORTED', // = 1 'MEDIA_ERR_NETWORK', // = 2 'MEDIA_ERR_DECODE', // = 3 'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4 'MEDIA_ERR_ENCRYPTED' // = 5 ]; vjs.MediaError.defaultMessages = { 1: 'You aborted the video playback', 2: 'A network error caused the video download to fail part-way.', 3: 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support.', 4: 'The video could not be loaded, either because the server or network failed or because the format is not supported.', 5: 'The video is encrypted and we do not have the keys to decrypt it.' }; // Add types as properties on MediaError // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; for (var errNum = 0; errNum < vjs.MediaError.errorTypes.length; errNum++) { vjs.MediaError[vjs.MediaError.errorTypes[errNum]] = errNum; // values should be accessible on both the class and instance vjs.MediaError.prototype[vjs.MediaError.errorTypes[errNum]] = errNum; } (function(){ var apiMap, specApi, browserApi, i; /** * Store the browser-specific methods for the fullscreen API * @type {Object|undefined} * @private */ vjs.browser.fullscreenAPI; // browser API methods // map approach from Screenful.js - https://github.com/sindresorhus/screenfull.js apiMap = [ // Spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html [ 'requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror' ], // WebKit [ 'webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror' ], // Old WebKit (Safari 5.1) [ 'webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror' ], // Mozilla [ 'mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror' ], // Microsoft [ 'msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError' ] ]; specApi = apiMap[0]; // determine the supported set of functions for (i=0; i * * * ``` * * After an instance has been created it can be accessed globally using `Video('example_video_1')`. * * @class * @extends vjs.Component */ vjs.Player = vjs.Component.extend({ /** * player's constructor function * * @constructs * @method init * @param {Element} tag The original video tag used for configuring options * @param {Object=} options Player options * @param {Function=} ready Ready callback function */ init: function(tag, options, ready){ this.tag = tag; // Store the original tag used to set options // Make sure tag ID exists tag.id = tag.id || 'vjs_video_' + vjs.guid++; // Store the tag attributes used to restore html5 element this.tagAttributes = tag && vjs.getElementAttributes(tag); // Set Options // The options argument overrides options set in the video tag // which overrides globally set options. // This latter part coincides with the load order // (tag must exist before Player) options = vjs.obj.merge(this.getTagSettings(tag), options); // Update Current Language this.language_ = options['language'] || vjs.options['language']; // Update Supported Languages this.languages_ = options['languages'] || vjs.options['languages']; // Cache for video property values. this.cache_ = {}; // Set poster this.poster_ = options['poster'] || ''; // Set controls this.controls_ = !!options['controls']; // Original tag settings stored in options // now remove immediately so native controls don't flash. // May be turned back on by HTML5 tech if nativeControlsForTouch is true tag.controls = false; // we don't want the player to report touch activity on itself // see enableTouchActivity in Component options.reportTouchActivity = false; // Set isAudio based on whether or not an audio tag was used this.isAudio(this.tag.nodeName.toLowerCase() === 'audio'); // Run base component initializing with new options. // Builds the element through createEl() // Inits and embeds any child components in opts vjs.Component.call(this, this, options, ready); // Update controls className. Can't do this when the controls are initially // set because the element doesn't exist yet. if (this.controls()) { this.addClass('vjs-controls-enabled'); } else { this.addClass('vjs-controls-disabled'); } if (this.isAudio()) { this.addClass('vjs-audio'); } // TODO: Make this smarter. Toggle user state between touching/mousing // using events, since devices can have both touch and mouse events. // if (vjs.TOUCH_ENABLED) { // this.addClass('vjs-touch-enabled'); // } // Make player easily findable by ID vjs.players[this.id_] = this; if (options['plugins']) { vjs.obj.each(options['plugins'], function(key, val){ this[key](val); }, this); } this.listenForUserActivity(); } }); /** * The player's stored language code * * @type {String} * @private */ vjs.Player.prototype.language_; /** * The player's language code * @param {String} languageCode The locale string * @return {String} The locale string when getting * @return {vjs.Player} self, when setting */ vjs.Player.prototype.language = function (languageCode) { if (languageCode === undefined) { return this.language_; } this.language_ = languageCode; return this; }; /** * The player's stored language dictionary * * @type {Object} * @private */ vjs.Player.prototype.languages_; vjs.Player.prototype.languages = function(){ return this.languages_; }; /** * Player instance options, surfaced using vjs.options * vjs.options = vjs.Player.prototype.options_ * Make changes in vjs.options, not here. * All options should use string keys so they avoid * renaming by closure compiler * @type {Object} * @private */ vjs.Player.prototype.options_ = vjs.options; /** * Destroys the video player and does any necessary cleanup * * myPlayer.dispose(); * * This is especially helpful if you are dynamically adding and removing videos * to/from the DOM. */ vjs.Player.prototype.dispose = function(){ this.trigger('dispose'); // prevent dispose from being called twice this.off('dispose'); // Kill reference to this player vjs.players[this.id_] = null; if (this.tag && this.tag['player']) { this.tag['player'] = null; } if (this.el_ && this.el_['player']) { this.el_['player'] = null; } if (this.tech) { this.tech.dispose(); } // Component dispose vjs.Component.prototype.dispose.call(this); }; vjs.Player.prototype.getTagSettings = function(tag){ var tagOptions, dataSetup, options = { 'sources': [], 'tracks': [] }; tagOptions = vjs.getElementAttributes(tag); dataSetup = tagOptions['data-setup']; // Check if data-setup attr exists. if (dataSetup !== null){ // Parse options JSON // If empty string, make it a parsable json object. vjs.obj.merge(tagOptions, vjs.JSON.parse(dataSetup || '{}')); } vjs.obj.merge(options, tagOptions); // Get tag children settings if (tag.hasChildNodes()) { var children, child, childName, i, j; children = tag.childNodes; for (i=0,j=children.length; i 0) { techOptions['startTime'] = this.cache_.currentTime; } this.cache_.src = source.src; } // Initialize tech instance this.tech = new window['videojs'][techName](this, techOptions); this.tech.ready(techReady); }; vjs.Player.prototype.unloadTech = function(){ this.isReady_ = false; this.tech.dispose(); this.tech = false; }; // There's many issues around changing the size of a Flash (or other plugin) object. // First is a plugin reload issue in Firefox that has been around for 11 years: https://bugzilla.mozilla.org/show_bug.cgi?id=90268 // Then with the new fullscreen API, Mozilla and webkit browsers will reload the flash object after going to fullscreen. // To get around this, we're unloading the tech, caching source and currentTime values, and reloading the tech once the plugin is resized. // reloadTech: function(betweenFn){ // vjs.log('unloadingTech') // this.unloadTech(); // vjs.log('unloadedTech') // if (betweenFn) { betweenFn.call(); } // vjs.log('LoadingTech') // this.loadTech(this.techName, { src: this.cache_.src }) // vjs.log('loadedTech') // }, // /* Player event handlers (how the player reacts to certain events) // ================================================================================ */ /** * Fired when the user agent begins looking for media data * @event loadstart */ vjs.Player.prototype.onLoadStart = function() { // TODO: Update to use `emptied` event instead. See #1277. this.removeClass('vjs-ended'); // reset the error state this.error(null); // If it's already playing we want to trigger a firstplay event now. // The firstplay event relies on both the play and loadstart events // which can happen in any order for a new source if (!this.paused()) { this.trigger('firstplay'); } else { // reset the hasStarted state this.hasStarted(false); } }; vjs.Player.prototype.hasStarted_ = false; vjs.Player.prototype.hasStarted = function(hasStarted){ if (hasStarted !== undefined) { // only update if this is a new value if (this.hasStarted_ !== hasStarted) { this.hasStarted_ = hasStarted; if (hasStarted) { this.addClass('vjs-has-started'); // trigger the firstplay event if this newly has played this.trigger('firstplay'); } else { this.removeClass('vjs-has-started'); } } return this; } return this.hasStarted_; }; /** * Fired when the player has initial duration and dimension information * @event loadedmetadata */ vjs.Player.prototype.onLoadedMetaData; /** * Fired when the player has downloaded data at the current playback position * @event loadeddata */ vjs.Player.prototype.onLoadedData; /** * Fired when the player has finished downloading the source data * @event loadedalldata */ vjs.Player.prototype.onLoadedAllData; /** * Fired whenever the media begins or resumes playback * @event play */ vjs.Player.prototype.onPlay = function(){ this.removeClass('vjs-ended'); this.removeClass('vjs-paused'); this.addClass('vjs-playing'); // hide the poster when the user hits play // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play this.hasStarted(true); }; /** * Fired whenever the media begins waiting * @event waiting */ vjs.Player.prototype.onWaiting = function(){ this.addClass('vjs-waiting'); }; /** * A handler for events that signal that waiting has ended * which is not consistent between browsers. See #1351 * @private */ vjs.Player.prototype.onWaitEnd = function(){ this.removeClass('vjs-waiting'); }; /** * Fired whenever the player is jumping to a new time * @event seeking */ vjs.Player.prototype.onSeeking = function(){ this.addClass('vjs-seeking'); }; /** * Fired when the player has finished jumping to a new time * @event seeked */ vjs.Player.prototype.onSeeked = function(){ this.removeClass('vjs-seeking'); }; /** * Fired the first time a video is played * * Not part of the HLS spec, and we're not sure if this is the best * implementation yet, so use sparingly. If you don't have a reason to * prevent playback, use `myPlayer.one('play');` instead. * * @event firstplay */ vjs.Player.prototype.onFirstPlay = function(){ //If the first starttime attribute is specified //then we will start at the given offset in seconds if(this.options_['starttime']){ this.currentTime(this.options_['starttime']); } this.addClass('vjs-has-started'); }; /** * Fired whenever the media has been paused * @event pause */ vjs.Player.prototype.onPause = function(){ this.removeClass('vjs-playing'); this.addClass('vjs-paused'); }; /** * Fired when the current playback position has changed * * During playback this is fired every 15-250 milliseconds, depending on the * playback technology in use. * @event timeupdate */ vjs.Player.prototype.onTimeUpdate; /** * Fired while the user agent is downloading media data * @event progress */ vjs.Player.prototype.onProgress = function(){ // Add custom event for when source is finished downloading. if (this.bufferedPercent() == 1) { this.trigger('loadedalldata'); } }; /** * Fired when the end of the media resource is reached (currentTime == duration) * @event ended */ vjs.Player.prototype.onEnded = function(){ this.addClass('vjs-ended'); if (this.options_['loop']) { this.currentTime(0); this.play(); } else if (!this.paused()) { this.pause(); } }; /** * Fired when the duration of the media resource is first known or changed * @event durationchange */ vjs.Player.prototype.onDurationChange = function(){ // Allows for caching value instead of asking player each time. // We need to get the techGet response and check for a value so we don't // accidentally cause the stack to blow up. var duration = this.techGet('duration'); if (duration) { if (duration < 0) { duration = Infinity; } this.duration(duration); // Determine if the stream is live and propagate styles down to UI. if (duration === Infinity) { this.addClass('vjs-live'); } else { this.removeClass('vjs-live'); } } }; /** * Fired when the volume changes * @event volumechange */ vjs.Player.prototype.onVolumeChange; /** * Fired when the player switches in or out of fullscreen mode * @event fullscreenchange */ vjs.Player.prototype.onFullscreenChange = function() { if (this.isFullscreen()) { this.addClass('vjs-fullscreen'); } else { this.removeClass('vjs-fullscreen'); } }; /** * Fired when an error occurs * @event error */ vjs.Player.prototype.onError; // /* Player API // ================================================================================ */ /** * Object for cached values. * @private */ vjs.Player.prototype.cache_; vjs.Player.prototype.getCache = function(){ return this.cache_; }; // Pass values to the playback tech vjs.Player.prototype.techCall = function(method, arg){ // If it's not ready yet, call method when it is if (this.tech && !this.tech.isReady_) { this.tech.ready(function(){ this[method](arg); }); // Otherwise call method now } else { try { this.tech[method](arg); } catch(e) { vjs.log(e); throw e; } } }; // Get calls can't wait for the tech, and sometimes don't need to. vjs.Player.prototype.techGet = function(method){ if (this.tech && this.tech.isReady_) { // Flash likes to die and reload when you hide or reposition it. // In these cases the object methods go away and we get errors. // When that happens we'll catch the errors and inform tech that it's not ready any more. try { return this.tech[method](); } catch(e) { // When building additional tech libs, an expected method may not be defined yet if (this.tech[method] === undefined) { vjs.log('Video.js: ' + method + ' method not defined for '+this.techName+' playback technology.', e); } else { // When a method isn't available on the object it throws a TypeError if (e.name == 'TypeError') { vjs.log('Video.js: ' + method + ' unavailable on '+this.techName+' playback technology element.', e); this.tech.isReady_ = false; } else { vjs.log(e); } } throw e; } } return; }; /** * start media playback * * myPlayer.play(); * * @return {vjs.Player} self */ vjs.Player.prototype.play = function(){ this.techCall('play'); return this; }; /** * Pause the video playback * * myPlayer.pause(); * * @return {vjs.Player} self */ vjs.Player.prototype.pause = function(){ this.techCall('pause'); return this; }; /** * Check if the player is paused * * var isPaused = myPlayer.paused(); * var isPlaying = !myPlayer.paused(); * * @return {Boolean} false if the media is currently playing, or true otherwise */ vjs.Player.prototype.paused = function(){ // The initial state of paused should be true (in Safari it's actually false) return (this.techGet('paused') === false) ? false : true; }; /** * Get or set the current time (in seconds) * * // get * var whereYouAt = myPlayer.currentTime(); * * // set * myPlayer.currentTime(120); // 2 minutes into the video * * @param {Number|String=} seconds The time to seek to * @return {Number} The time in seconds, when not setting * @return {vjs.Player} self, when the current time is set */ vjs.Player.prototype.currentTime = function(seconds){ if (seconds !== undefined) { this.techCall('setCurrentTime', seconds); return this; } // cache last currentTime and return. default to 0 seconds // // Caching the currentTime is meant to prevent a massive amount of reads on the tech's // currentTime when scrubbing, but may not provide much performance benefit afterall. // Should be tested. Also something has to read the actual current time or the cache will // never get updated. return this.cache_.currentTime = (this.techGet('currentTime') || 0); }; /** * Get the length in time of the video in seconds * * var lengthOfVideo = myPlayer.duration(); * * **NOTE**: The video must have started loading before the duration can be * known, and in the case of Flash, may not be known until the video starts * playing. * * @return {Number} The duration of the video in seconds */ vjs.Player.prototype.duration = function(seconds){ if (seconds !== undefined) { // cache the last set value for optimized scrubbing (esp. Flash) this.cache_.duration = parseFloat(seconds); return this; } if (this.cache_.duration === undefined) { this.onDurationChange(); } return this.cache_.duration || 0; }; /** * Calculates how much time is left. * * var timeLeft = myPlayer.remainingTime(); * * Not a native video element function, but useful * @return {Number} The time remaining in seconds */ vjs.Player.prototype.remainingTime = function(){ return this.duration() - this.currentTime(); }; // http://dev.w3.org/html5/spec/video.html#dom-media-buffered // Buffered returns a timerange object. // Kind of like an array of portions of the video that have been downloaded. /** * Get a TimeRange object with the times of the video that have been downloaded * * If you just want the percent of the video that's been downloaded, * use bufferedPercent. * * // Number of different ranges of time have been buffered. Usually 1. * numberOfRanges = bufferedTimeRange.length, * * // Time in seconds when the first range starts. Usually 0. * firstRangeStart = bufferedTimeRange.start(0), * * // Time in seconds when the first range ends * firstRangeEnd = bufferedTimeRange.end(0), * * // Length in seconds of the first time range * firstRangeLength = firstRangeEnd - firstRangeStart; * * @return {Object} A mock TimeRange object (following HTML spec) */ vjs.Player.prototype.buffered = function(){ var buffered = this.techGet('buffered'); if (!buffered || !buffered.length) { buffered = vjs.createTimeRange(0,0); } return buffered; }; /** * Get the percent (as a decimal) of the video that's been downloaded * * var howMuchIsDownloaded = myPlayer.bufferedPercent(); * * 0 means none, 1 means all. * (This method isn't in the HTML5 spec, but it's very convenient) * * @return {Number} A decimal between 0 and 1 representing the percent */ vjs.Player.prototype.bufferedPercent = function(){ var duration = this.duration(), buffered = this.buffered(), bufferedDuration = 0, start, end; if (!duration) { return 0; } for (var i=0; i duration) { end = duration; } bufferedDuration += end - start; } return bufferedDuration / duration; }; /** * Get the ending time of the last buffered time range * * This is used in the progress bar to encapsulate all time ranges. * @return {Number} The end of the last buffered time range */ vjs.Player.prototype.bufferedEnd = function(){ var buffered = this.buffered(), duration = this.duration(), end = buffered.end(buffered.length-1); if (end > duration) { end = duration; } return end; }; /** * Get or set the current volume of the media * * // get * var howLoudIsIt = myPlayer.volume(); * * // set * myPlayer.volume(0.5); // Set volume to half * * 0 is off (muted), 1.0 is all the way up, 0.5 is half way. * * @param {Number} percentAsDecimal The new volume as a decimal percent * @return {Number} The current volume, when getting * @return {vjs.Player} self, when setting */ vjs.Player.prototype.volume = function(percentAsDecimal){ var vol; if (percentAsDecimal !== undefined) { vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1 this.cache_.volume = vol; this.techCall('setVolume', vol); vjs.setLocalStorage('volume', vol); return this; } // Default to 1 when returning current volume. vol = parseFloat(this.techGet('volume')); return (isNaN(vol)) ? 1 : vol; }; /** * Get the current muted state, or turn mute on or off * * // get * var isVolumeMuted = myPlayer.muted(); * * // set * myPlayer.muted(true); // mute the volume * * @param {Boolean=} muted True to mute, false to unmute * @return {Boolean} True if mute is on, false if not, when getting * @return {vjs.Player} self, when setting mute */ vjs.Player.prototype.muted = function(muted){ if (muted !== undefined) { this.techCall('setMuted', muted); return this; } return this.techGet('muted') || false; // Default to false }; // Check if current tech can support native fullscreen // (e.g. with built in controls like iOS, so not our flash swf) vjs.Player.prototype.supportsFullScreen = function(){ return this.techGet('supportsFullScreen') || false; }; /** * is the player in fullscreen * @type {Boolean} * @private */ vjs.Player.prototype.isFullscreen_ = false; /** * Check if the player is in fullscreen mode * * // get * var fullscreenOrNot = myPlayer.isFullscreen(); * * // set * myPlayer.isFullscreen(true); // tell the player it's in fullscreen * * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official * property and instead document.fullscreenElement is used. But isFullscreen is * still a valuable property for internal player workings. * * @param {Boolean=} isFS Update the player's fullscreen state * @return {Boolean} true if fullscreen, false if not * @return {vjs.Player} self, when setting */ vjs.Player.prototype.isFullscreen = function(isFS){ if (isFS !== undefined) { this.isFullscreen_ = !!isFS; return this; } return this.isFullscreen_; }; /** * Old naming for isFullscreen() * @deprecated for lowercase 's' version */ vjs.Player.prototype.isFullScreen = function(isFS){ vjs.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")'); return this.isFullscreen(isFS); }; /** * Increase the size of the video to full screen * * myPlayer.requestFullscreen(); * * In some browsers, full screen is not supported natively, so it enters * "full window mode", where the video fills the browser window. * In browsers and devices that support native full screen, sometimes the * browser's default controls will be shown, and not the Video.js custom skin. * This includes most mobile devices (iOS, Android) and older versions of * Safari. * * @return {vjs.Player} self */ vjs.Player.prototype.requestFullscreen = function(){ var fsApi = vjs.browser.fullscreenAPI; this.isFullscreen(true); if (fsApi) { // the browser supports going fullscreen at the element level so we can // take the controls fullscreen as well as the video // Trigger fullscreenchange event after change // We have to specifically add this each time, and remove // when canceling fullscreen. Otherwise if there's multiple // players on a page, they would all be reacting to the same fullscreen // events vjs.on(document, fsApi['fullscreenchange'], vjs.bind(this, function(e){ this.isFullscreen(document[fsApi.fullscreenElement]); // If cancelling fullscreen, remove event listener. if (this.isFullscreen() === false) { vjs.off(document, fsApi['fullscreenchange'], arguments.callee); } this.trigger('fullscreenchange'); })); this.el_[fsApi.requestFullscreen](); } else if (this.tech.supportsFullScreen()) { // we can't take the video.js controls fullscreen but we can go fullscreen // with native controls this.techCall('enterFullScreen'); } else { // fullscreen isn't supported so we'll just stretch the video element to // fill the viewport this.enterFullWindow(); this.trigger('fullscreenchange'); } return this; }; /** * Old naming for requestFullscreen * @deprecated for lower case 's' version */ vjs.Player.prototype.requestFullScreen = function(){ vjs.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")'); return this.requestFullscreen(); }; /** * Return the video to its normal size after having been in full screen mode * * myPlayer.exitFullscreen(); * * @return {vjs.Player} self */ vjs.Player.prototype.exitFullscreen = function(){ var fsApi = vjs.browser.fullscreenAPI; this.isFullscreen(false); // Check for browser element fullscreen support if (fsApi) { document[fsApi.exitFullscreen](); } else if (this.tech.supportsFullScreen()) { this.techCall('exitFullScreen'); } else { this.exitFullWindow(); this.trigger('fullscreenchange'); } return this; }; /** * Old naming for exitFullscreen * @deprecated for exitFullscreen */ vjs.Player.prototype.cancelFullScreen = function(){ vjs.log.warn('player.cancelFullScreen() has been deprecated, use player.exitFullscreen()'); return this.exitFullscreen(); }; // When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us. vjs.Player.prototype.enterFullWindow = function(){ this.isFullWindow = true; // Storing original doc overflow value to return to when fullscreen is off this.docOrigOverflow = document.documentElement.style.overflow; // Add listener for esc key to exit fullscreen vjs.on(document, 'keydown', vjs.bind(this, this.fullWindowOnEscKey)); // Hide any scroll bars document.documentElement.style.overflow = 'hidden'; // Apply fullscreen styles vjs.addClass(document.body, 'vjs-full-window'); this.trigger('enterFullWindow'); }; vjs.Player.prototype.fullWindowOnEscKey = function(event){ if (event.keyCode === 27) { if (this.isFullscreen() === true) { this.exitFullscreen(); } else { this.exitFullWindow(); } } }; vjs.Player.prototype.exitFullWindow = function(){ this.isFullWindow = false; vjs.off(document, 'keydown', this.fullWindowOnEscKey); // Unhide scroll bars. document.documentElement.style.overflow = this.docOrigOverflow; // Remove fullscreen styles vjs.removeClass(document.body, 'vjs-full-window'); // Resize the box, controller, and poster to original sizes // this.positionAll(); this.trigger('exitFullWindow'); }; vjs.Player.prototype.selectSource = function(sources){ // Loop through each playback technology in the options order for (var i=0,j=this.options_['techOrder'];i 0) { // In milliseconds, if no more activity has occurred the // user will be considered inactive inactivityTimeout = this.setTimeout(function () { // Protect against the case where the inactivityTimeout can trigger just // before the next user activity is picked up by the activityCheck loop // causing a flicker if (!this.userActivity_) { this.userActive(false); } }, timeout); } } }, 250); }; /** * Gets or sets the current playback rate. * @param {Boolean} rate New playback rate to set. * @return {Number} Returns the new playback rate when setting * @return {Number} Returns the current playback rate when getting */ vjs.Player.prototype.playbackRate = function(rate) { if (rate !== undefined) { this.techCall('setPlaybackRate', rate); return this; } if (this.tech && this.tech['featuresPlaybackRate']) { return this.techGet('playbackRate'); } else { return 1.0; } }; /** * Store the current audio state * @type {Boolean} * @private */ vjs.Player.prototype.isAudio_ = false; /** * Gets or sets the audio flag * * @param {Boolean} bool True signals that this is an audio player. * @return {Boolean} Returns true if player is audio, false if not when getting * @return {vjs.Player} Returns the player if setting * @private */ vjs.Player.prototype.isAudio = function(bool) { if (bool !== undefined) { this.isAudio_ = !!bool; return this; } return this.isAudio_; }; /** * Returns the current state of network activity for the element, from * the codes in the list below. * - NETWORK_EMPTY (numeric value 0) * The element has not yet been initialised. All attributes are in * their initial states. * - NETWORK_IDLE (numeric value 1) * The element's resource selection algorithm is active and has * selected a resource, but it is not actually using the network at * this time. * - NETWORK_LOADING (numeric value 2) * The user agent is actively trying to download data. * - NETWORK_NO_SOURCE (numeric value 3) * The element's resource selection algorithm is active, but it has * not yet found a resource to use. * @return {Number} the current network activity state * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states */ vjs.Player.prototype.networkState = function(){ return this.techGet('networkState'); }; /** * Returns a value that expresses the current state of the element * with respect to rendering the current playback position, from the * codes in the list below. * - HAVE_NOTHING (numeric value 0) * No information regarding the media resource is available. * - HAVE_METADATA (numeric value 1) * Enough of the resource has been obtained that the duration of the * resource is available. * - HAVE_CURRENT_DATA (numeric value 2) * Data for the immediate current playback position is available. * - HAVE_FUTURE_DATA (numeric value 3) * Data for the immediate current playback position is available, as * well as enough data for the user agent to advance the current * playback position in the direction of playback. * - HAVE_ENOUGH_DATA (numeric value 4) * The user agent estimates that enough data is available for * playback to proceed uninterrupted. * @return {Number} the current playback rendering state * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate */ vjs.Player.prototype.readyState = function(){ return this.techGet('readyState'); }; /** * Text tracks are tracks of timed text events. * Captions - text displayed over the video for the hearing impaired * Subtitles - text displayed over the video for those who don't understand language in the video * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device */ /** * Get an array of associated text tracks. captions, subtitles, chapters, descriptions * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks * @return {Array} Array of track objects */ vjs.Player.prototype.textTracks = function(){ // cannot use techGet directly because it checks to see whether the tech is ready. // Flash is unlikely to be ready in time but textTracks should still work. return this.tech && this.tech['textTracks'](); }; vjs.Player.prototype.remoteTextTracks = function() { return this.tech && this.tech['remoteTextTracks'](); }; /** * Add a text track * In addition to the W3C settings we allow adding additional info through options. * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata * @param {String=} label Optional label * @param {String=} language Optional language */ vjs.Player.prototype.addTextTrack = function(kind, label, language) { return this.tech && this.tech['addTextTrack'](kind, label, language); }; vjs.Player.prototype.addRemoteTextTrack = function(options) { return this.tech && this.tech['addRemoteTextTrack'](options); }; vjs.Player.prototype.removeRemoteTextTrack = function(track) { this.tech && this.tech['removeRemoteTextTrack'](track); }; // Methods to add support for // initialTime: function(){ return this.techCall('initialTime'); }, // startOffsetTime: function(){ return this.techCall('startOffsetTime'); }, // played: function(){ return this.techCall('played'); }, // seekable: function(){ return this.techCall('seekable'); }, // videoTracks: function(){ return this.techCall('videoTracks'); }, // audioTracks: function(){ return this.techCall('audioTracks'); }, // videoWidth: function(){ return this.techCall('videoWidth'); }, // videoHeight: function(){ return this.techCall('videoHeight'); }, // defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); }, // mediaGroup: function(){ return this.techCall('mediaGroup'); }, // controller: function(){ return this.techCall('controller'); }, // defaultMuted: function(){ return this.techCall('defaultMuted'); } // TODO // currentSrcList: the array of sources including other formats and bitrates // playList: array of source lists in order of playback /** * Container of main controls * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor * @extends vjs.Component */ vjs.ControlBar = vjs.Component.extend(); vjs.ControlBar.prototype.options_ = { loadEvent: 'play', children: { 'playToggle': {}, 'currentTimeDisplay': {}, 'timeDivider': {}, 'durationDisplay': {}, 'remainingTimeDisplay': {}, 'liveDisplay': {}, 'progressControl': {}, 'fullscreenToggle': {}, 'volumeControl': {}, 'muteToggle': {}, // 'volumeMenuButton': {}, 'playbackRateMenuButton': {}, 'subtitlesButton': {}, 'captionsButton': {}, 'chaptersButton': {} } }; vjs.ControlBar.prototype.createEl = function(){ return vjs.createEl('div', { className: 'vjs-control-bar' }); }; /** * Displays the live indicator * TODO - Future make it click to snap to live * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.LiveDisplay = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); } }); vjs.LiveDisplay.prototype.createEl = function(){ var el = vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-live-controls vjs-control' }); this.contentEl_ = vjs.createEl('div', { className: 'vjs-live-display', innerHTML: '' + this.localize('Stream Type') + '' + this.localize('LIVE'), 'aria-live': 'off' }); el.appendChild(this.contentEl_); return el; }; /** * Button to toggle between play and pause * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor */ vjs.PlayToggle = vjs.Button.extend({ /** @constructor */ init: function(player, options){ vjs.Button.call(this, player, options); this.on(player, 'play', this.onPlay); this.on(player, 'pause', this.onPause); } }); vjs.PlayToggle.prototype.buttonText = 'Play'; vjs.PlayToggle.prototype.buildCSSClass = function(){ return 'vjs-play-control ' + vjs.Button.prototype.buildCSSClass.call(this); }; // OnClick - Toggle between play and pause vjs.PlayToggle.prototype.onClick = function(){ if (this.player_.paused()) { this.player_.play(); } else { this.player_.pause(); } }; // OnPlay - Add the vjs-playing class to the element so it can change appearance vjs.PlayToggle.prototype.onPlay = function(){ this.removeClass('vjs-paused'); this.addClass('vjs-playing'); this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" }; // OnPause - Add the vjs-paused class to the element so it can change appearance vjs.PlayToggle.prototype.onPause = function(){ this.removeClass('vjs-playing'); this.addClass('vjs-paused'); this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" }; /** * Displays the current time * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.CurrentTimeDisplay = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); this.on(player, 'timeupdate', this.updateContent); } }); vjs.CurrentTimeDisplay.prototype.createEl = function(){ var el = vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-current-time vjs-time-controls vjs-control' }); this.contentEl_ = vjs.createEl('div', { className: 'vjs-current-time-display', innerHTML: 'Current Time ' + '0:00', // label the current time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); el.appendChild(this.contentEl_); return el; }; vjs.CurrentTimeDisplay.prototype.updateContent = function(){ // Allows for smooth scrubbing, when player can't keep up. var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); this.contentEl_.innerHTML = '' + this.localize('Current Time') + ' ' + vjs.formatTime(time, this.player_.duration()); }; /** * Displays the duration * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.DurationDisplay = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, // however the durationchange event fires before this.player_.duration() is set, // so the value cannot be written out using this method. // Once the order of durationchange and this.player_.duration() being set is figured out, // this can be updated. this.on(player, 'timeupdate', this.updateContent); this.on(player, 'loadedmetadata', this.updateContent); } }); vjs.DurationDisplay.prototype.createEl = function(){ var el = vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-duration vjs-time-controls vjs-control' }); this.contentEl_ = vjs.createEl('div', { className: 'vjs-duration-display', innerHTML: '' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); el.appendChild(this.contentEl_); return el; }; vjs.DurationDisplay.prototype.updateContent = function(){ var duration = this.player_.duration(); if (duration) { this.contentEl_.innerHTML = '' + this.localize('Duration Time') + ' ' + vjs.formatTime(duration); // label the duration time for screen reader users } }; /** * The separator between the current time and duration * * Can be hidden if it's not needed in the design. * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.TimeDivider = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); } }); vjs.TimeDivider.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-time-divider', innerHTML: '
    /
    ' }); }; /** * Displays the time left in the video * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.RemainingTimeDisplay = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); this.on(player, 'timeupdate', this.updateContent); } }); vjs.RemainingTimeDisplay.prototype.createEl = function(){ var el = vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-remaining-time vjs-time-controls vjs-control' }); this.contentEl_ = vjs.createEl('div', { className: 'vjs-remaining-time-display', innerHTML: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); el.appendChild(this.contentEl_); return el; }; vjs.RemainingTimeDisplay.prototype.updateContent = function(){ if (this.player_.duration()) { this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ vjs.formatTime(this.player_.remainingTime()); } // Allows for smooth scrubbing, when player can't keep up. // var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); }; /** * Toggle fullscreen video * @param {vjs.Player|Object} player * @param {Object=} options * @class * @extends vjs.Button */ vjs.FullscreenToggle = vjs.Button.extend({ /** * @constructor * @memberof vjs.FullscreenToggle * @instance */ init: function(player, options){ vjs.Button.call(this, player, options); } }); vjs.FullscreenToggle.prototype.buttonText = 'Fullscreen'; vjs.FullscreenToggle.prototype.buildCSSClass = function(){ return 'vjs-fullscreen-control ' + vjs.Button.prototype.buildCSSClass.call(this); }; vjs.FullscreenToggle.prototype.onClick = function(){ if (!this.player_.isFullscreen()) { this.player_.requestFullscreen(); this.controlText_.innerHTML = this.localize('Non-Fullscreen'); } else { this.player_.exitFullscreen(); this.controlText_.innerHTML = this.localize('Fullscreen'); } }; /** * The Progress Control component contains the seek bar, load progress, * and play progress * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.ProgressControl = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); } }); vjs.ProgressControl.prototype.options_ = { children: { 'seekBar': {} } }; vjs.ProgressControl.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-progress-control vjs-control' }); }; /** * Seek Bar and holder for the progress bars * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.SeekBar = vjs.Slider.extend({ /** @constructor */ init: function(player, options){ vjs.Slider.call(this, player, options); this.on(player, 'timeupdate', this.updateARIAAttributes); player.ready(vjs.bind(this, this.updateARIAAttributes)); } }); vjs.SeekBar.prototype.options_ = { children: { 'loadProgressBar': {}, 'playProgressBar': {}, 'seekHandle': {} }, 'barName': 'playProgressBar', 'handleName': 'seekHandle' }; vjs.SeekBar.prototype.playerEvent = 'timeupdate'; vjs.SeekBar.prototype.createEl = function(){ return vjs.Slider.prototype.createEl.call(this, 'div', { className: 'vjs-progress-holder', 'aria-label': 'video progress bar' }); }; vjs.SeekBar.prototype.updateARIAAttributes = function(){ // Allows for smooth scrubbing, when player can't keep up. var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); this.el_.setAttribute('aria-valuenow',vjs.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete) this.el_.setAttribute('aria-valuetext',vjs.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) }; vjs.SeekBar.prototype.getPercent = function(){ return this.player_.currentTime() / this.player_.duration(); }; vjs.SeekBar.prototype.onMouseDown = function(event){ vjs.Slider.prototype.onMouseDown.call(this, event); this.player_.scrubbing = true; this.player_.addClass('vjs-scrubbing'); this.videoWasPlaying = !this.player_.paused(); this.player_.pause(); }; vjs.SeekBar.prototype.onMouseMove = function(event){ var newTime = this.calculateDistance(event) * this.player_.duration(); // Don't let video end while scrubbing. if (newTime == this.player_.duration()) { newTime = newTime - 0.1; } // Set new time (tell player to seek to new time) this.player_.currentTime(newTime); }; vjs.SeekBar.prototype.onMouseUp = function(event){ vjs.Slider.prototype.onMouseUp.call(this, event); this.player_.scrubbing = false; this.player_.removeClass('vjs-scrubbing'); if (this.videoWasPlaying) { this.player_.play(); } }; vjs.SeekBar.prototype.stepForward = function(){ this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users }; vjs.SeekBar.prototype.stepBack = function(){ this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users }; /** * Shows load progress * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.LoadProgressBar = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); this.on(player, 'progress', this.update); } }); vjs.LoadProgressBar.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-load-progress', innerHTML: '' + this.localize('Loaded') + ': 0%' }); }; vjs.LoadProgressBar.prototype.update = function(){ var i, start, end, part, buffered = this.player_.buffered(), duration = this.player_.duration(), bufferedEnd = this.player_.bufferedEnd(), children = this.el_.children, // get the percent width of a time compared to the total end percentify = function (time, end){ var percent = (time / end) || 0; // no NaN return (percent * 100) + '%'; }; // update the width of the progress bar this.el_.style.width = percentify(bufferedEnd, duration); // add child elements to represent the individual buffered time ranges for (i = 0; i < buffered.length; i++) { start = buffered.start(i), end = buffered.end(i), part = children[i]; if (!part) { part = this.el_.appendChild(vjs.createEl()); } // set the percent based on the width of the progress bar (bufferedEnd) part.style.left = percentify(start, bufferedEnd); part.style.width = percentify(end - start, bufferedEnd); } // remove unused buffered range elements for (i = children.length; i > buffered.length; i--) { this.el_.removeChild(children[i-1]); } }; /** * Shows play progress * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.PlayProgressBar = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); } }); vjs.PlayProgressBar.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-play-progress', innerHTML: '' + this.localize('Progress') + ': 0%' }); }; /** * The Seek Handle shows the current position of the playhead during playback, * and can be dragged to adjust the playhead. * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.SeekHandle = vjs.SliderHandle.extend({ init: function(player, options) { vjs.SliderHandle.call(this, player, options); this.on(player, 'timeupdate', this.updateContent); } }); /** * The default value for the handle content, which may be read by screen readers * * @type {String} * @private */ vjs.SeekHandle.prototype.defaultValue = '00:00'; /** @inheritDoc */ vjs.SeekHandle.prototype.createEl = function() { return vjs.SliderHandle.prototype.createEl.call(this, 'div', { className: 'vjs-seek-handle', 'aria-live': 'off' }); }; vjs.SeekHandle.prototype.updateContent = function() { var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); this.el_.innerHTML = '' + vjs.formatTime(time, this.player_.duration()) + ''; }; /** * The component for controlling the volume level * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.VolumeControl = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); // hide volume controls when they're not supported by the current tech if (player.tech && player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } this.on(player, 'loadstart', function(){ if (player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } else { this.removeClass('vjs-hidden'); } }); } }); vjs.VolumeControl.prototype.options_ = { children: { 'volumeBar': {} } }; vjs.VolumeControl.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-volume-control vjs-control' }); }; /** * The bar that contains the volume level and can be clicked on to adjust the level * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.VolumeBar = vjs.Slider.extend({ /** @constructor */ init: function(player, options){ vjs.Slider.call(this, player, options); this.on(player, 'volumechange', this.updateARIAAttributes); player.ready(vjs.bind(this, this.updateARIAAttributes)); } }); vjs.VolumeBar.prototype.updateARIAAttributes = function(){ // Current value of volume bar as a percentage this.el_.setAttribute('aria-valuenow',vjs.round(this.player_.volume()*100, 2)); this.el_.setAttribute('aria-valuetext',vjs.round(this.player_.volume()*100, 2)+'%'); }; vjs.VolumeBar.prototype.options_ = { children: { 'volumeLevel': {}, 'volumeHandle': {} }, 'barName': 'volumeLevel', 'handleName': 'volumeHandle' }; vjs.VolumeBar.prototype.playerEvent = 'volumechange'; vjs.VolumeBar.prototype.createEl = function(){ return vjs.Slider.prototype.createEl.call(this, 'div', { className: 'vjs-volume-bar', 'aria-label': 'volume level' }); }; vjs.VolumeBar.prototype.onMouseMove = function(event) { if (this.player_.muted()) { this.player_.muted(false); } this.player_.volume(this.calculateDistance(event)); }; vjs.VolumeBar.prototype.getPercent = function(){ if (this.player_.muted()) { return 0; } else { return this.player_.volume(); } }; vjs.VolumeBar.prototype.stepForward = function(){ this.player_.volume(this.player_.volume() + 0.1); }; vjs.VolumeBar.prototype.stepBack = function(){ this.player_.volume(this.player_.volume() - 0.1); }; /** * Shows volume level * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.VolumeLevel = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); } }); vjs.VolumeLevel.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-volume-level', innerHTML: '' }); }; /** * The volume handle can be dragged to adjust the volume level * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.VolumeHandle = vjs.SliderHandle.extend(); vjs.VolumeHandle.prototype.defaultValue = '00:00'; /** @inheritDoc */ vjs.VolumeHandle.prototype.createEl = function(){ return vjs.SliderHandle.prototype.createEl.call(this, 'div', { className: 'vjs-volume-handle' }); }; /** * A button component for muting the audio * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.MuteToggle = vjs.Button.extend({ /** @constructor */ init: function(player, options){ vjs.Button.call(this, player, options); this.on(player, 'volumechange', this.update); // hide mute toggle if the current tech doesn't support volume control if (player.tech && player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } this.on(player, 'loadstart', function(){ if (player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } else { this.removeClass('vjs-hidden'); } }); } }); vjs.MuteToggle.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-mute-control vjs-control', innerHTML: '
    ' + this.localize('Mute') + '
    ' }); }; vjs.MuteToggle.prototype.onClick = function(){ this.player_.muted( this.player_.muted() ? false : true ); }; vjs.MuteToggle.prototype.update = function(){ var vol = this.player_.volume(), level = 3; if (vol === 0 || this.player_.muted()) { level = 0; } else if (vol < 0.33) { level = 1; } else if (vol < 0.67) { level = 2; } // Don't rewrite the button text if the actual text doesn't change. // This causes unnecessary and confusing information for screen reader users. // This check is needed because this function gets called every time the volume level is changed. if(this.player_.muted()){ if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" } } else { if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" } } /* TODO improve muted icon classes */ for (var i = 0; i < 4; i++) { vjs.removeClass(this.el_, 'vjs-vol-'+i); } vjs.addClass(this.el_, 'vjs-vol-'+level); }; /** * Menu button with a popup for showing the volume slider. * @constructor */ vjs.VolumeMenuButton = vjs.MenuButton.extend({ /** @constructor */ init: function(player, options){ vjs.MenuButton.call(this, player, options); // Same listeners as MuteToggle this.on(player, 'volumechange', this.volumeUpdate); // hide mute toggle if the current tech doesn't support volume control if (player.tech && player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } this.on(player, 'loadstart', function(){ if (player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } else { this.removeClass('vjs-hidden'); } }); this.addClass('vjs-menu-button'); } }); vjs.VolumeMenuButton.prototype.createMenu = function(){ var menu = new vjs.Menu(this.player_, { contentElType: 'div' }); var vc = new vjs.VolumeBar(this.player_, this.options_['volumeBar']); vc.on('focus', function() { menu.lockShowing(); }); vc.on('blur', function() { menu.unlockShowing(); }); menu.addChild(vc); return menu; }; vjs.VolumeMenuButton.prototype.onClick = function(){ vjs.MuteToggle.prototype.onClick.call(this); vjs.MenuButton.prototype.onClick.call(this); }; vjs.VolumeMenuButton.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-volume-menu-button vjs-menu-button vjs-control', innerHTML: '
    ' + this.localize('Mute') + '
    ' }); }; vjs.VolumeMenuButton.prototype.volumeUpdate = vjs.MuteToggle.prototype.update; /** * The component for controlling the playback rate * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({ /** @constructor */ init: function(player, options){ vjs.MenuButton.call(this, player, options); this.updateVisibility(); this.updateLabel(); this.on(player, 'loadstart', this.updateVisibility); this.on(player, 'ratechange', this.updateLabel); } }); vjs.PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate'; vjs.PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate'; vjs.PlaybackRateMenuButton.prototype.createEl = function(){ var el = vjs.MenuButton.prototype.createEl.call(this); this.labelEl_ = vjs.createEl('div', { className: 'vjs-playback-rate-value', innerHTML: 1.0 }); el.appendChild(this.labelEl_); return el; }; // Menu creation vjs.PlaybackRateMenuButton.prototype.createMenu = function(){ var menu = new vjs.Menu(this.player()); var rates = this.player().options()['playbackRates']; if (rates) { for (var i = rates.length - 1; i >= 0; i--) { menu.addChild( new vjs.PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'}) ); } } return menu; }; vjs.PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){ // Current playback rate this.el().setAttribute('aria-valuenow', this.player().playbackRate()); }; vjs.PlaybackRateMenuButton.prototype.onClick = function(){ // select next rate option var currentRate = this.player().playbackRate(); var rates = this.player().options()['playbackRates']; // this will select first one if the last one currently selected var newRate = rates[0]; for (var i = 0; i currentRate) { newRate = rates[i]; break; } } this.player().playbackRate(newRate); }; vjs.PlaybackRateMenuButton.prototype.playbackRateSupported = function(){ return this.player().tech && this.player().tech['featuresPlaybackRate'] && this.player().options()['playbackRates'] && this.player().options()['playbackRates'].length > 0 ; }; /** * Hide playback rate controls when they're no playback rate options to select */ vjs.PlaybackRateMenuButton.prototype.updateVisibility = function(){ if (this.playbackRateSupported()) { this.removeClass('vjs-hidden'); } else { this.addClass('vjs-hidden'); } }; /** * Update button label when rate changed */ vjs.PlaybackRateMenuButton.prototype.updateLabel = function(){ if (this.playbackRateSupported()) { this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; } }; /** * The specific menu item type for selecting a playback rate * * @constructor */ vjs.PlaybackRateMenuItem = vjs.MenuItem.extend({ contentElType: 'button', /** @constructor */ init: function(player, options){ var label = this.label = options['rate']; var rate = this.rate = parseFloat(label, 10); // Modify options for parent MenuItem class's init. options['label'] = label; options['selected'] = rate === 1; vjs.MenuItem.call(this, player, options); this.on(player, 'ratechange', this.update); } }); vjs.PlaybackRateMenuItem.prototype.onClick = function(){ vjs.MenuItem.prototype.onClick.call(this); this.player().playbackRate(this.rate); }; vjs.PlaybackRateMenuItem.prototype.update = function(){ this.selected(this.player().playbackRate() == this.rate); }; /* Poster Image ================================================================================ */ /** * The component that handles showing the poster image. * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.PosterImage = vjs.Button.extend({ /** @constructor */ init: function(player, options){ vjs.Button.call(this, player, options); this.update(); player.on('posterchange', vjs.bind(this, this.update)); } }); /** * Clean up the poster image */ vjs.PosterImage.prototype.dispose = function(){ this.player().off('posterchange', this.update); vjs.Button.prototype.dispose.call(this); }; /** * Create the poster image element * @return {Element} */ vjs.PosterImage.prototype.createEl = function(){ var el = vjs.createEl('div', { className: 'vjs-poster', // Don't want poster to be tabbable. tabIndex: -1 }); // To ensure the poster image resizes while maintaining its original aspect // ratio, use a div with `background-size` when available. For browsers that // do not support `background-size` (e.g. IE8), fall back on using a regular // img element. if (!vjs.BACKGROUND_SIZE_SUPPORTED) { this.fallbackImg_ = vjs.createEl('img'); el.appendChild(this.fallbackImg_); } return el; }; /** * Event handler for updates to the player's poster source */ vjs.PosterImage.prototype.update = function(){ var url = this.player().poster(); this.setSrc(url); // If there's no poster source we should display:none on this component // so it's not still clickable or right-clickable if (url) { this.show(); } else { this.hide(); } }; /** * Set the poster source depending on the display method */ vjs.PosterImage.prototype.setSrc = function(url){ var backgroundImage; if (this.fallbackImg_) { this.fallbackImg_.src = url; } else { backgroundImage = ''; // Any falsey values should stay as an empty string, otherwise // this will throw an extra error if (url) { backgroundImage = 'url("' + url + '")'; } this.el_.style.backgroundImage = backgroundImage; } }; /** * Event handler for clicks on the poster image */ vjs.PosterImage.prototype.onClick = function(){ // We don't want a click to trigger playback when controls are disabled // but CSS should be hiding the poster to prevent that from happening this.player_.play(); }; /* Loading Spinner ================================================================================ */ /** * Loading spinner for waiting events * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor */ vjs.LoadingSpinner = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); // MOVING DISPLAY HANDLING TO CSS // player.on('canplay', vjs.bind(this, this.hide)); // player.on('canplaythrough', vjs.bind(this, this.hide)); // player.on('playing', vjs.bind(this, this.hide)); // player.on('seeking', vjs.bind(this, this.show)); // in some browsers seeking does not trigger the 'playing' event, // so we also need to trap 'seeked' if we are going to set a // 'seeking' event // player.on('seeked', vjs.bind(this, this.hide)); // player.on('ended', vjs.bind(this, this.hide)); // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner. // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing // player.on('stalled', vjs.bind(this, this.show)); // player.on('waiting', vjs.bind(this, this.show)); } }); vjs.LoadingSpinner.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-loading-spinner' }); }; /* Big Play Button ================================================================================ */ /** * Initial play button. Shows before the video has played. The hiding of the * big play button is done via CSS and player states. * @param {vjs.Player|Object} player * @param {Object=} options * @class * @constructor */ vjs.BigPlayButton = vjs.Button.extend(); vjs.BigPlayButton.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-big-play-button', innerHTML: '', 'aria-label': 'play video' }); }; vjs.BigPlayButton.prototype.onClick = function(){ this.player_.play(); }; /** * Display that an error has occurred making the video unplayable * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ vjs.ErrorDisplay = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); this.update(); this.on(player, 'error', this.update); } }); vjs.ErrorDisplay.prototype.createEl = function(){ var el = vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-error-display' }); this.contentEl_ = vjs.createEl('div'); el.appendChild(this.contentEl_); return el; }; vjs.ErrorDisplay.prototype.update = function(){ if (this.player().error()) { this.contentEl_.innerHTML = this.localize(this.player().error().message); } }; (function() { var createTrackHelper; /** * @fileoverview Media Technology Controller - Base class for media playback * technology controllers like Flash and HTML5 */ /** * Base class for media (HTML5 Video, Flash) controllers * @param {vjs.Player|Object} player Central player instance * @param {Object=} options Options object * @constructor */ vjs.MediaTechController = vjs.Component.extend({ /** @constructor */ init: function(player, options, ready){ options = options || {}; // we don't want the tech to report user activity automatically. // This is done manually in addControlsListeners options.reportTouchActivity = false; vjs.Component.call(this, player, options, ready); // Manually track progress in cases where the browser/flash player doesn't report it. if (!this['featuresProgressEvents']) { this.manualProgressOn(); } // Manually track timeupdates in cases where the browser/flash player doesn't report it. if (!this['featuresTimeupdateEvents']) { this.manualTimeUpdatesOn(); } this.initControlsListeners(); if (!this['featuresNativeTextTracks']) { this.emulateTextTracks(); } this.initTextTrackListeners(); } }); /** * Set up click and touch listeners for the playback element * On desktops, a click on the video itself will toggle playback, * on a mobile device a click on the video toggles controls. * (toggling controls is done by toggling the user state between active and * inactive) * * A tap can signal that a user has become active, or has become inactive * e.g. a quick tap on an iPhone movie should reveal the controls. Another * quick tap should hide them again (signaling the user is in an inactive * viewing state) * * In addition to this, we still want the user to be considered inactive after * a few seconds of inactivity. * * Note: the only part of iOS interaction we can't mimic with this setup * is a touch and hold on the video element counting as activity in order to * keep the controls showing, but that shouldn't be an issue. A touch and hold on * any controls will still keep the user active */ vjs.MediaTechController.prototype.initControlsListeners = function(){ var player, activateControls; player = this.player(); activateControls = function(){ if (player.controls() && !player.usingNativeControls()) { this.addControlsListeners(); } }; // Set up event listeners once the tech is ready and has an element to apply // listeners to this.ready(activateControls); this.on(player, 'controlsenabled', activateControls); this.on(player, 'controlsdisabled', this.removeControlsListeners); // if we're loading the playback object after it has started loading or playing the // video (often with autoplay on) then the loadstart event has already fired and we // need to fire it manually because many things rely on it. // Long term we might consider how we would do this for other events like 'canplay' // that may also have fired. this.ready(function(){ if (this.networkState && this.networkState() > 0) { this.player().trigger('loadstart'); } }); }; vjs.MediaTechController.prototype.addControlsListeners = function(){ var userWasActive; // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do // trigger mousedown/up. // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object // Any touch events are set to block the mousedown event from happening this.on('mousedown', this.onClick); // If the controls were hidden we don't want that to change without a tap event // so we'll check if the controls were already showing before reporting user // activity this.on('touchstart', function(event) { userWasActive = this.player_.userActive(); }); this.on('touchmove', function(event) { if (userWasActive){ this.player().reportUserActivity(); } }); this.on('touchend', function(event) { // Stop the mouse events from also happening event.preventDefault(); }); // Turn on component tap events this.emitTapEvents(); // The tap listener needs to come after the touchend listener because the tap // listener cancels out any reportedUserActivity when setting userActive(false) this.on('tap', this.onTap); }; /** * Remove the listeners used for click and tap controls. This is needed for * toggling to controls disabled, where a tap/touch should do nothing. */ vjs.MediaTechController.prototype.removeControlsListeners = function(){ // We don't want to just use `this.off()` because there might be other needed // listeners added by techs that extend this. this.off('tap'); this.off('touchstart'); this.off('touchmove'); this.off('touchleave'); this.off('touchcancel'); this.off('touchend'); this.off('click'); this.off('mousedown'); }; /** * Handle a click on the media element. By default will play/pause the media. */ vjs.MediaTechController.prototype.onClick = function(event){ // We're using mousedown to detect clicks thanks to Flash, but mousedown // will also be triggered with right-clicks, so we need to prevent that if (event.button !== 0) return; // When controls are disabled a click should not toggle playback because // the click is considered a control if (this.player().controls()) { if (this.player().paused()) { this.player().play(); } else { this.player().pause(); } } }; /** * Handle a tap on the media element. By default it will toggle the user * activity state, which hides and shows the controls. */ vjs.MediaTechController.prototype.onTap = function(){ this.player().userActive(!this.player().userActive()); }; /* Fallbacks for unsupported event types ================================================================================ */ // Manually trigger progress events based on changes to the buffered amount // Many flash players and older HTML5 browsers don't send progress or progress-like events vjs.MediaTechController.prototype.manualProgressOn = function(){ this.manualProgress = true; // Trigger progress watching when a source begins loading this.trackProgress(); }; vjs.MediaTechController.prototype.manualProgressOff = function(){ this.manualProgress = false; this.stopTrackingProgress(); }; vjs.MediaTechController.prototype.trackProgress = function(){ this.progressInterval = this.setInterval(function(){ // Don't trigger unless buffered amount is greater than last time var bufferedPercent = this.player().bufferedPercent(); if (this.bufferedPercent_ != bufferedPercent) { this.player().trigger('progress'); } this.bufferedPercent_ = bufferedPercent; if (bufferedPercent === 1) { this.stopTrackingProgress(); } }, 500); }; vjs.MediaTechController.prototype.stopTrackingProgress = function(){ this.clearInterval(this.progressInterval); }; /*! Time Tracking -------------------------------------------------------------- */ vjs.MediaTechController.prototype.manualTimeUpdatesOn = function(){ var player = this.player_; this.manualTimeUpdates = true; this.on(player, 'play', this.trackCurrentTime); this.on(player, 'pause', this.stopTrackingCurrentTime); // timeupdate is also called by .currentTime whenever current time is set // Watch for native timeupdate event this.one('timeupdate', function(){ // Update known progress support for this playback technology this['featuresTimeupdateEvents'] = true; // Turn off manual progress tracking this.manualTimeUpdatesOff(); }); }; vjs.MediaTechController.prototype.manualTimeUpdatesOff = function(){ var player = this.player_; this.manualTimeUpdates = false; this.stopTrackingCurrentTime(); this.off(player, 'play', this.trackCurrentTime); this.off(player, 'pause', this.stopTrackingCurrentTime); }; vjs.MediaTechController.prototype.trackCurrentTime = function(){ if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); } this.currentTimeInterval = this.setInterval(function(){ this.player().trigger('timeupdate'); }, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 }; // Turn off play progress tracking (when paused or dragging) vjs.MediaTechController.prototype.stopTrackingCurrentTime = function(){ this.clearInterval(this.currentTimeInterval); // #1002 - if the video ends right before the next timeupdate would happen, // the progress bar won't make it all the way to the end this.player().trigger('timeupdate'); }; vjs.MediaTechController.prototype.dispose = function() { // Turn off any manual progress or timeupdate tracking if (this.manualProgress) { this.manualProgressOff(); } if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); } vjs.Component.prototype.dispose.call(this); }; vjs.MediaTechController.prototype.setCurrentTime = function() { // improve the accuracy of manual timeupdates if (this.manualTimeUpdates) { this.player().trigger('timeupdate'); } }; // TODO: Consider looking at moving this into the text track display directly // https://github.com/videojs/video.js/issues/1863 vjs.MediaTechController.prototype.initTextTrackListeners = function() { var player = this.player_, tracks, textTrackListChanges = function() { var textTrackDisplay = player.getChild('textTrackDisplay'), controlBar; if (textTrackDisplay) { textTrackDisplay.updateDisplay(); } }; tracks = this.textTracks(); if (!tracks) { return; } tracks.addEventListener('removetrack', textTrackListChanges); tracks.addEventListener('addtrack', textTrackListChanges); this.on('dispose', vjs.bind(this, function() { tracks.removeEventListener('removetrack', textTrackListChanges); tracks.removeEventListener('addtrack', textTrackListChanges); })); }; vjs.MediaTechController.prototype.emulateTextTracks = function() { var player = this.player_, textTracksChanges, tracks, script; if (!window['WebVTT']) { script = document.createElement('script'); script.src = player.options()['vtt.js'] || '../node_modules/vtt.js/dist/vtt.js'; player.el().appendChild(script); window['WebVTT'] = true; } tracks = this.textTracks(); if (!tracks) { return; } textTracksChanges = function() { var i, track, textTrackDisplay; textTrackDisplay = player.getChild('textTrackDisplay'), textTrackDisplay.updateDisplay(); for (i = 0; i < this.length; i++) { track = this[i]; track.removeEventListener('cuechange', vjs.bind(textTrackDisplay, textTrackDisplay.updateDisplay)); if (track.mode === 'showing') { track.addEventListener('cuechange', vjs.bind(textTrackDisplay, textTrackDisplay.updateDisplay)); } } }; tracks.addEventListener('change', textTracksChanges); this.on('dispose', vjs.bind(this, function() { tracks.removeEventListener('change', textTracksChanges); })); }; /** * Provide default methods for text tracks. * * Html5 tech overrides these. */ /** * List of associated text tracks * @type {Array} * @private */ vjs.MediaTechController.prototype.textTracks_; vjs.MediaTechController.prototype.textTracks = function() { this.player_.textTracks_ = this.player_.textTracks_ || new vjs.TextTrackList(); return this.player_.textTracks_; }; vjs.MediaTechController.prototype.remoteTextTracks = function() { this.player_.remoteTextTracks_ = this.player_.remoteTextTracks_ || new vjs.TextTrackList(); return this.player_.remoteTextTracks_; }; createTrackHelper = function(self, kind, label, language, options) { var tracks = self.textTracks(), track; options = options || {}; options['kind'] = kind; if (label) { options['label'] = label; } if (language) { options['language'] = language; } options['player'] = self.player_; track = new vjs.TextTrack(options); tracks.addTrack_(track); return track; }; vjs.MediaTechController.prototype.addTextTrack = function(kind, label, language) { if (!kind) { throw new Error('TextTrack kind is required but was not provided'); } return createTrackHelper(this, kind, label, language); }; vjs.MediaTechController.prototype.addRemoteTextTrack = function(options) { var track = createTrackHelper(this, options['kind'], options['label'], options['language'], options); this.remoteTextTracks().addTrack_(track); return { track: track }; }; vjs.MediaTechController.prototype.removeRemoteTextTrack = function(track) { this.textTracks().removeTrack_(track); this.remoteTextTracks().removeTrack_(track); }; /** * Provide a default setPoster method for techs * * Poster support for techs should be optional, so we don't want techs to * break if they don't have a way to set a poster. */ vjs.MediaTechController.prototype.setPoster = function(){}; vjs.MediaTechController.prototype['featuresVolumeControl'] = true; // Resizing plugins using request fullscreen reloads the plugin vjs.MediaTechController.prototype['featuresFullscreenResize'] = false; vjs.MediaTechController.prototype['featuresPlaybackRate'] = false; // Optional events that we can manually mimic with timers // currently not triggered by video-js-swf vjs.MediaTechController.prototype['featuresProgressEvents'] = false; vjs.MediaTechController.prototype['featuresTimeupdateEvents'] = false; vjs.MediaTechController.prototype['featuresNativeTextTracks'] = false; /** * A functional mixin for techs that want to use the Source Handler pattern. * * ##### EXAMPLE: * * videojs.MediaTechController.withSourceHandlers.call(MyTech); * */ vjs.MediaTechController.withSourceHandlers = function(Tech){ /** * Register a source handler * Source handlers are scripts for handling specific formats. * The source handler pattern is used for adaptive formats (HLS, DASH) that * manually load video data and feed it into a Source Buffer (Media Source Extensions) * @param {Function} handler The source handler * @param {Boolean} first Register it before any existing handlers */ Tech['registerSourceHandler'] = function(handler, index){ var handlers = Tech.sourceHandlers; if (!handlers) { handlers = Tech.sourceHandlers = []; } if (index === undefined) { // add to the end of the list index = handlers.length; } handlers.splice(index, 0, handler); }; /** * Return the first source handler that supports the source * TODO: Answer question: should 'probably' be prioritized over 'maybe' * @param {Object} source The source object * @returns {Object} The first source handler that supports the source * @returns {null} Null if no source handler is found */ Tech.selectSourceHandler = function(source){ var handlers = Tech.sourceHandlers || [], can; for (var i = 0; i < handlers.length; i++) { can = handlers[i]['canHandleSource'](source); if (can) { return handlers[i]; } } return null; }; /** * Check if the tech can support the given source * @param {Object} srcObj The source object * @return {String} 'probably', 'maybe', or '' (empty string) */ Tech.canPlaySource = function(srcObj){ var sh = Tech.selectSourceHandler(srcObj); if (sh) { return sh['canHandleSource'](srcObj); } return ''; }; /** * Create a function for setting the source using a source object * and source handlers. * Should never be called unless a source handler was found. * @param {Object} source A source object with src and type keys * @return {vjs.MediaTechController} self */ Tech.prototype.setSource = function(source){ var sh = Tech.selectSourceHandler(source); if (!sh) { // Fall back to a native source hander when unsupported sources are // deliberately set if (Tech['nativeSourceHandler']) { sh = Tech['nativeSourceHandler']; } else { vjs.log.error('No source hander found for the current source.'); } } // Dispose any existing source handler this.disposeSourceHandler(); this.off('dispose', this.disposeSourceHandler); this.currentSource_ = source; this.sourceHandler_ = sh['handleSource'](source, this); this.on('dispose', this.disposeSourceHandler); return this; }; /** * Clean up any existing source handler */ Tech.prototype.disposeSourceHandler = function(){ if (this.sourceHandler_ && this.sourceHandler_['dispose']) { this.sourceHandler_['dispose'](); } }; }; vjs.media = {}; })(); /** * @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API */ /** * HTML5 Media Controller - Wrapper for HTML5 Media API * @param {vjs.Player|Object} player * @param {Object=} options * @param {Function=} ready * @constructor */ vjs.Html5 = vjs.MediaTechController.extend({ /** @constructor */ init: function(player, options, ready){ var nodes, nodesLength, i, node, nodeName, removeNodes; if (options['nativeCaptions'] === false || options['nativeTextTracks'] === false) { this['featuresNativeTextTracks'] = false; } vjs.MediaTechController.call(this, player, options, ready); this.setupTriggers(); var source = options['source']; // Set the source if one is provided // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source // anyway so the error gets fired. if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) { this.setSource(source); } if (this.el_.hasChildNodes()) { nodes = this.el_.childNodes; nodesLength = nodes.length; removeNodes = []; while (nodesLength--) { node = nodes[nodesLength]; nodeName = node.nodeName.toLowerCase(); if (nodeName === 'track') { if (!this['featuresNativeTextTracks']) { // Empty video tag tracks so the built-in player doesn't use them also. // This may not be fast enough to stop HTML5 browsers from reading the tags // so we'll need to turn off any default tracks if we're manually doing // captions and subtitles. videoElement.textTracks removeNodes.push(node); } else { this.remoteTextTracks().addTrack_(node['track']); } } } for (i=0; i