6080 lines
183 KiB
JavaScript
6080 lines
183 KiB
JavaScript
/**
|
|
* @fileoverview Main function src.
|
|
*/
|
|
|
|
// HTML5 Shiv. Must be in <head> to support older browsers.
|
|
document.createElement('video');document.createElement('audio');
|
|
|
|
/**
|
|
* Doubles as the main function for users to create a player instance and also
|
|
* the main library object.
|
|
*
|
|
* @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
|
|
*/
|
|
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]) {
|
|
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 = vjs;
|
|
window.videojs = window.vjs = vjs;
|
|
|
|
// CDN Version. Used to target right flash swf.
|
|
vjs.CDN_VERSION = 'GENERATED_CDN_VSN';
|
|
vjs.ACCESS_PROTOCOL = ('https:' == document.location.protocol ? 'https://' : 'http://');
|
|
|
|
/**
|
|
* 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': { 'swf': vjs.ACCESS_PROTOCOL + 'vjs.zencdn.net/c/video-js.swf' },
|
|
|
|
// 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!
|
|
|
|
// Included control sets
|
|
'children': {
|
|
'mediaLoader': {},
|
|
'posterImage': {},
|
|
'textTrackDisplay': {},
|
|
'loadingSpinner': {},
|
|
'bigPlayButton': {},
|
|
'controlBar': {}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Global player list
|
|
* @type {Object}
|
|
*/
|
|
vjs.players = {};
|
|
|
|
|
|
// Set CDN Version of swf
|
|
if (vjs.CDN_VERSION != 'GENERATED_CDN_VSN') {
|
|
videojs.options['flash']['swf'] = vjs.ACCESS_PROTOCOL + 'vjs.zencdn.net/'+vjs.CDN_VERSION+'/video-js.swf';
|
|
}
|
|
/**
|
|
* Core Object/Class for objects that use inheritance + contstructors
|
|
* @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 flattend 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
|
|
* @param {Object} props Functions and properties to be applied to the
|
|
* new object's prototype
|
|
* @return {vjs.CoreObject} Returns 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 constuctor because the `this` in `this.init`
|
|
// would still refer to the Child and cause an inifinite loop.
|
|
// We would instead have to do
|
|
// `ParentObject.prototype.init.apply(this, argumnents);`
|
|
// 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 instace of this Object class
|
|
* @return {vjs.CoreObject} Returns 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} type Type of event to bind to.
|
|
* @param {Function} fn Event listener.
|
|
*/
|
|
vjs.on = function(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 (document.addEventListener) {
|
|
elem.addEventListener(type, data.dispatcher, false);
|
|
} else if (document.attachEvent) {
|
|
elem.attachEvent('on' + type, data.dispatcher);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Removes event listeners from an element
|
|
* @param {Element|Object} elem Object to remove listeners from
|
|
* @param {String=} type Type of listener to remove. Don't include to remove all events from element.
|
|
* @param {Function} fn Specific listener to remove. Don't incldue to remove listeners for an event type.
|
|
*/
|
|
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; }
|
|
|
|
// 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
|
|
*/
|
|
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 (document.removeEventListener) {
|
|
elem.removeEventListener(type, data.dispatcher, false);
|
|
} else if (document.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}
|
|
*/
|
|
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) {
|
|
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.isDefaultPrevented = returnFalse;
|
|
|
|
// 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 {String} event Type of event to trigger
|
|
*/
|
|
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, recursively calls this function to bubble the event up the DOM.
|
|
if (parent && !event.isPropagationStopped()) {
|
|
vjs.trigger(parent, event);
|
|
|
|
// If at the top of the DOM, triggers the default action unless disabled.
|
|
} else if (!parent && !event.isDefaultPrevented()) {
|
|
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.isDefaultPrevented();
|
|
/* 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 attion 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 {[type]} type [description]
|
|
* @param {Function} fn [description]
|
|
* @return {[type]}
|
|
*/
|
|
vjs.one = function(elem, type, fn) {
|
|
vjs.on(elem, type, function(){
|
|
vjs.off(elem, type, arguments.callee);
|
|
fn.apply(this, arguments);
|
|
});
|
|
};
|
|
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}
|
|
*/
|
|
vjs.createEl = function(tagName, properties){
|
|
var el = document.createElement(tagName || 'div');
|
|
|
|
for (var propName in properties){
|
|
if (hasOwnProp.call(properties, propName)) {
|
|
//el[propName] = properties[propName];
|
|
// 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, properties[propName]);
|
|
} else {
|
|
el[propName] = properties[propName];
|
|
}
|
|
}
|
|
}
|
|
return el;
|
|
};
|
|
|
|
/**
|
|
* Uppercase the first letter of a string
|
|
* @param {String} string String to be uppercased
|
|
* @return {String}
|
|
*/
|
|
vjs.capitalize = function(string){
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
};
|
|
|
|
/**
|
|
* Object functions container
|
|
* @type {Object}
|
|
*/
|
|
vjs.obj = {};
|
|
|
|
/**
|
|
* Object.create shim for prototypal inheritance.
|
|
* https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
|
|
* @param {Object} obj Object to use as prototype
|
|
*/
|
|
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 {*}
|
|
*/
|
|
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}
|
|
*/
|
|
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.
|
|
*/
|
|
vjs.obj.deepMerge = function(obj1, obj2){
|
|
var key, val1, val2, objDef;
|
|
objDef = '[object Object]';
|
|
|
|
// Make a copy of obj1 so we're not ovewriting 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
|
|
*/
|
|
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
|
|
*/
|
|
vjs.obj.isPlain = function(obj){
|
|
return !!obj
|
|
&& typeof obj === 'object'
|
|
&& obj.toString() === '[object Object]'
|
|
&& obj.constructor === Object;
|
|
};
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
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 listneres are stored here.
|
|
* (also from jsninja.com, slightly modified and updated for closure compiler)
|
|
* @type {Object}
|
|
*/
|
|
vjs.cache = {};
|
|
|
|
/**
|
|
* Unique ID for an element or function
|
|
* @type {Number}
|
|
*/
|
|
vjs.guid = 1;
|
|
|
|
/**
|
|
* Unique attribute name to store an element's guid in
|
|
* @type {String}
|
|
* @constant
|
|
*/
|
|
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}
|
|
*/
|
|
vjs.getData = function(el){
|
|
var id = el[vjs.expando];
|
|
if (!id) {
|
|
id = el[vjs.expando] = vjs.guid++;
|
|
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}
|
|
*/
|
|
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
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
};
|
|
|
|
vjs.isEmpty = function(obj) {
|
|
for (var prop in obj) {
|
|
// Inlude null properties as empty.
|
|
if (obj[prop] !== null) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Add a CSS class name to an element
|
|
* @param {Element} element Element to add class name to
|
|
* @param {String} classToAdd Classname to add
|
|
*/
|
|
vjs.addClass = function(element, classToAdd){
|
|
if ((' '+element.className+' ').indexOf(' '+classToAdd+' ') == -1) {
|
|
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
|
|
*/
|
|
vjs.removeClass = function(element, classToRemove){
|
|
if (element.className.indexOf(classToRemove) == -1) { return; }
|
|
var classNames = element.className.split(' ');
|
|
// IE8 Does not support array.indexOf so using a for loop
|
|
for (var i = classNames.length - 1; i >= 0; i--) {
|
|
if (classNames[i] === classToRemove) {
|
|
classNames.splice(i,1);
|
|
}
|
|
}
|
|
// classNames.splice(classNames.indexOf(classToRemove),1);
|
|
element.className = classNames.join(' ');
|
|
};
|
|
|
|
/**
|
|
* Element for testing browser HTML5 video capabilities
|
|
* @type {Element}
|
|
* @constant
|
|
*/
|
|
vjs.TEST_VID = vjs.createEl('video');
|
|
|
|
/**
|
|
* Useragent for browser testing.
|
|
* @type {String}
|
|
* @constant
|
|
*/
|
|
vjs.USER_AGENT = navigator.userAgent;
|
|
|
|
/**
|
|
* Device is an iPhone
|
|
* @type {Boolean}
|
|
* @constant
|
|
*/
|
|
vjs.IS_IPHONE = !!vjs.USER_AGENT.match(/iPhone/i);
|
|
vjs.IS_IPAD = !!vjs.USER_AGENT.match(/iPad/i);
|
|
vjs.IS_IPOD = !!vjs.USER_AGENT.match(/iPod/i);
|
|
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 = !!vjs.USER_AGENT.match(/Android.*AppleWebKit/i);
|
|
vjs.ANDROID_VERSION = (function() {
|
|
var match = vjs.USER_AGENT.match(/Android (\d+)\./i);
|
|
if (match && match[1]) {
|
|
return match[1];
|
|
}
|
|
return null;
|
|
})();
|
|
|
|
vjs.IS_FIREFOX = function(){ return !!vjs.USER_AGENT.match('Firefox'); };
|
|
|
|
|
|
/**
|
|
* Get an element's attribute values, as defined on the HTML tag
|
|
* Attributs 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}
|
|
*/
|
|
vjs.getAttributeValues = function(tag){
|
|
var 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.
|
|
// Bookending with commas to allow for an easy string search.
|
|
var knownBooleans = ','+'autoplay,controls,loop,muted,default'+',';
|
|
|
|
if (tag && tag.attributes && tag.attributes.length > 0) {
|
|
var attrs = tag.attributes;
|
|
var attrName, attrVal;
|
|
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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}
|
|
*/
|
|
vjs.support = {};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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
|
|
*/
|
|
vjs.formatTime = function(seconds, guide) {
|
|
guide = guide || seconds; // Default to using seconds as guide
|
|
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);
|
|
|
|
// 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
|
|
*/
|
|
vjs.trim = function(string){
|
|
return string.toString().replace(/^\s+/, '').replace(/\s+$/, '');
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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
|
|
*/
|
|
vjs.createTimeRange = function(start, end){
|
|
return {
|
|
length: 1,
|
|
start: function() { return start; },
|
|
end: function() { return end; }
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Simple http request for retrieving external files (e.g. text tracks)
|
|
* @param {String} url URL of resource
|
|
* @param {Function=} onSuccess Success callback
|
|
* @param {Function=} onError Error callback
|
|
*/
|
|
vjs.get = function(url, onSuccess, onError){
|
|
var local = (url.indexOf('file:') === 0 || (window.location.href.indexOf('file:') === 0 && url.indexOf('http') === -1));
|
|
|
|
if (typeof XMLHttpRequest === 'undefined') {
|
|
window.XMLHttpRequest = 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.');
|
|
};
|
|
}
|
|
|
|
var request = new XMLHttpRequest();
|
|
|
|
try {
|
|
request.open('GET', url);
|
|
} catch(e) {
|
|
onError(e);
|
|
}
|
|
|
|
request.onreadystatechange = function() {
|
|
if (request.readyState === 4) {
|
|
if (request.status === 200 || local && request.status === 0) {
|
|
onSuccess(request.responseText);
|
|
} else {
|
|
if (onError) {
|
|
onError();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
request.send();
|
|
} catch(e) {
|
|
if (onError) {
|
|
onError(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Local Storage
|
|
================================================================================ */
|
|
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 abosolute 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
|
|
*/
|
|
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: '<a href="'+url+'">x</a>'
|
|
}).firstChild.href;
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
// usage: log('inside coolFunc',this,arguments);
|
|
// http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
|
|
vjs.log = function(){
|
|
vjs.log.history = vjs.log.history || []; // store logs to an array for reference
|
|
vjs.log.history.push(arguments);
|
|
if(window.console){
|
|
window.console.log(Array.prototype.slice.call(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;
|
|
|
|
return {
|
|
left: left,
|
|
top: top
|
|
};
|
|
};
|
|
/**
|
|
* @fileoverview Player Component - Base class for all UI objects
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Base UI Component class
|
|
* @param {Object} player Main Player
|
|
* @param {Object=} options
|
|
* @constructor
|
|
*/
|
|
vjs.Component = vjs.CoreObject.extend({
|
|
/** @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, element, or create using player ID and unique ID
|
|
this.id_ = options['id'] || ((options['el'] && options['el']['id']) ? options['el']['id'] : player.id() + '_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
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Dispose of the component and all child components.
|
|
*/
|
|
vjs.Component.prototype.dispose = function(){
|
|
// 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_;
|
|
|
|
/**
|
|
* Return the component's player.
|
|
* @return {vjs.Player}
|
|
*/
|
|
vjs.Component.prototype.player = function(){
|
|
return this.player_;
|
|
};
|
|
|
|
/**
|
|
* Component 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 whose values will be overwritten
|
|
* @return {Object} NEW merged object. Does not return obj1.
|
|
*/
|
|
vjs.Component.prototype.options = function(obj){
|
|
if (obj === undefined) return this.options_;
|
|
|
|
return this.options_ = vjs.obj.deepMerge(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);
|
|
};
|
|
|
|
/**
|
|
* Return the component's DOM element.
|
|
* @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_;
|
|
|
|
/**
|
|
* Return the component's 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_;
|
|
|
|
/**
|
|
* Return the component's ID.
|
|
* @return {String}
|
|
*/
|
|
vjs.Component.prototype.name = function(){
|
|
return this.name_;
|
|
};
|
|
|
|
/**
|
|
* Array of child components
|
|
* @type {Array}
|
|
* @private
|
|
*/
|
|
vjs.Component.prototype.children_;
|
|
|
|
/**
|
|
* Returns array of all child components.
|
|
* @return {Array}
|
|
*/
|
|
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 {Array}
|
|
*/
|
|
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 ID.
|
|
* @return {Array}
|
|
*/
|
|
vjs.Component.prototype.getChild = function(name){
|
|
return this.childNameIndex_[name];
|
|
};
|
|
|
|
/**
|
|
* Adds a child component inside this component.
|
|
* @param {String|vjs.Component} child The class name or instance of a child to add.
|
|
* @param {Object=} options Optional options, including options to be passed to
|
|
* children of the child.
|
|
* @return {vjs.Component} The child component, because it might be created in this process.
|
|
* @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, componentId;
|
|
|
|
// If 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 || {};
|
|
|
|
// Assume name of set is a lowercased name of the UI Class (PlayButton, etc.)
|
|
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;
|
|
};
|
|
|
|
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());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Initialize default child components from options
|
|
*/
|
|
vjs.Component.prototype.initChildren = function(){
|
|
var options = this.options_;
|
|
|
|
if (options && options['children']) {
|
|
var self = this;
|
|
|
|
// Loop through components and add them to the player
|
|
vjs.obj.each(options['children'], function(name, opts){
|
|
// Allow for disabling default components
|
|
// e.g. vjs.options['children']['posterImage'] = false
|
|
if (opts === false) return;
|
|
|
|
// Allow waiting to add components until a specific event is called
|
|
var tempAdd = function(){
|
|
// Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy.
|
|
self[name] = self.addChild(name, opts);
|
|
};
|
|
|
|
if (opts['loadEvent']) {
|
|
// this.one(opts.loadEvent, tempAdd)
|
|
} else {
|
|
tempAdd();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
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. Context will be the component.
|
|
* @param {String} type Event type e.g. 'click'
|
|
* @param {Function} fn Event listener
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.on = function(type, fn){
|
|
vjs.on(this.el_, type, vjs.bind(this, fn));
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove an event listener from the component's element
|
|
* @param {String=} type Optional event type. Without type it will remove all listeners.
|
|
* @param {Function=} fn Optional event listener. Without fn it will remove all listeners for a type.
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.off = function(type, fn){
|
|
vjs.off(this.el_, type, fn);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Add an event listener to be triggered only once and then removed
|
|
* @param {String} type Event type
|
|
* @param {Function} fn Event listener
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.one = function(type, fn) {
|
|
vjs.one(this.el_, type, vjs.bind(this, fn));
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Trigger an event on an element
|
|
* @param {String} type Event type to trigger
|
|
* @param {Event|Object} event Event object to be passed to the listener
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.trigger = function(type, event){
|
|
vjs.trigger(this.el_, type, event);
|
|
return this;
|
|
};
|
|
|
|
/* Ready
|
|
================================================================================ */
|
|
/**
|
|
* Is the component loaded.
|
|
* @type {Boolean}
|
|
* @private
|
|
*/
|
|
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 asynchrnously 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 happend
|
|
* 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_;
|
|
|
|
if (readyQueue && readyQueue.length > 0) {
|
|
|
|
for (var i = 0, j = readyQueue.length; i < j; i++) {
|
|
readyQueue[i].call(this);
|
|
}
|
|
|
|
// Reset Ready Queue
|
|
this.readyQueue_ = [];
|
|
|
|
// Allow for using event listeners also, in case you want to do something everytime a source is ready.
|
|
this.trigger('ready');
|
|
}
|
|
};
|
|
|
|
/* Display
|
|
============================================================================= */
|
|
|
|
/**
|
|
* 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.el_.style.display = 'block';
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Hide the component element if hidden
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.hide = function(){
|
|
this.el_.style.display = 'none';
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Fade a component in using CSS
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.fadeIn = function(){
|
|
this.removeClass('vjs-fade-out');
|
|
this.addClass('vjs-fade-in');
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Fade a component out using CSS
|
|
* @return {vjs.Component}
|
|
*/
|
|
vjs.Component.prototype.fadeOut = function(){
|
|
this.removeClass('vjs-fade-in');
|
|
this.addClass('vjs-fade-out');
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Lock an item in its visible state. To be used with fadeIn/fadeOut.
|
|
* @return {vjs.Component}
|
|
*/
|
|
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}
|
|
*/
|
|
vjs.Component.prototype.unlockShowing = function(){
|
|
this.removeClass('vjs-lock-showing');
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Disable component by making it unshowable
|
|
*/
|
|
vjs.Component.prototype.disable = function(){
|
|
this.hide();
|
|
this.show = function(){};
|
|
this.fadeIn = function(){};
|
|
};
|
|
|
|
// TODO: Get enable working
|
|
// vjs.Component.prototype.enable = function(){
|
|
// this.fadeIn = vjs.Component.prototype.fadeIn;
|
|
// this.show = vjs.Component.prototype.show;
|
|
// };
|
|
|
|
/**
|
|
* If a value is provided it will change the width of the player to that value
|
|
* otherwise the width is returned
|
|
* http://dev.w3.org/html5/spec/dimension-attributes.html#attr-dim-height
|
|
* Video tag width/height only work in pixels. No percents.
|
|
* But allowing limited percents use. e.g. width() will return number+%, not computed width
|
|
* @param {Number|String=} num Optional width number
|
|
* @param {[type]} skipListeners Skip the 'resize' event trigger
|
|
* @return {vjs.Component|Number|String} Returns 'this' if dimension was set.
|
|
* Otherwise it returns the dimension.
|
|
*/
|
|
vjs.Component.prototype.width = function(num, skipListeners){
|
|
return this.dimension('width', num, skipListeners);
|
|
};
|
|
|
|
/**
|
|
* Get or set the height of the player
|
|
* @param {Number|String=} num Optional new player height
|
|
* @param {Boolean=} skipListeners Optional skip resize event trigger
|
|
* @return {vjs.Component|Number|String} The player, or the dimension
|
|
*/
|
|
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 player.
|
|
*/
|
|
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.
|
|
* All for an integer, integer + 'px' or integer + '%';
|
|
* Known issue: hidden elements. Hidden elements officially have a width of 0.
|
|
* So 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|Number|String} Return the player if setting a dimension.
|
|
* Otherwise it returns the dimension.
|
|
*/
|
|
vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){
|
|
if (num !== undefined) {
|
|
|
|
// 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;
|
|
// }
|
|
}
|
|
};
|
|
/* Button - Base class for all buttons
|
|
================================================================================ */
|
|
/**
|
|
* Base class for all buttons
|
|
* @param {vjs.Player|Object} player
|
|
* @param {Object=} options
|
|
* @constructor
|
|
*/
|
|
vjs.Button = vjs.Component.extend({
|
|
/** @constructor */
|
|
init: function(player, options){
|
|
vjs.Component.call(this, player, options);
|
|
|
|
var touchstart = false;
|
|
this.on('touchstart', function() {
|
|
touchstart = true;
|
|
});
|
|
this.on('touchmove', function() {
|
|
touchstart = false;
|
|
});
|
|
var self = this;
|
|
this.on('touchend', function(event) {
|
|
if (touchstart) {
|
|
self.onClick(event);
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
});
|
|
|
|
this.on('click', this.onClick);
|
|
this.on('focus', this.onFocus);
|
|
this.on('blur', this.onBlur);
|
|
}
|
|
});
|
|
|
|
vjs.Button.prototype.createEl = function(type, props){
|
|
// Add standard Aria and Tabindex info
|
|
props = vjs.obj.merge({
|
|
className: this.buildCSSClass(),
|
|
innerHTML: '<div class="vjs-control-content"><span class="vjs-control-text">' + (this.buttonText || 'Need Text') + '</span></div>',
|
|
role: 'button',
|
|
'aria-live': 'polite', // let the screen reader user know that the text of the button may change
|
|
tabIndex: 0
|
|
}, props);
|
|
|
|
return vjs.Component.prototype.createEl.call(this, type, props);
|
|
};
|
|
|
|
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, 'keyup', 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, 'keyup', vjs.bind(this, this.onKeyPress));
|
|
};
|
|
/* Slider
|
|
================================================================================ */
|
|
/**
|
|
* Parent for seek bar and volume slider
|
|
* @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']);
|
|
|
|
player.on(this.playerEvent, vjs.bind(this, this.update));
|
|
|
|
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.player_.on('controlsvisible', vjs.bind(this, this.update));
|
|
|
|
// This is actually to fix the volume handle position. http://twitter.com/#!/gerritvanaaken/status/159046254519787520
|
|
// this.player_.one('timeupdate', vjs.bind(this, this.update));
|
|
|
|
player.ready(vjs.bind(this, this.update));
|
|
|
|
this.boundEvents = {};
|
|
}
|
|
});
|
|
|
|
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.boundEvents.move = vjs.bind(this, this.onMouseMove);
|
|
this.boundEvents.end = vjs.bind(this, this.onMouseUp);
|
|
|
|
vjs.on(document, 'mousemove', this.boundEvents.move);
|
|
vjs.on(document, 'mouseup', this.boundEvents.end);
|
|
vjs.on(document, 'touchmove', this.boundEvents.move);
|
|
vjs.on(document, 'touchend', this.boundEvents.end);
|
|
|
|
this.onMouseMove(event);
|
|
};
|
|
|
|
vjs.Slider.prototype.onMouseUp = function() {
|
|
vjs.unblockTextSelection();
|
|
vjs.off(document, 'mousemove', this.boundEvents.move, false);
|
|
vjs.off(document, 'mouseup', this.boundEvents.end, false);
|
|
vjs.off(document, 'touchmove', this.boundEvents.move, false);
|
|
vjs.off(document, 'touchend', this.boundEvents.end, false);
|
|
|
|
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 (isNaN(progress)) { 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
|
|
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(){
|
|
vjs.on(document, 'keyup', vjs.bind(this, this.onKeyPress));
|
|
};
|
|
|
|
vjs.Slider.prototype.onKeyPress = function(event){
|
|
if (event.which == 37) { // Left Arrow
|
|
event.preventDefault();
|
|
this.stepBack();
|
|
} else if (event.which == 39) { // Right Arrow
|
|
event.preventDefault();
|
|
this.stepForward();
|
|
}
|
|
};
|
|
|
|
vjs.Slider.prototype.onBlur = function(){
|
|
vjs.off(document, 'keyup', vjs.bind(this, 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}
|
|
*/
|
|
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: '<span class="vjs-control-text">'+this.defaultValue+'</span>'
|
|
}, props);
|
|
|
|
return vjs.Component.prototype.createEl.call(this, 'div', props);
|
|
};
|
|
/* Menu
|
|
================================================================================ */
|
|
/**
|
|
* The base for text track and settings menu buttons.
|
|
* @param {vjs.Player|Object} player
|
|
* @param {Object=} options
|
|
* @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;
|
|
};
|
|
|
|
/**
|
|
* Menu item
|
|
* @param {vjs.Player|Object} player
|
|
* @param {Object=} options
|
|
* @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.options_['label']
|
|
}, props));
|
|
};
|
|
|
|
/** @inheritDoc */
|
|
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.menu = this.createMenu();
|
|
|
|
// Add list to element
|
|
this.addChild(this.menu);
|
|
|
|
// Automatically hide empty menu buttons
|
|
if (this.items && this.items.length === 0) {
|
|
this.hide();
|
|
}
|
|
|
|
this.on('keyup', this.onKeyPress);
|
|
this.el_.setAttribute('aria-haspopup', true);
|
|
this.el_.setAttribute('role', 'button');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Track the state of the menu button
|
|
* @type {Boolean}
|
|
*/
|
|
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.el().appendChild(vjs.createEl('li', {
|
|
className: 'vjs-menu-title',
|
|
innerHTML: vjs.capitalize(this.kind_),
|
|
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){
|
|
event.preventDefault();
|
|
|
|
// Check for space bar (32) or enter (13) keys
|
|
if (event.which == 32 || event.which == 13) {
|
|
if (this.buttonPressed_){
|
|
this.unpressButton();
|
|
} else {
|
|
this.pressButton();
|
|
}
|
|
// Check for escape (27) key
|
|
} else if (event.which == 27){
|
|
if (this.buttonPressed_){
|
|
this.unpressButton();
|
|
}
|
|
}
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* Main player class. A player instance is returned by _V_(id);
|
|
* @param {Element} tag The original video tag used for configuring options
|
|
* @param {Object=} options Player options
|
|
* @param {Function=} ready Ready callback function
|
|
* @constructor
|
|
*/
|
|
vjs.Player = vjs.Component.extend({
|
|
/** @constructor */
|
|
init: function(tag, options, ready){
|
|
this.tag = tag; // Store the original tag used to set options
|
|
|
|
// 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);
|
|
|
|
// Cache for video property values.
|
|
this.cache_ = {};
|
|
|
|
// Set poster
|
|
this.poster_ = options['poster'];
|
|
// Set controls
|
|
this.controls_ = options['controls'];
|
|
// Use native controls for iOS and Android by default
|
|
// until controls are more stable on those devices.
|
|
if (options['customControlsOnMobile'] !== true && (vjs.IS_IOS || vjs.IS_ANDROID)) {
|
|
tag.controls = options['controls'];
|
|
this.controls_ = false;
|
|
} else {
|
|
// Original tag settings stored in options
|
|
// now remove immediately so native controls don't flash.
|
|
tag.controls = false;
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Firstplay event implimentation. Not sold on the event yet.
|
|
// Could probably just check currentTime==0?
|
|
this.one('play', function(e){
|
|
var fpEvent = { type: 'firstplay', target: this.el_ };
|
|
// Using vjs.trigger so we can check if default was prevented
|
|
var keepGoing = vjs.trigger(this.el_, fpEvent);
|
|
|
|
if (!keepGoing) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
});
|
|
|
|
this.on('ended', this.onEnded);
|
|
this.on('play', this.onPlay);
|
|
this.on('firstplay', this.onFirstPlay);
|
|
this.on('pause', this.onPause);
|
|
this.on('progress', this.onProgress);
|
|
this.on('durationchange', this.onDurationChange);
|
|
this.on('error', this.onError);
|
|
this.on('fullscreenchange', this.onFullscreenChange);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
|
|
vjs.Player.prototype.dispose = function(){
|
|
// this.isReady_ = false;
|
|
|
|
// 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; }
|
|
|
|
// Ensure that tracking progress and time progress will stop and plater deleted
|
|
this.stopTrackingProgress();
|
|
this.stopTrackingCurrentTime();
|
|
|
|
if (this.tech) { this.tech.dispose(); }
|
|
|
|
// Component dispose
|
|
vjs.Component.prototype.dispose.call(this);
|
|
};
|
|
|
|
vjs.Player.prototype.getTagSettings = function(tag){
|
|
var options = {
|
|
'sources': [],
|
|
'tracks': []
|
|
};
|
|
|
|
vjs.obj.merge(options, vjs.getAttributeValues(tag));
|
|
|
|
// Get tag children settings
|
|
if (tag.hasChildNodes()) {
|
|
var child, childName,
|
|
children = tag.childNodes,
|
|
i = 0,
|
|
j = children.length;
|
|
|
|
for (; i < j; i++) {
|
|
child = children[i];
|
|
// Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
|
|
childName = child.nodeName.toLowerCase();
|
|
|
|
if (childName === 'source') {
|
|
options['sources'].push(vjs.getAttributeValues(child));
|
|
|
|
} else if (childName === 'track') {
|
|
options['tracks'].push(vjs.getAttributeValues(child));
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return options;
|
|
};
|
|
|
|
vjs.Player.prototype.createEl = function(){
|
|
var el = this.el_ = vjs.Component.prototype.createEl.call(this, 'div');
|
|
var tag = this.tag;
|
|
|
|
// Remove width/height attrs from tag so CSS can make it 100% width/height
|
|
tag.removeAttribute('width');
|
|
tag.removeAttribute('height');
|
|
// Empty video tag sources and 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
|
|
if (tag.hasChildNodes()) {
|
|
var nrOfChildNodes = tag.childNodes.length;
|
|
for (var i=0,j=tag.childNodes;i<nrOfChildNodes;i++) {
|
|
if (j[0].nodeName.toLowerCase() == 'source' || j[0].nodeName.toLowerCase() == 'track') {
|
|
tag.removeChild(j[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make sure tag ID exists
|
|
tag.id = tag.id || 'vjs_video_' + vjs.guid++;
|
|
|
|
// Give video tag ID and class to player div
|
|
// ID will now reference player box, not the video tag
|
|
el.id = tag.id;
|
|
el.className = tag.className;
|
|
|
|
// Update tag id/class for use as HTML5 playback tech
|
|
// Might think we should do this after embedding in container so .vjs-tech class
|
|
// doesn't flash 100% width/height, but class only applies with .video-js parent
|
|
tag.id += '_html5_api';
|
|
tag.className = 'vjs-tech';
|
|
|
|
// Make player findable on elements
|
|
tag['player'] = el['player'] = this;
|
|
// Default state of video is paused
|
|
this.addClass('vjs-paused');
|
|
|
|
// Make box use width/height of tag, or rely on default implementation
|
|
// Enforce with CSS since width/height attrs don't work on divs
|
|
this.width(this.options_['width'], true); // (true) Skip resize listener on load
|
|
this.height(this.options_['height'], true);
|
|
|
|
// Wrap video tag in div (el/box) container
|
|
if (tag.parentNode) {
|
|
tag.parentNode.insertBefore(el, tag);
|
|
}
|
|
vjs.insertFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup.
|
|
|
|
return el;
|
|
};
|
|
|
|
// /* Media Technology (tech)
|
|
// ================================================================================ */
|
|
// Load/Create an instance of playback technlogy including element and API methods
|
|
// And append playback element in player div.
|
|
vjs.Player.prototype.loadTech = function(techName, source){
|
|
|
|
// Pause and remove current playback technology
|
|
if (this.tech) {
|
|
this.unloadTech();
|
|
|
|
// If the first time loading, HTML5 tag will exist but won't be initialized
|
|
// So we need to remove it if we're not loading HTML5
|
|
} else if (techName !== 'Html5' && this.tag) {
|
|
this.el_.removeChild(this.tag);
|
|
this.tag.player = null;
|
|
this.tag = null;
|
|
}
|
|
|
|
this.techName = techName;
|
|
|
|
// Turn off API access because we're loading a new tech that might load asynchronously
|
|
this.isReady_ = false;
|
|
|
|
var techReady = function(){
|
|
this.player_.triggerReady();
|
|
|
|
// Manually track progress in cases where the browser/flash player doesn't report it.
|
|
if (!this.features.progressEvents) {
|
|
this.player_.manualProgressOn();
|
|
}
|
|
|
|
// Manually track timeudpates in cases where the browser/flash player doesn't report it.
|
|
if (!this.features.timeupdateEvents) {
|
|
this.player_.manualTimeUpdatesOn();
|
|
}
|
|
};
|
|
|
|
// Grab tech-specific options from player options and add source and parent element to use.
|
|
var techOptions = vjs.obj.merge({ 'source': source, 'parentEl': this.el_ }, this.options_[techName.toLowerCase()]);
|
|
|
|
if (source) {
|
|
if (source.src == this.cache_.src && this.cache_.currentTime > 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();
|
|
|
|
// Turn off any manual progress or timeupdate tracking
|
|
if (this.manualProgress) { this.manualProgressOff(); }
|
|
|
|
if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); }
|
|
|
|
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')
|
|
// },
|
|
|
|
/* 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.Player.prototype.manualProgressOn = function(){
|
|
this.manualProgress = true;
|
|
|
|
// Trigger progress watching when a source begins loading
|
|
this.trackProgress();
|
|
|
|
// Watch for a native progress event call on the tech element
|
|
// In HTML5, some older versions don't support the progress event
|
|
// So we're assuming they don't, and turning off manual progress if they do.
|
|
// As opposed to doing user agent detection
|
|
this.tech.one('progress', function(){
|
|
|
|
// Update known progress support for this playback technology
|
|
this.features.progressEvents = true;
|
|
|
|
// Turn off manual progress tracking
|
|
this.player_.manualProgressOff();
|
|
});
|
|
};
|
|
|
|
vjs.Player.prototype.manualProgressOff = function(){
|
|
this.manualProgress = false;
|
|
this.stopTrackingProgress();
|
|
};
|
|
|
|
vjs.Player.prototype.trackProgress = function(){
|
|
|
|
this.progressInterval = setInterval(vjs.bind(this, function(){
|
|
// Don't trigger unless buffered amount is greater than last time
|
|
// log(this.cache_.bufferEnd, this.buffered().end(0), this.duration())
|
|
/* TODO: update for multiple buffered regions */
|
|
if (this.cache_.bufferEnd < this.buffered().end(0)) {
|
|
this.trigger('progress');
|
|
} else if (this.bufferedPercent() == 1) {
|
|
this.stopTrackingProgress();
|
|
this.trigger('progress'); // Last update
|
|
}
|
|
}), 500);
|
|
};
|
|
vjs.Player.prototype.stopTrackingProgress = function(){ clearInterval(this.progressInterval); };
|
|
|
|
/* Time Tracking -------------------------------------------------------------- */
|
|
vjs.Player.prototype.manualTimeUpdatesOn = function(){
|
|
this.manualTimeUpdates = true;
|
|
|
|
this.on('play', this.trackCurrentTime);
|
|
this.on('pause', this.stopTrackingCurrentTime);
|
|
// timeupdate is also called by .currentTime whenever current time is set
|
|
|
|
// Watch for native timeupdate event
|
|
this.tech.one('timeupdate', function(){
|
|
// Update known progress support for this playback technology
|
|
this.features.timeupdateEvents = true;
|
|
// Turn off manual progress tracking
|
|
this.player_.manualTimeUpdatesOff();
|
|
});
|
|
};
|
|
|
|
vjs.Player.prototype.manualTimeUpdatesOff = function(){
|
|
this.manualTimeUpdates = false;
|
|
this.stopTrackingCurrentTime();
|
|
this.off('play', this.trackCurrentTime);
|
|
this.off('pause', this.stopTrackingCurrentTime);
|
|
};
|
|
|
|
vjs.Player.prototype.trackCurrentTime = function(){
|
|
if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); }
|
|
this.currentTimeInterval = setInterval(vjs.bind(this, function(){
|
|
this.trigger('timeupdate');
|
|
}), 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
|
|
};
|
|
|
|
// Turn off play progress tracking (when paused or dragging)
|
|
vjs.Player.prototype.stopTrackingCurrentTime = function(){ clearInterval(this.currentTimeInterval); };
|
|
|
|
// /* Player event handlers (how the player reacts to certain events)
|
|
// ================================================================================ */
|
|
vjs.Player.prototype.onEnded = function(){
|
|
if (this.options_['loop']) {
|
|
this.currentTime(0);
|
|
this.play();
|
|
}
|
|
};
|
|
|
|
vjs.Player.prototype.onPlay = function(){
|
|
vjs.removeClass(this.el_, 'vjs-paused');
|
|
vjs.addClass(this.el_, 'vjs-playing');
|
|
};
|
|
|
|
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']);
|
|
}
|
|
};
|
|
|
|
vjs.Player.prototype.onPause = function(){
|
|
vjs.removeClass(this.el_, 'vjs-playing');
|
|
vjs.addClass(this.el_, 'vjs-paused');
|
|
};
|
|
|
|
vjs.Player.prototype.onProgress = function(){
|
|
// Add custom event for when source is finished downloading.
|
|
if (this.bufferedPercent() == 1) {
|
|
this.trigger('loadedalldata');
|
|
}
|
|
};
|
|
|
|
// Update duration with durationchange event
|
|
// Allows for cacheing value instead of asking player each time.
|
|
vjs.Player.prototype.onDurationChange = function(){
|
|
this.duration(this.techGet('duration'));
|
|
};
|
|
|
|
vjs.Player.prototype.onError = function(e) {
|
|
vjs.log('Video Error', e);
|
|
};
|
|
|
|
vjs.Player.prototype.onFullscreenChange = function(e) {
|
|
if (this.isFullScreen) {
|
|
this.addClass('vjs-fullscreen');
|
|
} else {
|
|
this.removeClass('vjs-fullscreen');
|
|
}
|
|
};
|
|
|
|
// /* 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){
|
|
|
|
// Make sure there is a tech
|
|
// if (!this.tech) {
|
|
// return;
|
|
// }
|
|
|
|
if (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
|
|
* http://dev.w3.org/html5/spec/video.html#dom-media-play
|
|
* We're triggering the 'play' event here instead of relying on the
|
|
* media element to allow using event.preventDefault() to stop
|
|
* play from happening if desired. Usecase: preroll ads.
|
|
*/
|
|
vjs.Player.prototype.play = function(){
|
|
this.techCall('play');
|
|
return this;
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-pause
|
|
vjs.Player.prototype.pause = function(){
|
|
this.techCall('pause');
|
|
return this;
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-paused
|
|
// The initial state of paused should be true (in Safari it's actually false)
|
|
vjs.Player.prototype.paused = function(){
|
|
return (this.techGet('paused') === false) ? false : true;
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-currenttime
|
|
vjs.Player.prototype.currentTime = function(seconds){
|
|
if (seconds !== undefined) {
|
|
|
|
// Cache the last set value for smoother scrubbing.
|
|
this.cache_.lastSetCurrentTime = seconds;
|
|
|
|
this.techCall('setCurrentTime', seconds);
|
|
|
|
// Improve the accuracy of manual timeupdates
|
|
if (this.manualTimeUpdates) { this.trigger('timeupdate'); }
|
|
|
|
return this;
|
|
}
|
|
|
|
// Cache last currentTime and return
|
|
// Default to 0 seconds
|
|
return this.cache_.currentTime = (this.techGet('currentTime') || 0);
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-duration
|
|
// Duration should return NaN if not available. ParseFloat will turn false-ish values to NaN.
|
|
vjs.Player.prototype.duration = function(seconds){
|
|
if (seconds !== undefined) {
|
|
|
|
// Cache the last set value for optimiized scrubbing (esp. Flash)
|
|
this.cache_.duration = parseFloat(seconds);
|
|
|
|
return this;
|
|
}
|
|
|
|
return this.cache_.duration;
|
|
};
|
|
|
|
// Calculates how much time is left. Not in spec, but useful.
|
|
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.
|
|
// So far no browsers return more than one range (portion)
|
|
vjs.Player.prototype.buffered = function(){
|
|
var buffered = this.techGet('buffered'),
|
|
start = 0,
|
|
// Default end to 0 and store in values
|
|
end = this.cache_.bufferEnd = this.cache_.bufferEnd || 0;
|
|
|
|
if (buffered && buffered.length > 0 && buffered.end(0) !== end) {
|
|
end = buffered.end(0);
|
|
// Storing values allows them be overridden by setBufferedFromProgress
|
|
this.cache_.bufferEnd = end;
|
|
}
|
|
|
|
return vjs.createTimeRange(start, end);
|
|
};
|
|
|
|
// Calculates amount of buffer is full. Not in spec but useful.
|
|
vjs.Player.prototype.bufferedPercent = function(){
|
|
return (this.duration()) ? this.buffered().end(0) / this.duration() : 0;
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-volume
|
|
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;
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#attr-media-muted
|
|
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 lik iOS, so not our flash swf)
|
|
vjs.Player.prototype.supportsFullScreen = function(){ return this.techGet('supportsFullScreen') || false; };
|
|
|
|
// Turn on fullscreen (or window) mode
|
|
vjs.Player.prototype.requestFullScreen = function(){
|
|
var requestFullScreen = vjs.support.requestFullScreen;
|
|
this.isFullScreen = true;
|
|
|
|
if (requestFullScreen) {
|
|
// 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
|
|
vjs.on(document, requestFullScreen.eventName, vjs.bind(this, function(){
|
|
this.isFullScreen = document[requestFullScreen.isFullScreen];
|
|
|
|
// If cancelling fullscreen, remove event listener.
|
|
if (this.isFullScreen === false) {
|
|
vjs.off(document, requestFullScreen.eventName, arguments.callee);
|
|
}
|
|
|
|
}));
|
|
|
|
// Flash and other plugins get reloaded when you take their parent to fullscreen.
|
|
// To fix that we'll remove the tech, and reload it after the resize has finished.
|
|
if (this.tech.features.fullscreenResize === false && this.options_['flash']['iFrameMode'] !== true) {
|
|
|
|
this.pause();
|
|
this.unloadTech();
|
|
|
|
vjs.on(document, requestFullScreen.eventName, vjs.bind(this, function(){
|
|
vjs.off(document, requestFullScreen.eventName, arguments.callee);
|
|
this.loadTech(this.techName, { src: this.cache_.src });
|
|
}));
|
|
|
|
this.el_[requestFullScreen.requestFn]();
|
|
|
|
} else {
|
|
this.el_[requestFullScreen.requestFn]();
|
|
}
|
|
|
|
this.trigger('fullscreenchange');
|
|
|
|
} 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;
|
|
};
|
|
|
|
vjs.Player.prototype.cancelFullScreen = function(){
|
|
var requestFullScreen = vjs.support.requestFullScreen;
|
|
|
|
this.isFullScreen = false;
|
|
|
|
// Check for browser element fullscreen support
|
|
if (requestFullScreen) {
|
|
|
|
// Flash and other plugins get reloaded when you take their parent to fullscreen.
|
|
// To fix that we'll remove the tech, and reload it after the resize has finished.
|
|
if (this.tech.features.fullscreenResize === false && this.options_['flash']['iFrameMode'] !== true) {
|
|
|
|
this.pause();
|
|
this.unloadTech();
|
|
|
|
vjs.on(document, requestFullScreen.eventName, vjs.bind(this, function(){
|
|
vjs.off(document, requestFullScreen.eventName, arguments.callee);
|
|
this.loadTech(this.techName, { src: this.cache_.src });
|
|
}));
|
|
|
|
document[requestFullScreen.cancelFn]();
|
|
} else {
|
|
document[requestFullScreen.cancelFn]();
|
|
}
|
|
|
|
this.trigger('fullscreenchange');
|
|
|
|
} else if (this.tech.supportsFullScreen()) {
|
|
this.techCall('exitFullScreen');
|
|
} else {
|
|
this.exitFullWindow();
|
|
this.trigger('fullscreenchange');
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
// 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.cancelFullScreen();
|
|
} 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<j.length;i++) {
|
|
var techName = vjs.capitalize(j[i]),
|
|
tech = window['videojs'][techName];
|
|
|
|
// Check if the browser supports this technology
|
|
if (tech.isSupported()) {
|
|
// Loop through each source object
|
|
for (var a=0,b=sources;a<b.length;a++) {
|
|
var source = b[a];
|
|
|
|
// Check if source can be played with this technology
|
|
if (tech['canPlaySource'](source)) {
|
|
return { source: source, tech: techName };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// src is a pretty powerful function
|
|
// If you pass it an array of source objects, it will find the best source to play and use that object.src
|
|
// If the new source requires a new playback technology, it will switch to that.
|
|
// If you pass it an object, it will set the source to object.src
|
|
// If you pass it anything else (url string) it will set the video source to that
|
|
vjs.Player.prototype.src = function(source){
|
|
// Case: Array of source objects to choose from and pick the best to play
|
|
if (source instanceof Array) {
|
|
|
|
var sourceTech = this.selectSource(source),
|
|
techName;
|
|
|
|
if (sourceTech) {
|
|
source = sourceTech.source;
|
|
techName = sourceTech.tech;
|
|
|
|
// If this technology is already loaded, set source
|
|
if (techName == this.techName) {
|
|
this.src(source); // Passing the source object
|
|
// Otherwise load this technology with chosen source
|
|
} else {
|
|
this.loadTech(techName, source);
|
|
}
|
|
} else {
|
|
this.el_.appendChild(vjs.createEl('p', {
|
|
innerHTML: 'Sorry, no compatible source and playback technology were found for this video. Try using another browser like <a href="http://bit.ly/ccMUEC">Chrome</a> or download the latest <a href="http://adobe.ly/mwfN1">Adobe Flash Player</a>.'
|
|
}));
|
|
}
|
|
|
|
// Case: Source object { src: '', type: '' ... }
|
|
} else if (source instanceof Object) {
|
|
|
|
if (window['videojs'][this.techName]['canPlaySource'](source)) {
|
|
this.src(source.src);
|
|
} else {
|
|
// Send through tech loop to check for a compatible technology.
|
|
this.src([source]);
|
|
}
|
|
|
|
// Case: URL String (http://myvideo...)
|
|
} else {
|
|
// Cache for getting last set source
|
|
this.cache_.src = source;
|
|
|
|
if (!this.isReady_) {
|
|
this.ready(function(){
|
|
this.src(source);
|
|
});
|
|
} else {
|
|
this.techCall('src', source);
|
|
if (this.options_['preload'] == 'auto') {
|
|
this.load();
|
|
}
|
|
if (this.options_['autoplay']) {
|
|
this.play();
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
// Begin loading the src data
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-load
|
|
vjs.Player.prototype.load = function(){
|
|
this.techCall('load');
|
|
return this;
|
|
};
|
|
|
|
// http://dev.w3.org/html5/spec/video.html#dom-media-currentsrc
|
|
vjs.Player.prototype.currentSrc = function(){
|
|
return this.techGet('currentSrc') || this.cache_.src || '';
|
|
};
|
|
|
|
// Attributes/Options
|
|
vjs.Player.prototype.preload = function(value){
|
|
if (value !== undefined) {
|
|
this.techCall('setPreload', value);
|
|
this.options_['preload'] = value;
|
|
return this;
|
|
}
|
|
return this.techGet('preload');
|
|
};
|
|
vjs.Player.prototype.autoplay = function(value){
|
|
if (value !== undefined) {
|
|
this.techCall('setAutoplay', value);
|
|
this.options_['autoplay'] = value;
|
|
return this;
|
|
}
|
|
return this.techGet('autoplay', value);
|
|
};
|
|
vjs.Player.prototype.loop = function(value){
|
|
if (value !== undefined) {
|
|
this.techCall('setLoop', value);
|
|
this.options_['loop'] = value;
|
|
return this;
|
|
}
|
|
return this.techGet('loop');
|
|
};
|
|
|
|
/**
|
|
* The url of the poster image source.
|
|
* @type {String}
|
|
* @private
|
|
*/
|
|
vjs.Player.prototype.poster_;
|
|
|
|
/**
|
|
* Get or set the poster image source url.
|
|
* @param {String} src Poster image source URL
|
|
* @return {String} Poster image source URL or null
|
|
*/
|
|
vjs.Player.prototype.poster = function(src){
|
|
if (src !== undefined) {
|
|
this.poster_ = src;
|
|
}
|
|
return this.poster_;
|
|
};
|
|
|
|
/**
|
|
* Whether or not the controls are showing
|
|
* @type {Boolean}
|
|
* @private
|
|
*/
|
|
vjs.Player.prototype.controls_;
|
|
|
|
/**
|
|
* Get or set whether or not the controls are showing.
|
|
* @param {Boolean} controls Set controls to showing or not
|
|
* @return {Boolean} Controls are showing
|
|
*/
|
|
vjs.Player.prototype.controls = function(controls){
|
|
if (controls !== undefined) {
|
|
// Don't trigger a change event unless it actually changed
|
|
if (this.controls_ !== controls) {
|
|
this.controls_ = !!controls; // force boolean
|
|
this.trigger('controlschange');
|
|
}
|
|
}
|
|
return this.controls_;
|
|
};
|
|
|
|
vjs.Player.prototype.error = function(){ return this.techGet('error'); };
|
|
vjs.Player.prototype.ended = function(){ return this.techGet('ended'); };
|
|
|
|
// Methods to add support for
|
|
// networkState: function(){ return this.techCall('networkState'); },
|
|
// readyState: function(){ return this.techCall('readyState'); },
|
|
// seeking: function(){ return this.techCall('seeking'); },
|
|
// 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'); },
|
|
// playbackRate: function(){ return this.techCall('playbackRate'); },
|
|
// 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
|
|
|
|
// RequestFullscreen API
|
|
(function(){
|
|
var prefix, requestFS, div;
|
|
|
|
div = document.createElement('div');
|
|
|
|
requestFS = {};
|
|
|
|
// Current W3C Spec
|
|
// http://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html#api
|
|
// Mozilla Draft: https://wiki.mozilla.org/Gecko:FullScreenAPI#fullscreenchange_event
|
|
if (div.cancelFullscreen !== undefined) {
|
|
requestFS.requestFn = 'requestFullscreen';
|
|
requestFS.cancelFn = 'exitFullscreen';
|
|
requestFS.eventName = 'fullscreenchange';
|
|
requestFS.isFullScreen = 'fullScreen';
|
|
|
|
// Webkit (Chrome/Safari) and Mozilla (Firefox) have working implementations
|
|
// that use prefixes and vary slightly from the new W3C spec. Specifically,
|
|
// using 'exit' instead of 'cancel', and lowercasing the 'S' in Fullscreen.
|
|
// Other browsers don't have any hints of which version they might follow yet,
|
|
// so not going to try to predict by looping through all prefixes.
|
|
} else {
|
|
|
|
if (document.mozCancelFullScreen) {
|
|
prefix = 'moz';
|
|
requestFS.isFullScreen = prefix + 'FullScreen';
|
|
} else {
|
|
prefix = 'webkit';
|
|
requestFS.isFullScreen = prefix + 'IsFullScreen';
|
|
}
|
|
|
|
if (div[prefix + 'RequestFullScreen']) {
|
|
requestFS.requestFn = prefix + 'RequestFullScreen';
|
|
requestFS.cancelFn = prefix + 'CancelFullScreen';
|
|
}
|
|
requestFS.eventName = prefix + 'fullscreenchange';
|
|
}
|
|
|
|
if (document[requestFS.cancelFn]) {
|
|
vjs.support.requestFullScreen = requestFS;
|
|
}
|
|
|
|
})();
|
|
/**
|
|
* Container of main controls
|
|
* @param {vjs.Player|Object} player
|
|
* @param {Object=} options
|
|
* @constructor
|
|
*/
|
|
vjs.ControlBar = vjs.Component.extend({
|
|
/** @constructor */
|
|
init: function(player, options){
|
|
vjs.Component.call(this, player, options);
|
|
|
|
if (!player.controls()) {
|
|
this.disable();
|
|
}
|
|
|
|
player.one('play', vjs.bind(this, function(){
|
|
var touchstart,
|
|
fadeIn = vjs.bind(this, this.fadeIn),
|
|
fadeOut = vjs.bind(this, this.fadeOut);
|
|
|
|
this.fadeIn();
|
|
|
|
if ( !('ontouchstart' in window) ) {
|
|
this.player_.on('mouseover', fadeIn);
|
|
this.player_.on('mouseout', fadeOut);
|
|
this.player_.on('pause', vjs.bind(this, this.lockShowing));
|
|
this.player_.on('play', vjs.bind(this, this.unlockShowing));
|
|
}
|
|
|
|
touchstart = false;
|
|
this.player_.on('touchstart', function() {
|
|
touchstart = true;
|
|
});
|
|
this.player_.on('touchmove', function() {
|
|
touchstart = false;
|
|
});
|
|
this.player_.on('touchend', vjs.bind(this, function(event) {
|
|
var idx;
|
|
if (touchstart) {
|
|
idx = this.el().className.search('fade-in');
|
|
if (idx !== -1) {
|
|
this.fadeOut();
|
|
} else {
|
|
this.fadeIn();
|
|
}
|
|
}
|
|
touchstart = false;
|
|
|
|
if (!this.player_.paused()) {
|
|
event.preventDefault();
|
|
}
|
|
}));
|
|
}));
|
|
}
|
|
});
|
|
|
|
vjs.ControlBar.prototype.options_ = {
|
|
loadEvent: 'play',
|
|
children: {
|
|
'playToggle': {},
|
|
'currentTimeDisplay': {},
|
|
'timeDivider': {},
|
|
'durationDisplay': {},
|
|
'remainingTimeDisplay': {},
|
|
'progressControl': {},
|
|
'fullscreenToggle': {},
|
|
'volumeControl': {},
|
|
'muteToggle': {}
|
|
// 'volumeMenuButton': {}
|
|
}
|
|
};
|
|
|
|
vjs.ControlBar.prototype.createEl = function(){
|
|
return vjs.createEl('div', {
|
|
className: 'vjs-control-bar'
|
|
});
|
|
};
|
|
|
|
vjs.ControlBar.prototype.fadeIn = function(){
|
|
vjs.Component.prototype.fadeIn.call(this);
|
|
this.player_.trigger('controlsvisible');
|
|
};
|
|
|
|
vjs.ControlBar.prototype.fadeOut = function(){
|
|
vjs.Component.prototype.fadeOut.call(this);
|
|
this.player_.trigger('controlshidden');
|
|
};/**
|
|
* Button to toggle between play and pause
|
|
* @param {vjs.Player|Object} player
|
|
* @param {Object=} options
|
|
* @constructor
|
|
*/
|
|
vjs.PlayToggle = vjs.Button.extend({
|
|
/** @constructor */
|
|
init: function(player, options){
|
|
vjs.Button.call(this, player, options);
|
|
|
|
player.on('play', vjs.bind(this, this.onPlay));
|
|
player.on('pause', vjs.bind(this, 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(){
|
|
vjs.removeClass(this.el_, 'vjs-paused');
|
|
vjs.addClass(this.el_, 'vjs-playing');
|
|
this.el_.children[0].children[0].innerHTML = '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(){
|
|
vjs.removeClass(this.el_, 'vjs-playing');
|
|
vjs.addClass(this.el_, 'vjs-paused');
|
|
this.el_.children[0].children[0].innerHTML = '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);
|
|
|
|
player.on('timeupdate', vjs.bind(this, 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.content = vjs.createEl('div', {
|
|
className: 'vjs-current-time-display',
|
|
innerHTML: '<span class="vjs-control-text">Current Time </span>' + '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(vjs.createEl('div').appendChild(this.content));
|
|
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.content.innerHTML = '<span class="vjs-control-text">Current Time </span>' + 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);
|
|
|
|
player.on('timeupdate', vjs.bind(this, this.updateContent)); // this might need to be changes 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.
|
|
}
|
|
});
|
|
|
|
vjs.DurationDisplay.prototype.createEl = function(){
|
|
var el = vjs.Component.prototype.createEl.call(this, 'div', {
|
|
className: 'vjs-duration vjs-time-controls vjs-control'
|
|
});
|
|
|
|
this.content = vjs.createEl('div', {
|
|
className: 'vjs-duration-display',
|
|
innerHTML: '<span class="vjs-control-text">Duration Time </span>' + '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(vjs.createEl('div').appendChild(this.content));
|
|
return el;
|
|
};
|
|
|
|
vjs.DurationDisplay.prototype.updateContent = function(){
|
|
if (this.player_.duration()) {
|
|
this.content.innerHTML = '<span class="vjs-control-text">Duration Time </span>' + vjs.formatTime(this.player_.duration()); // label the duration time for screen reader users
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Time Separator (Not used in main skin, but still available, and could be used as a 'spare element')
|
|
* @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: '<div><span>/</span></div>'
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
player.on('timeupdate', vjs.bind(this, 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.content = vjs.createEl('div', {
|
|
className: 'vjs-remaining-time-display',
|
|
innerHTML: '<span class="vjs-control-text">Remaining Time </span>' + '-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(vjs.createEl('div').appendChild(this.content));
|
|
return el;
|
|
};
|
|
|
|
vjs.RemainingTimeDisplay.prototype.updateContent = function(){
|
|
if (this.player_.duration()) {
|
|
if (this.player_.duration()) {
|
|
this.content.innerHTML = '<span class="vjs-control-text">Remaining Time </span>' + '-'+ 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.content.innerHTML = vjs.formatTime(time, this.player_.duration());
|
|
};/**
|
|
* Toggle fullscreen video
|
|
* @param {vjs.Player|Object} player
|
|
* @param {Object=} options
|
|
* @constructor
|
|
*/
|
|
vjs.FullscreenToggle = vjs.Button.extend({
|
|
/** @constructor */
|
|
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.el_.children[0].children[0].innerHTML = 'Non-Fullscreen'; // change the button text to "Non-Fullscreen"
|
|
} else {
|
|
this.player_.cancelFullScreen();
|
|
this.el_.children[0].children[0].innerHTML = 'Fullscreen'; // change the button to "Fullscreen"
|
|
}
|
|
};/**
|
|
* Seek, 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);
|
|
player.on('timeupdate', vjs.bind(this, 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.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;
|
|
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 progres
|
|
* @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);
|
|
player.on('progress', vjs.bind(this, this.update));
|
|
}
|
|
});
|
|
|
|
vjs.LoadProgressBar.prototype.createEl = function(){
|
|
return vjs.Component.prototype.createEl.call(this, 'div', {
|
|
className: 'vjs-load-progress',
|
|
innerHTML: '<span class="vjs-control-text">Loaded: 0%</span>'
|
|
});
|
|
};
|
|
|
|
vjs.LoadProgressBar.prototype.update = function(){
|
|
if (this.el_.style) { this.el_.style.width = vjs.round(this.player_.bufferedPercent() * 100, 2) + '%'; }
|
|
};
|
|
|
|
|
|
/**
|
|
* 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: '<span class="vjs-control-text">Progress: 0%</span>'
|
|
});
|
|
};
|
|
|
|
/**
|
|
* SeekBar component 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.SeekHandle = vjs.SliderHandle.extend();
|
|
|
|
/** @inheritDoc */
|
|
vjs.SeekHandle.prototype.defaultValue = '00:00';
|
|
|
|
/** @inheritDoc */
|
|
vjs.SeekHandle.prototype.createEl = function(){
|
|
return vjs.SliderHandle.prototype.createEl.call(this, 'div', {
|
|
className: 'vjs-seek-handle'
|
|
});
|
|
};/**
|
|
* Control the volume
|
|
* @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.features && player.tech.features.volumeControl === false) {
|
|
this.addClass('vjs-hidden');
|
|
}
|
|
player.on('loadstart', vjs.bind(this, function(){
|
|
if (player.tech.features && player.tech.features.volumeContr |