/**
* @fileoverview main function src.
*/
// html5 shiv. must be in
to support older browsers.
document.createelement('video');
document.createelement('audio');
document.createelement('track');
/**
* doubles as the main function for users to create a player instance and also
* the main library object.
*
* **aliases** videojs, _v_ (deprecated)
*
* the `vjs` function can be used to initialize or retrieve a player.
*
* var myplayer = vjs('my_video_id');
*
* @param {string|element} id video element or video element id
* @param {object=} options optional options object for config/settings
* @param {function=} ready optional ready callback
* @return {vjs.player} a player instance
* @namespace
*/
var vjs = function(id, options, ready){
var tag; // element of id
// allow for element or id to be passed in
// string id
if (typeof id === 'string') {
// adjust for jquery id syntax
if (id.indexof('#') === 0) {
id = id.slice(1);
}
// if a player instance has already been created for this id return it.
if (vjs.players[id]) {
return vjs.players[id];
// otherwise get element for id
} else {
tag = vjs.el(id);
}
// id is a media element
} else {
tag = id;
}
// check for a useable element
if (!tag || !tag.nodename) { // re: nodename, could be a box div also
throw new typeerror('the element or id supplied is not valid. (videojs)'); // returns
}
// element may have a player attr referring to an already created player instance.
// if not, set up a new player and return the instance.
return tag['player'] || new vjs.player(tag, options, ready);
};
// extended name, also available externally, window.videojs
var videojs = window['videojs'] = vjs;
// cdn version. used to target right flash swf.
vjs.cdn_version = '4.11';
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': {},
// default of web browser is 300x150. should rely on source width/height.
'width': 300,
'height': 150,
// defaultvolume: 0.85,
'defaultvolume': 0.00, // the freakin seaguls are driving me crazy!
// default playback rates
'playbackrates': [],
// add playback rate selection by adding rates
// 'playbackrates': [0.5, 1, 1.5, 2],
// default inactivity timeout
'inactivitytimeout': 2000,
// included control sets
'children': {
'medialoader': {},
'posterimage': {},
'texttrackdisplay': {},
'loadingspinner': {},
'bigplaybutton': {},
'controlbar': {},
'errordisplay': {}
},
'language': document.getelementsbytagname('html')[0].getattribute('lang') || navigator.languages && navigator.languages[0] || navigator.userlanguage || navigator.language || 'en',
// locales and their language translations
'languages': {},
// default message to show when a video cannot be played.
'notsupportedmessage': 'no compatible source was found for this video.'
};
// set cdn version of swf
// the added (+) blocks the replace from changing this 4.11 string
if (vjs.cdn_version !== 'generated'+'_cdn_vsn') {
videojs.options['flash']['swf'] = vjs.access_protocol + 'vjs.zencdn.net/'+vjs.cdn_version+'/video-js.swf';
}
/**
* utility function for adding languages to the default options. useful for
* amending multiple language support at runtime.
*
* example: vjs.addlanguage('es', {'hello':'hola'});
*
* @param {string} code the language code or dictionary property
* @param {object} data the data values to be translated
* @return {object} the resulting global languages dictionary object
*/
vjs.addlanguage = function(code, data){
if(vjs.options['languages'][code] !== undefined) {
vjs.options['languages'][code] = vjs.util.mergeoptions(vjs.options['languages'][code], data);
} else {
vjs.options['languages'][code] = data;
}
return vjs.options['languages'];
};
/**
* global player list
* @type {object}
*/
vjs.players = {};
/*!
* custom universal module definition (umd)
*
* video.js will never be a non-browser lib so we can simplify umd a bunch and
* still support requirejs and browserify. this also needs to be closure
* compiler compatible, so string keys are used.
*/
if (typeof define === 'function' && define['amd']) {
define([], function(){ return videojs; });
// checking that module is an object too because of umdjs/umd#35
} else if (typeof exports === 'object' && typeof module === 'object') {
module['exports'] = videojs;
}
/**
* core object/class for objects that use inheritance + constructors
*
* to create a class that can be subclassed itself, extend the coreobject class.
*
* var animal = coreobject.extend();
* var horse = animal.extend();
*
* the constructor can be defined through the init property of an object argument.
*
* var animal = coreobject.extend({
* init: function(name, sound){
* this.name = name;
* }
* });
*
* other methods and properties can be added the same way, or directly to the
* prototype.
*
* var animal = coreobject.extend({
* init: function(name){
* this.name = name;
* },
* getname: function(){
* return this.name;
* },
* sound: '...'
* });
*
* animal.prototype.makesound = function(){
* alert(this.sound);
* };
*
* to create an instance of a class, use the create method.
*
* var fluffy = animal.create('fluffy');
* fluffy.getname(); // -> fluffy
*
* methods and properties can be overridden in subclasses.
*
* var horse = animal.extend({
* sound: 'neighhhhh!'
* });
*
* var horsey = horse.create('horsey');
* horsey.getname(); // -> horsey
* horsey.makesound(); // -> alert: neighhhhh!
*
* @class
* @constructor
*/
vjs.coreobject = vjs['coreobject'] = function(){};
// manually exporting vjs['coreobject'] here for closure compiler
// because of the use of the extend/create class methods
// if we didn't do this, those functions would get flattened to something like
// `a = ...` and `this.prototype` would refer to the global object instead of
// coreobject
/**
* create a new object that inherits from this object
*
* var animal = coreobject.extend();
* var horse = animal.extend();
*
* @param {object} props functions and properties to be applied to the
* new object's prototype
* @return {vjs.coreobject} an object that inherits from coreobject
* @this {*}
*/
vjs.coreobject.extend = function(props){
var init, subobj;
props = props || {};
// set up the constructor using the supplied init method
// or using the init of the parent object
// make sure to check the unobfuscated version for external libs
init = props['init'] || props.init || this.prototype['init'] || this.prototype.init || function(){};
// in resig's simple class inheritance (previously used) the constructor
// is a function that calls `this.init.apply(arguments)`
// however that would prevent us from using `parentobject.call(this);`
// in a child constructor because the `this` in `this.init`
// would still refer to the child and cause an infinite loop.
// we would instead have to do
// `parentobject.prototype.init.apply(this, arguments);`
// bleh. we're not creating a _super() function, so it's good to keep
// the parent constructor reference simple.
subobj = function(){
init.apply(this, arguments);
};
// inherit from this object's prototype
subobj.prototype = vjs.obj.create(this.prototype);
// reset the constructor property for subobj otherwise
// instances of subobj would have the constructor of the parent object
subobj.prototype.constructor = subobj;
// make the class extendable
subobj.extend = vjs.coreobject.extend;
// make a function for creating instances
subobj.create = vjs.coreobject.create;
// extend subobj's prototype with functions and other properties from props
for (var name in props) {
if (props.hasownproperty(name)) {
subobj.prototype[name] = props[name];
}
}
return subobj;
};
/**
* create a new instance of this object class
*
* var myanimal = animal.create();
*
* @return {vjs.coreobject} an instance of a coreobject subclass
* @this {*}
*/
vjs.coreobject.create = function(){
// create a new object that inherits from this object's prototype
var inst = vjs.obj.create(this.prototype);
// apply this constructor function to the new object
this.apply(inst, arguments);
// return the new object
return inst;
};
/**
* @fileoverview event system (john resig - secrets of a js ninja http://jsninja.com/)
* (original book version wasn't completely usable, so fixed some things and made closure compiler compatible)
* this should work very similarly to jquery's events, however it's based off the book version which isn't as
* robust as jquery's, so there's probably some differences.
*/
/**
* add an event listener to element
* it stores the handler function in a separate cache object
* and adds a generic handler to the element's event,
* along with a unique id (guid) to the element.
* @param {element|object} elem element or object to bind listeners to
* @param {string|array} type type of event to bind to.
* @param {function} fn event listener.
* @private
*/
vjs.on = function(elem, type, fn){
if (vjs.obj.isarray(type)) {
return _handlemultipleevents(vjs.on, elem, type, fn);
}
var data = vjs.getdata(elem);
// we need a place to store all our handler data
if (!data.handlers) data.handlers = {};
if (!data.handlers[type]) data.handlers[type] = [];
if (!fn.guid) fn.guid = vjs.guid++;
data.handlers[type].push(fn);
if (!data.dispatcher) {
data.disabled = false;
data.dispatcher = function (event){
if (data.disabled) return;
event = vjs.fixevent(event);
var handlers = data.handlers[event.type];
if (handlers) {
// copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
var handlerscopy = handlers.slice(0);
for (var m = 0, n = handlerscopy.length; m < n; m++) {
if (event.isimmediatepropagationstopped()) {
break;
} else {
handlerscopy[m].call(elem, event);
}
}
}
};
}
if (data.handlers[type].length == 1) {
if (elem.addeventlistener) {
elem.addeventlistener(type, data.dispatcher, false);
} else if (elem.attachevent) {
elem.attachevent('on' + type, data.dispatcher);
}
}
};
/**
* removes event listeners from an element
* @param {element|object} elem object to remove listeners from
* @param {string|array=} type type of listener to remove. don't include to remove all events from element.
* @param {function} fn specific listener to remove. don't include to remove listeners for an event type.
* @private
*/
vjs.off = function(elem, type, fn) {
// don't want to add a cache object through getdata if not needed
if (!vjs.hasdata(elem)) return;
var data = vjs.getdata(elem);
// if no events exist, nothing to unbind
if (!data.handlers) { return; }
if (vjs.obj.isarray(type)) {
return _handlemultipleevents(vjs.off, elem, type, fn);
}
// utility function
var removetype = function(t){
data.handlers[t] = [];
vjs.cleanupevents(elem,t);
};
// are we removing all bound events?
if (!type) {
for (var t in data.handlers) removetype(t);
return;
}
var handlers = data.handlers[type];
// if no handlers exist, nothing to unbind
if (!handlers) return;
// if no listener was provided, remove all listeners for type
if (!fn) {
removetype(type);
return;
}
// we're only removing a single handler
if (fn.guid) {
for (var n = 0; n < handlers.length; n++) {
if (handlers[n].guid === fn.guid) {
handlers.splice(n--, 1);
}
}
}
vjs.cleanupevents(elem, type);
};
/**
* clean up the listener cache and dispatchers
* @param {element|object} elem element to clean up
* @param {string} type type of event to clean up
* @private
*/
vjs.cleanupevents = function(elem, type) {
var data = vjs.getdata(elem);
// remove the events of a particular type if there are none left
if (data.handlers[type].length === 0) {
delete data.handlers[type];
// data.handlers[type] = null;
// setting to null was causing an error with data.handlers
// remove the meta-handler from the element
if (elem.removeeventlistener) {
elem.removeeventlistener(type, data.dispatcher, false);
} else if (elem.detachevent) {
elem.detachevent('on' + type, data.dispatcher);
}
}
// remove the events object if there are no types left
if (vjs.isempty(data.handlers)) {
delete data.handlers;
delete data.dispatcher;
delete data.disabled;
// data.handlers = null;
// data.dispatcher = null;
// data.disabled = null;
}
// finally remove the expando if there is no data left
if (vjs.isempty(data)) {
vjs.removedata(elem);
}
};
/**
* fix a native event to have standard property values
* @param {object} event event object to fix
* @return {object}
* @private
*/
vjs.fixevent = function(event) {
function returntrue() { return true; }
function returnfalse() { return false; }
// test if fixing up is needed
// used to check if !event.stoppropagation instead of ispropagationstopped
// but native events return true for stoppropagation, but don't have
// other expected methods like ispropagationstopped. seems to be a problem
// with the javascript ninja code. so we're just overriding all events now.
if (!event || !event.ispropagationstopped) {
var old = event || window.event;
event = {};
// clone the old object so that we can modify the values event = {};
// ie8 doesn't like when you mess with native event properties
// firefox returns false for event.hasownproperty('type') and other props
// which makes copying more difficult.
// todo: probably best to create a whitelist of event props
for (var key in old) {
// safari 6.0.3 warns you if you try to copy deprecated layerx/y
// chrome warns you if you try to copy deprecated keyboardevent.keylocation
if (key !== 'layerx' && key !== 'layery' && key !== 'keylocation') {
// chrome 32+ warns if you try to copy deprecated returnvalue, but
// we still want to if preventdefault isn't supported (ie8).
if (!(key == 'returnvalue' && old.preventdefault)) {
event[key] = old[key];
}
}
}
// the event occurred on this element
if (!event.target) {
event.target = event.srcelement || document;
}
// handle which other element the event is related to
event.relatedtarget = event.fromelement === event.target ?
event.toelement :
event.fromelement;
// stop the default browser action
event.preventdefault = function () {
if (old.preventdefault) {
old.preventdefault();
}
event.returnvalue = false;
event.isdefaultprevented = returntrue;
event.defaultprevented = true;
};
event.isdefaultprevented = returnfalse;
event.defaultprevented = false;
// stop the event from bubbling
event.stoppropagation = function () {
if (old.stoppropagation) {
old.stoppropagation();
}
event.cancelbubble = true;
event.ispropagationstopped = returntrue;
};
event.ispropagationstopped = returnfalse;
// stop the event from bubbling and executing other handlers
event.stopimmediatepropagation = function () {
if (old.stopimmediatepropagation) {
old.stopimmediatepropagation();
}
event.isimmediatepropagationstopped = returntrue;
event.stoppropagation();
};
event.isimmediatepropagationstopped = returnfalse;
// handle mouse position
if (event.clientx != null) {
var doc = document.documentelement, body = document.body;
event.pagex = event.clientx +
(doc && doc.scrollleft || body && body.scrollleft || 0) -
(doc && doc.clientleft || body && body.clientleft || 0);
event.pagey = event.clienty +
(doc && doc.scrolltop || body && body.scrolltop || 0) -
(doc && doc.clienttop || body && body.clienttop || 0);
}
// handle key presses
event.which = event.charcode || event.keycode;
// fix button for mouse clicks:
// 0 == left; 1 == middle; 2 == right
if (event.button != null) {
event.button = (event.button & 1 ? 0 :
(event.button & 4 ? 1 :
(event.button & 2 ? 2 : 0)));
}
}
// returns fixed-up instance
return event;
};
/**
* trigger an event for an element
* @param {element|object} elem element to trigger an event on
* @param {event|object|string} event a string (the type) or an event object with a type attribute
* @private
*/
vjs.trigger = function(elem, event) {
// fetches element data and a reference to the parent (for bubbling).
// don't want to add a data object to cache for every parent,
// so checking hasdata first.
var elemdata = (vjs.hasdata(elem)) ? vjs.getdata(elem) : {};
var parent = elem.parentnode || elem.ownerdocument;
// type = event.type || event,
// handler;
// if an event name was passed as a string, creates an event out of it
if (typeof event === 'string') {
event = { type:event, target:elem };
}
// normalizes the event properties.
event = vjs.fixevent(event);
// if the passed element has a dispatcher, executes the established handlers.
if (elemdata.dispatcher) {
elemdata.dispatcher.call(elem, event);
}
// unless explicitly stopped or the event does not bubble (e.g. media events)
// recursively calls this function to bubble the event up the dom.
if (parent && !event.ispropagationstopped() && event.bubbles !== false) {
vjs.trigger(parent, event);
// if at the top of the dom, triggers the default action unless disabled.
} else if (!parent && !event.defaultprevented) {
var targetdata = vjs.getdata(event.target);
// checks if the target has a default action for this event.
if (event.target[event.type]) {
// temporarily disables event dispatching on the target as we have already executed the handler.
targetdata.disabled = true;
// executes the default action.
if (typeof event.target[event.type] === 'function') {
event.target[event.type]();
}
// re-enables event dispatching.
targetdata.disabled = false;
}
}
// inform the triggerer if the default was prevented by returning false
return !event.defaultprevented;
/* original version of js ninja events wasn't complete.
* we've since updated to the latest version, but keeping this around
* for now just in case.
*/
// // added in addition to book. book code was broke.
// event = typeof event === 'object' ?
// event[vjs.expando] ?
// event :
// new vjs.event(type, event) :
// new vjs.event(type);
// event.type = type;
// if (handler) {
// handler.call(elem, event);
// }
// // clean up the event in case it is being reused
// event.result = undefined;
// event.target = elem;
};
/**
* trigger a listener only once for an event
* @param {element|object} elem element or object to
* @param {string|array} type
* @param {function} fn
* @private
*/
vjs.one = function(elem, type, fn) {
if (vjs.obj.isarray(type)) {
return _handlemultipleevents(vjs.one, elem, type, fn);
}
var func = function(){
vjs.off(elem, type, func);
fn.apply(this, arguments);
};
// copy the guid to the new function so it can removed using the original function's id
func.guid = fn.guid = fn.guid || vjs.guid++;
vjs.on(elem, type, func);
};
/**
* loops through an array of event types and calls the requested method for each type.
* @param {function} fn the event method we want to use.
* @param {element|object} elem element or object to bind listeners to
* @param {string} type type of event to bind to.
* @param {function} callback event listener.
* @private
*/
function _handlemultipleevents(fn, elem, type, callback) {
vjs.arr.foreach(type, function(type) {
fn(elem, type, callback); //call the event method for each one of the types
});
}
var hasownprop = object.prototype.hasownproperty;
/**
* creates an element and applies properties.
* @param {string=} tagname name of tag to be created.
* @param {object=} properties element properties to be applied.
* @return {element}
* @private
*/
vjs.createel = function(tagname, properties){
var el;
tagname = tagname || 'div';
properties = properties || {};
el = document.createelement(tagname);
vjs.obj.each(properties, function(propname, val){
// not remembering why we were checking for dash
// but using setattribute means you have to use getattribute
// the check for dash checks for the aria-* attributes, like aria-label, aria-valuemin.
// the additional check for "role" is because the default method for adding attributes does not
// add the attribute "role". my guess is because it's not a valid attribute in some namespaces, although
// browsers handle the attribute just fine. the w3c allows for aria-* attributes to be used in pre-html5 docs.
// http://www.w3.org/tr/wai-aria-primer/#ariahtml. using setattribute gets around this problem.
if (propname.indexof('aria-') !== -1 || propname == 'role') {
el.setattribute(propname, val);
} else {
el[propname] = val;
}
});
return el;
};
/**
* uppercase the first letter of a string
* @param {string} string string to be uppercased
* @return {string}
* @private
*/
vjs.capitalize = function(string){
return string.charat(0).touppercase() + string.slice(1);
};
/**
* object functions container
* @type {object}
* @private
*/
vjs.obj = {};
/**
* object.create shim for prototypal inheritance
*
* https://developer.mozilla.org/en-us/docs/javascript/reference/global_objects/object/create
*
* @function
* @param {object} obj object to use as prototype
* @private
*/
vjs.obj.create = object.create || function(obj){
//create a new function called 'f' which is just an empty object.
function f() {}
//the prototype of the 'f' function should point to the
//parameter of the anonymous function.
f.prototype = obj;
//create a new constructor function based off of the 'f' function.
return new f();
};
/**
* loop through each property in an object and call a function
* whose arguments are (key,value)
* @param {object} obj object of properties
* @param {function} fn function to be called on each property.
* @this {*}
* @private
*/
vjs.obj.each = function(obj, fn, context){
for (var key in obj) {
if (hasownprop.call(obj, key)) {
fn.call(context || this, key, obj[key]);
}
}
};
/**
* merge two objects together and return the original.
* @param {object} obj1
* @param {object} obj2
* @return {object}
* @private
*/
vjs.obj.merge = function(obj1, obj2){
if (!obj2) { return obj1; }
for (var key in obj2){
if (hasownprop.call(obj2, key)) {
obj1[key] = obj2[key];
}
}
return obj1;
};
/**
* merge two objects, and merge any properties that are objects
* instead of just overwriting one. uses to merge options hashes
* where deeper default settings are important.
* @param {object} obj1 object to override
* @param {object} obj2 overriding object
* @return {object} new object. obj1 and obj2 will be untouched.
* @private
*/
vjs.obj.deepmerge = function(obj1, obj2){
var key, val1, val2;
// make a copy of obj1 so we're not overwriting original values.
// like prototype.options_ and all sub options objects
obj1 = vjs.obj.copy(obj1);
for (key in obj2){
if (hasownprop.call(obj2, key)) {
val1 = obj1[key];
val2 = obj2[key];
// check if both properties are pure objects and do a deep merge if so
if (vjs.obj.isplain(val1) && vjs.obj.isplain(val2)) {
obj1[key] = vjs.obj.deepmerge(val1, val2);
} else {
obj1[key] = obj2[key];
}
}
}
return obj1;
};
/**
* make a copy of the supplied object
* @param {object} obj object to copy
* @return {object} copy of object
* @private
*/
vjs.obj.copy = function(obj){
return vjs.obj.merge({}, obj);
};
/**
* check if an object is plain, and not a dom node or any object sub-instance
* @param {object} obj object to check
* @return {boolean} true if plain, false otherwise
* @private
*/
vjs.obj.isplain = function(obj){
return !!obj
&& typeof obj === 'object'
&& obj.tostring() === '[object object]'
&& obj.constructor === object;
};
/**
* check if an object is array
* since instanceof array will not work on arrays created in another frame we need to use array.isarray, but since ie8 does not support array.isarray we need this shim
* @param {object} obj object to check
* @return {boolean} true if plain, false otherwise
* @private
*/
vjs.obj.isarray = array.isarray || function(arr) {
return object.prototype.tostring.call(arr) === '[object array]';
};
/**
* check to see whether the input is nan or not.
* nan is the only javascript construct that isn't equal to itself
* @param {number} num number to check
* @return {boolean} true if nan, false otherwise
* @private
*/
vjs.isnan = function(num) {
return num !== num;
};
/**
* bind (a.k.a proxy or context). a simple method for changing the context of a function
it also stores a unique id on the function so it can be easily removed from events
* @param {*} context the object to bind as scope
* @param {function} fn the function to be bound to a scope
* @param {number=} uid an optional unique id for the function to be set
* @return {function}
* @private
*/
vjs.bind = function(context, fn, uid) {
// make sure the function has a unique id
if (!fn.guid) { fn.guid = vjs.guid++; }
// create the new function that changes the context
var ret = function() {
return fn.apply(context, arguments);
};
// allow for the ability to individualize this function
// needed in the case where multiple objects might share the same prototype
// if both items add an event listener with the same function, then you try to remove just one
// it will remove both because they both have the same guid.
// when using this, you need to use the bind method when you remove the listener as well.
// currently used in text tracks
ret.guid = (uid) ? uid + '_' + fn.guid : fn.guid;
return ret;
};
/**
* element data store. allows for binding data to an element without putting it directly on the element.
* ex. event listeners are stored here.
* (also from jsninja.com, slightly modified and updated for closure compiler)
* @type {object}
* @private
*/
vjs.cache = {};
/**
* unique id for an element or function
* @type {number}
* @private
*/
vjs.guid = 1;
/**
* unique attribute name to store an element's guid in
* @type {string}
* @constant
* @private
*/
vjs.expando = 'vdata' + (new date()).gettime();
/**
* returns the cache object where data for an element is stored
* @param {element} el element to store data for.
* @return {object}
* @private
*/
vjs.getdata = function(el){
var id = el[vjs.expando];
if (!id) {
id = el[vjs.expando] = vjs.guid++;
vjs.cache[id] = {};
}
return vjs.cache[id];
};
/**
* returns the cache object where data for an element is stored
* @param {element} el element to store data for.
* @return {object}
* @private
*/
vjs.hasdata = function(el){
var id = el[vjs.expando];
return !(!id || vjs.isempty(vjs.cache[id]));
};
/**
* delete data for the element from the cache and the guid attr from getelementbyid
* @param {element} el remove data for an element
* @private
*/
vjs.removedata = function(el){
var id = el[vjs.expando];
if (!id) { return; }
// remove all stored data
// changed to = null
// http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/
// vjs.cache[id] = null;
delete vjs.cache[id];
// remove the expando property from the dom node
try {
delete el[vjs.expando];
} catch(e) {
if (el.removeattribute) {
el.removeattribute(vjs.expando);
} else {
// ie doesn't appear to support removeattribute on the document element
el[vjs.expando] = null;
}
}
};
/**
* check if an object is empty
* @param {object} obj the object to check for emptiness
* @return {boolean}
* @private
*/
vjs.isempty = function(obj) {
for (var prop in obj) {
// inlude null properties as empty.
if (obj[prop] !== null) {
return false;
}
}
return true;
};
/**
* check if an element has a css class
* @param {element} element element to check
* @param {string} classtocheck classname to check
* @private
*/
vjs.hasclass = function(element, classtocheck){
return ((' ' + element.classname + ' ').indexof(' ' + classtocheck + ' ') !== -1);
};
/**
* add a css class name to an element
* @param {element} element element to add class name to
* @param {string} classtoadd classname to add
* @private
*/
vjs.addclass = function(element, classtoadd){
if (!vjs.hasclass(element, classtoadd)) {
element.classname = element.classname === '' ? classtoadd : element.classname + ' ' + classtoadd;
}
};
/**
* remove a css class name from an element
* @param {element} element element to remove from class name
* @param {string} classtoadd classname to remove
* @private
*/
vjs.removeclass = function(element, classtoremove){
var classnames, i;
if (!vjs.hasclass(element, classtoremove)) {return;}
classnames = element.classname.split(' ');
// no arr.indexof in ie8, and we don't want to add a big shim
for (i = classnames.length - 1; i >= 0; i--) {
if (classnames[i] === classtoremove) {
classnames.splice(i,1);
}
}
element.classname = classnames.join(' ');
};
/**
* element for testing browser html5 video capabilities
* @type {element}
* @constant
* @private
*/
vjs.test_vid = vjs.createel('video');
/**
* useragent for browser testing.
* @type {string}
* @constant
* @private
*/
vjs.user_agent = navigator.useragent;
/**
* device is an iphone
* @type {boolean}
* @constant
* @private
*/
vjs.is_iphone = (/iphone/i).test(vjs.user_agent);
vjs.is_ipad = (/ipad/i).test(vjs.user_agent);
vjs.is_ipod = (/ipod/i).test(vjs.user_agent);
vjs.is_ios = vjs.is_iphone || vjs.is_ipad || vjs.is_ipod;
vjs.ios_version = (function(){
var match = vjs.user_agent.match(/os (\d+)_/i);
if (match && match[1]) { return match[1]; }
})();
vjs.is_android = (/android/i).test(vjs.user_agent);
vjs.android_version = (function() {
// this matches android major.minor.patch versions
// android_version is major.minor as a number, if minor isn't available, then only major is returned
var match = vjs.user_agent.match(/android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i),
major,
minor;
if (!match) {
return null;
}
major = match[1] && parsefloat(match[1]);
minor = match[2] && parsefloat(match[2]);
if (major && minor) {
return parsefloat(match[1] + '.' + match[2]);
} else if (major) {
return major;
} else {
return null;
}
})();
// old android is defined as version older than 2.3, and requiring a webkit version of the android browser
vjs.is_old_android = vjs.is_android && (/webkit/i).test(vjs.user_agent) && vjs.android_version < 2.3;
vjs.is_firefox = (/firefox/i).test(vjs.user_agent);
vjs.is_chrome = (/chrome/i).test(vjs.user_agent);
vjs.touch_enabled = !!(('ontouchstart' in window) || window.documenttouch && document instanceof window.documenttouch);
vjs.background_size_supported = 'backgroundsize' in vjs.test_vid.style;
/**
* apply attributes to an html element.
* @param {element} el target element.
* @param {object=} attributes element attributes to be applied.
* @private
*/
vjs.setelementattributes = function(el, attributes){
vjs.obj.each(attributes, function(attrname, attrvalue) {
if (attrvalue === null || typeof attrvalue === 'undefined' || attrvalue === false) {
el.removeattribute(attrname);
} else {
el.setattribute(attrname, (attrvalue === true ? '' : attrvalue));
}
});
};
/**
* get an element's attribute values, as defined on the html tag
* attributes are not the same as properties. they're defined on the tag
* or with setattribute (which shouldn't be used with html)
* this will return true or false for boolean attributes.
* @param {element} tag element from which to get tag attributes
* @return {object}
* @private
*/
vjs.getelementattributes = function(tag){
var obj, knownbooleans, attrs, attrname, attrval;
obj = {};
// known boolean attributes
// we can check for matching boolean properties, but older browsers
// won't know about html5 boolean attributes that we still read from
knownbooleans = ','+'autoplay,controls,loop,muted,default'+',';
if (tag && tag.attributes && tag.attributes.length > 0) {
attrs = tag.attributes;
for (var i = attrs.length - 1; i >= 0; i--) {
attrname = attrs[i].name;
attrval = attrs[i].value;
// check for known booleans
// the matching element property will return a value for typeof
if (typeof tag[attrname] === 'boolean' || knownbooleans.indexof(','+attrname+',') !== -1) {
// the value of an included boolean attribute is typically an empty
// string ('') which would equal false if we just check for a false value.
// we also don't want support bad code like autoplay='false'
attrval = (attrval !== null) ? true : false;
}
obj[attrname] = attrval;
}
}
return obj;
};
/**
* get the computed style value for an element
* from http://robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/
* @param {element} el element to get style value for
* @param {string} strcssrule style name
* @return {string} style value
* @private
*/
vjs.getcomputeddimension = function(el, strcssrule){
var strvalue = '';
if(document.defaultview && document.defaultview.getcomputedstyle){
strvalue = document.defaultview.getcomputedstyle(el, '').getpropertyvalue(strcssrule);
} else if(el.currentstyle){
// ie8 width/height support
strvalue = el['client'+strcssrule.substr(0,1).touppercase() + strcssrule.substr(1)] + 'px';
}
return strvalue;
};
/**
* insert an element as the first child node of another
* @param {element} child element to insert
* @param {[type]} parent element to insert child into
* @private
*/
vjs.insertfirst = function(child, parent){
if (parent.firstchild) {
parent.insertbefore(child, parent.firstchild);
} else {
parent.appendchild(child);
}
};
/**
* object to hold browser support information
* @type {object}
* @private
*/
vjs.browser = {};
/**
* shorthand for document.getelementbyid()
* also allows for css (jquery) id syntax. but nothing other than ids.
* @param {string} id element id
* @return {element} element with supplied id
* @private
*/
vjs.el = function(id){
if (id.indexof('#') === 0) {
id = id.slice(1);
}
return document.getelementbyid(id);
};
/**
* format seconds as a time string, h:mm:ss or m:ss
* supplying a guide (in seconds) will force a number of leading zeros
* to cover the length of the guide
* @param {number} seconds number of seconds to be turned into a string
* @param {number} guide number (in seconds) to model the string after
* @return {string} time formatted as h:mm:ss or m:ss
* @private
*/
vjs.formattime = function(seconds, guide) {
// default to using seconds as guide
guide = guide || seconds;
var s = math.floor(seconds % 60),
m = math.floor(seconds / 60 % 60),
h = math.floor(seconds / 3600),
gm = math.floor(guide / 60 % 60),
gh = math.floor(guide / 3600);
// handle invalid times
if (isnan(seconds) || seconds === infinity) {
// '-' is false for all relational operators (e.g. <, >=) so this setting
// will add the minimum number of fields specified by the guide
h = m = s = '-';
}
// check if we need to show hours
h = (h > 0 || gh > 0) ? h + ':' : '';
// if hours are showing, we may need to add a leading zero.
// always show at least one digit of minutes.
m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':';
// check if leading zero is need for seconds
s = (s < 10) ? '0' + s : s;
return h + m + s;
};
// attempt to block the ability to select text while dragging controls
vjs.blocktextselection = function(){
document.body.focus();
document.onselectstart = function () { return false; };
};
// turn off text selection blocking
vjs.unblocktextselection = function(){ document.onselectstart = function () { return true; }; };
/**
* trim whitespace from the ends of a string.
* @param {string} string string to trim
* @return {string} trimmed string
* @private
*/
vjs.trim = function(str){
return (str+'').replace(/^\s+|\s+$/g, '');
};
/**
* should round off a number to a decimal place
* @param {number} num number to round
* @param {number} dec number of decimal places to round to
* @return {number} rounded number
* @private
*/
vjs.round = function(num, dec) {
if (!dec) { dec = 0; }
return math.round(num*math.pow(10,dec))/math.pow(10,dec);
};
/**
* should create a fake timerange object
* mimics an html5 time range instance, which has functions that
* return the start and end times for a range
* timeranges are returned by the buffered() method
* @param {number} start start time in seconds
* @param {number} end end time in seconds
* @return {object} fake timerange object
* @private
*/
vjs.createtimerange = function(start, end){
return {
length: 1,
start: function() { return start; },
end: function() { return end; }
};
};
/**
* add to local storage (may removable)
* @private
*/
vjs.setlocalstorage = function(key, value){
try {
// ie was throwing errors referencing the var anywhere without this
var localstorage = window.localstorage || false;
if (!localstorage) { return; }
localstorage[key] = value;
} catch(e) {
if (e.code == 22 || e.code == 1014) { // webkit == 22 / firefox == 1014
vjs.log('localstorage full (videojs)', e);
} else {
if (e.code == 18) {
vjs.log('localstorage not allowed (videojs)', e);
} else {
vjs.log('localstorage error (videojs)', e);
}
}
}
};
/**
* get absolute version of relative url. used to tell flash correct url.
* http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
* @param {string} url url to make absolute
* @return {string} absolute url
* @private
*/
vjs.getabsoluteurl = function(url){
// check if absolute url
if (!url.match(/^https?:\/\//)) {
// convert to absolute url. flash hosted off-site needs an absolute url.
url = vjs.createel('div', {
innerhtml: 'x'
}).firstchild.href;
}
return url;
};
/**
* resolve and parse the elements of a url
* @param {string} url the url to parse
* @return {object} an object of url details
*/
vjs.parseurl = function(url) {
var div, a, addtobody, props, details;
props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
// add the url to an anchor and let the browser parse the url
a = vjs.createel('a', { href: url });
// ie8 (and 9?) fix
// ie8 doesn't parse the url correctly until the anchor is actually
// added to the body, and an innerhtml is needed to trigger the parsing
addtobody = (a.host === '' && a.protocol !== 'file:');
if (addtobody) {
div = vjs.createel('div');
div.innerhtml = '';
a = div.firstchild;
// prevent the div from affecting layout
div.setattribute('style', 'display:none; position:absolute;');
document.body.appendchild(div);
}
// copy the specific url properties to a new object
// this is also needed for ie8 because the anchor loses its
// properties when it's removed from the dom
details = {};
for (var i = 0; i < props.length; i++) {
details[props[i]] = a[props[i]];
}
if (addtobody) {
document.body.removechild(div);
}
return details;
};
/**
* log messages to the console and history based on the type of message
*
* @param {string} type the type of message, or `null` for `log`
* @param {[type]} args the args to be passed to the log
* @private
*/
function _logtype(type, args){
var argsarray, noop, console;
// convert args to an array to get array functions
argsarray = array.prototype.slice.call(args);
// if there's no console then don't try to output messages
// they will still be stored in vjs.log.history
// was setting these once outside of this function, but containing them
// in the function makes it easier to test cases where console doesn't exist
noop = function(){};
console = window['console'] || {
'log': noop,
'warn': noop,
'error': noop
};
if (type) {
// add the type to the front of the message
argsarray.unshift(type.touppercase()+':');
} else {
// default to log with no prefix
type = 'log';
}
// add to history
vjs.log.history.push(argsarray);
// add console prefix after adding to history
argsarray.unshift('videojs:');
// call appropriate log function
if (console[type].apply) {
console[type].apply(console, argsarray);
} else {
// ie8 doesn't allow error.apply, but it will just join() the array anyway
console[type](argsarray.join(' '));
}
}
/**
* log plain debug messages
*/
vjs.log = function(){
_logtype(null, arguments);
};
/**
* keep a history of log messages
* @type {array}
*/
vjs.log.history = [];
/**
* log error messages
*/
vjs.log.error = function(){
_logtype('error', arguments);
};
/**
* log warning messages
*/
vjs.log.warn = function(){
_logtype('warn', arguments);
};
// offset left
// getboundingclientrect technique from john resig http://ejohn.org/blog/getboundingclientrect-is-awesome/
vjs.findposition = function(el) {
var box, docel, body, clientleft, scrollleft, left, clienttop, scrolltop, top;
if (el.getboundingclientrect && el.parentnode) {
box = el.getboundingclientrect();
}
if (!box) {
return {
left: 0,
top: 0
};
}
docel = document.documentelement;
body = document.body;
clientleft = docel.clientleft || body.clientleft || 0;
scrollleft = window.pagexoffset || body.scrollleft;
left = box.left + scrollleft - clientleft;
clienttop = docel.clienttop || body.clienttop || 0;
scrolltop = window.pageyoffset || body.scrolltop;
top = box.top + scrolltop - clienttop;
// android sometimes returns slightly off decimal values, so need to round
return {
left: vjs.round(left),
top: vjs.round(top)
};
};
/**
* array functions container
* @type {object}
* @private
*/
vjs.arr = {};
/*
* loops through an array and runs a function for each item inside it.
* @param {array} array the array
* @param {function} callback the function to be run for each item
* @param {*} thisarg the `this` binding of callback
* @returns {array} the array
* @private
*/
vjs.arr.foreach = function(array, callback, thisarg) {
if (vjs.obj.isarray(array) && callback instanceof function) {
for (var i = 0, len = array.length; i < len; ++i) {
callback.call(thisarg || vjs, array[i], i, array);
}
}
return array;
};
/**
* simple http request for retrieving external files (e.g. text tracks)
*
* ##### example
*
* // using url string
* videojs.xhr('http://example.com/myfile.vtt', function(error, response, responsebody){});
*
* // or options block
* videojs.xhr({
* uri: 'http://example.com/myfile.vtt',
* method: 'get',
* responsetype: 'text'
* }, function(error, response, responsebody){
* if (error) {
* // log the error
* } else {
* // successful, do something with the response
* }
* });
*
*
* api is modeled after the raynos/xhr, which we hope to use after
* getting browserify implemented.
* https://github.com/raynos/xhr/blob/master/index.js
*
* @param {object|string} options options block or url string
* @param {function} callback the callback function
* @returns {object} the request
*/
vjs.xhr = function(options, callback){
var xhr, request, urlinfo, winloc, fileurl, crossorigin, aborttimeout, successhandler, errorhandler;
// if options is a string it's the url
if (typeof options === 'string') {
options = {
uri: options
};
}
// merge with default options
videojs.util.mergeoptions({
method: 'get',
timeout: 45 * 1000
}, options);
callback = callback || function(){};
successhandler = function(){
window.cleartimeout(aborttimeout);
callback(null, request, request.response || request.responsetext);
};
errorhandler = function(err){
window.cleartimeout(aborttimeout);
if (!err || typeof err === 'string') {
err = new error(err);
}
callback(err, request);
};
xhr = window.xmlhttprequest;
if (typeof xhr === 'undefined') {
// shim xmlhttprequest for older ies
xhr = function () {
try { return new window.activexobject('msxml2.xmlhttp.6.0'); } catch (e) {}
try { return new window.activexobject('msxml2.xmlhttp.3.0'); } catch (f) {}
try { return new window.activexobject('msxml2.xmlhttp'); } catch (g) {}
throw new error('this browser does not support xmlhttprequest.');
};
}
request = new xhr();
// store a reference to the url on the request instance
request.uri = options.uri;
urlinfo = vjs.parseurl(options.uri);
winloc = window.location;
// check if url is for another domain/origin
// ie8 doesn't know location.origin, so we won't rely on it here
crossorigin = (urlinfo.protocol + urlinfo.host) !== (winloc.protocol + winloc.host);
// xdomainrequest -- use for ie if xmlhttprequest2 isn't available
// 'withcredentials' is only available in xmlhttprequest2
// also xdomainrequest has a lot of gotchas, so only use if cross domain
if (crossorigin && window.xdomainrequest && !('withcredentials' in request)) {
request = new window.xdomainrequest();
request.onload = successhandler;
request.onerror = errorhandler;
// these blank handlers need to be set to fix ie9
// http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/
request.onprogress = function(){};
request.ontimeout = function(){};
// xmlhttprequest
} else {
fileurl = (urlinfo.protocol == 'file:' || winloc.protocol == 'file:');
request.onreadystatechange = function() {
if (request.readystate === 4) {
if (request.timedout) {
return errorhandler('timeout');
}
if (request.status === 200 || fileurl && request.status === 0) {
successhandler();
} else {
errorhandler();
}
}
};
if (options.timeout) {
aborttimeout = window.settimeout(function() {
if (request.readystate !== 4) {
request.timedout = true;
request.abort();
}
}, options.timeout);
}
}
// open the connection
try {
// third arg is async, or ignored by xdomainrequest
request.open(options.method || 'get', options.uri, true);
} catch(err) {
return errorhandler(err);
}
// withcredentials only supported by xmlhttprequest2
if(options.withcredentials) {
request.withcredentials = true;
}
if (options.responsetype) {
request.responsetype = options.responsetype;
}
// send the request
try {
request.send();
} catch(err) {
return errorhandler(err);
}
return request;
};
/**
* utility functions namespace
* @namespace
* @type {object}
*/
vjs.util = {};
/**
* merge two options objects, recursively merging any plain object properties as
* well. previously `deepmerge`
*
* @param {object} obj1 object to override values in
* @param {object} obj2 overriding object
* @return {object} new object -- obj1 and obj2 will be untouched
*/
vjs.util.mergeoptions = function(obj1, obj2){
var key, val1, val2;
// make a copy of obj1 so we're not overwriting original values.
// like prototype.options_ and all sub options objects
obj1 = vjs.obj.copy(obj1);
for (key in obj2){
if (obj2.hasownproperty(key)) {
val1 = obj1[key];
val2 = obj2[key];
// check if both properties are pure objects and do a deep merge if so
if (vjs.obj.isplain(val1) && vjs.obj.isplain(val2)) {
obj1[key] = vjs.util.mergeoptions(val1, val2);
} else {
obj1[key] = obj2[key];
}
}
}
return obj1;
};/**
* @fileoverview player component - base class for all ui objects
*
*/
/**
* base ui component class
*
* components are embeddable ui objects that are represented by both a
* javascript object and an element in the dom. they can be children of other
* components, and can have many children themselves.
*
* // adding a button to the player
* var button = player.addchild('button');
* button.el(); // -> button element
*
*
*
button
*
*
* components are also event emitters.
*
* button.on('click', function(){
* console.log('button clicked!');
* });
*
* button.trigger('customevent');
*
* @param {object} player main player
* @param {object=} options
* @class
* @constructor
* @extends vjs.coreobject
*/
vjs.component = vjs.coreobject.extend({
/**
* the constructor function for the class
*
* @constructor
*/
init: function(player, options, ready){
this.player_ = player;
// make a copy of prototype.options_ to protect against overriding global defaults
this.options_ = vjs.obj.copy(this.options_);
// updated options with supplied options
options = this.options(options);
// get id from options or options element if one is supplied
this.id_ = options['id'] || (options['el'] && options['el']['id']);
// if there was no id from the options, generate one
if (!this.id_) {
// don't require the player id function in the case of mock players
this.id_ = ((player.id && player.id()) || 'no_player') + '_component_' + vjs.guid++;
}
this.name_ = options['name'] || null;
// create element if one wasn't provided in options
this.el_ = options['el'] || this.createel();
this.children_ = [];
this.childindex_ = {};
this.childnameindex_ = {};
// add any child components in options
this.initchildren();
this.ready(ready);
// don't want to trigger ready here or it will before init is actually
// finished for all children that run this constructor
if (options.reporttouchactivity !== false) {
this.enabletouchactivity();
}
}
});
/**
* dispose of the component and all child components
*/
vjs.component.prototype.dispose = function(){
this.trigger({ type: 'dispose', 'bubbles': false });
// dispose all children.
if (this.children_) {
for (var i = this.children_.length - 1; i >= 0; i--) {
if (this.children_[i].dispose) {
this.children_[i].dispose();
}
}
}
// delete child references
this.children_ = null;
this.childindex_ = null;
this.childnameindex_ = null;
// remove all event listeners.
this.off();
// remove element from dom
if (this.el_.parentnode) {
this.el_.parentnode.removechild(this.el_);
}
vjs.removedata(this.el_);
this.el_ = null;
};
/**
* reference to main player instance
*
* @type {vjs.player}
* @private
*/
vjs.component.prototype.player_ = true;
/**
* return the component's player
*
* @return {vjs.player}
*/
vjs.component.prototype.player = function(){
return this.player_;
};
/**
* the component's options object
*
* @type {object}
* @private
*/
vjs.component.prototype.options_;
/**
* deep merge of options objects
*
* whenever a property is an object on both options objects
* the two properties will be merged using vjs.obj.deepmerge.
*
* this is used for merging options for child components. we
* want it to be easy to override individual options on a child
* component without having to rewrite all the other default options.
*
* parent.prototype.options_ = {
* children: {
* 'childone': { 'foo': 'bar', 'asdf': 'fdsa' },
* 'childtwo': {},
* 'childthree': {}
* }
* }
* newoptions = {
* children: {
* 'childone': { 'foo': 'baz', 'abc': '123' }
* 'childtwo': null,
* 'childfour': {}
* }
* }
*
* this.options(newoptions);
*
* result
*
* {
* children: {
* 'childone': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' },
* 'childtwo': null, // disabled. won't be initialized.
* 'childthree': {},
* 'childfour': {}
* }
* }
*
* @param {object} obj object of new option values
* @return {object} a new object of this.options_ and obj merged
*/
vjs.component.prototype.options = function(obj){
if (obj === undefined) return this.options_;
return this.options_ = vjs.util.mergeoptions(this.options_, obj);
};
/**
* the dom element for the component
*
* @type {element}
* @private
*/
vjs.component.prototype.el_;
/**
* create the component's dom element
*
* @param {string=} tagname element's node type. e.g. 'div'
* @param {object=} attributes an object of element attributes that should be set on the element
* @return {element}
*/
vjs.component.prototype.createel = function(tagname, attributes){
return vjs.createel(tagname, attributes);
};
vjs.component.prototype.localize = function(string){
var lang = this.player_.language(),
languages = this.player_.languages();
if (languages && languages[lang] && languages[lang][string]) {
return languages[lang][string];
}
return string;
};
/**
* get the component's dom element
*
* var domel = mycomponent.el();
*
* @return {element}
*/
vjs.component.prototype.el = function(){
return this.el_;
};
/**
* an optional element where, if defined, children will be inserted instead of
* directly in `el_`
*
* @type {element}
* @private
*/
vjs.component.prototype.contentel_;
/**
* return the component's dom element for embedding content.
* will either be el_ or a new element defined in createel.
*
* @return {element}
*/
vjs.component.prototype.contentel = function(){
return this.contentel_ || this.el_;
};
/**
* the id for the component
*
* @type {string}
* @private
*/
vjs.component.prototype.id_;
/**
* get the component's id
*
* var id = mycomponent.id();
*
* @return {string}
*/
vjs.component.prototype.id = function(){
return this.id_;
};
/**
* the name for the component. often used to reference the component.
*
* @type {string}
* @private
*/
vjs.component.prototype.name_;
/**
* get the component's name. the name is often used to reference the component.
*
* var name = mycomponent.name();
*
* @return {string}
*/
vjs.component.prototype.name = function(){
return this.name_;
};
/**
* array of child components
*
* @type {array}
* @private
*/
vjs.component.prototype.children_;
/**
* get an array of all child components
*
* var kids = mycomponent.children();
*
* @return {array} the children
*/
vjs.component.prototype.children = function(){
return this.children_;
};
/**
* object of child components by id
*
* @type {object}
* @private
*/
vjs.component.prototype.childindex_;
/**
* returns a child component with the provided id
*
* @return {vjs.component}
*/
vjs.component.prototype.getchildbyid = function(id){
return this.childindex_[id];
};
/**
* object of child components by name
*
* @type {object}
* @private
*/
vjs.component.prototype.childnameindex_;
/**
* returns a child component with the provided name
*
* @return {vjs.component}
*/
vjs.component.prototype.getchild = function(name){
return this.childnameindex_[name];
};
/**
* adds a child component inside this component
*
* mycomponent.el();
* // ->
* mycomonent.children();
* // [empty array]
*
* var mybutton = mycomponent.addchild('mybutton');
* // ->
mybutton
* // -> mybutton === mycomonent.children()[0];
*
* pass in options for child constructors and options for children of the child
*
* var mybutton = mycomponent.addchild('mybutton', {
* text: 'press me',
* children: {
* buttonchildexample: {
* buttonchildoption: true
* }
* }
* });
*
* @param {string|vjs.component} child the class name or instance of a child to add
* @param {object=} options options, including options to be passed to children of the child.
* @return {vjs.component} the child component (created by this process if a string was used)
* @suppress {accesscontrols|checkregexp|checktypes|checkvars|const|constantproperty|deprecated|duplicate|es5strict|fileoverviewtags|globalthis|invalidcasts|missingproperties|nonstandardjsdocs|strictmoduledepcheck|undefinednames|undefinedvars|unknowndefines|uselesscode|visibility}
*/
vjs.component.prototype.addchild = function(child, options){
var component, componentclass, componentname;
// if child is a string, create new component with options
if (typeof child === 'string') {
componentname = child;
// make sure options is at least an empty object to protect against errors
options = options || {};
// if no componentclass in options, assume componentclass is the name lowercased
// (e.g. playbutton)
componentclass = options['componentclass'] || vjs.capitalize(componentname);
// set name through options
options['name'] = componentname;
// create a new object & element for this controls set
// if there's no .player_, this is a player
// closure compiler throws an 'incomplete alias' warning if we use the vjs variable directly.
// every class should be exported, so this should never be a problem here.
component = new window['videojs'][componentclass](this.player_ || this, options);
// child is a component instance
} else {
component = child;
}
this.children_.push(component);
if (typeof component.id === 'function') {
this.childindex_[component.id()] = component;
}
// if a name wasn't used to create the component, check if we can use the
// name function of the component
componentname = componentname || (component.name && component.name());
if (componentname) {
this.childnameindex_[componentname] = component;
}
// add the ui object's element to the container div (box)
// having an element is not required
if (typeof component['el'] === 'function' && component['el']()) {
this.contentel().appendchild(component['el']());
}
// return so it can stored on parent object if desired.
return component;
};
/**
* remove a child component from this component's list of children, and the
* child component's element from this component's element
*
* @param {vjs.component} component component to remove
*/
vjs.component.prototype.removechild = function(component){
if (typeof component === 'string') {
component = this.getchild(component);
}
if (!component || !this.children_) return;
var childfound = false;
for (var i = this.children_.length - 1; i >= 0; i--) {
if (this.children_[i] === component) {
childfound = true;
this.children_.splice(i,1);
break;
}
}
if (!childfound) return;
this.childindex_[component.id] = null;
this.childnameindex_[component.name] = null;
var compel = component.el();
if (compel && compel.parentnode === this.contentel()) {
this.contentel().removechild(component.el());
}
};
/**
* add and initialize default child components from options
*
* // when an instance of mycomponent is created, all children in options
* // will be added to the instance by their name strings and options
* mycomponent.prototype.options_.children = {
* mychildcomponent: {
* mychildoption: true
* }
* }
*
* // or when creating the component
* var mycomp = new mycomponent(player, {
* children: {
* mychildcomponent: {
* mychildoption: true
* }
* }
* });
*
* the children option can also be an array of child names or
* child options objects (that also include a 'name' key).
*
* var mycomp = new mycomponent(player, {
* children: [
* 'button',
* {
* name: 'button',
* someotheroption: true
* }
* ]
* });
*
*/
vjs.component.prototype.initchildren = function(){
var parent, parentoptions, children, child, name, opts, handleadd;
parent = this;
parentoptions = parent.options();
children = parentoptions['children'];
if (children) {
handleadd = function(name, opts){
// allow options for children to be set at the parent options
// e.g. videojs(id, { controlbar: false });
// instead of videojs(id, { children: { controlbar: false });
if (parentoptions[name] !== undefined) {
opts = parentoptions[name];
}
// allow for disabling default components
// e.g. vjs.options['children']['posterimage'] = false
if (opts === false) return;
// create and add the child component.
// add a direct reference to the child by name on the parent instance.
// if two of the same component are used, different names should be supplied
// for each
parent[name] = parent.addchild(name, opts);
};
// allow for an array of children details to passed in the options
if (vjs.obj.isarray(children)) {
for (var i = 0; i < children.length; i++) {
child = children[i];
if (typeof child == 'string') {
// ['mycomponent']
name = child;
opts = {};
} else {
// [{ name: 'mycomponent', otheroption: true }]
name = child.name;
opts = child;
}
handleadd(name, opts);
}
} else {
vjs.obj.each(children, handleadd);
}
}
};
/**
* allows sub components to stack css class names
*
* @return {string} the constructed class name
*/
vjs.component.prototype.buildcssclass = function(){
// child classes can include a function that does:
// return 'class name' + this._super();
return '';
};
/* events
============================================================================= */
/**
* add an event listener to this component's element
*
* var myfunc = function(){
* var mycomponent = this;
* // do something when the event is fired
* };
*
* mycomponent.on('eventtype', myfunc);
*
* the context of myfunc will be mycomponent unless previously bound.
*
* alternatively, you can add a listener to another element or component.
*
* mycomponent.on(otherelement, 'eventname', myfunc);
* mycomponent.on(othercomponent, 'eventname', myfunc);
*
* the benefit of using this over `vjs.on(otherelement, 'eventname', myfunc)`
* and `othercomponent.on('eventname', myfunc)` is that this way the listeners
* will be automatically cleaned up when either component is disposed.
* it will also bind mycomponent as the context of myfunc.
*
* **note**: when using this on elements in the page other than window
* and document (both permanent), if you remove the element from the dom
* you need to call `vjs.trigger(el, 'dispose')` on it to clean up
* references to it and allow the browser to garbage collect it.
*
* @param {string|vjs.component} first the event type or other component
* @param {function|string} second the event handler or event type
* @param {function} third the event handler
* @return {vjs.component} self
*/
vjs.component.prototype.on = function(first, second, third){
var target, type, fn, removeondispose, cleanremover, thiscomponent;
if (typeof first === 'string' || vjs.obj.isarray(first)) {
vjs.on(this.el_, first, vjs.bind(this, second));
// targeting another component or element
} else {
target = first;
type = second;
fn = vjs.bind(this, third);
thiscomponent = this;
// when this component is disposed, remove the listener from the other component
removeondispose = function(){
thiscomponent.off(target, type, fn);
};
// use the same function id so we can remove it later it using the id
// of the original listener
removeondispose.guid = fn.guid;
this.on('dispose', removeondispose);
// if the other component is disposed first we need to clean the reference
// to the other component in this component's removeondispose listener
// otherwise we create a memory leak.
cleanremover = function(){
thiscomponent.off('dispose', removeondispose);
};
// add the same function id so we can easily remove it later
cleanremover.guid = fn.guid;
// check if this is a dom node
if (first.nodename) {
// add the listener to the other element
vjs.on(target, type, fn);
vjs.on(target, 'dispose', cleanremover);
// should be a component
// not using `instanceof vjs.component` because it makes mock players difficult
} else if (typeof first.on === 'function') {
// add the listener to the other component
target.on(type, fn);
target.on('dispose', cleanremover);
}
}
return this;
};
/**
* remove an event listener from this component's element
*
* mycomponent.off('eventtype', myfunc);
*
* if myfunc is excluded, all listeners for the event type will be removed.
* if eventtype is excluded, all listeners will be removed from the component.
*
* alternatively you can use `off` to remove listeners that were added to other
* elements or components using `mycomponent.on(othercomponent...`.
* in this case both the event type and listener function are required.
*
* mycomponent.off(otherelement, 'eventtype', myfunc);
* mycomponent.off(othercomponent, 'eventtype', myfunc);
*
* @param {string=|vjs.component} first the event type or other component
* @param {function=|string} second the listener function or event type
* @param {function=} third the listener for other component
* @return {vjs.component}
*/
vjs.component.prototype.off = function(first, second, third){
var target, othercomponent, type, fn, otherel;
if (!first || typeof first === 'string' || vjs.obj.isarray(first)) {
vjs.off(this.el_, first, second);
} else {
target = first;
type = second;
// ensure there's at least a guid, even if the function hasn't been used
fn = vjs.bind(this, third);
// remove the dispose listener on this component,
// which was given the same guid as the event listener
this.off('dispose', fn);
if (first.nodename) {
// remove the listener
vjs.off(target, type, fn);
// remove the listener for cleaning the dispose listener
vjs.off(target, 'dispose', fn);
} else {
target.off(type, fn);
target.off('dispose', fn);
}
}
return this;
};
/**
* add an event listener to be triggered only once and then removed
*
* mycomponent.one('eventname', myfunc);
*
* alternatively you can add a listener to another element or component
* that will be triggered only once.
*
* mycomponent.one(otherelement, 'eventname', myfunc);
* mycomponent.one(othercomponent, 'eventname', myfunc);
*
* @param {string|vjs.component} first the event type or other component
* @param {function|string} second the listener function or event type
* @param {function=} third the listener function for other component
* @return {vjs.component}
*/
vjs.component.prototype.one = function(first, second, third) {
var target, type, fn, thiscomponent, newfunc;
if (typeof first === 'string' || vjs.obj.isarray(first)) {
vjs.one(this.el_, first, vjs.bind(this, second));
} else {
target = first;
type = second;
fn = vjs.bind(this, third);
thiscomponent = this;
newfunc = function(){
thiscomponent.off(target, type, newfunc);
fn.apply(this, arguments);
};
// keep the same function id so we can remove it later
newfunc.guid = fn.guid;
this.on(target, type, newfunc);
}
return this;
};
/**
* trigger an event on an element
*
* mycomponent.trigger('eventname');
* mycomponent.trigger({'type':'eventname'});
*
* @param {event|object|string} event a string (the type) or an event object with a type attribute
* @return {vjs.component} self
*/
vjs.component.prototype.trigger = function(event){
vjs.trigger(this.el_, event);
return this;
};
/* ready
================================================================================ */
/**
* is the component loaded
* this can mean different things depending on the component.
*
* @private
* @type {boolean}
*/
vjs.component.prototype.isready_;
/**
* trigger ready as soon as initialization is finished
*
* allows for delaying ready. override on a sub class prototype.
* if you set this.isreadyoninitfinish_ it will affect all components.
* specially used when waiting for the flash player to asynchronously load.
*
* @type {boolean}
* @private
*/
vjs.component.prototype.isreadyoninitfinish_ = true;
/**
* list of ready listeners
*
* @type {array}
* @private
*/
vjs.component.prototype.readyqueue_;
/**
* bind a listener to the component's ready state
*
* different from event listeners in that if the ready event has already happened
* it will trigger the function immediately.
*
* @param {function} fn ready listener
* @return {vjs.component}
*/
vjs.component.prototype.ready = function(fn){
if (fn) {
if (this.isready_) {
fn.call(this);
} else {
if (this.readyqueue_ === undefined) {
this.readyqueue_ = [];
}
this.readyqueue_.push(fn);
}
}
return this;
};
/**
* trigger the ready listeners
*
* @return {vjs.component}
*/
vjs.component.prototype.triggerready = function(){
this.isready_ = true;
var readyqueue = this.readyqueue_;
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
============================================================================= */
/**
* check if a component's element has a css class name
*
* @param {string} classtocheck classname to check
* @return {vjs.component}
*/
vjs.component.prototype.hasclass = function(classtocheck){
return vjs.hasclass(this.el_, classtocheck);
};
/**
* add a css class name to the component's element
*
* @param {string} classtoadd classname to add
* @return {vjs.component}
*/
vjs.component.prototype.addclass = function(classtoadd){
vjs.addclass(this.el_, classtoadd);
return this;
};
/**
* remove a css class name from the component's element
*
* @param {string} classtoremove classname to remove
* @return {vjs.component}
*/
vjs.component.prototype.removeclass = function(classtoremove){
vjs.removeclass(this.el_, classtoremove);
return this;
};
/**
* show the component element if hidden
*
* @return {vjs.component}
*/
vjs.component.prototype.show = function(){
this.el_.style.display = 'block';
return this;
};
/**
* hide the component element if currently showing
*
* @return {vjs.component}
*/
vjs.component.prototype.hide = function(){
this.el_.style.display = 'none';
return this;
};
/**
* lock an item in its visible state
* to be used with fadein/fadeout.
*
* @return {vjs.component}
* @private
*/
vjs.component.prototype.lockshowing = function(){
this.addclass('vjs-lock-showing');
return this;
};
/**
* unlock an item to be hidden
* to be used with fadein/fadeout.
*
* @return {vjs.component}
* @private
*/
vjs.component.prototype.unlockshowing = function(){
this.removeclass('vjs-lock-showing');
return this;
};
/**
* disable component by making it unshowable
*
* currently private because we're moving towards more css-based states.
* @private
*/
vjs.component.prototype.disable = function(){
this.hide();
this.show = function(){};
};
/**
* set or get the width of the component (css values)
*
* setting the video tag dimension values only works with values in pixels.
* percent values will not work.
* some percents can be used, but width()/height() will return the number + %,
* not the actual computed width/height.
*
* @param {number|string=} num optional width number
* @param {boolean} skiplisteners skip the 'resize' event trigger
* @return {vjs.component} this component, when setting the width
* @return {number|string} the width, when getting
*/
vjs.component.prototype.width = function(num, skiplisteners){
return this.dimension('width', num, skiplisteners);
};
/**
* get or set the height of the component (css values)
*
* setting the video tag dimension values only works with values in pixels.
* percent values will not work.
* some percents can be used, but width()/height() will return the number + %,
* not the actual computed width/height.
*
* @param {number|string=} num new component height
* @param {boolean=} skiplisteners skip the resize event trigger
* @return {vjs.component} this component, when setting the height
* @return {number|string} the height, when getting
*/
vjs.component.prototype.height = function(num, skiplisteners){
return this.dimension('height', num, skiplisteners);
};
/**
* set both width and height at the same time
*
* @param {number|string} width
* @param {number|string} height
* @return {vjs.component} the component
*/
vjs.component.prototype.dimensions = function(width, height){
// skip resize listeners on width for optimization
return this.width(width, true).height(height);
};
/**
* get or set width or height
*
* this is the shared code for the width() and height() methods.
* all for an integer, integer + 'px' or integer + '%';
*
* known issue: hidden elements officially have a width of 0. we're defaulting
* to the style.width value and falling back to computedstyle which has the
* hidden element issue. info, but probably not an efficient fix:
* http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/
*
* @param {string} widthorheight 'width' or 'height'
* @param {number|string=} num new dimension
* @param {boolean=} skiplisteners skip resize event trigger
* @return {vjs.component} the component if a dimension was set
* @return {number|string} the dimension if nothing was set
* @private
*/
vjs.component.prototype.dimension = function(widthorheight, num, skiplisteners){
if (num !== undefined) {
if (num === null || vjs.isnan(num)) {
num = 0;
}
// check if using css width/height (% or px) and adjust
if ((''+num).indexof('%') !== -1 || (''+num).indexof('px') !== -1) {
this.el_.style[widthorheight] = num;
} else if (num === 'auto') {
this.el_.style[widthorheight] = '';
} else {
this.el_.style[widthorheight] = num+'px';
}
// skiplisteners allows us to avoid triggering the resize event when setting both width and height
if (!skiplisteners) { this.trigger('resize'); }
// return component
return this;
}
// not setting a value, so getting it
// make sure element exists
if (!this.el_) return 0;
// get dimension value from style
var val = this.el_.style[widthorheight];
var pxindex = val.indexof('px');
if (pxindex !== -1) {
// return the pixel value with no 'px'
return parseint(val.slice(0,pxindex), 10);
// no px so using % or no style was set, so falling back to offsetwidth/height
// if component has display:none, offset will return 0
// todo: handle display:none and no dimension style using px
} else {
return parseint(this.el_['offset'+vjs.capitalize(widthorheight)], 10);
// computedstyle version.
// only difference is if the element is hidden it will return
// the percent value (e.g. '100%'')
// instead of zero like offsetwidth returns.
// var val = vjs.getcomputedstylevalue(this.el_, widthorheight);
// var pxindex = val.indexof('px');
// if (pxindex !== -1) {
// return val.slice(0, pxindex);
// } else {
// return val;
// }
}
};
/**
* fired when the width and/or height of the component changes
* @event resize
*/
vjs.component.prototype.onresize;
/**
* emit 'tap' events when touch events are supported
*
* this is used to support toggling the controls through a tap on the video.
*
* we're requiring them to be enabled because otherwise every component would
* have this extra overhead unnecessarily, on mobile devices where extra
* overhead is especially bad.
* @private
*/
vjs.component.prototype.emittapevents = function(){
var touchstart, firsttouch, touchtime, couldbetap, notap,
xdiff, ydiff, touchdistance, tapmovementthreshold;
// track the start time so we can determine how long the touch lasted
touchstart = 0;
firsttouch = null;
// maximum movement allowed during a touch event to still be considered a tap
tapmovementthreshold = 22;
this.on('touchstart', function(event) {
// if more than one finger, don't consider treating this as a click
if (event.touches.length === 1) {
firsttouch = event.touches[0];
// record start time so we can detect a tap vs. "touch and hold"
touchstart = new date().gettime();
// reset couldbetap tracking
couldbetap = true;
}
});
this.on('touchmove', function(event) {
// if more than one finger, don't consider treating this as a click
if (event.touches.length > 1) {
couldbetap = false;
} else if (firsttouch) {
// some devices will throw touchmoves for all but the slightest of taps.
// so, if we moved only a small distance, this could still be a tap
xdiff = event.touches[0].pagex - firsttouch.pagex;
ydiff = event.touches[0].pagey - firsttouch.pagey;
touchdistance = math.sqrt(xdiff * xdiff + ydiff * ydiff);
if (touchdistance > tapmovementthreshold) {
couldbetap = false;
}
}
});
notap = function(){
couldbetap = false;
};
// todo: listen to the original target. http://youtu.be/dujfpxokup8?t=13m8s
this.on('touchleave', notap);
this.on('touchcancel', notap);
// when the touch ends, measure how long it took and trigger the appropriate
// event
this.on('touchend', function(event) {
firsttouch = null;
// proceed only if the touchmove/leave/cancel event didn't happen
if (couldbetap === true) {
// measure how long the touch lasted
touchtime = new date().gettime() - touchstart;
// the touch needs to be quick in order to consider it a tap
if (touchtime < 250) {
event.preventdefault(); // don't let browser turn this into a click
this.trigger('tap');
// it may be good to copy the touchend event object and change the
// type to tap, if the other event properties aren't exact after
// vjs.fixevent runs (e.g. event.target)
}
}
});
};
/**
* report user touch activity when touch events occur
*
* user activity is used to determine when controls should show/hide. it's
* relatively simple when it comes to mouse events, because any mouse event
* should show the controls. so we capture mouse events that bubble up to the
* player and report activity when that happens.
*
* with touch events it isn't as easy. we can't rely on touch events at the
* player level, because a tap (touchstart + touchend) on the video itself on
* mobile devices is meant to turn controls off (and on). user activity is
* checked asynchronously, so what could happen is a tap event on the video
* turns the controls off, then the touchend event bubbles up to the player,
* which if it reported user activity, would turn the controls right back on.
* (we also don't want to completely block touch events from bubbling up)
*
* also a touchmove, touch+hold, and anything other than a tap is not supposed
* to turn the controls back on on a mobile device.
*
* here we're setting the default component behavior to report user activity
* whenever touch events happen, and this can be turned off by components that
* want touch events to act differently.
*/
vjs.component.prototype.enabletouchactivity = function() {
var report, touchholding, touchend;
// don't continue if the root player doesn't support reporting user activity
if (!this.player().reportuseractivity) {
return;
}
// listener for reporting that the user is active
report = vjs.bind(this.player(), this.player().reportuseractivity);
this.on('touchstart', function() {
report();
// for as long as the they are touching the device or have their mouse down,
// we consider them active even if they're not moving their finger or mouse.
// so we want to continue to update that they are active
this.clearinterval(touchholding);
// report at the same interval as activitycheck
touchholding = this.setinterval(report, 250);
});
touchend = function(event) {
report();
// stop the interval that maintains activity if the touch is holding
this.clearinterval(touchholding);
};
this.on('touchmove', report);
this.on('touchend', touchend);
this.on('touchcancel', touchend);
};
/**
* creates timeout and sets up disposal automatically.
* @param {function} fn the function to run after the timeout.
* @param {number} timeout number of ms to delay before executing specified function.
* @return {number} returns the timeout id
*/
vjs.component.prototype.settimeout = function(fn, timeout) {
fn = vjs.bind(this, fn);
// window.settimeout would be preferable here, but due to some bizarre issue with sinon and/or phantomjs, we can't.
var timeoutid = settimeout(fn, timeout);
var disposefn = function() {
this.cleartimeout(timeoutid);
};
disposefn.guid = 'vjs-timeout-'+ timeoutid;
this.on('dispose', disposefn);
return timeoutid;
};
/**
* clears a timeout and removes the associated dispose listener
* @param {number} timeoutid the id of the timeout to clear
* @return {number} returns the timeout id
*/
vjs.component.prototype.cleartimeout = function(timeoutid) {
cleartimeout(timeoutid);
var disposefn = function(){};
disposefn.guid = 'vjs-timeout-'+ timeoutid;
this.off('dispose', disposefn);
return timeoutid;
};
/**
* creates an interval and sets up disposal automatically.
* @param {function} fn the function to run every n seconds.
* @param {number} interval number of ms to delay before executing specified function.
* @return {number} returns the interval id
*/
vjs.component.prototype.setinterval = function(fn, interval) {
fn = vjs.bind(this, fn);
var intervalid = setinterval(fn, interval);
var disposefn = function() {
this.clearinterval(intervalid);
};
disposefn.guid = 'vjs-interval-'+ intervalid;
this.on('dispose', disposefn);
return intervalid;
};
/**
* clears an interval and removes the associated dispose listener
* @param {number} intervalid the id of the interval to clear
* @return {number} returns the interval id
*/
vjs.component.prototype.clearinterval = function(intervalid) {
clearinterval(intervalid);
var disposefn = function(){};
disposefn.guid = 'vjs-interval-'+ intervalid;
this.off('dispose', disposefn);
return intervalid;
};
/* button - base class for all buttons
================================================================================ */
/**
* base class for all buttons
* @param {vjs.player|object} player
* @param {object=} options
* @class
* @constructor
*/
vjs.button = vjs.component.extend({
/**
* @constructor
* @inheritdoc
*/
init: function(player, options){
vjs.component.call(this, player, options);
this.emittapevents();
this.on('tap', this.onclick);
this.on('click', this.onclick);
this.on('focus', this.onfocus);
this.on('blur', this.onblur);
}
});
vjs.button.prototype.createel = function(type, props){
var el;
// add standard aria and tabindex info
props = vjs.obj.merge({
classname: this.buildcssclass(),
'role': 'button',
'aria-live': 'polite', // let the screen reader user know that the text of the button may change
tabindex: 0
}, props);
el = vjs.component.prototype.createel.call(this, type, props);
// if innerhtml hasn't been overridden (bigplaybutton), add content elements
if (!props.innerhtml) {
this.contentel_ = vjs.createel('div', {
classname: 'vjs-control-content'
});
this.controltext_ = vjs.createel('span', {
classname: 'vjs-control-text',
innerhtml: this.localize(this.buttontext) || 'need text'
});
this.contentel_.appendchild(this.controltext_);
el.appendchild(this.contentel_);
}
return el;
};
vjs.button.prototype.buildcssclass = function(){
// todo: change vjs-control to vjs-button?
return 'vjs-control ' + vjs.component.prototype.buildcssclass.call(this);
};
// click - override with specific functionality for button
vjs.button.prototype.onclick = function(){};
// focus - add keyboard functionality to element
vjs.button.prototype.onfocus = function(){
vjs.on(document, 'keydown', vjs.bind(this, this.onkeypress));
};
// keypress (document level) - trigger click when keys are pressed
vjs.button.prototype.onkeypress = function(event){
// check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) {
event.preventdefault();
this.onclick();
}
};
// blur - remove keyboard triggers
vjs.button.prototype.onblur = function(){
vjs.off(document, 'keydown', vjs.bind(this, this.onkeypress));
};
/* slider
================================================================================ */
/**
* the base functionality for sliders like the volume bar and seek bar
*
* @param {vjs.player|object} player
* @param {object=} options
* @constructor
*/
vjs.slider = vjs.component.extend({
/** @constructor */
init: function(player, options){
vjs.component.call(this, player, options);
// set property names to bar and handle to match with the child slider class is looking for
this.bar = this.getchild(this.options_['barname']);
this.handle = this.getchild(this.options_['handlename']);
this.on('mousedown', this.onmousedown);
this.on('touchstart', this.onmousedown);
this.on('focus', this.onfocus);
this.on('blur', this.onblur);
this.on('click', this.onclick);
this.on(player, 'controlsvisible', this.update);
this.on(player, this.playerevent, this.update);
}
});
vjs.slider.prototype.createel = function(type, props) {
props = props || {};
// add the slider element class to all sub classes
props.classname = props.classname + ' vjs-slider';
props = vjs.obj.merge({
'role': 'slider',
'aria-valuenow': 0,
'aria-valuemin': 0,
'aria-valuemax': 100,
tabindex: 0
}, props);
return vjs.component.prototype.createel.call(this, type, props);
};
vjs.slider.prototype.onmousedown = function(event){
event.preventdefault();
vjs.blocktextselection();
this.addclass('vjs-sliding');
this.on(document, 'mousemove', this.onmousemove);
this.on(document, 'mouseup', this.onmouseup);
this.on(document, 'touchmove', this.onmousemove);
this.on(document, 'touchend', this.onmouseup);
this.onmousemove(event);
};
// to be overridden by a subclass
vjs.slider.prototype.onmousemove = function(){};
vjs.slider.prototype.onmouseup = function() {
vjs.unblocktextselection();
this.removeclass('vjs-sliding');
this.off(document, 'mousemove', this.onmousemove);
this.off(document, 'mouseup', this.onmouseup);
this.off(document, 'touchmove', this.onmousemove);
this.off(document, 'touchend', this.onmouseup);
this.update();
};
vjs.slider.prototype.update = function(){
// in volumebar init we have a settimeout for update that pops and update to the end of the
// execution stack. the player is destroyed before then update will cause an error
if (!this.el_) return;
// if scrubbing, we could use a cached value to make the handle keep up with the user's mouse.
// on html5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later.
// var progress = (this.player_.scrubbing) ? this.player_.getcache().currenttime / this.player_.duration() : this.player_.currenttime() / this.player_.duration();
var barprogress,
progress = this.getpercent(),
handle = this.handle,
bar = this.bar;
// protect against no duration and other division issues
if (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
if (bar) {
bar.el().style.width = vjs.round(barprogress * 100, 2) + '%';
}
};
vjs.slider.prototype.calculatedistance = function(event){
var el, box, boxx, boxy, boxw, boxh, handle, pagex, pagey;
el = this.el_;
box = vjs.findposition(el);
boxw = boxh = el.offsetwidth;
handle = this.handle;
if (this.options()['vertical']) {
boxy = box.top;
if (event.changedtouches) {
pagey = event.changedtouches[0].pagey;
} else {
pagey = event.pagey;
}
if (handle) {
var handleh = handle.el().offsetheight;
// adjusted x and width, so handle doesn't go outside the bar
boxy = boxy + (handleh / 2);
boxh = boxh - handleh;
}
// percent that the click is through the adjusted area
return math.max(0, math.min(1, ((boxy - pagey) + boxh) / boxh));
} else {
boxx = box.left;
if (event.changedtouches) {
pagex = event.changedtouches[0].pagex;
} else {
pagex = event.pagex;
}
if (handle) {
var handlew = handle.el().offsetwidth;
// adjusted x and width, so handle doesn't go outside the bar
boxx = boxx + (handlew / 2);
boxw = boxw - handlew;
}
// percent that the click is through the adjusted area
return math.max(0, math.min(1, (pagex - boxx) / boxw));
}
};
vjs.slider.prototype.onfocus = function(){
this.on(document, 'keydown', this.onkeypress);
};
vjs.slider.prototype.onkeypress = function(event){
if (event.which == 37 || event.which == 40) { // left and down arrows
event.preventdefault();
this.stepback();
} else if (event.which == 38 || event.which == 39) { // up and right arrows
event.preventdefault();
this.stepforward();
}
};
vjs.slider.prototype.onblur = function(){
this.off(document, 'keydown', this.onkeypress);
};
/**
* listener for click events on slider, used to prevent clicks
* from bubbling up to parent elements like button menus.
* @param {object} event event object
*/
vjs.slider.prototype.onclick = function(event){
event.stopimmediatepropagation();
event.preventdefault();
};
/**
* seekbar behavior includes play progress bar, and seek handle
* needed so it can determine seek position based on handle position/size
* @param {vjs.player|object} player
* @param {object=} options
* @constructor
*/
vjs.sliderhandle = vjs.component.extend();
/**
* default value of the slider
*
* @type {number}
* @private
*/
vjs.sliderhandle.prototype.defaultvalue = 0;
/** @inheritdoc */
vjs.sliderhandle.prototype.createel = function(type, props) {
props = props || {};
// add the slider element class to all sub classes
props.classname = props.classname + ' vjs-slider-handle';
props = vjs.obj.merge({
innerhtml: ''+this.defaultvalue+''
}, props);
return vjs.component.prototype.createel.call(this, 'div', props);
};
/* menu
================================================================================ */
/**
* the menu component is used to build pop up menus, including subtitle and
* captions selection menus.
*
* @param {vjs.player|object} player
* @param {object=} options
* @class
* @constructor
*/
vjs.menu = vjs.component.extend();
/**
* add a menu item to the menu
* @param {object|string} component component or component type to add
*/
vjs.menu.prototype.additem = function(component){
this.addchild(component);
component.on('click', vjs.bind(this, function(){
this.unlockshowing();
}));
};
/** @inheritdoc */
vjs.menu.prototype.createel = function(){
var contenteltype = this.options().contenteltype || 'ul';
this.contentel_ = vjs.createel(contenteltype, {
classname: 'vjs-menu-content'
});
var el = vjs.component.prototype.createel.call(this, 'div', {
append: this.contentel_,
classname: 'vjs-menu'
});
el.appendchild(this.contentel_);
// prevent clicks from bubbling up. needed for menu buttons,
// where a click on the parent is significant
vjs.on(el, 'click', function(event){
event.preventdefault();
event.stopimmediatepropagation();
});
return el;
};
/**
* the component for a menu item. `
`
*
* @param {vjs.player|object} player
* @param {object=} options
* @class
* @constructor
*/
vjs.menuitem = vjs.button.extend({
/** @constructor */
init: function(player, options){
vjs.button.call(this, player, options);
this.selected(options['selected']);
}
});
/** @inheritdoc */
vjs.menuitem.prototype.createel = function(type, props){
return vjs.button.prototype.createel.call(this, 'li', vjs.obj.merge({
classname: 'vjs-menu-item',
innerhtml: this.localize(this.options_['label'])
}, props));
};
/**
* handle a click on the menu item, and set it to selected
*/
vjs.menuitem.prototype.onclick = function(){
this.selected(true);
};
/**
* set this menu item as selected or not
* @param {boolean} selected
*/
vjs.menuitem.prototype.selected = function(selected){
if (selected) {
this.addclass('vjs-selected');
this.el_.setattribute('aria-selected',true);
} else {
this.removeclass('vjs-selected');
this.el_.setattribute('aria-selected',false);
}
};
/**
* a button class with a popup menu
* @param {vjs.player|object} player
* @param {object=} options
* @constructor
*/
vjs.menubutton = vjs.button.extend({
/** @constructor */
init: function(player, options){
vjs.button.call(this, player, options);
this.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('keydown', this.onkeypress);
this.el_.setattribute('aria-haspopup', true);
this.el_.setattribute('role', 'button');
}
});
/**
* track the state of the menu button
* @type {boolean}
* @private
*/
vjs.menubutton.prototype.buttonpressed_ = false;
vjs.menubutton.prototype.createmenu = function(){
var menu = new vjs.menu(this.player_);
// add a title list item to the top
if (this.options().title) {
menu.contentel().appendchild(vjs.createel('li', {
classname: 'vjs-menu-title',
innerhtml: vjs.capitalize(this.options().title),
tabindex: -1
}));
}
this.items = this['createitems']();
if (this.items) {
// add menu items to the menu
for (var i = 0; i < this.items.length; i++) {
menu.additem(this.items[i]);
}
}
return menu;
};
/**
* create the list of menu items. specific to each subclass.
*/
vjs.menubutton.prototype.createitems = function(){};
/** @inheritdoc */
vjs.menubutton.prototype.buildcssclass = function(){
return this.classname + ' vjs-menu-button ' + vjs.button.prototype.buildcssclass.call(this);
};
// focus - add keyboard functionality to element
// this function is not needed anymore. instead, the keyboard functionality is handled by
// treating the button as triggering a submenu. when the button is pressed, the submenu
// appears. pressing the button again makes the submenu disappear.
vjs.menubutton.prototype.onfocus = function(){};
// can't turn off list display that we turned on with focus, because list would go away.
vjs.menubutton.prototype.onblur = function(){};
vjs.menubutton.prototype.onclick = function(){
// when you click the button it adds focus, which will show the menu indefinitely.
// so we'll remove focus when the mouse leaves the button.
// focus is needed for tab navigation.
this.one('mouseout', vjs.bind(this, function(){
this.menu.unlockshowing();
this.el_.blur();
}));
if (this.buttonpressed_){
this.unpressbutton();
} else {
this.pressbutton();
}
};
vjs.menubutton.prototype.onkeypress = function(event){
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);
};
/**
* custom mediaerror to mimic the html5 mediaerror
* @param {number} code the media error code
*/
vjs.mediaerror = function(code){
if (typeof code === 'number') {
this.code = code;
} else if (typeof code === 'string') {
// default code is zero, so this is a custom error
this.message = code;
} else if (typeof code === 'object') { // object
vjs.obj.merge(this, code);
}
if (!this.message) {
this.message = vjs.mediaerror.defaultmessages[this.code] || '';
}
};
/**
* the error code that refers two one of the defined
* mediaerror types
* @type {number}
*/
vjs.mediaerror.prototype.code = 0;
/**
* an optional message to be shown with the error.
* message is not part of the html5 video spec
* but allows for more informative custom errors.
* @type {string}
*/
vjs.mediaerror.prototype.message = '';
/**
* an optional status code that can be set by plugins
* to allow even more detail about the error.
* for example the hls plugin might provide the specific
* http status code that was returned when the error
* occurred, then allowing a custom error overlay
* to display more information.
* @type {[type]}
*/
vjs.mediaerror.prototype.status = null;
vjs.mediaerror.errortypes = [
'media_err_custom', // = 0
'media_err_aborted', // = 1
'media_err_network', // = 2
'media_err_decode', // = 3
'media_err_src_not_supported', // = 4
'media_err_encrypted' // = 5
];
vjs.mediaerror.defaultmessages = {
1: 'you aborted the video playback',
2: 'a network error caused the video download to fail part-way.',
3: 'the video playback was aborted due to a corruption problem or because the video used features your browser did not support.',
4: 'the video could not be loaded, either because the server or network failed or because the format is not supported.',
5: 'the video is encrypted and we do not have the keys to decrypt it.'
};
// add types as properties on mediaerror
// e.g. mediaerror.media_err_src_not_supported = 4;
for (var errnum = 0; errnum < vjs.mediaerror.errortypes.length; errnum++) {
vjs.mediaerror[vjs.mediaerror.errortypes[errnum]] = errnum;
// values should be accessible on both the class and instance
vjs.mediaerror.prototype[vjs.mediaerror.errortypes[errnum]] = errnum;
}
(function(){
var apimap, specapi, browserapi, i;
/**
* store the browser-specific methods for the fullscreen api
* @type {object|undefined}
* @private
*/
vjs.browser.fullscreenapi;
// browser api methods
// map approach from screenful.js - https://github.com/sindresorhus/screenfull.js
apimap = [
// spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/overview.html
[
'requestfullscreen',
'exitfullscreen',
'fullscreenelement',
'fullscreenenabled',
'fullscreenchange',
'fullscreenerror'
],
// webkit
[
'webkitrequestfullscreen',
'webkitexitfullscreen',
'webkitfullscreenelement',
'webkitfullscreenenabled',
'webkitfullscreenchange',
'webkitfullscreenerror'
],
// old webkit (safari 5.1)
[
'webkitrequestfullscreen',
'webkitcancelfullscreen',
'webkitcurrentfullscreenelement',
'webkitcancelfullscreen',
'webkitfullscreenchange',
'webkitfullscreenerror'
],
// mozilla
[
'mozrequestfullscreen',
'mozcancelfullscreen',
'mozfullscreenelement',
'mozfullscreenenabled',
'mozfullscreenchange',
'mozfullscreenerror'
],
// microsoft
[
'msrequestfullscreen',
'msexitfullscreen',
'msfullscreenelement',
'msfullscreenenabled',
'msfullscreenchange',
'msfullscreenerror'
]
];
specapi = apimap[0];
// determine the supported set of functions
for (i=0; i
*