1
0
mirror of https://github.com/videojs/video.js.git synced 2025-07-13 01:30:17 +02:00

@heff converted all classes to use ES6 classes. closes #1993

This commit is contained in:
heff
2015-04-14 13:08:32 -07:00
parent 7f70f09621
commit a02ee27802
74 changed files with 6364 additions and 6449 deletions

View File

@ -9,6 +9,7 @@ CHANGELOG
* @OleLaursen added a Danish translation ([view](https://github.com/videojs/video.js/pull/1899)) * @OleLaursen added a Danish translation ([view](https://github.com/videojs/video.js/pull/1899))
* @dn5 Added new translations (Bosnian, Serbian, Croatian) ([view](https://github.com/videojs/video.js/pull/1897)) * @dn5 Added new translations (Bosnian, Serbian, Croatian) ([view](https://github.com/videojs/video.js/pull/1897))
* @mmcc (and others) converted the whole project to use ES6, Babel and Browserify ([view](https://github.com/videojs/video.js/pull/1976)) * @mmcc (and others) converted the whole project to use ES6, Babel and Browserify ([view](https://github.com/videojs/video.js/pull/1976))
* @heff converted all classes to use ES6 classes ([view](https://github.com/videojs/video.js/pull/1993))
-------------------- --------------------

View File

@ -27,7 +27,7 @@
"global": "^4.3.0" "global": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"babelify": "^5.0.4", "babelify": "^6.0.1",
"blanket": "^1.1.6", "blanket": "^1.1.6",
"browserify-istanbul": "^0.2.1", "browserify-istanbul": "^0.2.1",
"browserify-versionify": "^1.0.4", "browserify-versionify": "^1.0.4",

View File

@ -1,4 +1,3 @@
import Component from './component';
import Button from './button'; import Button from './button';
/* Big Play Button /* Big Play Button
@ -11,20 +10,21 @@ import Button from './button';
* @class * @class
* @constructor * @constructor
*/ */
var BigPlayButton = Button.extend(); class BigPlayButton extends Button {
Component.registerComponent('BigPlayButton', BigPlayButton); createEl() {
return super.createEl('div', {
BigPlayButton.prototype.createEl = function(){
return Button.prototype.createEl.call(this, 'div', {
className: 'vjs-big-play-button', className: 'vjs-big-play-button',
innerHTML: '<span aria-hidden="true"></span>', innerHTML: '<span aria-hidden="true"></span>',
'aria-label': 'play video' 'aria-label': 'play video'
}); });
}; }
BigPlayButton.prototype.onClick = function(){ onClick() {
this.player_.play(); this.player_.play();
}; }
}
Button.registerComponent('BigPlayButton', BigPlayButton);
export default BigPlayButton; export default BigPlayButton;

View File

@ -12,13 +12,10 @@ import document from 'global/document';
* @class * @class
* @constructor * @constructor
*/ */
var Button = Component.extend({ class Button extends Component {
/**
* @constructor constructor(player, options) {
* @inheritDoc super(player, options);
*/
init: function(player, options){
Component.call(this, player, options);
this.emitTapEvents(); this.emitTapEvents();
@ -27,11 +24,8 @@ var Button = Component.extend({
this.on('focus', this.onFocus); this.on('focus', this.onFocus);
this.on('blur', this.onBlur); this.on('blur', this.onBlur);
} }
});
Component.registerComponent('Button', Button); createEl(type, props) {
Button.prototype.createEl = function(type, props){
// Add standard Aria and Tabindex info // Add standard Aria and Tabindex info
props = Lib.obj.merge({ props = Lib.obj.merge({
className: this.buildCSSClass(), className: this.buildCSSClass(),
@ -40,7 +34,7 @@ Button.prototype.createEl = function(type, props){
tabIndex: 0 tabIndex: 0
}, props); }, props);
let el = Component.prototype.createEl.call(this, type, props); let el = super.createEl(type, props);
// if innerHTML hasn't been overridden (bigPlayButton), add content elements // if innerHTML hasn't been overridden (bigPlayButton), add content elements
if (!props.innerHTML) { if (!props.innerHTML) {
@ -58,33 +52,37 @@ Button.prototype.createEl = function(type, props){
} }
return el; return el;
}; }
Button.prototype.buildCSSClass = function(){ buildCSSClass() {
// TODO: Change vjs-control to vjs-button? // TODO: Change vjs-control to vjs-button?
return 'vjs-control ' + Component.prototype.buildCSSClass.call(this); return 'vjs-control ' + super.buildCSSClass();
}; }
// Click - Override with specific functionality for button // Click - Override with specific functionality for button
Button.prototype.onClick = function(){}; onClick() {}
// Focus - Add keyboard functionality to element // Focus - Add keyboard functionality to element
Button.prototype.onFocus = function(){ onFocus() {
Events.on(document, 'keydown', Lib.bind(this, this.onKeyPress)); Events.on(document, 'keydown', Lib.bind(this, this.onKeyPress));
}; }
// KeyPress (document level) - Trigger click when keys are pressed // KeyPress (document level) - Trigger click when keys are pressed
Button.prototype.onKeyPress = function(event){ onKeyPress(event) {
// Check for space bar (32) or enter (13) keys // Check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) { if (event.which == 32 || event.which == 13) {
event.preventDefault(); event.preventDefault();
this.onClick(); this.onClick();
} }
}; }
// Blur - Remove keyboard triggers // Blur - Remove keyboard triggers
Button.prototype.onBlur = function(){ onBlur() {
Events.off(document, 'keydown', Lib.bind(this, this.onKeyPress)); Events.off(document, 'keydown', Lib.bind(this, this.onKeyPress));
}; }
}
Component.registerComponent('Button', Button);
export default Button; export default Button;

View File

@ -3,7 +3,6 @@
* *
*/ */
import CoreObject from './core-object.js';
import * as Lib from './lib.js'; import * as Lib from './lib.js';
import * as VjsUtil from './util.js'; import * as VjsUtil from './util.js';
import * as Events from './events.js'; import * as Events from './events.js';
@ -38,14 +37,16 @@ import window from 'global/window';
* @constructor * @constructor
* @extends vjs.CoreObject * @extends vjs.CoreObject
*/ */
var Component = CoreObject.extend({ class Component {
/**
* the constructor function for the class constructor(player, options, ready){
*
* @constructor // The component might be the player itself and we can't pass `this` to super
*/ if (!player && this.play) {
init: function(player, options, ready){ this.player_ = player = this;
} else {
this.player_ = player; this.player_ = player;
}
// Make a copy of prototype.options_ to protect against overriding global defaults // Make a copy of prototype.options_ to protect against overriding global defaults
this.options_ = Lib.obj.copy(this.options_); this.options_ = Lib.obj.copy(this.options_);
@ -54,7 +55,7 @@ var Component = CoreObject.extend({
options = this.options(options); options = this.options(options);
// Get ID from options or options element if one is supplied // Get ID from options or options element if one is supplied
this.id_ = options['id'] || (options['el'] && options['el']['id']); this.id_ = options.id || (options.el && options.el.id);
// If there was no ID from the options, generate one // If there was no ID from the options, generate one
if (!this.id_) { if (!this.id_) {
@ -65,14 +66,20 @@ var Component = CoreObject.extend({
this.name_ = options['name'] || null; this.name_ = options['name'] || null;
// Create element if one wasn't provided in options // Create element if one wasn't provided in options
this.el_ = options['el'] || this.createEl(); if (options.el) {
this.el_ = options.el;
} else if (options.createEl !== false) {
this.el_ = this.createEl();
}
this.children_ = []; this.children_ = [];
this.childIndex_ = {}; this.childIndex_ = {};
this.childNameIndex_ = {}; this.childNameIndex_ = {};
// Add any child components in options // Add any child components in options
if (options.initChildren !== false) {
this.initChildren(); this.initChildren();
}
this.ready(ready); this.ready(ready);
// Don't want to trigger ready here or it will before init is actually // Don't want to trigger ready here or it will before init is actually
@ -82,12 +89,17 @@ var Component = CoreObject.extend({
this.enableTouchActivity(); this.enableTouchActivity();
} }
} }
});
/** // Temp for ES6 class transition, remove before 5.0
init() {
// console.log('init called on Component');
Component.apply(this, arguments);
}
/**
* Dispose of the component and all child components * Dispose of the component and all child components
*/ */
Component.prototype.dispose = function(){ dispose() {
this.trigger({ type: 'dispose', 'bubbles': false }); this.trigger({ type: 'dispose', 'bubbles': false });
// Dispose all children. // Dispose all children.
@ -114,34 +126,18 @@ Component.prototype.dispose = function(){
Lib.removeData(this.el_); Lib.removeData(this.el_);
this.el_ = null; this.el_ = null;
}; }
/** /**
* Reference to main player instance
*
* @type {vjs.Player}
* @private
*/
Component.prototype.player_ = true;
/**
* Return the component's player * Return the component's player
* *
* @return {vjs.Player} * @return {vjs.Player}
*/ */
Component.prototype.player = function(){ player() {
return this.player_; return this.player_;
}; }
/** /**
* The component's options object
*
* @type {Object}
* @private
*/
Component.prototype.options_;
/**
* Deep merge of options objects * Deep merge of options objects
* *
* Whenever a property is an object on both options objects * Whenever a property is an object on both options objects
@ -182,162 +178,105 @@ Component.prototype.options_;
* @param {Object} obj Object of new option values * @param {Object} obj Object of new option values
* @return {Object} A NEW object of this.options_ and obj merged * @return {Object} A NEW object of this.options_ and obj merged
*/ */
Component.prototype.options = function(obj){ options(obj){
if (obj === undefined) return this.options_; if (obj === undefined) return this.options_;
return this.options_ = VjsUtil.mergeOptions(this.options_, obj); return this.options_ = VjsUtil.mergeOptions(this.options_, obj);
};
/**
* The DOM element for the component
*
* @type {Element}
* @private
*/
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}
*/
Component.prototype.createEl = function(tagName, attributes){
return Lib.createEl(tagName, attributes);
};
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 * Get the component's DOM element
* *
* var domEl = myComponent.el(); * var domEl = myComponent.el();
* *
* @return {Element} * @return {Element}
*/ */
Component.prototype.el = function(){ el(){
return this.el_; return this.el_;
}; }
/** /**
* An optional element where, if defined, children will be inserted instead of * Create the component's DOM element
* directly in `el_`
* *
* @type {Element} * @param {String=} tagName Element's node type. e.g. 'div'
* @private * @param {Object=} attributes An object of element attributes that should be set on the element
* @return {Element}
*/ */
Component.prototype.contentEl_; createEl(tagName, attributes){
return Lib.createEl(tagName, attributes);
}
/** localize(string){
* Return the component's DOM element for embedding content. var lang = this.player_.language(),
* Will either be el_ or a new element defined in createEl. languages = this.player_.languages();
if (languages && languages[lang] && languages[lang][string]) {
return languages[lang][string];
}
return string;
}
/**
* Return the component's DOM element where children are inserted.
* Will either be the same as el() or a new element defined in createEl().
* *
* @return {Element} * @return {Element}
*/ */
Component.prototype.contentEl = function(){ contentEl(){
return this.contentEl_ || this.el_; return this.contentEl_ || this.el_;
}; }
/** /**
* The ID for the component
*
* @type {String}
* @private
*/
Component.prototype.id_;
/**
* Get the component's ID * Get the component's ID
* *
* var id = myComponent.id(); * var id = myComponent.id();
* *
* @return {String} * @return {String}
*/ */
Component.prototype.id = function(){ id(){
return this.id_; return this.id_;
}; }
/** /**
* The name for the component. Often used to reference the component.
*
* @type {String}
* @private
*/
Component.prototype.name_;
/**
* Get the component's name. The name is often used to reference the component. * Get the component's name. The name is often used to reference the component.
* *
* var name = myComponent.name(); * var name = myComponent.name();
* *
* @return {String} * @return {String}
*/ */
Component.prototype.name = function(){ name(){
return this.name_; return this.name_;
}; }
/** /**
* Array of child components
*
* @type {Array}
* @private
*/
Component.prototype.children_;
/**
* Get an array of all child components * Get an array of all child components
* *
* var kids = myComponent.children(); * var kids = myComponent.children();
* *
* @return {Array} The children * @return {Array} The children
*/ */
Component.prototype.children = function(){ children(){
return this.children_; return this.children_;
}; }
/** /**
* Object of child components by ID
*
* @type {Object}
* @private
*/
Component.prototype.childIndex_;
/**
* Returns a child component with the provided ID * Returns a child component with the provided ID
* *
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.getChildById = function(id){ getChildById(id){
return this.childIndex_[id]; return this.childIndex_[id];
}; }
/** /**
* Object of child components by name
*
* @type {Object}
* @private
*/
Component.prototype.childNameIndex_;
/**
* Returns a child component with the provided name * Returns a child component with the provided name
* *
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.getChild = function(name){ getChild(name){
return this.childNameIndex_[name]; return this.childNameIndex_[name];
}; }
/** /**
* Adds a child component inside this component * Adds a child component inside this component
* *
* myComponent.el(); * myComponent.el();
@ -365,8 +304,10 @@ Component.prototype.getChild = function(name){
* @return {vjs.Component} The child component (created by this process if a string was used) * @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} * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility}
*/ */
Component.prototype.addChild = function(child, options){ addChild(child, options){
let component, componentName; let component;
let componentName;
// If child is a string, create nt with options // If child is a string, create nt with options
if (typeof child === 'string') { if (typeof child === 'string') {
let componentName = child; let componentName = child;
@ -412,28 +353,28 @@ Component.prototype.addChild = function(child, options){
// Add the UI object's element to the container div (box) // Add the UI object's element to the container div (box)
// Having an element is not required // Having an element is not required
if (typeof component['el'] === 'function' && component['el']()) { if (typeof component.el === 'function' && component.el()) {
this.contentEl().appendChild(component['el']()); this.contentEl().appendChild(component.el());
} }
// Return so it can stored on parent object if desired. // Return so it can stored on parent object if desired.
return component; return component;
}; }
/** /**
* Remove a child component from this component's list of children, and the * Remove a child component from this component's list of children, and the
* child component's element from this component's element * child component's element from this component's element
* *
* @param {vjs.Component} component Component to remove * @param {vjs.Component} component Component to remove
*/ */
Component.prototype.removeChild = function(component){ removeChild(component){
if (typeof component === 'string') { if (typeof component === 'string') {
component = this.getChild(component); component = this.getChild(component);
} }
if (!component || !this.children_) return; if (!component || !this.children_) return;
var childFound = false; let childFound = false;
for (var i = this.children_.length - 1; i >= 0; i--) { for (var i = this.children_.length - 1; i >= 0; i--) {
if (this.children_[i] === component) { if (this.children_[i] === component) {
childFound = true; childFound = true;
@ -451,9 +392,9 @@ Component.prototype.removeChild = function(component){
if (compEl && compEl.parentNode === this.contentEl()) { if (compEl && compEl.parentNode === this.contentEl()) {
this.contentEl().removeChild(component.el()); this.contentEl().removeChild(component.el());
} }
}; }
/** /**
* Add and initialize default child components from options * Add and initialize default child components from options
* *
* // when an instance of MyComponent is created, all children in options * // when an instance of MyComponent is created, all children in options
@ -487,14 +428,13 @@ Component.prototype.removeChild = function(component){
* }); * });
* *
*/ */
Component.prototype.initChildren = function(){ initChildren() {
let children = this.options_.children;
if (children) {
let parent = this; let parent = this;
let parentOptions = parent.options(); let parentOptions = parent.options();
let children = parentOptions['children']; let handleAdd = function(name, opts){
let handleAdd;
if (children) {
handleAdd = function(name, opts){
// Allow options for children to be set at the parent options // Allow options for children to be set at the parent options
// e.g. videojs(id, { controlBar: false }); // e.g. videojs(id, { controlBar: false });
// instead of videojs(id, { children: { controlBar: false }); // instead of videojs(id, { children: { controlBar: false });
@ -535,23 +475,20 @@ Component.prototype.initChildren = function(){
Lib.obj.each(children, handleAdd); Lib.obj.each(children, handleAdd);
} }
} }
}; }
/** /**
* Allows sub components to stack CSS class names * Allows sub components to stack CSS class names
* *
* @return {String} The constructed class name * @return {String} The constructed class name
*/ */
Component.prototype.buildCSSClass = function(){ buildCSSClass(){
// Child classes can include a function that does: // Child classes can include a function that does:
// return 'CLASS NAME' + this._super(); // return 'CLASS NAME' + this._super();
return ''; return '';
}; }
/* Events /**
============================================================================= */
/**
* Add an event listener to this component's element * Add an event listener to this component's element
* *
* var myFunc = function(){ * var myFunc = function(){
@ -583,7 +520,7 @@ Component.prototype.buildCSSClass = function(){
* @param {Function} third The event handler * @param {Function} third The event handler
* @return {vjs.Component} self * @return {vjs.Component} self
*/ */
Component.prototype.on = function(first, second, third){ on(first, second, third){
var target, type, fn, removeOnDispose, cleanRemover, thisComponent; var target, type, fn, removeOnDispose, cleanRemover, thisComponent;
if (typeof first === 'string' || Lib.obj.isArray(first)) { if (typeof first === 'string' || Lib.obj.isArray(first)) {
@ -630,9 +567,9 @@ Component.prototype.on = function(first, second, third){
} }
return this; return this;
}; }
/** /**
* Remove an event listener from this component's element * Remove an event listener from this component's element
* *
* myComponent.off('eventType', myFunc); * myComponent.off('eventType', myFunc);
@ -652,7 +589,7 @@ Component.prototype.on = function(first, second, third){
* @param {Function=} third The listener for other component * @param {Function=} third The listener for other component
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.off = function(first, second, third){ off(first, second, third){
var target, otherComponent, type, fn, otherEl; var target, otherComponent, type, fn, otherEl;
if (!first || typeof first === 'string' || Lib.obj.isArray(first)) { if (!first || typeof first === 'string' || Lib.obj.isArray(first)) {
@ -679,9 +616,9 @@ Component.prototype.off = function(first, second, third){
} }
return this; return this;
}; }
/** /**
* Add an event listener to be triggered only once and then removed * Add an event listener to be triggered only once and then removed
* *
* myComponent.one('eventName', myFunc); * myComponent.one('eventName', myFunc);
@ -697,7 +634,7 @@ Component.prototype.off = function(first, second, third){
* @param {Function=} third The listener function for other component * @param {Function=} third The listener function for other component
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.one = function(first, second, third) { one(first, second, third) {
var target, type, fn, thisComponent, newFunc; var target, type, fn, thisComponent, newFunc;
if (typeof first === 'string' || Lib.obj.isArray(first)) { if (typeof first === 'string' || Lib.obj.isArray(first)) {
@ -719,9 +656,9 @@ Component.prototype.one = function(first, second, third) {
} }
return this; return this;
}; }
/** /**
* Trigger an event on an element * Trigger an event on an element
* *
* myComponent.trigger('eventName'); * myComponent.trigger('eventName');
@ -730,43 +667,12 @@ Component.prototype.one = function(first, second, third) {
* @param {Event|Object|String} event A string (the type) or an event object with a type attribute * @param {Event|Object|String} event A string (the type) or an event object with a type attribute
* @return {vjs.Component} self * @return {vjs.Component} self
*/ */
Component.prototype.trigger = function(event){ trigger(event){
Events.trigger(this.el_, event); Events.trigger(this.el_, event);
return this; return this;
}; }
/* Ready /**
================================================================================ */
/**
* Is the component loaded
* This can mean different things depending on the component.
*
* @private
* @type {Boolean}
*/
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
*/
Component.prototype.isReadyOnInitFinish_ = true;
/**
* List of ready listeners
*
* @type {Array}
* @private
*/
Component.prototype.readyQueue_;
/**
* Bind a listener to the component's ready state * Bind a listener to the component's ready state
* *
* Different from event listeners in that if the ready event has already happened * Different from event listeners in that if the ready event has already happened
@ -775,26 +681,24 @@ Component.prototype.readyQueue_;
* @param {Function} fn Ready listener * @param {Function} fn Ready listener
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.ready = function(fn){ ready(fn){
if (fn) { if (fn) {
if (this.isReady_) { if (this.isReady_) {
fn.call(this); fn.call(this);
} else { } else {
if (this.readyQueue_ === undefined) { this.readyQueue_ = this.readyQueue_ || [];
this.readyQueue_ = [];
}
this.readyQueue_.push(fn); this.readyQueue_.push(fn);
} }
} }
return this; return this;
}; }
/** /**
* Trigger the ready listeners * Trigger the ready listeners
* *
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.triggerReady = function(){ triggerReady(){
this.isReady_ = true; this.isReady_ = true;
var readyQueue = this.readyQueue_; var readyQueue = this.readyQueue_;
@ -811,99 +715,85 @@ Component.prototype.triggerReady = function(){
// Allow for using event listeners also, in case you want to do something everytime a source is ready. // Allow for using event listeners also, in case you want to do something everytime a source is ready.
this.trigger('ready'); this.trigger('ready');
} }
}; }
/* Display /**
============================================================================= */
/**
* Check if a component's element has a CSS class name * Check if a component's element has a CSS class name
* *
* @param {String} classToCheck Classname to check * @param {String} classToCheck Classname to check
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.hasClass = function(classToCheck){ hasClass(classToCheck){
return Lib.hasClass(this.el_, classToCheck); return Lib.hasClass(this.el_, classToCheck);
}; }
/** /**
* Add a CSS class name to the component's element * Add a CSS class name to the component's element
* *
* @param {String} classToAdd Classname to add * @param {String} classToAdd Classname to add
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.addClass = function(classToAdd){ addClass(classToAdd){
Lib.addClass(this.el_, classToAdd); Lib.addClass(this.el_, classToAdd);
return this; return this;
}; }
/** /**
* Remove a CSS class name from the component's element * Remove a CSS class name from the component's element
* *
* @param {String} classToRemove Classname to remove * @param {String} classToRemove Classname to remove
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.removeClass = function(classToRemove){ removeClass(classToRemove){
Lib.removeClass(this.el_, classToRemove); Lib.removeClass(this.el_, classToRemove);
return this; return this;
}; }
/** /**
* Show the component element if hidden * Show the component element if hidden
* *
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.show = function(){ show(){
this.removeClass('vjs-hidden'); this.removeClass('vjs-hidden');
return this; return this;
}; }
/** /**
* Hide the component element if currently showing * Hide the component element if currently showing
* *
* @return {vjs.Component} * @return {vjs.Component}
*/ */
Component.prototype.hide = function(){ hide(){
this.addClass('vjs-hidden'); this.addClass('vjs-hidden');
return this; return this;
}; }
/** /**
* Lock an item in its visible state * Lock an item in its visible state
* To be used with fadeIn/fadeOut. * To be used with fadeIn/fadeOut.
* *
* @return {vjs.Component} * @return {vjs.Component}
* @private * @private
*/ */
Component.prototype.lockShowing = function(){ lockShowing(){
this.addClass('vjs-lock-showing'); this.addClass('vjs-lock-showing');
return this; return this;
}; }
/** /**
* Unlock an item to be hidden * Unlock an item to be hidden
* To be used with fadeIn/fadeOut. * To be used with fadeIn/fadeOut.
* *
* @return {vjs.Component} * @return {vjs.Component}
* @private * @private
*/ */
Component.prototype.unlockShowing = function(){ unlockShowing(){
this.removeClass('vjs-lock-showing'); this.removeClass('vjs-lock-showing');
return this; return this;
}; }
/** /**
* Disable component by making it unshowable
*
* Currently private because we're moving towards more css-based states.
* @private
*/
Component.prototype.disable = function(){
this.hide();
this.show = function(){};
};
/**
* Set or get the width of the component (CSS values) * Set or get the width of the component (CSS values)
* *
* Setting the video tag dimension values only works with values in pixels. * Setting the video tag dimension values only works with values in pixels.
@ -916,11 +806,11 @@ Component.prototype.disable = function(){
* @return {vjs.Component} This component, when setting the width * @return {vjs.Component} This component, when setting the width
* @return {Number|String} The width, when getting * @return {Number|String} The width, when getting
*/ */
Component.prototype.width = function(num, skipListeners){ width(num, skipListeners){
return this.dimension('width', num, skipListeners); return this.dimension('width', num, skipListeners);
}; }
/** /**
* Get or set the height of the component (CSS values) * Get or set the height of the component (CSS values)
* *
* Setting the video tag dimension values only works with values in pixels. * Setting the video tag dimension values only works with values in pixels.
@ -933,23 +823,23 @@ Component.prototype.width = function(num, skipListeners){
* @return {vjs.Component} This component, when setting the height * @return {vjs.Component} This component, when setting the height
* @return {Number|String} The height, when getting * @return {Number|String} The height, when getting
*/ */
Component.prototype.height = function(num, skipListeners){ height(num, skipListeners){
return this.dimension('height', num, skipListeners); return this.dimension('height', num, skipListeners);
}; }
/** /**
* Set both width and height at the same time * Set both width and height at the same time
* *
* @param {Number|String} width * @param {Number|String} width
* @param {Number|String} height * @param {Number|String} height
* @return {vjs.Component} The component * @return {vjs.Component} The component
*/ */
Component.prototype.dimensions = function(width, height){ dimensions(width, height){
// Skip resize listeners on width for optimization // Skip resize listeners on width for optimization
return this.width(width, true).height(height); return this.width(width, true).height(height);
}; }
/** /**
* Get or set width or height * Get or set width or height
* *
* This is the shared code for the width() and height() methods. * This is the shared code for the width() and height() methods.
@ -967,7 +857,7 @@ Component.prototype.dimensions = function(width, height){
* @return {Number|String} The dimension if nothing was set * @return {Number|String} The dimension if nothing was set
* @private * @private
*/ */
Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ dimension(widthOrHeight, num, skipListeners){
if (num !== undefined) { if (num !== undefined) {
// Set to zero if null or literally NaN (NaN !== NaN) // Set to zero if null or literally NaN (NaN !== NaN)
if (num === null || num !== num) { if (num === null || num !== num) {
@ -1021,15 +911,9 @@ Component.prototype.dimension = function(widthOrHeight, num, skipListeners){
// return val; // return val;
// } // }
} }
}; }
/** /**
* Fired when the width and/or height of the component changes
* @event resize
*/
Component.prototype.onResize;
/**
* Emit 'tap' events when touch events are supported * Emit 'tap' events when touch events are supported
* *
* This is used to support toggling the controls through a tap on the video. * This is used to support toggling the controls through a tap on the video.
@ -1039,7 +923,7 @@ Component.prototype.onResize;
* overhead is especially bad. * overhead is especially bad.
* @private * @private
*/ */
Component.prototype.emitTapEvents = function(){ emitTapEvents(){
var touchStart, firstTouch, touchTime, couldBeTap, noTap, var touchStart, firstTouch, touchTime, couldBeTap, noTap,
xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold; xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold;
@ -1106,9 +990,9 @@ Component.prototype.emitTapEvents = function(){
} }
} }
}); });
}; }
/** /**
* Report user touch activity when touch events occur * Report user touch activity when touch events occur
* *
* User activity is used to determine when controls should show/hide. It's * User activity is used to determine when controls should show/hide. It's
@ -1131,7 +1015,7 @@ Component.prototype.emitTapEvents = function(){
* whenever touch events happen, and this can be turned off by components that * whenever touch events happen, and this can be turned off by components that
* want touch events to act differently. * want touch events to act differently.
*/ */
Component.prototype.enableTouchActivity = function() { enableTouchActivity() {
var report, touchHolding, touchEnd; var report, touchHolding, touchEnd;
// Don't continue if the root player doesn't support reporting user activity // Don't continue if the root player doesn't support reporting user activity
@ -1161,15 +1045,15 @@ Component.prototype.enableTouchActivity = function() {
this.on('touchmove', report); this.on('touchmove', report);
this.on('touchend', touchEnd); this.on('touchend', touchEnd);
this.on('touchcancel', touchEnd); this.on('touchcancel', touchEnd);
}; }
/** /**
* Creates timeout and sets up disposal automatically. * Creates timeout and sets up disposal automatically.
* @param {Function} fn The function to run after the timeout. * @param {Function} fn The function to run after the timeout.
* @param {Number} timeout Number of ms to delay before executing specified function. * @param {Number} timeout Number of ms to delay before executing specified function.
* @return {Number} Returns the timeout ID * @return {Number} Returns the timeout ID
*/ */
Component.prototype.setTimeout = function(fn, timeout) { setTimeout(fn, timeout) {
fn = Lib.bind(this, fn); fn = Lib.bind(this, fn);
// window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't. // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't.
@ -1184,15 +1068,15 @@ Component.prototype.setTimeout = function(fn, timeout) {
this.on('dispose', disposeFn); this.on('dispose', disposeFn);
return timeoutId; return timeoutId;
}; }
/** /**
* Clears a timeout and removes the associated dispose listener * Clears a timeout and removes the associated dispose listener
* @param {Number} timeoutId The id of the timeout to clear * @param {Number} timeoutId The id of the timeout to clear
* @return {Number} Returns the timeout ID * @return {Number} Returns the timeout ID
*/ */
Component.prototype.clearTimeout = function(timeoutId) { clearTimeout(timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
var disposeFn = function(){}; var disposeFn = function(){};
@ -1201,15 +1085,15 @@ Component.prototype.clearTimeout = function(timeoutId) {
this.off('dispose', disposeFn); this.off('dispose', disposeFn);
return timeoutId; return timeoutId;
}; }
/** /**
* Creates an interval and sets up disposal automatically. * Creates an interval and sets up disposal automatically.
* @param {Function} fn The function to run every N seconds. * @param {Function} fn The function to run every N seconds.
* @param {Number} interval Number of ms to delay before executing specified function. * @param {Number} interval Number of ms to delay before executing specified function.
* @return {Number} Returns the interval ID * @return {Number} Returns the interval ID
*/ */
Component.prototype.setInterval = function(fn, interval) { setInterval(fn, interval) {
fn = Lib.bind(this, fn); fn = Lib.bind(this, fn);
var intervalId = setInterval(fn, interval); var intervalId = setInterval(fn, interval);
@ -1223,14 +1107,14 @@ Component.prototype.setInterval = function(fn, interval) {
this.on('dispose', disposeFn); this.on('dispose', disposeFn);
return intervalId; return intervalId;
}; }
/** /**
* Clears an interval and removes the associated dispose listener * Clears an interval and removes the associated dispose listener
* @param {Number} intervalId The id of the interval to clear * @param {Number} intervalId The id of the interval to clear
* @return {Number} Returns the interval ID * @return {Number} Returns the interval ID
*/ */
Component.prototype.clearInterval = function(intervalId) { clearInterval(intervalId) {
clearInterval(intervalId); clearInterval(intervalId);
var disposeFn = function(){}; var disposeFn = function(){};
@ -1239,25 +1123,68 @@ Component.prototype.clearInterval = function(intervalId) {
this.off('dispose', disposeFn); this.off('dispose', disposeFn);
return intervalId; return intervalId;
}; }
Component.components = {}; static registerComponent(name, comp){
if (!Component.components_) {
Component.components_ = {};
}
Component.registerComponent = function(name, comp){ Component.components_[name] = comp;
Component.components[name] = comp;
return comp; return comp;
}; }
Component.getComponent = function(name){ static getComponent(name){
if (Component.components[name]) { if (Component.components_ && Component.components_[name]) {
return Component.components[name]; return Component.components_[name];
} }
if (window && window.videojs && window.videojs[name]) { if (window && window.videojs && window.videojs[name]) {
Lib.log.warn('The '+name+' component was added to the videojs object when it should be registered using videojs.registerComponent'); Lib.log.warn('The '+name+' component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)');
return window.videojs[name]; return window.videojs[name];
} }
}; }
static extend(props){
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
let 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.
let subObj = function(){
init.apply(this, arguments);
};
// Inherit from this object's prototype
subObj.prototype = Lib.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 = Component.extend;
// Make a function for creating instances
// subObj.create = 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;
}
}
Component.registerComponent('Component', Component); Component.registerComponent('Component', Component);
export default Component; export default Component;

View File

@ -1,15 +1,22 @@
import Component from '../component'; import Component from '../component.js';
import * as Lib from '../lib'; import * as Lib from '../lib.js';
import PlayToggle from './play-toggle'; // Required children
import CurrentTimeDisplay from './time-display'; import PlayToggle from './play-toggle.js';
import LiveDisplay from './live-display'; import CurrentTimeDisplay from './current-time-display.js';
import ProgressControl from './progress-control'; import DurationDisplay from './duration-display.js';
import FullscreenToggle from './fullscreen-toggle'; import TimeDivider from './time-divider.js';
import VolumeControl from './volume-control'; import RemainingTimeDisplay from './remaining-time-display.js';
import VolumeMenuButton from './volume-menu-button'; import LiveDisplay from './live-display.js';
import MuteToggle from './mute-toggle'; import ProgressControl from './progress-control/progress-control.js';
import PlaybackRateMenuButton from './playback-rate-menu-button'; import FullscreenToggle from './fullscreen-toggle.js';
import VolumeControl from './volume-control/volume-control.js';
import VolumeMenuButton from './volume-menu-button.js';
import MuteToggle from './mute-toggle.js';
import ChaptersButton from './text-track-controls/chapters-button.js';
import SubtitlesButton from './text-track-controls/subtitles-button.js';
import CaptionsButton from './text-track-controls/captions-button.js';
import PlaybackRateMenuButton from './playback-rate-menu/playback-rate-menu-button.js';
/** /**
* Container of main controls * Container of main controls
@ -19,9 +26,13 @@ import PlaybackRateMenuButton from './playback-rate-menu-button';
* @constructor * @constructor
* @extends vjs.Component * @extends vjs.Component
*/ */
var ControlBar = Component.extend(); class ControlBar extends Component {
createEl() {
Component.registerComponent('ControlBar', ControlBar); return Lib.createEl('div', {
className: 'vjs-control-bar'
});
}
}
ControlBar.prototype.options_ = { ControlBar.prototype.options_ = {
loadEvent: 'play', loadEvent: 'play',
@ -44,8 +55,5 @@ ControlBar.prototype.options_ = {
} }
}; };
ControlBar.prototype.createEl = function(){ Component.registerComponent('ControlBar', ControlBar);
return Lib.createEl('div', { export default ControlBar;
className: 'vjs-control-bar'
});
};

View File

@ -0,0 +1,42 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
/**
* Displays the current time
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class CurrentTimeDisplay extends Component {
constructor(player, options){
super(player, options);
this.on(player, 'timeupdate', this.updateContent);
}
createEl() {
let el = super.createEl('div', {
className: 'vjs-current-time vjs-time-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-current-time-display',
innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00', // label the current time for screen reader users
'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
});
el.appendChild(this.contentEl_);
return el;
}
updateContent() {
// Allows for smooth scrubbing, when player can't keep up.
let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Current Time') + '</span> ' + Lib.formatTime(time, this.player_.duration());
}
}
Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
export default CurrentTimeDisplay;

View File

@ -0,0 +1,48 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
/**
* Displays the duration
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class DurationDisplay extends Component {
constructor(player, options){
super(player, options);
// this might need to be changed to 'durationchange' instead of 'timeupdate' eventually,
// however the durationchange event fires before this.player_.duration() is set,
// so the value cannot be written out using this method.
// Once the order of durationchange and this.player_.duration() being set is figured out,
// this can be updated.
this.on(player, 'timeupdate', this.updateContent);
}
createEl() {
let el = super.createEl('div', {
className: 'vjs-duration vjs-time-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-duration-display',
innerHTML: '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + '0:00', // label the duration time for screen reader users
'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
});
el.appendChild(this.contentEl_);
return el;
}
updateContent() {
let duration = this.player_.duration();
if (duration) {
this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + Lib.formatTime(duration); // label the duration time for screen reader users
}
}
}
Component.registerComponent('DurationDisplay', DurationDisplay);
export default DurationDisplay;

View File

@ -1,4 +1,3 @@
import Component from '../component';
import Button from '../button'; import Button from '../button';
/** /**
@ -8,26 +7,13 @@ import Button from '../button';
* @class * @class
* @extends vjs.Button * @extends vjs.Button
*/ */
var FullscreenToggle = Button.extend({ class FullscreenToggle extends Button {
/**
* @constructor buildCSSClass() {
* @memberof vjs.FullscreenToggle return 'vjs-fullscreen-control ' + super.buildCSSClass();
* @instance
*/
init: function(player, options){
Button.call(this, player, options);
} }
});
Component.registerComponent('FullscreenToggle', FullscreenToggle); onClick() {
FullscreenToggle.prototype.buttonText = 'Fullscreen';
FullscreenToggle.prototype.buildCSSClass = function(){
return 'vjs-fullscreen-control ' + Button.prototype.buildCSSClass.call(this);
};
FullscreenToggle.prototype.onClick = function(){
if (!this.player_.isFullscreen()) { if (!this.player_.isFullscreen()) {
this.player_.requestFullscreen(); this.player_.requestFullscreen();
this.controlText_.innerHTML = this.localize('Non-Fullscreen'); this.controlText_.innerHTML = this.localize('Non-Fullscreen');
@ -35,6 +21,11 @@ FullscreenToggle.prototype.onClick = function(){
this.player_.exitFullscreen(); this.player_.exitFullscreen();
this.controlText_.innerHTML = this.localize('Fullscreen'); this.controlText_.innerHTML = this.localize('Fullscreen');
} }
}; }
}
FullscreenToggle.prototype.buttonText = 'Fullscreen';
Button.registerComponent('FullscreenToggle', FullscreenToggle);
export default FullscreenToggle; export default FullscreenToggle;

View File

@ -8,16 +8,10 @@ import * as Lib from '../lib';
* @param {Object=} options * @param {Object=} options
* @constructor * @constructor
*/ */
var LiveDisplay = Component.extend({ class LiveDisplay extends Component {
init: function(player, options){
Component.call(this, player, options);
}
});
Component.registerComponent('LiveDisplay', LiveDisplay); createEl() {
var el = super.createEl('div', {
LiveDisplay.prototype.createEl = function(){
var el = Component.prototype.createEl.call(this, 'div', {
className: 'vjs-live-controls vjs-control' className: 'vjs-live-controls vjs-control'
}); });
@ -30,6 +24,9 @@ LiveDisplay.prototype.createEl = function(){
el.appendChild(this.contentEl_); el.appendChild(this.contentEl_);
return el; return el;
}; }
}
Component.registerComponent('LiveDisplay', LiveDisplay);
export default LiveDisplay; export default LiveDisplay;

View File

@ -9,10 +9,10 @@ import * as Lib from '../lib';
* @param {Object=} options * @param {Object=} options
* @constructor * @constructor
*/ */
var MuteToggle = Button.extend({ class MuteToggle extends Button {
/** @constructor */
init: function(player, options){ constructor(player, options) {
Button.call(this, player, options); super(player, options);
this.on(player, 'volumechange', this.update); this.on(player, 'volumechange', this.update);
@ -29,20 +29,19 @@ var MuteToggle = Button.extend({
} }
}); });
} }
});
MuteToggle.prototype.createEl = function(){ createEl() {
return Button.prototype.createEl.call(this, 'div', { return super.createEl('div', {
className: 'vjs-mute-control vjs-control', className: 'vjs-mute-control vjs-control',
innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>' innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>'
}); });
}; }
MuteToggle.prototype.onClick = function(){ onClick() {
this.player_.muted( this.player_.muted() ? false : true ); this.player_.muted( this.player_.muted() ? false : true );
}; }
MuteToggle.prototype.update = function(){ update() {
var vol = this.player_.volume(), var vol = this.player_.volume(),
level = 3; level = 3;
@ -57,14 +56,10 @@ MuteToggle.prototype.update = function(){
// Don't rewrite the button text if the actual text doesn't change. // Don't rewrite the button text if the actual text doesn't change.
// This causes unnecessary and confusing information for screen reader users. // This causes unnecessary and confusing information for screen reader users.
// This check is needed because this function gets called every time the volume level is changed. // This check is needed because this function gets called every time the volume level is changed.
if(this.player_.muted()){ let toMute = this.player_.muted() ? 'Unmute' : 'Mute';
if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ let localizedMute = this.localize(toMute);
this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" if (this.el_.children[0].children[0].innerHTML !== localizedMute) {
} this.el_.children[0].children[0].innerHTML = localizedMute;
} else {
if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){
this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute"
}
} }
/* TODO improve muted icon classes */ /* TODO improve muted icon classes */
@ -72,7 +67,9 @@ MuteToggle.prototype.update = function(){
Lib.removeClass(this.el_, 'vjs-vol-'+i); Lib.removeClass(this.el_, 'vjs-vol-'+i);
} }
Lib.addClass(this.el_, 'vjs-vol-'+level); Lib.addClass(this.el_, 'vjs-vol-'+level);
}; }
}
Component.registerComponent('MuteToggle', MuteToggle); Component.registerComponent('MuteToggle', MuteToggle);
export default MuteToggle; export default MuteToggle;

View File

@ -1,5 +1,4 @@
import Button from '../button'; import Button from '../button';
import Component from '../component';
import * as Lib from '../lib'; import * as Lib from '../lib';
/** /**
@ -9,45 +8,45 @@ import * as Lib from '../lib';
* @class * @class
* @constructor * @constructor
*/ */
var PlayToggle = Button.extend({ class PlayToggle extends Button {
/** @constructor */
init: function(player, options){ constructor(player, options){
Button.call(this, player, options); super(player, options);
this.on(player, 'play', this.onPlay); this.on(player, 'play', this.onPlay);
this.on(player, 'pause', this.onPause); this.on(player, 'pause', this.onPause);
} }
});
Component.registerComponent('PlayToggle', PlayToggle); buildCSSClass() {
return 'vjs-play-control ' + super.buildCSSClass();
}
PlayToggle.prototype.buttonText = 'Play'; // OnClick - Toggle between play and pause
onClick() {
PlayToggle.prototype.buildCSSClass = function(){
return 'vjs-play-control ' + Button.prototype.buildCSSClass.call(this);
};
// OnClick - Toggle between play and pause
PlayToggle.prototype.onClick = function(){
if (this.player_.paused()) { if (this.player_.paused()) {
this.player_.play(); this.player_.play();
} else { } else {
this.player_.pause(); this.player_.pause();
} }
}; }
// OnPlay - Add the vjs-playing class to the element so it can change appearance // OnPlay - Add the vjs-playing class to the element so it can change appearance
PlayToggle.prototype.onPlay = function(){ onPlay() {
this.removeClass('vjs-paused'); this.removeClass('vjs-paused');
this.addClass('vjs-playing'); this.addClass('vjs-playing');
this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause"
}; }
// OnPause - Add the vjs-paused class to the element so it can change appearance // OnPause - Add the vjs-paused class to the element so it can change appearance
PlayToggle.prototype.onPause = function(){ onPause() {
this.removeClass('vjs-playing'); this.removeClass('vjs-playing');
this.addClass('vjs-paused'); this.addClass('vjs-paused');
this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play"
}; }
}
PlayToggle.prototype.buttonText = 'Play';
Button.registerComponent('PlayToggle', PlayToggle);
export default PlayToggle; export default PlayToggle;

View File

@ -1,139 +0,0 @@
import Component from '../component';
import Menu, { MenuButton, MenuItem } from '../menu';
import * as Lib from '../lib';
/**
* The component for controlling the playback rate
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let PlaybackRateMenuButton = MenuButton.extend({
/** @constructor */
init: function(player, options){
MenuButton.call(this, player, options);
this.updateVisibility();
this.updateLabel();
this.on(player, 'loadstart', this.updateVisibility);
this.on(player, 'ratechange', this.updateLabel);
}
});
PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate';
PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate';
PlaybackRateMenuButton.prototype.createEl = function(){
let el = MenuButton.prototype.createEl.call(this);
this.labelEl_ = Lib.createEl('div', {
className: 'vjs-playback-rate-value',
innerHTML: 1.0
});
el.appendChild(this.labelEl_);
return el;
};
// Menu creation
PlaybackRateMenuButton.prototype.createMenu = function(){
let menu = new Menu(this.player());
let rates = this.player().options()['playbackRates'];
if (rates) {
for (let i = rates.length - 1; i >= 0; i--) {
menu.addChild(
new PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'})
);
}
}
return menu;
};
PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){
// Current playback rate
this.el().setAttribute('aria-valuenow', this.player().playbackRate());
};
PlaybackRateMenuButton.prototype.onClick = function(){
// select next rate option
let currentRate = this.player().playbackRate();
let rates = this.player().options()['playbackRates'];
// this will select first one if the last one currently selected
let newRate = rates[0];
for (let i = 0; i <rates.length ; i++) {
if (rates[i] > currentRate) {
newRate = rates[i];
break;
}
}
this.player().playbackRate(newRate);
};
PlaybackRateMenuButton.prototype.playbackRateSupported = function(){
return this.player().tech
&& this.player().tech['featuresPlaybackRate']
&& this.player().options()['playbackRates']
&& this.player().options()['playbackRates'].length > 0
;
};
/**
* Hide playback rate controls when they're no playback rate options to select
*/
PlaybackRateMenuButton.prototype.updateVisibility = function(){
if (this.playbackRateSupported()) {
this.removeClass('vjs-hidden');
} else {
this.addClass('vjs-hidden');
}
};
/**
* Update button label when rate changed
*/
PlaybackRateMenuButton.prototype.updateLabel = function(){
if (this.playbackRateSupported()) {
this.labelEl_.innerHTML = this.player().playbackRate() + 'x';
}
};
/**
* The specific menu item type for selecting a playback rate
*
* @constructor
*/
var PlaybackRateMenuItem = MenuItem.extend({
contentElType: 'button',
/** @constructor */
init: function(player, options){
let label = this.label = options['rate'];
let rate = this.rate = parseFloat(label, 10);
// Modify options for parent MenuItem class's init.
options['label'] = label;
options['selected'] = rate === 1;
MenuItem.call(this, player, options);
this.on(player, 'ratechange', this.update);
}
});
Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
PlaybackRateMenuItem.prototype.onClick = function(){
MenuItem.prototype.onClick.call(this);
this.player().playbackRate(this.rate);
};
PlaybackRateMenuItem.prototype.update = function(){
this.selected(this.player().playbackRate() == this.rate);
};
Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
export default PlaybackRateMenuButton;
export { PlaybackRateMenuItem };

View File

@ -0,0 +1,108 @@
import MenuButton from '../../menu/menu-button.js';
import Menu from '../../menu/menu.js';
import PlaybackRateMenuItem from './playback-rate-menu-item.js';
import * as Lib from '../../lib.js';
/**
* The component for controlling the playback rate
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class PlaybackRateMenuButton extends MenuButton {
constructor(player, options){
super(player, options);
this.updateVisibility();
this.updateLabel();
this.on(player, 'loadstart', this.updateVisibility);
this.on(player, 'ratechange', this.updateLabel);
}
createEl() {
let el = super.createEl();
this.labelEl_ = Lib.createEl('div', {
className: 'vjs-playback-rate-value',
innerHTML: 1.0
});
el.appendChild(this.labelEl_);
return el;
}
// Menu creation
createMenu() {
let menu = new Menu(this.player());
let rates = this.player().options()['playbackRates'];
if (rates) {
for (let i = rates.length - 1; i >= 0; i--) {
menu.addChild(
new PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'})
);
}
}
return menu;
}
updateARIAAttributes() {
// Current playback rate
this.el().setAttribute('aria-valuenow', this.player().playbackRate());
}
onClick() {
// select next rate option
let currentRate = this.player().playbackRate();
let rates = this.player().options()['playbackRates'];
// this will select first one if the last one currently selected
let newRate = rates[0];
for (let i = 0; i <rates.length ; i++) {
if (rates[i] > currentRate) {
newRate = rates[i];
break;
}
}
this.player().playbackRate(newRate);
}
playbackRateSupported() {
return this.player().tech
&& this.player().tech['featuresPlaybackRate']
&& this.player().options()['playbackRates']
&& this.player().options()['playbackRates'].length > 0
;
}
/**
* Hide playback rate controls when they're no playback rate options to select
*/
updateVisibility() {
if (this.playbackRateSupported()) {
this.removeClass('vjs-hidden');
} else {
this.addClass('vjs-hidden');
}
}
/**
* Update button label when rate changed
*/
updateLabel() {
if (this.playbackRateSupported()) {
this.labelEl_.innerHTML = this.player().playbackRate() + 'x';
}
}
}
PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate';
PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate';
MenuButton.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
export default PlaybackRateMenuButton;

View File

@ -0,0 +1,39 @@
import MenuItem from '../../menu/menu-item.js';
/**
* The specific menu item type for selecting a playback rate
*
* @constructor
*/
class PlaybackRateMenuItem extends MenuItem {
constructor(player, options){
let label = options['rate'];
let rate = parseFloat(label, 10);
// Modify options for parent MenuItem class's init.
options['label'] = label;
options['selected'] = rate === 1;
super(player, options);
this.label = label;
this.rate = rate;
this.on(player, 'ratechange', this.update);
}
onClick() {
super.onClick();
this.player().playbackRate(this.rate);
}
update() {
this.selected(this.player().playbackRate() == this.rate);
}
}
PlaybackRateMenuItem.prototype.contentElType = 'button';
MenuItem.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
export default PlaybackRateMenuItem;

View File

@ -1,242 +0,0 @@
import Component from '../component';
import Slider, { SliderHandle } from '../slider';
import * as Lib from '../lib';
/**
* The Progress Control component contains the seek bar, load progress,
* and play progress
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let ProgressControl = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
}
});
Component.registerComponent('ProgressControl', ProgressControl);
ProgressControl.prototype.options_ = {
children: {
'seekBar': {}
}
};
ProgressControl.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-progress-control vjs-control'
});
};
/**
* Seek Bar and holder for the progress bars
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var SeekBar = Slider.extend({
/** @constructor */
init: function(player, options){
Slider.call(this, player, options);
this.on(player, 'timeupdate', this.updateARIAAttributes);
player.ready(Lib.bind(this, this.updateARIAAttributes));
}
});
Component.registerComponent('SeekBar', SeekBar);
SeekBar.prototype.options_ = {
children: {
'loadProgressBar': {},
'playProgressBar': {},
'seekHandle': {}
},
'barName': 'playProgressBar',
'handleName': 'seekHandle'
};
SeekBar.prototype.playerEvent = 'timeupdate';
SeekBar.prototype.createEl = function(){
return Slider.prototype.createEl.call(this, 'div', {
className: 'vjs-progress-holder',
'aria-label': 'video progress bar'
});
};
SeekBar.prototype.updateARIAAttributes = function(){
// Allows for smooth scrubbing, when player can't keep up.
let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
this.el_.setAttribute('aria-valuenow', Lib.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete)
this.el_.setAttribute('aria-valuetext', Lib.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete)
};
SeekBar.prototype.getPercent = function(){
return this.player_.currentTime() / this.player_.duration();
};
SeekBar.prototype.onMouseDown = function(event){
Slider.prototype.onMouseDown.call(this, event);
this.player_.scrubbing = true;
this.player_.addClass('vjs-scrubbing');
this.videoWasPlaying = !this.player_.paused();
this.player_.pause();
};
SeekBar.prototype.onMouseMove = function(event){
let newTime = this.calculateDistance(event) * this.player_.duration();
// Don't let video end while scrubbing.
if (newTime == this.player_.duration()) { newTime = newTime - 0.1; }
// Set new time (tell player to seek to new time)
this.player_.currentTime(newTime);
};
SeekBar.prototype.onMouseUp = function(event){
Slider.prototype.onMouseUp.call(this, event);
this.player_.scrubbing = false;
this.player_.removeClass('vjs-scrubbing');
if (this.videoWasPlaying) {
this.player_.play();
}
};
SeekBar.prototype.stepForward = function(){
this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users
};
SeekBar.prototype.stepBack = function(){
this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users
};
/**
* Shows load progress
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var LoadProgressBar = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
this.on(player, 'progress', this.update);
}
});
Component.registerComponent('LoadProgressBar', LoadProgressBar);
LoadProgressBar.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-load-progress',
innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>'
});
};
LoadProgressBar.prototype.update = function(){
let buffered = this.player_.buffered();
let duration = this.player_.duration();
let bufferedEnd = this.player_.bufferedEnd();
let children = this.el_.children;
// get the percent width of a time compared to the total end
let percentify = function (time, end){
let percent = (time / end) || 0; // no NaN
return (percent * 100) + '%';
};
// update the width of the progress bar
this.el_.style.width = percentify(bufferedEnd, duration);
// add child elements to represent the individual buffered time ranges
for (let i = 0; i < buffered.length; i++) {
let start = buffered.start(i);
let end = buffered.end(i);
let part = children[i];
if (!part) {
part = this.el_.appendChild(Lib.createEl());
}
// set the percent based on the width of the progress bar (bufferedEnd)
part.style.left = percentify(start, bufferedEnd);
part.style.width = percentify(end - start, bufferedEnd);
}
// remove unused buffered range elements
for (let i = children.length; i > buffered.length; i--) {
this.el_.removeChild(children[i-1]);
}
};
/**
* Shows play progress
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var PlayProgressBar = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
}
});
Component.registerComponent('PlayProgressBar', PlayProgressBar);
PlayProgressBar.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-play-progress',
innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>'
});
};
/**
* The Seek Handle shows the current position of the playhead during playback,
* and can be dragged to adjust the playhead.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var SeekHandle = SliderHandle.extend({
init: function(player, options) {
SliderHandle.call(this, player, options);
this.on(player, 'timeupdate', this.updateContent);
}
});
Component.registerComponent('SeekHandle', SeekHandle);
/**
* The default value for the handle content, which may be read by screen readers
*
* @type {String}
* @private
*/
SeekHandle.prototype.defaultValue = '00:00';
/** @inheritDoc */
SeekHandle.prototype.createEl = function() {
return SliderHandle.prototype.createEl.call(this, 'div', {
className: 'vjs-seek-handle',
'aria-live': 'off'
});
};
SeekHandle.prototype.updateContent = function() {
let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
this.el_.innerHTML = '<span class="vjs-control-text">' + Lib.formatTime(time, this.player_.duration()) + '</span>';
};
export default ProgressControl;
export { SeekBar, LoadProgressBar, PlayProgressBar, SeekHandle };

View File

@ -0,0 +1,64 @@
import Component from '../../component.js';
import * as Lib from '../../lib.js';
/**
* Shows load progress
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class LoadProgressBar extends Component {
constructor(player, options){
super(player, options);
this.on(player, 'progress', this.update);
}
createEl() {
return super.createEl('div', {
className: 'vjs-load-progress',
innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>'
});
}
update() {
let buffered = this.player_.buffered();
let duration = this.player_.duration();
let bufferedEnd = this.player_.bufferedEnd();
let children = this.el_.children;
// get the percent width of a time compared to the total end
let percentify = function (time, end){
let percent = (time / end) || 0; // no NaN
return (percent * 100) + '%';
};
// update the width of the progress bar
this.el_.style.width = percentify(bufferedEnd, duration);
// add child elements to represent the individual buffered time ranges
for (let i = 0; i < buffered.length; i++) {
let start = buffered.start(i);
let end = buffered.end(i);
let part = children[i];
if (!part) {
part = this.el_.appendChild(Lib.createEl());
}
// set the percent based on the width of the progress bar (bufferedEnd)
part.style.left = percentify(start, bufferedEnd);
part.style.width = percentify(end - start, bufferedEnd);
}
// remove unused buffered range elements
for (let i = children.length; i > buffered.length; i--) {
this.el_.removeChild(children[i-1]);
}
}
}
Component.registerComponent('LoadProgressBar', LoadProgressBar);
export default LoadProgressBar;

View File

@ -0,0 +1,22 @@
import Component from '../../component.js';
/**
* Shows play progress
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class PlayProgressBar extends Component {
createEl() {
return super.createEl('div', {
className: 'vjs-play-progress',
innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>'
});
}
}
Component.registerComponent('PlayProgressBar', PlayProgressBar);
export default PlayProgressBar;

View File

@ -0,0 +1,27 @@
import Component from '../../component.js';
import SeekBar from './seek-bar.js';
/**
* The Progress Control component contains the seek bar, load progress,
* and play progress
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class ProgressControl extends Component {
createEl() {
return super.createEl('div', {
className: 'vjs-progress-control vjs-control'
});
}
}
ProgressControl.prototype.options_ = {
children: {
'seekBar': {}
}
};
Component.registerComponent('ProgressControl', ProgressControl);
export default ProgressControl;

View File

@ -0,0 +1,93 @@
import Slider from '../../slider/slider.js';
import LoadProgressBar from './load-progress-bar.js';
import PlayProgressBar from './play-progress-bar.js';
import SeekHandle from './seek-handle.js';
import * as Lib from '../../lib.js';
/**
* Seek Bar and holder for the progress bars
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class SeekBar extends Slider {
constructor(player, options){
super(player, options);
this.on(player, 'timeupdate', this.updateARIAAttributes);
player.ready(Lib.bind(this, this.updateARIAAttributes));
}
createEl() {
return super.createEl('div', {
className: 'vjs-progress-holder',
'aria-label': 'video progress bar'
});
}
updateARIAAttributes() {
// Allows for smooth scrubbing, when player can't keep up.
let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
this.el_.setAttribute('aria-valuenow', Lib.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete)
this.el_.setAttribute('aria-valuetext', Lib.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete)
}
getPercent() {
return this.player_.currentTime() / this.player_.duration();
}
onMouseDown(event) {
super.onMouseDown(event);
this.player_.scrubbing = true;
this.player_.addClass('vjs-scrubbing');
this.videoWasPlaying = !this.player_.paused();
this.player_.pause();
}
onMouseMove(event) {
let newTime = this.calculateDistance(event) * this.player_.duration();
// Don't let video end while scrubbing.
if (newTime == this.player_.duration()) { newTime = newTime - 0.1; }
// Set new time (tell player to seek to new time)
this.player_.currentTime(newTime);
}
onMouseUp(event) {
super.onMouseUp(event);
this.player_.scrubbing = false;
this.player_.removeClass('vjs-scrubbing');
if (this.videoWasPlaying) {
this.player_.play();
}
}
stepForward() {
this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users
}
stepBack() {
this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users
}
}
SeekBar.prototype.options_ = {
children: {
'loadProgressBar': {},
'playProgressBar': {},
'seekHandle': {}
},
'barName': 'playProgressBar',
'handleName': 'seekHandle'
};
SeekBar.prototype.playerEvent = 'timeupdate';
Slider.registerComponent('SeekBar', SeekBar);
export default SeekBar;

View File

@ -0,0 +1,43 @@
import SliderHandle from '../../slider/slider-handle.js';
import * as Lib from '../../lib.js';
/**
* The Seek Handle shows the current position of the playhead during playback,
* and can be dragged to adjust the playhead.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class SeekHandle extends SliderHandle {
constructor(player, options) {
super(player, options);
this.on(player, 'timeupdate', this.updateContent);
}
/** @inheritDoc */
createEl() {
return super.createEl.call('div', {
className: 'vjs-seek-handle',
'aria-live': 'off'
});
}
updateContent() {
let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
this.el_.innerHTML = '<span class="vjs-control-text">' + Lib.formatTime(time, this.player_.duration()) + '</span>';
}
}
/**
* The default value for the handle content, which may be read by screen readers
*
* @type {String}
* @private
*/
SeekHandle.prototype.defaultValue = '00:00';
SliderHandle.registerComponent('SeekHandle', SeekHandle);
export default SeekHandle;

View File

@ -0,0 +1,46 @@
import Component from '../component.js';
import * as Lib from '../lib';
/**
* Displays the time left in the video
* @param {Player|Object} player
* @param {Object=} options
* @constructor
*/
class RemainingTimeDisplay extends Component {
constructor(player, options){
super(player, options);
this.on(player, 'timeupdate', this.updateContent);
}
createEl() {
let el = super.createEl('div', {
className: 'vjs-remaining-time vjs-time-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-remaining-time-display',
innerHTML: '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-0:00', // label the remaining time for screen reader users
'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
});
el.appendChild(this.contentEl_);
return el;
}
updateContent() {
if (this.player_.duration()) {
this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-'+ Lib.formatTime(this.player_.remainingTime());
}
// Allows for smooth scrubbing, when player can't keep up.
// var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
// this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration());
}
}
Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
export default RemainingTimeDisplay;

View File

@ -0,0 +1,25 @@
import TextTrackMenuItem from './text-track-menu-item.js';
class CaptionSettingsMenuItem extends TextTrackMenuItem {
constructor(player, options) {
options['track'] = {
'kind': options['kind'],
'player': player,
'label': options['kind'] + ' settings',
'default': false,
mode: 'disabled'
};
super(player, options);
this.addClass('vjs-texttrack-settings');
}
onClick() {
this.player().getChild('textTrackSettings').show();
}
}
TextTrackMenuItem.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
export default CaptionSettingsMenuItem;

View File

@ -0,0 +1,49 @@
import TextTrackButton from './text-track-button.js';
import CaptionSettingsMenuItem from './caption-settings-menu-item.js';
/**
* The button component for toggling and selecting captions
*
* @constructor
*/
class CaptionsButton extends TextTrackButton {
constructor(player, options, ready){
super(player, options, ready);
this.el_.setAttribute('aria-label','Captions Menu');
}
update() {
let threshold = 2;
super.update();
// if native, then threshold is 1 because no settings button
if (this.player().tech && this.player().tech['featuresNativeTextTracks']) {
threshold = 1;
}
if (this.items && this.items.length > threshold) {
this.show();
} else {
this.hide();
}
}
createItems() {
let items = [];
if (!(this.player().tech && this.player().tech['featuresNativeTextTracks'])) {
items.push(new CaptionSettingsMenuItem(this.player_, { 'kind': this.kind_ }));
}
return super.createItems(items);
}
}
CaptionsButton.prototype.kind_ = 'captions';
CaptionsButton.prototype.buttonText = 'Captions';
CaptionsButton.prototype.className = 'vjs-captions-button';
TextTrackButton.registerComponent('CaptionsButton', CaptionsButton);
export default CaptionsButton;

View File

@ -0,0 +1,109 @@
import TextTrackButton from './text-track-button.js';
import TextTrackMenuItem from './text-track-menu-item.js';
import ChaptersTrackMenuItem from './chapters-track-menu-item.js';
import Menu from '../../menu/menu.js';
import * as Lib from '../../lib.js';
import window from 'global/window';
// Chapters act much differently than other text tracks
// Cues are navigation vs. other tracks of alternative languages
/**
* The button component for toggling and selecting chapters
*
* @constructor
*/
class ChaptersButton extends TextTrackButton {
constructor(player, options, ready){
super(player, options, ready);
this.el_.setAttribute('aria-label','Chapters Menu');
}
// Create a menu item for each text track
createItems() {
let items = [];
let tracks = this.player_.textTracks();
if (!tracks) {
return items;
}
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
if (track['kind'] === this.kind_) {
items.push(new TextTrackMenuItem(this.player_, {
'track': track
}));
}
}
return items;
}
createMenu() {
let tracks = this.player_.textTracks() || [];
let chaptersTrack;
let items = this.items = [];
for (let i = 0, l = tracks.length; i < l; i++) {
let track = tracks[i];
if (track['kind'] == this.kind_) {
if (!track.cues) {
track['mode'] = 'hidden';
/* jshint loopfunc:true */
// TODO see if we can figure out a better way of doing this https://github.com/videojs/video.js/issues/1864
window.setTimeout(Lib.bind(this, function() {
this.createMenu();
}), 100);
/* jshint loopfunc:false */
} else {
chaptersTrack = track;
break;
}
}
}
let menu = this.menu;
if (menu === undefined) {
menu = new Menu(this.player_);
menu.contentEl().appendChild(Lib.createEl('li', {
className: 'vjs-menu-title',
innerHTML: Lib.capitalize(this.kind_),
tabindex: -1
}));
}
if (chaptersTrack) {
let cues = chaptersTrack['cues'], cue;
for (let i = 0, l = cues.length; i < l; i++) {
cue = cues[i];
let mi = new ChaptersTrackMenuItem(this.player_, {
'track': chaptersTrack,
'cue': cue
});
items.push(mi);
menu.addChild(mi);
}
this.addChild(menu);
}
if (this.items.length > 0) {
this.show();
}
return menu;
}
}
ChaptersButton.prototype.kind_ = 'chapters';
ChaptersButton.prototype.buttonText = 'Chapters';
ChaptersButton.prototype.className = 'vjs-chapters-button';
TextTrackButton.registerComponent('ChaptersButton', ChaptersButton);
export default ChaptersButton;

View File

@ -0,0 +1,41 @@
import MenuItem from '../../menu/menu-item.js';
import * as Lib from '../../lib.js';
/**
* @constructor
*/
class ChaptersTrackMenuItem extends MenuItem {
constructor(player, options){
let track = options['track'];
let cue = options['cue'];
let currentTime = player.currentTime();
// Modify options for parent MenuItem class's init.
options['label'] = cue.text;
options['selected'] = (cue['startTime'] <= currentTime && currentTime < cue['endTime']);
super(player, options);
this.track = track;
this.cue = cue;
track.addEventListener('cuechange', Lib.bind(this, this.update));
}
onClick() {
super.onClick();
this.player_.currentTime(this.cue.startTime);
this.update(this.cue.startTime);
}
update() {
let cue = this.cue;
let currentTime = this.player_.currentTime();
// vjs.log(currentTime, cue.startTime);
this.selected(cue['startTime'] <= currentTime && currentTime < cue['endTime']);
}
}
MenuItem.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
export default ChaptersTrackMenuItem;

View File

@ -0,0 +1,43 @@
import TextTrackMenuItem from './text-track-menu-item.js';
/**
* A special menu item for turning of a specific type of text track
*
* @constructor
*/
class OffTextTrackMenuItem extends TextTrackMenuItem {
constructor(player, options){
// Create pseudo track info
// Requires options['kind']
options['track'] = {
'kind': options['kind'],
'player': player,
'label': options['kind'] + ' off',
'default': false,
'mode': 'disabled'
};
super(player, options);
this.selected(true);
}
handleTracksChange(event){
let tracks = this.player().textTracks();
let selected = true;
for (let i = 0, l = tracks.length; i < l; i++) {
let track = tracks[i];
if (track['kind'] === this.track['kind'] && track['mode'] === 'showing') {
selected = false;
break;
}
}
this.selected(selected);
}
}
TextTrackMenuItem.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
export default OffTextTrackMenuItem;

View File

@ -0,0 +1,22 @@
import TextTrackButton from './text-track-button.js';
/**
* The button component for toggling and selecting subtitles
*
* @constructor
*/
class SubtitlesButton extends TextTrackButton {
constructor(player, options, ready){
super(player, options, ready);
this.el_.setAttribute('aria-label','Subtitles Menu');
}
}
SubtitlesButton.prototype.kind_ = 'subtitles';
SubtitlesButton.prototype.buttonText = 'Subtitles';
SubtitlesButton.prototype.className = 'vjs-subtitles-button';
TextTrackButton.registerComponent('SubtitlesButton', SubtitlesButton);
export default SubtitlesButton;

View File

@ -0,0 +1,65 @@
import MenuButton from '../../menu/menu-button.js';
import * as Lib from '../../lib.js';
import TextTrackMenuItem from './text-track-menu-item.js';
import OffTextTrackMenuItem from './off-text-track-menu-item.js';
/**
* The base class for buttons that toggle specific text track types (e.g. subtitles)
*
* @constructor
*/
class TextTrackButton extends MenuButton {
constructor(player, options){
super(player, options);
let tracks = this.player_.textTracks();
if (this.items.length <= 1) {
this.hide();
}
if (!tracks) {
return;
}
let updateHandler = Lib.bind(this, this.update);
tracks.addEventListener('removetrack', updateHandler);
tracks.addEventListener('addtrack', updateHandler);
this.player_.on('dispose', function() {
tracks.removeEventListener('removetrack', updateHandler);
tracks.removeEventListener('addtrack', updateHandler);
});
}
// Create a menu item for each text track
createItems(items=[]) {
// Add an OFF menu item to turn all tracks off
items.push(new OffTextTrackMenuItem(this.player_, { 'kind': this.kind_ }));
let tracks = this.player_.textTracks();
if (!tracks) {
return items;
}
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
// only add tracks that are of the appropriate kind and have a label
if (track['kind'] === this.kind_) {
items.push(new TextTrackMenuItem(this.player_, {
'track': track
}));
}
}
return items;
}
}
MenuButton.registerComponent('TextTrackButton', TextTrackButton);
export default TextTrackButton;

View File

@ -0,0 +1,91 @@
import MenuItem from '../../menu/menu-item.js';
import * as Lib from '../../lib.js';
import window from 'global/window';
import document from 'global/document';
/**
* The specific menu item type for selecting a language within a text track kind
*
* @constructor
*/
class TextTrackMenuItem extends MenuItem {
constructor(player, options){
let track = options['track'];
let tracks = player.textTracks();
// Modify options for parent MenuItem class's init.
options['label'] = track['label'] || track['language'] || 'Unknown';
options['selected'] = track['default'] || track['mode'] === 'showing';
super(player, options);
this.track = track;
if (tracks) {
let changeHandler = Lib.bind(this, this.handleTracksChange);
tracks.addEventListener('change', changeHandler);
this.on('dispose', function() {
tracks.removeEventListener('change', changeHandler);
});
}
// iOS7 doesn't dispatch change events to TextTrackLists when an
// associated track's mode changes. Without something like
// Object.observe() (also not present on iOS7), it's not
// possible to detect changes to the mode attribute and polyfill
// the change event. As a poor substitute, we manually dispatch
// change events whenever the controls modify the mode.
if (tracks && tracks.onchange === undefined) {
let event;
this.on(['tap', 'click'], function() {
if (typeof window.Event !== 'object') {
// Android 2.3 throws an Illegal Constructor error for window.Event
try {
event = new window.Event('change');
} catch(err){}
}
if (!event) {
event = document.createEvent('Event');
event.initEvent('change', true, true);
}
tracks.dispatchEvent(event);
});
}
}
onClick(event) {
let kind = this.track['kind'];
let tracks = this.player_.textTracks();
super.onClick(event);
if (!tracks) return;
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
if (track['kind'] !== kind) {
continue;
}
if (track === this.track) {
track['mode'] = 'showing';
} else {
track['mode'] = 'disabled';
}
}
}
handleTracksChange(event){
this.selected(this.track['mode'] === 'showing');
}
}
MenuItem.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
export default TextTrackMenuItem;

View File

@ -1,154 +0,0 @@
import Component from '../component';
import * as Lib from '../lib';
/**
* Displays the current time
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let CurrentTimeDisplay = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
this.on(player, 'timeupdate', this.updateContent);
}
});
Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
CurrentTimeDisplay.prototype.createEl = function(){
let el = Component.prototype.createEl.call(this, 'div', {
className: 'vjs-current-time vjs-time-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-current-time-display',
innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00', // label the current time for screen reader users
'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
});
el.appendChild(this.contentEl_);
return el;
};
CurrentTimeDisplay.prototype.updateContent = function(){
// Allows for smooth scrubbing, when player can't keep up.
let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Current Time') + '</span> ' + Lib.formatTime(time, this.player_.duration());
};
/**
* Displays the duration
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var DurationDisplay = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
// this might need to be changed to 'durationchange' instead of 'timeupdate' eventually,
// however the durationchange event fires before this.player_.duration() is set,
// so the value cannot be written out using this method.
// Once the order of durationchange and this.player_.duration() being set is figured out,
// this can be updated.
this.on(player, 'timeupdate', this.updateContent);
}
});
Component.registerComponent('DurationDisplay', DurationDisplay);
DurationDisplay.prototype.createEl = function(){
let el = Component.prototype.createEl.call(this, 'div', {
className: 'vjs-duration vjs-time-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-duration-display',
innerHTML: '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + '0:00', // label the duration time for screen reader users
'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
});
el.appendChild(this.contentEl_);
return el;
};
DurationDisplay.prototype.updateContent = function(){
let duration = this.player_.duration();
if (duration) {
this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + Lib.formatTime(duration); // label the duration time for screen reader users
}
};
/**
* The separator between the current time and duration
*
* Can be hidden if it's not needed in the design.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var TimeDivider = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
}
});
Component.registerComponent('TimeDivider', TimeDivider);
TimeDivider.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-time-divider',
innerHTML: '<div><span>/</span></div>'
});
};
/**
* Displays the time left in the video
* @param {Player|Object} player
* @param {Object=} options
* @constructor
*/
var RemainingTimeDisplay = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
this.on(player, 'timeupdate', this.updateContent);
}
});
Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
RemainingTimeDisplay.prototype.createEl = function(){
let el = Component.prototype.createEl.call(this, 'div', {
className: 'vjs-remaining-time vjs-time-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-remaining-time-display',
innerHTML: '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-0:00', // label the remaining time for screen reader users
'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
});
el.appendChild(this.contentEl_);
return el;
};
RemainingTimeDisplay.prototype.updateContent = function(){
if (this.player_.duration()) {
this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-'+ Lib.formatTime(this.player_.remainingTime());
}
// Allows for smooth scrubbing, when player can't keep up.
// var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
// this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration());
};
export default CurrentTimeDisplay;
export { DurationDisplay, TimeDivider, RemainingTimeDisplay };

View File

@ -0,0 +1,24 @@
import Component from '../component.js';
/**
* The separator between the current time and duration
*
* Can be hidden if it's not needed in the design.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class TimeDivider extends Component {
createEl() {
return super.createEl('div', {
className: 'vjs-time-divider',
innerHTML: '<div><span>/</span></div>'
});
}
}
Component.registerComponent('TimeDivider', TimeDivider);
export default TimeDivider;

View File

@ -1,155 +0,0 @@
import Component from '../component';
import * as Lib from '../lib';
import Slider, { SliderHandle } from '../slider';
/**
* The component for controlling the volume level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let VolumeControl = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
// hide volume controls when they're not supported by the current tech
if (player.tech && player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
}
this.on(player, 'loadstart', function(){
if (player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
} else {
this.removeClass('vjs-hidden');
}
});
}
});
Component.registerComponent('VolumeControl', VolumeControl);
VolumeControl.prototype.options_ = {
children: {
'volumeBar': {}
}
};
VolumeControl.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-volume-control vjs-control'
});
};
/**
* The bar that contains the volume level and can be clicked on to adjust the level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var VolumeBar = Slider.extend({
/** @constructor */
init: function(player, options){
Slider.call(this, player, options);
this.on(player, 'volumechange', this.updateARIAAttributes);
player.ready(Lib.bind(this, this.updateARIAAttributes));
}
});
Component.registerComponent('VolumeBar', VolumeBar);
VolumeBar.prototype.updateARIAAttributes = function(){
// Current value of volume bar as a percentage
this.el_.setAttribute('aria-valuenow', Lib.round(this.player_.volume()*100, 2));
this.el_.setAttribute('aria-valuetext', Lib.round(this.player_.volume()*100, 2)+'%');
};
VolumeBar.prototype.options_ = {
children: {
'volumeLevel': {},
'volumeHandle': {}
},
'barName': 'volumeLevel',
'handleName': 'volumeHandle'
};
VolumeBar.prototype.playerEvent = 'volumechange';
VolumeBar.prototype.createEl = function(){
return Slider.prototype.createEl.call(this, 'div', {
className: 'vjs-volume-bar',
'aria-label': 'volume level'
});
};
VolumeBar.prototype.onMouseMove = function(event) {
if (this.player_.muted()) {
this.player_.muted(false);
}
this.player_.volume(this.calculateDistance(event));
};
VolumeBar.prototype.getPercent = function(){
if (this.player_.muted()) {
return 0;
} else {
return this.player_.volume();
}
};
VolumeBar.prototype.stepForward = function(){
this.player_.volume(this.player_.volume() + 0.1);
};
VolumeBar.prototype.stepBack = function(){
this.player_.volume(this.player_.volume() - 0.1);
};
/**
* Shows volume level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var VolumeLevel = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
}
});
Component.registerComponent('VolumeLevel', VolumeLevel);
VolumeLevel.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-volume-level',
innerHTML: '<span class="vjs-control-text"></span>'
});
};
/**
* The volume handle can be dragged to adjust the volume level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var VolumeHandle = SliderHandle.extend();
Component.registerComponent('VolumeHandle', VolumeHandle);
VolumeHandle.prototype.defaultValue = '00:00';
/** @inheritDoc */
VolumeHandle.prototype.createEl = function(){
return SliderHandle.prototype.createEl.call(this, 'div', {
className: 'vjs-volume-handle'
});
};
export default VolumeControl;
export { VolumeBar, VolumeLevel, VolumeHandle };

View File

@ -0,0 +1,74 @@
import Slider from '../../slider/slider.js';
import * as Lib from '../../lib.js';
// Required children
import VolumeHandle from './volume-handle.js';
import VolumeLevel from './volume-level.js';
/**
* The bar that contains the volume level and can be clicked on to adjust the level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class VolumeBar extends Slider {
constructor(player, options){
super(player, options);
this.on(player, 'volumechange', this.updateARIAAttributes);
player.ready(Lib.bind(this, this.updateARIAAttributes));
}
createEl() {
return super.createEl('div', {
className: 'vjs-volume-bar',
'aria-label': 'volume level'
});
}
onMouseMove(event) {
if (this.player_.muted()) {
this.player_.muted(false);
}
this.player_.volume(this.calculateDistance(event));
}
getPercent() {
if (this.player_.muted()) {
return 0;
} else {
return this.player_.volume();
}
}
stepForward() {
this.player_.volume(this.player_.volume() + 0.1);
}
stepBack() {
this.player_.volume(this.player_.volume() - 0.1);
}
updateARIAAttributes() {
// Current value of volume bar as a percentage
this.el_.setAttribute('aria-valuenow', Lib.round(this.player_.volume()*100, 2));
this.el_.setAttribute('aria-valuetext', Lib.round(this.player_.volume()*100, 2)+'%');
}
}
VolumeBar.prototype.options_ = {
children: {
'volumeLevel': {},
'volumeHandle': {}
},
'barName': 'volumeLevel',
'handleName': 'volumeHandle'
};
VolumeBar.prototype.playerEvent = 'volumechange';
Slider.registerComponent('VolumeBar', VolumeBar);
export default VolumeBar;

View File

@ -0,0 +1,47 @@
import Component from '../../component.js';
import * as Lib from '../../lib.js';
// Required children
import VolumeBar from './volume-bar.js';
/**
* The component for controlling the volume level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class VolumeControl extends Component {
constructor(player, options){
super(player, options);
// hide volume controls when they're not supported by the current tech
if (player.tech && player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
}
this.on(player, 'loadstart', function(){
if (player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
} else {
this.removeClass('vjs-hidden');
}
});
}
createEl() {
return super.createEl('div', {
className: 'vjs-volume-control vjs-control'
});
}
}
VolumeControl.prototype.options_ = {
children: {
'volumeBar': {}
}
};
Component.registerComponent('VolumeControl', VolumeControl);
export default VolumeControl;

View File

@ -0,0 +1,24 @@
import SliderHandle from '../../slider/slider-handle.js';
/**
* The volume handle can be dragged to adjust the volume level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class VolumeHandle extends SliderHandle {
/** @inheritDoc */
createEl() {
return super.createEl('div', {
className: 'vjs-volume-handle'
});
}
}
VolumeHandle.prototype.defaultValue = '00:00';
SliderHandle.registerComponent('VolumeHandle', VolumeHandle);
export default VolumeHandle;

View File

@ -0,0 +1,22 @@
import Component from '../../component.js';
/**
* Shows volume level
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class VolumeLevel extends Component {
createEl() {
return super.createEl('div', {
className: 'vjs-volume-level',
innerHTML: '<span class="vjs-control-text"></span>'
});
}
}
Component.registerComponent('VolumeLevel', VolumeLevel);
export default VolumeLevel;

View File

@ -1,18 +1,18 @@
import Button from '../button'; import Button from '../button.js';
import Component from '../component'; import Menu from '../menu/menu.js';
import Menu, { MenuButton } from '../menu'; import MenuButton from '../menu/menu-button.js';
import MuteToggle from './mute-toggle'; import MuteToggle from './mute-toggle.js';
import * as Lib from '../lib'; import * as Lib from '../lib.js';
import { VolumeBar } from './volume-control'; import VolumeBar from './volume-control/volume-bar.js';
/** /**
* Menu button with a popup for showing the volume slider. * Menu button with a popup for showing the volume slider.
* @constructor * @constructor
*/ */
let VolumeMenuButton = MenuButton.extend({ class VolumeMenuButton extends MenuButton {
/** @constructor */
init: function(player, options){ constructor(player, options){
MenuButton.call(this, player, options); super(player, options);
// Same listeners as MuteToggle // Same listeners as MuteToggle
this.on(player, 'volumechange', this.volumeUpdate); this.on(player, 'volumechange', this.volumeUpdate);
@ -30,9 +30,8 @@ let VolumeMenuButton = MenuButton.extend({
}); });
this.addClass('vjs-menu-button'); this.addClass('vjs-menu-button');
} }
});
VolumeMenuButton.prototype.createMenu = function(){ createMenu() {
let menu = new Menu(this.player_, { let menu = new Menu(this.player_, {
contentElType: 'div' contentElType: 'div'
}); });
@ -45,21 +44,23 @@ VolumeMenuButton.prototype.createMenu = function(){
}); });
menu.addChild(vc); menu.addChild(vc);
return menu; return menu;
}; }
VolumeMenuButton.prototype.onClick = function(){ onClick() {
MuteToggle.prototype.onClick.call(this); MuteToggle.prototype.onClick.call(this);
MenuButton.prototype.onClick.call(this); super.onClick();
}; }
VolumeMenuButton.prototype.createEl = function(){ createEl() {
return Button.prototype.createEl.call(this, 'div', { return super.createEl('div', {
className: 'vjs-volume-menu-button vjs-menu-button vjs-control', className: 'vjs-volume-menu-button vjs-menu-button vjs-control',
innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>' innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>'
}); });
}; }
}
VolumeMenuButton.prototype.volumeUpdate = MuteToggle.prototype.update; VolumeMenuButton.prototype.volumeUpdate = MuteToggle.prototype.update;
Component.registerComponent('VolumeMenuButton', VolumeMenuButton); Button.registerComponent('VolumeMenuButton', VolumeMenuButton);
export default VolumeMenuButton; export default VolumeMenuButton;

View File

@ -7,19 +7,17 @@ import * as Lib from './lib';
* @param {Object=} options * @param {Object=} options
* @constructor * @constructor
*/ */
let ErrorDisplay = Component.extend({ class ErrorDisplay extends Component {
init: function(player, options){
Component.call(this, player, options); constructor(player, options) {
super(player, options);
this.update(); this.update();
this.on(player, 'error', this.update); this.on(player, 'error', this.update);
} }
});
Component.registerComponent('ErrorDisplay', ErrorDisplay); createEl() {
var el = super.createEl('div', {
ErrorDisplay.prototype.createEl = function(){
var el = Component.prototype.createEl.call(this, 'div', {
className: 'vjs-error-display' className: 'vjs-error-display'
}); });
@ -27,12 +25,14 @@ ErrorDisplay.prototype.createEl = function(){
el.appendChild(this.contentEl_); el.appendChild(this.contentEl_);
return el; return el;
}; }
ErrorDisplay.prototype.update = function(){ update() {
if (this.player().error()) { if (this.player().error()) {
this.contentEl_.innerHTML = this.localize(this.player().error().message); this.contentEl_.innerHTML = this.localize(this.player().error().message);
} }
}; }
}
Component.registerComponent('ErrorDisplay', ErrorDisplay);
export default ErrorDisplay; export default ErrorDisplay;

View File

@ -9,39 +9,13 @@ import Component from './component';
* @class * @class
* @constructor * @constructor
*/ */
let LoadingSpinner = Component.extend({ class LoadingSpinner extends Component {
/** @constructor */ createEl() {
init: function(player, options){ return super.createEl('div', {
Component.call(this, player, options);
// MOVING DISPLAY HANDLING TO CSS
// player.on('canplay', vjs.bind(this, this.hide));
// player.on('canplaythrough', vjs.bind(this, this.hide));
// player.on('playing', vjs.bind(this, this.hide));
// player.on('seeking', vjs.bind(this, this.show));
// in some browsers seeking does not trigger the 'playing' event,
// so we also need to trap 'seeked' if we are going to set a
// 'seeking' event
// player.on('seeked', vjs.bind(this, this.hide));
// player.on('ended', vjs.bind(this, this.hide));
// Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner.
// Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing
// player.on('stalled', vjs.bind(this, this.show));
// player.on('waiting', vjs.bind(this, this.show));
}
});
Component.registerComponent('LoadingSpinner', LoadingSpinner);
LoadingSpinner.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-loading-spinner' className: 'vjs-loading-spinner'
}); });
}; }
}
Component.registerComponent('LoadingSpinner', LoadingSpinner);
export default LoadingSpinner; export default LoadingSpinner;

View File

@ -1,684 +0,0 @@
/**
* @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API
*/
import MediaTechController from './media';
import Component from '../component';
import * as Lib from '../lib';
import * as VjsUtil from '../util';
import document from 'global/document';
/**
* HTML5 Media Controller - Wrapper for HTML5 Media API
* @param {vjs.Player|Object} player
* @param {Object=} options
* @param {Function=} ready
* @constructor
*/
var Html5 = MediaTechController.extend({
/** @constructor */
init: function(player, options, ready){
if (options['nativeCaptions'] === false || options['nativeTextTracks'] === false) {
this['featuresNativeTextTracks'] = false;
}
MediaTechController.call(this, player, options, ready);
this.setupTriggers();
const source = options['source'];
// Set the source if one is provided
// 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
// 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
// anyway so the error gets fired.
if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) {
this.setSource(source);
}
if (this.el_.hasChildNodes()) {
let nodes = this.el_.childNodes;
let nodesLength = nodes.length;
let removeNodes = [];
while (nodesLength--) {
let node = nodes[nodesLength];
let nodeName = node.nodeName.toLowerCase();
if (nodeName === 'track') {
if (!this['featuresNativeTextTracks']) {
// Empty video tag tracks so the built-in player doesn't use them also.
// This may not be fast enough to stop HTML5 browsers from reading the tags
// so we'll need to turn off any default tracks if we're manually doing
// captions and subtitles. videoElement.textTracks
removeNodes.push(node);
} else {
this.remoteTextTracks().addTrack_(node['track']);
}
}
}
for (let i=0; i<removeNodes.length; i++) {
this.el_.removeChild(removeNodes[i]);
}
}
if (this['featuresNativeTextTracks']) {
this.on('loadstart', Lib.bind(this, this.hideCaptions));
}
// Determine if native controls should be used
// Our goal should be to get the custom controls on mobile solid everywhere
// so we can remove this all together. Right now this will block custom
// controls on touch enabled laptops like the Chrome Pixel
if (Lib.TOUCH_ENABLED && player.options()['nativeControlsForTouch'] === true) {
this.useNativeControls();
}
// Chrome and Safari both have issues with autoplay.
// In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
// In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
// This fixes both issues. Need to wait for API, so it updates displays correctly
player.ready(function(){
if (this.tag && this.options_['autoplay'] && this.paused()) {
delete this.tag['poster']; // Chrome Fix. Fixed in Chrome v16.
this.play();
}
});
this.triggerReady();
}
});
Component.registerComponent('Html5', Html5);
Html5.prototype.dispose = function(){
Html5.disposeMediaElement(this.el_);
MediaTechController.prototype.dispose.call(this);
};
Html5.prototype.createEl = function(){
let player = this.player_;
let el = player.tag;
// Check if this browser supports moving the element into the box.
// On the iPhone video will break if you move the element,
// So we have to create a brand new element.
if (!el || this['movingMediaElementInDOM'] === false) {
// If the original tag is still there, clone and remove it.
if (el) {
const clone = el.cloneNode(false);
Html5.disposeMediaElement(el);
el = clone;
player.tag = null;
} else {
el = Lib.createEl('video');
// determine if native controls should be used
let attributes = VjsUtil.mergeOptions({}, player.tagAttributes);
if (!Lib.TOUCH_ENABLED || player.options()['nativeControlsForTouch'] !== true) {
delete attributes.controls;
}
Lib.setElementAttributes(el,
Lib.obj.merge(attributes, {
id: player.id() + '_html5_api',
class: 'vjs-tech'
})
);
}
// associate the player with the new tag
el['player'] = player;
if (player.options_.tracks) {
for (let i = 0; i < player.options_.tracks.length; i++) {
const track = player.options_.tracks[i];
let trackEl = document.createElement('track');
trackEl.kind = track.kind;
trackEl.label = track.label;
trackEl.srclang = track.srclang;
trackEl.src = track.src;
if ('default' in track) {
trackEl.setAttribute('default', 'default');
}
el.appendChild(trackEl);
}
}
Lib.insertFirst(el, player.el());
}
// Update specific tag settings, in case they were overridden
let settingsAttrs = ['autoplay','preload','loop','muted'];
for (let i = settingsAttrs.length - 1; i >= 0; i--) {
const attr = settingsAttrs[i];
let overwriteAttrs = {};
if (typeof player.options_[attr] !== 'undefined') {
overwriteAttrs[attr] = player.options_[attr];
}
Lib.setElementAttributes(el, overwriteAttrs);
}
return el;
// jenniisawesome = true;
};
Html5.prototype.hideCaptions = function() {
let tracks = this.el_.querySelectorAll('track');
let i = tracks.length;
const kinds = {
'captions': 1,
'subtitles': 1
};
while (i--) {
let track = tracks[i].track;
if ((track && track['kind'] in kinds) &&
(!tracks[i]['default'])) {
track.mode = 'disabled';
}
}
};
// Make video events trigger player events
// May seem verbose here, but makes other APIs possible.
// Triggers removed using this.off when disposed
Html5.prototype.setupTriggers = function(){
for (let i = Html5.Events.length - 1; i >= 0; i--) {
this.on(Html5.Events[i], this.eventHandler);
}
};
Html5.prototype.eventHandler = function(evt){
// In the case of an error on the video element, set the error prop
// on the player and let the player handle triggering the event. On
// some platforms, error events fire that do not cause the error
// property on the video element to be set. See #1465 for an example.
if (evt.type == 'error' && this.error()) {
this.player().error(this.error().code);
// in some cases we pass the event directly to the player
} else {
// No need for media events to bubble up.
evt.bubbles = false;
this.player().trigger(evt);
}
};
Html5.prototype.useNativeControls = function(){
let tech = this;
let player = this.player();
// If the player controls are enabled turn on the native controls
tech.setControls(player.controls());
// Update the native controls when player controls state is updated
let controlsOn = function(){
tech.setControls(true);
};
let controlsOff = function(){
tech.setControls(false);
};
player.on('controlsenabled', controlsOn);
player.on('controlsdisabled', controlsOff);
// Clean up when not using native controls anymore
let cleanUp = function(){
player.off('controlsenabled', controlsOn);
player.off('controlsdisabled', controlsOff);
};
tech.on('dispose', cleanUp);
player.on('usingcustomcontrols', cleanUp);
// Update the state of the player to using native controls
player.usingNativeControls(true);
};
Html5.prototype.play = function(){ this.el_.play(); };
Html5.prototype.pause = function(){ this.el_.pause(); };
Html5.prototype.paused = function(){ return this.el_.paused; };
Html5.prototype.currentTime = function(){ return this.el_.currentTime; };
Html5.prototype.setCurrentTime = function(seconds){
try {
this.el_.currentTime = seconds;
} catch(e) {
Lib.log(e, 'Video is not ready. (Video.js)');
// this.warning(VideoJS.warnings.videoNotReady);
}
};
Html5.prototype.duration = function(){ return this.el_.duration || 0; };
Html5.prototype.buffered = function(){ return this.el_.buffered; };
Html5.prototype.volume = function(){ return this.el_.volume; };
Html5.prototype.setVolume = function(percentAsDecimal){ this.el_.volume = percentAsDecimal; };
Html5.prototype.muted = function(){ return this.el_.muted; };
Html5.prototype.setMuted = function(muted){ this.el_.muted = muted; };
Html5.prototype.width = function(){ return this.el_.offsetWidth; };
Html5.prototype.height = function(){ return this.el_.offsetHeight; };
Html5.prototype.supportsFullScreen = function(){
if (typeof this.el_.webkitEnterFullScreen == 'function') {
// Seems to be broken in Chromium/Chrome && Safari in Leopard
if (/Android/.test(Lib.USER_AGENT) || !/Chrome|Mac OS X 10.5/.test(Lib.USER_AGENT)) {
return true;
}
}
return false;
};
Html5.prototype.enterFullScreen = function(){
var video = this.el_;
if ('webkitDisplayingFullscreen' in video) {
this.one('webkitbeginfullscreen', function() {
this.player_.isFullscreen(true);
this.one('webkitendfullscreen', function() {
this.player_.isFullscreen(false);
this.player_.trigger('fullscreenchange');
});
this.player_.trigger('fullscreenchange');
});
}
if (video.paused && video.networkState <= video.HAVE_METADATA) {
// attempt to prime the video element for programmatic access
// this isn't necessary on the desktop but shouldn't hurt
this.el_.play();
// playing and pausing synchronously during the transition to fullscreen
// can get iOS ~6.1 devices into a play/pause loop
this.setTimeout(function(){
video.pause();
video.webkitEnterFullScreen();
}, 0);
} else {
video.webkitEnterFullScreen();
}
};
Html5.prototype.exitFullScreen = function(){
this.el_.webkitExitFullScreen();
};
Html5.prototype.src = function(src) {
if (src === undefined) {
return this.el_.src;
} else {
// Setting src through `src` instead of `setSrc` will be deprecated
this.setSrc(src);
}
};
Html5.prototype.setSrc = function(src) {
this.el_.src = src;
};
Html5.prototype.load = function(){ this.el_.load(); };
Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; };
Html5.prototype.poster = function(){ return this.el_.poster; };
Html5.prototype.setPoster = function(val){ this.el_.poster = val; };
Html5.prototype.preload = function(){ return this.el_.preload; };
Html5.prototype.setPreload = function(val){ this.el_.preload = val; };
Html5.prototype.autoplay = function(){ return this.el_.autoplay; };
Html5.prototype.setAutoplay = function(val){ this.el_.autoplay = val; };
Html5.prototype.controls = function(){ return this.el_.controls; };
Html5.prototype.setControls = function(val){ this.el_.controls = !!val; };
Html5.prototype.loop = function(){ return this.el_.loop; };
Html5.prototype.setLoop = function(val){ this.el_.loop = val; };
Html5.prototype.error = function(){ return this.el_.error; };
Html5.prototype.seeking = function(){ return this.el_.seeking; };
Html5.prototype.ended = function(){ return this.el_.ended; };
Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; };
Html5.prototype.playbackRate = function(){ return this.el_.playbackRate; };
Html5.prototype.setPlaybackRate = function(val){ this.el_.playbackRate = val; };
Html5.prototype.networkState = function(){ return this.el_.networkState; };
Html5.prototype.readyState = function(){ return this.el_.readyState; };
Html5.prototype.textTracks = function() {
if (!this['featuresNativeTextTracks']) {
return MediaTechController.prototype.textTracks.call(this);
}
return this.el_.textTracks;
};
Html5.prototype.addTextTrack = function(kind, label, language) {
if (!this['featuresNativeTextTracks']) {
return MediaTechController.prototype.addTextTrack.call(this, kind, label, language);
}
return this.el_.addTextTrack(kind, label, language);
};
Html5.prototype.addRemoteTextTrack = function(options) {
if (!this['featuresNativeTextTracks']) {
return MediaTechController.prototype.addRemoteTextTrack.call(this, options);
}
var track = document.createElement('track');
options = options || {};
if (options['kind']) {
track['kind'] = options['kind'];
}
if (options['label']) {
track['label'] = options['label'];
}
if (options['language'] || options['srclang']) {
track['srclang'] = options['language'] || options['srclang'];
}
if (options['default']) {
track['default'] = options['default'];
}
if (options['id']) {
track['id'] = options['id'];
}
if (options['src']) {
track['src'] = options['src'];
}
this.el().appendChild(track);
if (track.track['kind'] === 'metadata') {
track['track']['mode'] = 'hidden';
} else {
track['track']['mode'] = 'disabled';
}
track['onload'] = function() {
var tt = track['track'];
if (track.readyState >= 2) {
if (tt['kind'] === 'metadata' && tt['mode'] !== 'hidden') {
tt['mode'] = 'hidden';
} else if (tt['kind'] !== 'metadata' && tt['mode'] !== 'disabled') {
tt['mode'] = 'disabled';
}
track['onload'] = null;
}
};
this.remoteTextTracks().addTrack_(track.track);
return track;
};
Html5.prototype.removeRemoteTextTrack = function(track) {
if (!this['featuresNativeTextTracks']) {
return MediaTechController.prototype.removeRemoteTextTrack.call(this, track);
}
var tracks, i;
this.remoteTextTracks().removeTrack_(track);
tracks = this.el()['querySelectorAll']('track');
for (i = 0; i < tracks.length; i++) {
if (tracks[i] === track || tracks[i]['track'] === track) {
tracks[i]['parentNode']['removeChild'](tracks[i]);
break;
}
}
};
/* HTML5 Support Testing ---------------------------------------------------- */
/**
* Check if HTML5 video is supported by this browser/device
* @return {Boolean}
*/
Html5.isSupported = function(){
// IE9 with no Media Player is a LIAR! (#984)
try {
Lib.TEST_VID['volume'] = 0.5;
} catch (e) {
return false;
}
return !!Lib.TEST_VID.canPlayType;
};
// Add Source Handler pattern functions to this tech
MediaTechController.withSourceHandlers(Html5);
/**
* The default native source handler.
* This simply passes the source to the video element. Nothing fancy.
* @param {Object} source The source object
* @param {vjs.Html5} tech The instance of the HTML5 tech
*/
Html5.nativeSourceHandler = {};
/**
* Check if the video element can handle the source natively
* @param {Object} source The source object
* @return {String} 'probably', 'maybe', or '' (empty string)
*/
Html5.nativeSourceHandler.canHandleSource = function(source){
var match, ext;
function canPlayType(type){
// IE9 on Windows 7 without MediaPlayer throws an error here
// https://github.com/videojs/video.js/issues/519
try {
return Lib.TEST_VID.canPlayType(type);
} catch(e) {
return '';
}
}
// If a type was provided we should rely on that
if (source.type) {
return canPlayType(source.type);
} else if (source.src) {
// If no type, fall back to checking 'video/[EXTENSION]'
ext = Lib.getFileExtension(source.src);
return canPlayType('video/'+ext);
}
return '';
};
/**
* Pass the source to the video element
* Adaptive source handlers will have more complicated workflows before passing
* video data to the video element
* @param {Object} source The source object
* @param {vjs.Html5} tech The instance of the Html5 tech
*/
Html5.nativeSourceHandler.handleSource = function(source, tech){
tech.setSrc(source.src);
};
/**
* Clean up the source handler when disposing the player or switching sources..
* (no cleanup is needed when supporting the format natively)
*/
Html5.nativeSourceHandler.dispose = function(){};
// Register the native source handler
Html5.registerSourceHandler(Html5.nativeSourceHandler);
/**
* Check if the volume can be changed in this browser/device.
* Volume cannot be changed in a lot of mobile devices.
* Specifically, it can't be changed from 1 on iOS.
* @return {Boolean}
*/
Html5.canControlVolume = function(){
var volume = Lib.TEST_VID.volume;
Lib.TEST_VID.volume = (volume / 2) + 0.1;
return volume !== Lib.TEST_VID.volume;
};
/**
* Check if playbackRate is supported in this browser/device.
* @return {[type]} [description]
*/
Html5.canControlPlaybackRate = function(){
var playbackRate = Lib.TEST_VID.playbackRate;
Lib.TEST_VID.playbackRate = (playbackRate / 2) + 0.1;
return playbackRate !== Lib.TEST_VID.playbackRate;
};
/**
* Check to see if native text tracks are supported by this browser/device
* @return {Boolean}
*/
Html5.supportsNativeTextTracks = function() {
var supportsTextTracks;
// Figure out native text track support
// If mode is a number, we cannot change it because it'll disappear from view.
// Browsers with numeric modes include IE10 and older (<=2013) samsung android models.
// Firefox isn't playing nice either with modifying the mode
// TODO: Investigate firefox: https://github.com/videojs/video.js/issues/1862
supportsTextTracks = !!Lib.TEST_VID.textTracks;
if (supportsTextTracks && Lib.TEST_VID.textTracks.length > 0) {
supportsTextTracks = typeof Lib.TEST_VID.textTracks[0]['mode'] !== 'number';
}
if (supportsTextTracks && Lib.IS_FIREFOX) {
supportsTextTracks = false;
}
return supportsTextTracks;
};
/**
* Set the tech's volume control support status
* @type {Boolean}
*/
Html5.prototype['featuresVolumeControl'] = Html5.canControlVolume();
/**
* Set the tech's playbackRate support status
* @type {Boolean}
*/
Html5.prototype['featuresPlaybackRate'] = Html5.canControlPlaybackRate();
/**
* Set the tech's status on moving the video element.
* In iOS, if you move a video element in the DOM, it breaks video playback.
* @type {Boolean}
*/
Html5.prototype['movingMediaElementInDOM'] = !Lib.IS_IOS;
/**
* Set the the tech's fullscreen resize support status.
* HTML video is able to automatically resize when going to fullscreen.
* (No longer appears to be used. Can probably be removed.)
*/
Html5.prototype['featuresFullscreenResize'] = true;
/**
* Set the tech's progress event support status
* (this disables the manual progress events of the MediaTechController)
*/
Html5.prototype['featuresProgressEvents'] = true;
/**
* Sets the tech's status on native text track support
* @type {Boolean}
*/
Html5.prototype['featuresNativeTextTracks'] = Html5.supportsNativeTextTracks();
// HTML5 Feature detection and Device Fixes --------------------------------- //
let canPlayType;
const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
const mp4RE = /^video\/mp4/i;
Html5.patchCanPlayType = function() {
// Android 4.0 and above can play HLS to some extent but it reports being unable to do so
if (Lib.ANDROID_VERSION >= 4.0) {
if (!canPlayType) {
canPlayType = Lib.TEST_VID.constructor.prototype.canPlayType;
}
Lib.TEST_VID.constructor.prototype.canPlayType = function(type) {
if (type && mpegurlRE.test(type)) {
return 'maybe';
}
return canPlayType.call(this, type);
};
}
// Override Android 2.2 and less canPlayType method which is broken
if (Lib.IS_OLD_ANDROID) {
if (!canPlayType) {
canPlayType = Lib.TEST_VID.constructor.prototype.canPlayType;
}
Lib.TEST_VID.constructor.prototype.canPlayType = function(type){
if (type && mp4RE.test(type)) {
return 'maybe';
}
return canPlayType.call(this, type);
};
}
};
Html5.unpatchCanPlayType = function() {
var r = Lib.TEST_VID.constructor.prototype.canPlayType;
Lib.TEST_VID.constructor.prototype.canPlayType = canPlayType;
canPlayType = null;
return r;
};
// by default, patch the video element
Html5.patchCanPlayType();
// List of all HTML5 events (various uses).
Html5.Events = 'loadstart,suspend,abort,error,emptied,stalled,loadedmetadata,loadeddata,canplay,canplaythrough,playing,waiting,seeking,seeked,ended,durationchange,timeupdate,progress,play,pause,ratechange,volumechange'.split(',');
Html5.disposeMediaElement = function(el){
if (!el) { return; }
el['player'] = null;
if (el.parentNode) {
el.parentNode.removeChild(el);
}
// remove any child track or source nodes to prevent their loading
while(el.hasChildNodes()) {
el.removeChild(el.firstChild);
}
// remove any src reference. not setting `src=''` because that causes a warning
// in firefox
el.removeAttribute('src');
// force the media element to update its loading state by calling load()
// however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
if (typeof el.load === 'function') {
// wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
(function() {
try {
el.load();
} catch (e) {
// not supported
}
})();
}
};
export default Html5;

View File

@ -1,527 +0,0 @@
/**
* @fileoverview Media Technology Controller - Base class for media playback
* technology controllers like Flash and HTML5
*/
import Component from '../component';
import TextTrack from '../tracks/text-track';
import TextTrackList from '../tracks/text-track-list';
import * as Lib from '../lib';
import window from 'global/window';
import document from 'global/document';
/**
* Base class for media (HTML5 Video, Flash) controllers
* @param {vjs.Player|Object} player Central player instance
* @param {Object=} options Options object
* @constructor
*/
let MediaTechController = Component.extend({
/** @constructor */
init: function(player, options, ready){
options = options || {};
// we don't want the tech to report user activity automatically.
// This is done manually in addControlsListeners
options.reportTouchActivity = false;
Component.call(this, player, options, ready);
// Manually track progress in cases where the browser/flash player doesn't report it.
if (!this['featuresProgressEvents']) {
this.manualProgressOn();
}
// Manually track timeupdates in cases where the browser/flash player doesn't report it.
if (!this['featuresTimeupdateEvents']) {
this.manualTimeUpdatesOn();
}
this.initControlsListeners();
if (!this['featuresNativeTextTracks']) {
this.emulateTextTracks();
}
this.initTextTrackListeners();
}
});
Component.registerComponent('MediaTechController', MediaTechController);
/**
* Set up click and touch listeners for the playback element
* On desktops, a click on the video itself will toggle playback,
* on a mobile device a click on the video toggles controls.
* (toggling controls is done by toggling the user state between active and
* inactive)
*
* A tap can signal that a user has become active, or has become inactive
* e.g. a quick tap on an iPhone movie should reveal the controls. Another
* quick tap should hide them again (signaling the user is in an inactive
* viewing state)
*
* In addition to this, we still want the user to be considered inactive after
* a few seconds of inactivity.
*
* Note: the only part of iOS interaction we can't mimic with this setup
* is a touch and hold on the video element counting as activity in order to
* keep the controls showing, but that shouldn't be an issue. A touch and hold on
* any controls will still keep the user active
*/
MediaTechController.prototype.initControlsListeners = function(){
let player = this.player();
let activateControls = function(){
if (player.controls() && !player.usingNativeControls()) {
this.addControlsListeners();
}
};
// Set up event listeners once the tech is ready and has an element to apply
// listeners to
this.ready(activateControls);
this.on(player, 'controlsenabled', activateControls);
this.on(player, 'controlsdisabled', this.removeControlsListeners);
// if we're loading the playback object after it has started loading or playing the
// video (often with autoplay on) then the loadstart event has already fired and we
// need to fire it manually because many things rely on it.
// Long term we might consider how we would do this for other events like 'canplay'
// that may also have fired.
this.ready(function(){
if (this.networkState && this.networkState() > 0) {
this.player().trigger('loadstart');
}
});
};
MediaTechController.prototype.addControlsListeners = function(){
let userWasActive;
// Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do
// trigger mousedown/up.
// http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object
// Any touch events are set to block the mousedown event from happening
this.on('mousedown', this.onClick);
// If the controls were hidden we don't want that to change without a tap event
// so we'll check if the controls were already showing before reporting user
// activity
this.on('touchstart', function(event) {
userWasActive = this.player_.userActive();
});
this.on('touchmove', function(event) {
if (userWasActive){
this.player().reportUserActivity();
}
});
this.on('touchend', function(event) {
// Stop the mouse events from also happening
event.preventDefault();
});
// Turn on component tap events
this.emitTapEvents();
// The tap listener needs to come after the touchend listener because the tap
// listener cancels out any reportedUserActivity when setting userActive(false)
this.on('tap', this.onTap);
};
/**
* Remove the listeners used for click and tap controls. This is needed for
* toggling to controls disabled, where a tap/touch should do nothing.
*/
MediaTechController.prototype.removeControlsListeners = function(){
// We don't want to just use `this.off()` because there might be other needed
// listeners added by techs that extend this.
this.off('tap');
this.off('touchstart');
this.off('touchmove');
this.off('touchleave');
this.off('touchcancel');
this.off('touchend');
this.off('click');
this.off('mousedown');
};
/**
* Handle a click on the media element. By default will play/pause the media.
*/
MediaTechController.prototype.onClick = function(event){
// We're using mousedown to detect clicks thanks to Flash, but mousedown
// will also be triggered with right-clicks, so we need to prevent that
if (event.button !== 0) return;
// When controls are disabled a click should not toggle playback because
// the click is considered a control
if (this.player().controls()) {
if (this.player().paused()) {
this.player().play();
} else {
this.player().pause();
}
}
};
/**
* Handle a tap on the media element. By default it will toggle the user
* activity state, which hides and shows the controls.
*/
MediaTechController.prototype.onTap = function(){
this.player().userActive(!this.player().userActive());
};
/* Fallbacks for unsupported event types
================================================================================ */
// Manually trigger progress events based on changes to the buffered amount
// Many flash players and older HTML5 browsers don't send progress or progress-like events
MediaTechController.prototype.manualProgressOn = function(){
this.manualProgress = true;
// Trigger progress watching when a source begins loading
this.trackProgress();
};
MediaTechController.prototype.manualProgressOff = function(){
this.manualProgress = false;
this.stopTrackingProgress();
};
MediaTechController.prototype.trackProgress = function(){
this.progressInterval = this.setInterval(function(){
// Don't trigger unless buffered amount is greater than last time
let bufferedPercent = this.player().bufferedPercent();
if (this.bufferedPercent_ != bufferedPercent) {
this.player().trigger('progress');
}
this.bufferedPercent_ = bufferedPercent;
if (bufferedPercent === 1) {
this.stopTrackingProgress();
}
}, 500);
};
MediaTechController.prototype.stopTrackingProgress = function(){ this.clearInterval(this.progressInterval); };
/*! Time Tracking -------------------------------------------------------------- */
MediaTechController.prototype.manualTimeUpdatesOn = function(){
let player = this.player_;
this.manualTimeUpdates = true;
this.on(player, 'play', this.trackCurrentTime);
this.on(player, 'pause', this.stopTrackingCurrentTime);
// timeupdate is also called by .currentTime whenever current time is set
// Watch for native timeupdate event
this.one('timeupdate', function(){
// Update known progress support for this playback technology
this['featuresTimeupdateEvents'] = true;
// Turn off manual progress tracking
this.manualTimeUpdatesOff();
});
};
MediaTechController.prototype.manualTimeUpdatesOff = function(){
let player = this.player_;
this.manualTimeUpdates = false;
this.stopTrackingCurrentTime();
this.off(player, 'play', this.trackCurrentTime);
this.off(player, 'pause', this.stopTrackingCurrentTime);
};
MediaTechController.prototype.trackCurrentTime = function(){
if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); }
this.currentTimeInterval = this.setInterval(function(){
this.player().trigger('timeupdate');
}, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
};
// Turn off play progress tracking (when paused or dragging)
MediaTechController.prototype.stopTrackingCurrentTime = function(){
this.clearInterval(this.currentTimeInterval);
// #1002 - if the video ends right before the next timeupdate would happen,
// the progress bar won't make it all the way to the end
this.player().trigger('timeupdate');
};
MediaTechController.prototype.dispose = function() {
// Turn off any manual progress or timeupdate tracking
if (this.manualProgress) { this.manualProgressOff(); }
if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); }
Component.prototype.dispose.call(this);
};
MediaTechController.prototype.setCurrentTime = function() {
// improve the accuracy of manual timeupdates
if (this.manualTimeUpdates) { this.player().trigger('timeupdate'); }
};
// TODO: Consider looking at moving this into the text track display directly
// https://github.com/videojs/video.js/issues/1863
MediaTechController.prototype.initTextTrackListeners = function() {
let player = this.player_;
let textTrackListChanges = function() {
let textTrackDisplay = player.getChild('textTrackDisplay');
if (textTrackDisplay) {
textTrackDisplay.updateDisplay();
}
};
let tracks = this.textTracks();
if (!tracks) return;
tracks.addEventListener('removetrack', textTrackListChanges);
tracks.addEventListener('addtrack', textTrackListChanges);
this.on('dispose', Lib.bind(this, function() {
tracks.removeEventListener('removetrack', textTrackListChanges);
tracks.removeEventListener('addtrack', textTrackListChanges);
}));
};
MediaTechController.prototype.emulateTextTracks = function() {
let player = this.player_;
if (!window['WebVTT']) {
let script = document.createElement('script');
script.src = player.options()['vtt.js'] || '../node_modules/vtt.js/dist/vtt.js';
player.el().appendChild(script);
window['WebVTT'] = true;
}
let tracks = this.textTracks();
if (!tracks) {
return;
}
let textTracksChanges = function() {
let textTrackDisplay = player.getChild('textTrackDisplay');
textTrackDisplay.updateDisplay();
for (let i = 0; i < this.length; i++) {
let track = this[i];
track.removeEventListener('cuechange', Lib.bind(textTrackDisplay, textTrackDisplay.updateDisplay));
if (track.mode === 'showing') {
track.addEventListener('cuechange', Lib.bind(textTrackDisplay, textTrackDisplay.updateDisplay));
}
}
};
tracks.addEventListener('change', textTracksChanges);
this.on('dispose', Lib.bind(this, function() {
tracks.removeEventListener('change', textTracksChanges);
}));
};
/**
* Provide default methods for text tracks.
*
* Html5 tech overrides these.
*/
/**
* List of associated text tracks
* @type {Array}
* @private
*/
MediaTechController.prototype.textTracks_;
MediaTechController.prototype.textTracks = function() {
this.player_.textTracks_ = this.player_.textTracks_ || new TextTrackList();
return this.player_.textTracks_;
};
MediaTechController.prototype.remoteTextTracks = function() {
this.player_.remoteTextTracks_ = this.player_.remoteTextTracks_ || new TextTrackList();
return this.player_.remoteTextTracks_;
};
let createTrackHelper = function(self, kind, label, language, options) {
let tracks = self.textTracks();
options = options || {};
options['kind'] = kind;
if (label) {
options['label'] = label;
}
if (language) {
options['language'] = language;
}
options['player'] = self.player_;
let track = new TextTrack(options);
tracks.addTrack_(track);
return track;
};
MediaTechController.prototype.addTextTrack = function(kind, label, language) {
if (!kind) {
throw new Error('TextTrack kind is required but was not provided');
}
return createTrackHelper(this, kind, label, language);
};
MediaTechController.prototype.addRemoteTextTrack = function(options) {
let track = createTrackHelper(this, options['kind'], options['label'], options['language'], options);
this.remoteTextTracks().addTrack_(track);
return {
track: track
};
};
MediaTechController.prototype.removeRemoteTextTrack = function(track) {
this.textTracks().removeTrack_(track);
this.remoteTextTracks().removeTrack_(track);
};
/**
* Provide a default setPoster method for techs
*
* Poster support for techs should be optional, so we don't want techs to
* break if they don't have a way to set a poster.
*/
MediaTechController.prototype.setPoster = function(){};
MediaTechController.prototype['featuresVolumeControl'] = true;
// Resizing plugins using request fullscreen reloads the plugin
MediaTechController.prototype['featuresFullscreenResize'] = false;
MediaTechController.prototype['featuresPlaybackRate'] = false;
// Optional events that we can manually mimic with timers
// currently not triggered by video-js-swf
MediaTechController.prototype['featuresProgressEvents'] = false;
MediaTechController.prototype['featuresTimeupdateEvents'] = false;
MediaTechController.prototype['featuresNativeTextTracks'] = false;
/**
* A functional mixin for techs that want to use the Source Handler pattern.
*
* ##### EXAMPLE:
*
* videojs.MediaTechController.withSourceHandlers.call(MyTech);
*
*/
MediaTechController.withSourceHandlers = function(Tech){
/**
* Register a source handler
* Source handlers are scripts for handling specific formats.
* The source handler pattern is used for adaptive formats (HLS, DASH) that
* manually load video data and feed it into a Source Buffer (Media Source Extensions)
* @param {Function} handler The source handler
* @param {Boolean} first Register it before any existing handlers
*/
Tech.registerSourceHandler = function(handler, index){
let handlers = Tech.sourceHandlers;
if (!handlers) {
handlers = Tech.sourceHandlers = [];
}
if (index === undefined) {
// add to the end of the list
index = handlers.length;
}
handlers.splice(index, 0, handler);
};
/**
* Return the first source handler that supports the source
* TODO: Answer question: should 'probably' be prioritized over 'maybe'
* @param {Object} source The source object
* @returns {Object} The first source handler that supports the source
* @returns {null} Null if no source handler is found
*/
Tech.selectSourceHandler = function(source){
let handlers = Tech.sourceHandlers || [];
let can;
for (let i = 0; i < handlers.length; i++) {
can = handlers[i].canHandleSource(source);
if (can) {
return handlers[i];
}
}
return null;
};
/**
* Check if the tech can support the given source
* @param {Object} srcObj The source object
* @return {String} 'probably', 'maybe', or '' (empty string)
*/
Tech.canPlaySource = function(srcObj){
let sh = Tech.selectSourceHandler(srcObj);
if (sh) {
return sh.canHandleSource(srcObj);
}
return '';
};
/**
* Create a function for setting the source using a source object
* and source handlers.
* Should never be called unless a source handler was found.
* @param {Object} source A source object with src and type keys
* @return {vjs.MediaTechController} self
*/
Tech.prototype.setSource = function(source){
let sh = Tech.selectSourceHandler(source);
if (!sh) {
// Fall back to a native source hander when unsupported sources are
// deliberately set
if (Tech.nativeSourceHandler) {
sh = Tech.nativeSourceHandler;
} else {
Lib.log.error('No source hander found for the current source.');
}
}
// Dispose any existing source handler
this.disposeSourceHandler();
this.off('dispose', this.disposeSourceHandler);
this.currentSource_ = source;
this.sourceHandler_ = sh.handleSource(source, this);
this.on('dispose', this.disposeSourceHandler);
return this;
};
/**
* Clean up any existing source handler
*/
Tech.prototype.disposeSourceHandler = function(){
if (this.sourceHandler_ && this.sourceHandler_.dispose) {
this.sourceHandler_.dispose();
}
};
};
export default MediaTechController;

View File

@ -1,237 +0,0 @@
import Button from './button';
import Component from './component';
import * as Lib from './lib';
import * as Events from './events';
/* 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
*/
let Menu = Component.extend();
/**
* Add a menu item to the menu
* @param {Object|String} component Component or component type to add
*/
Menu.prototype.addItem = function(component){
this.addChild(component);
component.on('click', Lib.bind(this, function(){
this.unlockShowing();
}));
};
/** @inheritDoc */
Menu.prototype.createEl = function(){
let contentElType = this.options().contentElType || 'ul';
this.contentEl_ = Lib.createEl(contentElType, {
className: 'vjs-menu-content'
});
var el = 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
Events.on(el, 'click', function(event){
event.preventDefault();
event.stopImmediatePropagation();
});
return el;
};
/**
* The component for a menu item. `<li>`
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
var MenuItem = Button.extend({
/** @constructor */
init: function(player, options){
Button.call(this, player, options);
this.selected(options['selected']);
}
});
/** @inheritDoc */
MenuItem.prototype.createEl = function(type, props){
return Button.prototype.createEl.call(this, 'li', Lib.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
*/
MenuItem.prototype.onClick = function(){
this.selected(true);
};
/**
* Set this menu item as selected or not
* @param {Boolean} selected
*/
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
*/
var MenuButton = Button.extend({
/** @constructor */
init: function(player, options){
Button.call(this, player, options);
this.update();
this.on('keydown', this.onKeyPress);
this.el_.setAttribute('aria-haspopup', true);
this.el_.setAttribute('role', 'button');
}
});
MenuButton.prototype.update = function() {
let menu = this.createMenu();
if (this.menu) {
this.removeChild(this.menu);
}
this.menu = menu;
this.addChild(menu);
if (this.items && this.items.length === 0) {
this.hide();
} else if (this.items && this.items.length > 1) {
this.show();
}
};
/**
* Track the state of the menu button
* @type {Boolean}
* @private
*/
MenuButton.prototype.buttonPressed_ = false;
MenuButton.prototype.createMenu = function(){
var menu = new Menu(this.player_);
// Add a title list item to the top
if (this.options().title) {
menu.contentEl().appendChild(Lib.createEl('li', {
className: 'vjs-menu-title',
innerHTML: Lib.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.
*/
MenuButton.prototype.createItems = function(){};
/** @inheritDoc */
MenuButton.prototype.buildCSSClass = function(){
return this.className + ' vjs-menu-button ' + 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.
MenuButton.prototype.onFocus = function(){};
// Can't turn off list display that we turned on with focus, because list would go away.
MenuButton.prototype.onBlur = function(){};
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', Lib.bind(this, function(){
this.menu.unlockShowing();
this.el_.blur();
}));
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
};
MenuButton.prototype.onKeyPress = function(event){
// Check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) {
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
event.preventDefault();
// Check for escape (27) key
} else if (event.which == 27){
if (this.buttonPressed_){
this.unpressButton();
}
event.preventDefault();
}
};
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
}
};
MenuButton.prototype.unpressButton = function(){
this.buttonPressed_ = false;
this.menu.unlockShowing();
this.el_.setAttribute('aria-pressed', false);
};
Component.registerComponent('Menu', Menu);
Component.registerComponent('MenuButton', MenuButton);
Component.registerComponent('MenuItem', MenuItem);
export default Menu;
export { MenuItem, MenuButton };

141
src/js/menu/menu-button.js Normal file
View File

@ -0,0 +1,141 @@
import Button from '../button.js';
import Menu from './menu.js';
import * as Lib from '../lib.js';
/**
* A button class with a popup menu
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class MenuButton extends Button {
constructor(player, options){
super(player, options);
this.update();
this.on('keydown', this.onKeyPress);
this.el_.setAttribute('aria-haspopup', true);
this.el_.setAttribute('role', 'button');
}
update() {
let menu = this.createMenu();
if (this.menu) {
this.removeChild(this.menu);
}
this.menu = menu;
this.addChild(menu);
/**
* Track the state of the menu button
* @type {Boolean}
* @private
*/
this.buttonPressed_ = false;
if (this.items && this.items.length === 0) {
this.hide();
} else if (this.items && this.items.length > 1) {
this.show();
}
}
createMenu() {
var menu = new Menu(this.player_);
// Add a title list item to the top
if (this.options().title) {
menu.contentEl().appendChild(Lib.createEl('li', {
className: 'vjs-menu-title',
innerHTML: Lib.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.
*/
createItems(){}
/** @inheritDoc */
buildCSSClass() {
return this.className + ' vjs-menu-button ' + super.buildCSSClass();
}
// 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.
onFocus() {}
// Can't turn off list display that we turned on with focus, because list would go away.
onBlur() {}
onClick() {
// 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', Lib.bind(this, function(){
this.menu.unlockShowing();
this.el_.blur();
}));
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
}
onKeyPress(event) {
// Check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) {
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
event.preventDefault();
// Check for escape (27) key
} else if (event.which == 27){
if (this.buttonPressed_){
this.unpressButton();
}
event.preventDefault();
}
}
pressButton() {
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
}
}
unpressButton() {
this.buttonPressed_ = false;
this.menu.unlockShowing();
this.el_.setAttribute('aria-pressed', false);
}
}
Button.registerComponent('MenuButton', MenuButton);
export default MenuButton;

51
src/js/menu/menu-item.js Normal file
View File

@ -0,0 +1,51 @@
import Button from '../button.js';
import * as Lib from '../lib.js';
/**
* The component for a menu item. `<li>`
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
class MenuItem extends Button {
constructor(player, options) {
super(player, options);
this.selected(options['selected']);
}
/** @inheritDoc */
createEl(type, props) {
return super.createEl('li', Lib.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
*/
onClick() {
this.selected(true);
}
/**
* Set this menu item as selected or not
* @param {Boolean} selected
*/
selected(selected) {
if (selected) {
this.addClass('vjs-selected');
this.el_.setAttribute('aria-selected',true);
} else {
this.removeClass('vjs-selected');
this.el_.setAttribute('aria-selected',false);
}
}
}
Button.registerComponent('MenuItem', MenuItem);
export default MenuItem;

52
src/js/menu/menu.js Normal file
View File

@ -0,0 +1,52 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
import * as Events from '../events.js';
/* 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
*/
class Menu extends Component {
/**
* Add a menu item to the menu
* @param {Object|String} component Component or component type to add
*/
addItem(component) {
this.addChild(component);
component.on('click', Lib.bind(this, function(){
this.unlockShowing();
}));
}
createEl() {
let contentElType = this.options().contentElType || 'ul';
this.contentEl_ = Lib.createEl(contentElType, {
className: 'vjs-menu-content'
});
var el = super.createEl('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
Events.on(el, 'click', function(event){
event.preventDefault();
event.stopImmediatePropagation();
});
return el;
}
}
Component.registerComponent('Menu', Menu);
export default Menu;

File diff suppressed because it is too large Load Diff

101
src/js/poster-image.js Normal file
View File

@ -0,0 +1,101 @@
import Button from './button';
import * as Lib from './lib';
/* Poster Image
================================================================================ */
/**
* The component that handles showing the poster image.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class PosterImage extends Button {
constructor(player, options){
super(player, options);
this.update();
player.on('posterchange', Lib.bind(this, this.update));
}
/**
* Clean up the poster image
*/
dispose() {
this.player().off('posterchange', this.update);
super.dispose();
}
/**
* Create the poster image element
* @return {Element}
*/
createEl() {
let el = Lib.createEl('div', {
className: 'vjs-poster',
// Don't want poster to be tabbable.
tabIndex: -1
});
// To ensure the poster image resizes while maintaining its original aspect
// ratio, use a div with `background-size` when available. For browsers that
// do not support `background-size` (e.g. IE8), fall back on using a regular
// img element.
if (!Lib.BACKGROUND_SIZE_SUPPORTED) {
this.fallbackImg_ = Lib.createEl('img');
el.appendChild(this.fallbackImg_);
}
return el;
}
/**
* Event handler for updates to the player's poster source
*/
update() {
let url = this.player().poster();
this.setSrc(url);
// If there's no poster source we should display:none on this component
// so it's not still clickable or right-clickable
if (url) {
this.show();
} else {
this.hide();
}
}
/**
* Set the poster source depending on the display method
*/
setSrc(url) {
if (this.fallbackImg_) {
this.fallbackImg_.src = url;
} else {
let backgroundImage = '';
// Any falsey values should stay as an empty string, otherwise
// this will throw an extra error
if (url) {
backgroundImage = 'url("' + url + '")';
}
this.el_.style.backgroundImage = backgroundImage;
}
}
/**
* Event handler for clicks on the poster image
*/
onClick() {
// We don't want a click to trigger playback when controls are disabled
// but CSS should be hiding the poster to prevent that from happening
this.player_.play();
}
}
Button.registerComponent('PosterImage', PosterImage);
export default PosterImage;

View File

@ -1,102 +0,0 @@
import Button from './button';
import * as Lib from './lib';
import Component from './component';
/* Poster Image
================================================================================ */
/**
* The component that handles showing the poster image.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let PosterImage = Button.extend({
/** @constructor */
init: function(player, options){
Button.call(this, player, options);
this.update();
player.on('posterchange', Lib.bind(this, this.update));
}
});
Component.registerComponent('PosterImage', PosterImage);
/**
* Clean up the poster image
*/
PosterImage.prototype.dispose = function(){
this.player().off('posterchange', this.update);
Button.prototype.dispose.call(this);
};
/**
* Create the poster image element
* @return {Element}
*/
PosterImage.prototype.createEl = function(){
let el = Lib.createEl('div', {
className: 'vjs-poster',
// Don't want poster to be tabbable.
tabIndex: -1
});
// To ensure the poster image resizes while maintaining its original aspect
// ratio, use a div with `background-size` when available. For browsers that
// do not support `background-size` (e.g. IE8), fall back on using a regular
// img element.
if (!Lib.BACKGROUND_SIZE_SUPPORTED) {
this.fallbackImg_ = Lib.createEl('img');
el.appendChild(this.fallbackImg_);
}
return el;
};
/**
* Event handler for updates to the player's poster source
*/
PosterImage.prototype.update = function(){
let url = this.player().poster();
this.setSrc(url);
// If there's no poster source we should display:none on this component
// so it's not still clickable or right-clickable
if (url) {
this.show();
} else {
this.hide();
}
};
/**
* Set the poster source depending on the display method
*/
PosterImage.prototype.setSrc = function(url){
if (this.fallbackImg_) {
this.fallbackImg_.src = url;
} else {
let backgroundImage = '';
// Any falsey values should stay as an empty string, otherwise
// this will throw an extra error
if (url) {
backgroundImage = 'url("' + url + '")';
}
this.el_.style.backgroundImage = backgroundImage;
}
};
/**
* Event handler for clicks on the poster image
*/
PosterImage.prototype.onClick = function(){
// We don't want a click to trigger playback when controls are disabled
// but CSS should be hiding the poster to prevent that from happening
this.player_.play();
};
export default PosterImage;

View File

@ -1,290 +0,0 @@
import Component from './component';
import * as Lib from './lib';
import document from 'global/document';
/* Slider
================================================================================ */
/**
* The base functionality for sliders like the volume bar and seek bar
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let Slider = Component.extend({
/** @constructor */
init: function(player, options){
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']);
// Set a horizontal or vertical class on the slider depending on the slider type
this.vertical(!!this.options()['vertical']);
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);
}
});
Component.registerComponent('Slider', Slider);
Slider.prototype.createEl = function(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider';
props = Lib.obj.merge({
'role': 'slider',
'aria-valuenow': 0,
'aria-valuemin': 0,
'aria-valuemax': 100,
tabIndex: 0
}, props);
return Component.prototype.createEl.call(this, type, props);
};
Slider.prototype.onMouseDown = function(event){
event.preventDefault();
Lib.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
Slider.prototype.onMouseMove = function(){};
Slider.prototype.onMouseUp = function() {
Lib.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();
};
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();
let progress = this.getPercent();
let bar = this.bar;
// If there's no bar...
if (!bar) return;
// Protect against no duration and other division issues
if (typeof progress !== 'number' ||
progress !== progress ||
progress < 0 ||
progress === Infinity) {
progress = 0;
}
// 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.
let barProgress = this.updateHandlePosition(progress);
// Convert to a percentage for setting
let percentage = Lib.round(barProgress * 100, 2) + '%';
// Set the new bar width or height
if (this.vertical()) {
bar.el().style.height = percentage;
} else {
bar.el().style.width = percentage;
}
};
/**
* Update the handle position.
*/
Slider.prototype.updateHandlePosition = function(progress) {
let handle = this.handle;
if (!handle) return;
let vertical = this.vertical();
let box = this.el_;
let boxSize, handleSize;
if (vertical) {
boxSize = box.offsetHeight;
handleSize = handle.el().offsetHeight;
} else {
boxSize = box.offsetWidth;
handleSize = handle.el().offsetWidth;
}
// The width of the handle in percent of the containing box
// In IE, widths may not be ready yet causing NaN
let handlePercent = (handleSize) ? handleSize / boxSize : 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.
let boxAdjustedPercent = 1 - handlePercent;
// Adjust the progress that we'll use to set widths to the new adjusted box width
let adjustedProgress = progress * boxAdjustedPercent;
// The bar does reach the left side, so we need to account for this in the bar's width
let barProgress = adjustedProgress + (handlePercent / 2);
let percentage = Lib.round(adjustedProgress * 100, 2) + '%';
if (vertical) {
handle.el().style.bottom = percentage;
} else {
handle.el().style.left = percentage;
}
return barProgress;
};
Slider.prototype.calculateDistance = function(event){
let el = this.el_;
let box = Lib.findPosition(el);
let boxW = el.offsetWidth;
let boxH = el.offsetHeight;
let handle = this.handle;
if (this.options()['vertical']) {
let boxY = box.top;
let pageY;
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 {
let boxX = box.left;
let pageX;
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));
}
};
Slider.prototype.onFocus = function(){
this.on(document, 'keydown', this.onKeyPress);
};
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();
}
};
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
*/
Slider.prototype.onClick = function(event){
event.stopImmediatePropagation();
event.preventDefault();
};
Slider.prototype.vertical_ = false;
Slider.prototype.vertical = function(bool) {
if (bool === undefined) {
return this.vertical_;
}
this.vertical_ = !!bool;
if (this.vertical_) {
this.addClass('vjs-slider-vertical');
} else {
this.addClass('vjs-slider-horizontal');
}
return this;
};
/**
* 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
*/
var SliderHandle = Component.extend();
Component.registerComponent('Slider', Slider);
/**
* Default value of the slider
*
* @type {Number}
* @private
*/
SliderHandle.prototype.defaultValue = 0;
/** @inheritDoc */
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 = Lib.obj.merge({
innerHTML: '<span class="vjs-control-text">'+this.defaultValue+'</span>'
}, props);
return Component.prototype.createEl.call(this, 'div', props);
};
export default Slider;
export { SliderHandle };

View File

@ -0,0 +1,28 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
/**
* 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
*/
class SliderHandle extends Component {
/** @inheritDoc */
createEl(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider-handle';
props = Lib.obj.merge({
innerHTML: '<span class="vjs-control-text">'+(this.defaultValue || 0)+'</span>'
}, props);
return super.createEl('div', props);
}
}
Component.registerComponent('SliderHandle', SliderHandle);
export default SliderHandle;

257
src/js/slider/slider.js Normal file
View File

@ -0,0 +1,257 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
import document from 'global/document';
/* Slider
================================================================================ */
/**
* The base functionality for sliders like the volume bar and seek bar
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class Slider extends Component {
constructor(player, options) {
super(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']);
// Set a horizontal or vertical class on the slider depending on the slider type
this.vertical(!!this.options()['vertical']);
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);
}
createEl(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider';
props = Lib.obj.merge({
'role': 'slider',
'aria-valuenow': 0,
'aria-valuemin': 0,
'aria-valuemax': 100,
tabIndex: 0
}, props);
return super.createEl(type, props);
}
onMouseDown(event) {
event.preventDefault();
Lib.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
onMouseMove() {}
onMouseUp() {
Lib.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();
}
update() {
// 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();
let progress = this.getPercent();
let bar = this.bar;
// If there's no bar...
if (!bar) return;
// Protect against no duration and other division issues
if (typeof progress !== 'number' ||
progress !== progress ||
progress < 0 ||
progress === Infinity) {
progress = 0;
}
// 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.
let barProgress = this.updateHandlePosition(progress);
// Convert to a percentage for setting
let percentage = Lib.round(barProgress * 100, 2) + '%';
// Set the new bar width or height
if (this.vertical()) {
bar.el().style.height = percentage;
} else {
bar.el().style.width = percentage;
}
}
/**
* Update the handle position.
*/
updateHandlePosition(progress) {
let handle = this.handle;
if (!handle) return;
let vertical = this.vertical();
let box = this.el_;
let boxSize, handleSize;
if (vertical) {
boxSize = box.offsetHeight;
handleSize = handle.el().offsetHeight;
} else {
boxSize = box.offsetWidth;
handleSize = handle.el().offsetWidth;
}
// The width of the handle in percent of the containing box
// In IE, widths may not be ready yet causing NaN
let handlePercent = (handleSize) ? handleSize / boxSize : 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.
let boxAdjustedPercent = 1 - handlePercent;
// Adjust the progress that we'll use to set widths to the new adjusted box width
let adjustedProgress = progress * boxAdjustedPercent;
// The bar does reach the left side, so we need to account for this in the bar's width
let barProgress = adjustedProgress + (handlePercent / 2);
let percentage = Lib.round(adjustedProgress * 100, 2) + '%';
if (vertical) {
handle.el().style.bottom = percentage;
} else {
handle.el().style.left = percentage;
}
return barProgress;
}
calculateDistance(event){
let el = this.el_;
let box = Lib.findPosition(el);
let boxW = el.offsetWidth;
let boxH = el.offsetHeight;
let handle = this.handle;
if (this.options()['vertical']) {
let boxY = box.top;
let pageY;
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 {
let boxX = box.left;
let pageX;
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));
}
}
onFocus() {
this.on(document, 'keydown', this.onKeyPress);
}
onKeyPress(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();
}
}
onBlur() {
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
*/
onClick(event) {
event.stopImmediatePropagation();
event.preventDefault();
}
vertical(bool) {
if (bool === undefined) {
return this.vertical_ || false;
}
this.vertical_ = !!bool;
if (this.vertical_) {
this.addClass('vjs-slider-vertical');
} else {
this.addClass('vjs-slider-horizontal');
}
return this;
}
}
Component.registerComponent('Slider', Slider);
export default Slider;

View File

@ -4,7 +4,7 @@
* Not using setupTriggers. Using global onEvent func to distribute events * Not using setupTriggers. Using global onEvent func to distribute events
*/ */
import MediaTechController from './media'; import Tech from './tech';
import * as Lib from '../lib'; import * as Lib from '../lib';
import FlashRtmpDecorator from './flash-rtmp'; import FlashRtmpDecorator from './flash-rtmp';
import Component from '../component'; import Component from '../component';
@ -19,10 +19,10 @@ let navigator = window.navigator;
* @param {Function=} ready * @param {Function=} ready
* @constructor * @constructor
*/ */
var Flash = MediaTechController.extend({ class Flash extends Tech {
/** @constructor */
init: function(player, options, ready){ constructor(player, options, ready){
MediaTechController.call(this, player, options, ready); super(player, options, ready);
let { source, parentEl } = options; let { source, parentEl } = options;
@ -103,32 +103,25 @@ var Flash = MediaTechController.extend({
this.el_ = Flash.embed(options['swf'], placeHolder, flashVars, params, attributes); this.el_ = Flash.embed(options['swf'], placeHolder, flashVars, params, attributes);
} }
});
Component.registerComponent('Flash', Flash); play() {
Flash.prototype.dispose = function(){
MediaTechController.prototype.dispose.call(this);
};
Flash.prototype.play = function(){
this.el_.vjs_play(); this.el_.vjs_play();
}; }
Flash.prototype.pause = function(){ pause() {
this.el_.vjs_pause(); this.el_.vjs_pause();
}; }
Flash.prototype.src = function(src){ src(src) {
if (src === undefined) { if (src === undefined) {
return this['currentSrc'](); return this['currentSrc']();
} }
// Setting src through `src` not `setSrc` will be deprecated // Setting src through `src` not `setSrc` will be deprecated
return this.setSrc(src); return this.setSrc(src);
}; }
Flash.prototype.setSrc = function(src){ setSrc(src) {
// Make sure source URL is absolute. // Make sure source URL is absolute.
src = Lib.getAbsoluteURL(src); src = Lib.getAbsoluteURL(src);
this.el_.vjs_src(src); this.el_.vjs_src(src);
@ -139,53 +132,56 @@ Flash.prototype.setSrc = function(src){
var tech = this; var tech = this;
this.setTimeout(function(){ tech.play(); }, 0); this.setTimeout(function(){ tech.play(); }, 0);
} }
}; }
Flash.prototype['setCurrentTime'] = function(time){ setCurrentTime(time) {
this.lastSeekTarget_ = time; this.lastSeekTarget_ = time;
this.el_.vjs_setProperty('currentTime', time); this.el_.vjs_setProperty('currentTime', time);
MediaTechController.prototype.setCurrentTime.call(this); super.setCurrentTime();
}; }
Flash.prototype['currentTime'] = function(time){ currentTime(time) {
// when seeking make the reported time keep up with the requested time // when seeking make the reported time keep up with the requested time
// by reading the time we're seeking to // by reading the time we're seeking to
if (this.seeking()) { if (this.seeking()) {
return this.lastSeekTarget_ || 0; return this.lastSeekTarget_ || 0;
} }
return this.el_.vjs_getProperty('currentTime'); return this.el_.vjs_getProperty('currentTime');
}; }
Flash.prototype['currentSrc'] = function(){ currentSrc() {
if (this.currentSource_) { if (this.currentSource_) {
return this.currentSource_.src; return this.currentSource_.src;
} else { } else {
return this.el_.vjs_getProperty('currentSrc'); return this.el_.vjs_getProperty('currentSrc');
} }
}; }
Flash.prototype.load = function(){ load() {
this.el_.vjs_load(); this.el_.vjs_load();
}; }
Flash.prototype.poster = function(){ poster() {
this.el_.vjs_getProperty('poster'); this.el_.vjs_getProperty('poster');
}; }
Flash.prototype['setPoster'] = function(){
// poster images are not handled by the Flash tech so make this a no-op // poster images are not handled by the Flash tech so make this a no-op
}; setPoster() {}
Flash.prototype.buffered = function(){ buffered() {
return Lib.createTimeRange(0, this.el_.vjs_getProperty('buffered')); return Lib.createTimeRange(0, this.el_.vjs_getProperty('buffered'));
}; }
Flash.prototype.supportsFullScreen = function(){ supportsFullScreen() {
return false; // Flash does not allow fullscreen through javascript return false; // Flash does not allow fullscreen through javascript
}; }
Flash.prototype.enterFullScreen = function(){ enterFullScreen() {
return false; return false;
}; }
}
// Create setters and getters for attributes // Create setters and getters for attributes
const _api = Flash.prototype; const _api = Flash.prototype;
@ -219,7 +215,7 @@ Flash.isSupported = function(){
}; };
// Add Source Handler pattern functions to this tech // Add Source Handler pattern functions to this tech
MediaTechController.withSourceHandlers(Flash); Tech.withSourceHandlers(Flash);
/** /**
* The default native source handler. * The default native source handler.
@ -421,4 +417,5 @@ Flash.getEmbedCode = function(swf, flashVars, params, attributes){
// Run Flash through the RTMP decorator // Run Flash through the RTMP decorator
FlashRtmpDecorator(Flash); FlashRtmpDecorator(Flash);
Tech.registerComponent('Flash', Flash);
export default Flash; export default Flash;

682
src/js/tech/html5.js Normal file
View File

@ -0,0 +1,682 @@
/**
* @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API
*/
import Tech from './tech.js';
import Component from '../component';
import * as Lib from '../lib';
import * as VjsUtil from '../util';
import document from 'global/document';
/**
* HTML5 Media Controller - Wrapper for HTML5 Media API
* @param {vjs.Player|Object} player
* @param {Object=} options
* @param {Function=} ready
* @constructor
*/
class Html5 extends Tech {
constructor(player, options, ready){
super(player, options, ready);
this.setupTriggers();
const source = options['source'];
// Set the source if one is provided
// 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
// 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
// anyway so the error gets fired.
if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) {
this.setSource(source);
}
if (this.el_.hasChildNodes()) {
let nodes = this.el_.childNodes;
let nodesLength = nodes.length;
let removeNodes = [];
while (nodesLength--) {
let node = nodes[nodesLength];
let nodeName = node.nodeName.toLowerCase();
if (nodeName === 'track') {
if (!this['featuresNativeTextTracks']) {
// Empty video tag tracks so the built-in player doesn't use them also.
// This may not be fast enough to stop HTML5 browsers from reading the tags
// so we'll need to turn off any default tracks if we're manually doing
// captions and subtitles. videoElement.textTracks
removeNodes.push(node);
} else {
this.remoteTextTracks().addTrack_(node['track']);
}
}
}
for (let i=0; i<removeNodes.length; i++) {
this.el_.removeChild(removeNodes[i]);
}
}
if (this['featuresNativeTextTracks']) {
this.on('loadstart', Lib.bind(this, this.hideCaptions));
}
// Determine if native controls should be used
// Our goal should be to get the custom controls on mobile solid everywhere
// so we can remove this all together. Right now this will block custom
// controls on touch enabled laptops like the Chrome Pixel
if (Lib.TOUCH_ENABLED && player.options()['nativeControlsForTouch'] === true) {
this.useNativeControls();
}
// Chrome and Safari both have issues with autoplay.
// In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
// In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
// This fixes both issues. Need to wait for API, so it updates displays correctly
player.ready(function(){
if (this.tag && this.options_['autoplay'] && this.paused()) {
delete this.tag['poster']; // Chrome Fix. Fixed in Chrome v16.
this.play();
}
});
this.triggerReady();
}
dispose() {
Html5.disposeMediaElement(this.el_);
super.dispose();
}
createEl() {
let player = this.player_;
let el = player.tag;
// Check if this browser supports moving the element into the box.
// On the iPhone video will break if you move the element,
// So we have to create a brand new element.
if (!el || this['movingMediaElementInDOM'] === false) {
// If the original tag is still there, clone and remove it.
if (el) {
const clone = el.cloneNode(false);
Html5.disposeMediaElement(el);
el = clone;
player.tag = null;
} else {
el = Lib.createEl('video');
// determine if native controls should be used
let attributes = VjsUtil.mergeOptions({}, player.tagAttributes);
if (!Lib.TOUCH_ENABLED || player.options()['nativeControlsForTouch'] !== true) {
delete attributes.controls;
}
Lib.setElementAttributes(el,
Lib.obj.merge(attributes, {
id: player.id() + '_html5_api',
class: 'vjs-tech'
})
);
}
// associate the player with the new tag
el['player'] = player;
if (player.options_.tracks) {
for (let i = 0; i < player.options_.tracks.length; i++) {
const track = player.options_.tracks[i];
let trackEl = document.createElement('track');
trackEl.kind = track.kind;
trackEl.label = track.label;
trackEl.srclang = track.srclang;
trackEl.src = track.src;
if ('default' in track) {
trackEl.setAttribute('default', 'default');
}
el.appendChild(trackEl);
}
}
Lib.insertFirst(el, player.el());
}
// Update specific tag settings, in case they were overridden
let settingsAttrs = ['autoplay','preload','loop','muted'];
for (let i = settingsAttrs.length - 1; i >= 0; i--) {
const attr = settingsAttrs[i];
let overwriteAttrs = {};
if (typeof player.options_[attr] !== 'undefined') {
overwriteAttrs[attr] = player.options_[attr];
}
Lib.setElementAttributes(el, overwriteAttrs);
}
return el;
// jenniisawesome = true;
}
hideCaptions() {
let tracks = this.el_.querySelectorAll('track');
let i = tracks.length;
const kinds = {
'captions': 1,
'subtitles': 1
};
while (i--) {
let track = tracks[i].track;
if ((track && track['kind'] in kinds) &&
(!tracks[i]['default'])) {
track.mode = 'disabled';
}
}
}
// Make video events trigger player events
// May seem verbose here, but makes other APIs possible.
// Triggers removed using this.off when disposed
setupTriggers() {
for (let i = Html5.Events.length - 1; i >= 0; i--) {
this.on(Html5.Events[i], this.eventHandler);
}
}
eventHandler(evt) {
// In the case of an error on the video element, set the error prop
// on the player and let the player handle triggering the event. On
// some platforms, error events fire that do not cause the error
// property on the video element to be set. See #1465 for an example.
if (evt.type == 'error' && this.error()) {
this.player().error(this.error().code);
// in some cases we pass the event directly to the player
} else {
// No need for media events to bubble up.
evt.bubbles = false;
this.player().trigger(evt);
}
}
useNativeControls() {
let tech = this;
let player = this.player();
// If the player controls are enabled turn on the native controls
tech.setControls(player.controls());
// Update the native controls when player controls state is updated
let controlsOn = function(){
tech.setControls(true);
};
let controlsOff = function(){
tech.setControls(false);
};
player.on('controlsenabled', controlsOn);
player.on('controlsdisabled', controlsOff);
// Clean up when not using native controls anymore
let cleanUp = function(){
player.off('controlsenabled', controlsOn);
player.off('controlsdisabled', controlsOff);
};
tech.on('dispose', cleanUp);
player.on('usingcustomcontrols', cleanUp);
// Update the state of the player to using native controls
player.usingNativeControls(true);
}
play() { this.el_.play(); }
pause() { this.el_.pause(); }
paused() { return this.el_.paused; }
currentTime() { return this.el_.currentTime; }
setCurrentTime(seconds) {
try {
this.el_.currentTime = seconds;
} catch(e) {
Lib.log(e, 'Video is not ready. (Video.js)');
// this.warning(VideoJS.warnings.videoNotReady);
}
}
duration() { return this.el_.duration || 0; }
buffered() { return this.el_.buffered; }
volume() { return this.el_.volume; }
setVolume(percentAsDecimal) { this.el_.volume = percentAsDecimal; }
muted() { return this.el_.muted; }
setMuted(muted) { this.el_.muted = muted; }
width() { return this.el_.offsetWidth; }
height() { return this.el_.offsetHeight; }
supportsFullScreen() {
if (typeof this.el_.webkitEnterFullScreen == 'function') {
// Seems to be broken in Chromium/Chrome && Safari in Leopard
if (/Android/.test(Lib.USER_AGENT) || !/Chrome|Mac OS X 10.5/.test(Lib.USER_AGENT)) {
return true;
}
}
return false;
}
enterFullScreen() {
var video = this.el_;
if ('webkitDisplayingFullscreen' in video) {
this.one('webkitbeginfullscreen', function() {
this.player_.isFullscreen(true);
this.one('webkitendfullscreen', function() {
this.player_.isFullscreen(false);
this.player_.trigger('fullscreenchange');
});
this.player_.trigger('fullscreenchange');
});
}
if (video.paused && video.networkState <= video.HAVE_METADATA) {
// attempt to prime the video element for programmatic access
// this isn't necessary on the desktop but shouldn't hurt
this.el_.play();
// playing and pausing synchronously during the transition to fullscreen
// can get iOS ~6.1 devices into a play/pause loop
this.setTimeout(function(){
video.pause();
video.webkitEnterFullScreen();
}, 0);
} else {
video.webkitEnterFullScreen();
}
}
exitFullScreen() {
this.el_.webkitExitFullScreen();
}
src(src) {
if (src === undefined) {
return this.el_.src;
} else {
// Setting src through `src` instead of `setSrc` will be deprecated
this.setSrc(src);
}
}
setSrc(src) { this.el_.src = src; }
load(){ this.el_.load(); }
currentSrc() { return this.el_.currentSrc; }
poster() { return this.el_.poster; }
setPoster(val) { this.el_.poster = val; }
preload() { return this.el_.preload; }
setPreload(val) { this.el_.preload = val; }
autoplay() { return this.el_.autoplay; }
setAutoplay(val) { this.el_.autoplay = val; }
controls() { return this.el_.controls; }
setControls(val) { this.el_.controls = !!val; }
loop() { return this.el_.loop; }
setLoop(val) { this.el_.loop = val; }
error() { return this.el_.error; }
seeking() { return this.el_.seeking; }
ended() { return this.el_.ended; }
defaultMuted() { return this.el_.defaultMuted; }
playbackRate() { return this.el_.playbackRate; }
setPlaybackRate(val) { this.el_.playbackRate = val; }
networkState() { return this.el_.networkState; }
readyState() { return this.el_.readyState; }
textTracks() {
if (!this['featuresNativeTextTracks']) {
return super.textTracks();
}
return this.el_.textTracks;
}
addTextTrack(kind, label, language) {
if (!this['featuresNativeTextTracks']) {
return super.addTextTrack(kind, label, language);
}
return this.el_.addTextTrack(kind, label, language);
}
addRemoteTextTrack(options) {
if (!this['featuresNativeTextTracks']) {
return super.addRemoteTextTrack(options);
}
var track = document.createElement('track');
options = options || {};
if (options['kind']) {
track['kind'] = options['kind'];
}
if (options['label']) {
track['label'] = options['label'];
}
if (options['language'] || options['srclang']) {
track['srclang'] = options['language'] || options['srclang'];
}
if (options['default']) {
track['default'] = options['default'];
}
if (options['id']) {
track['id'] = options['id'];
}
if (options['src']) {
track['src'] = options['src'];
}
this.el().appendChild(track);
if (track.track['kind'] === 'metadata') {
track['track']['mode'] = 'hidden';
} else {
track['track']['mode'] = 'disabled';
}
track['onload'] = function() {
var tt = track['track'];
if (track.readyState >= 2) {
if (tt['kind'] === 'metadata' && tt['mode'] !== 'hidden') {
tt['mode'] = 'hidden';
} else if (tt['kind'] !== 'metadata' && tt['mode'] !== 'disabled') {
tt['mode'] = 'disabled';
}
track['onload'] = null;
}
};
this.remoteTextTracks().addTrack_(track.track);
return track;
}
removeRemoteTextTrack(track) {
if (!this['featuresNativeTextTracks']) {
return super.removeRemoteTextTrack(track);
}
var tracks, i;
this.remoteTextTracks().removeTrack_(track);
tracks = this.el()['querySelectorAll']('track');
for (i = 0; i < tracks.length; i++) {
if (tracks[i] === track || tracks[i]['track'] === track) {
tracks[i]['parentNode']['removeChild'](tracks[i]);
break;
}
}
}
}
/* HTML5 Support Testing ---------------------------------------------------- */
/**
* Check if HTML5 video is supported by this browser/device
* @return {Boolean}
*/
Html5.isSupported = function(){
// IE9 with no Media Player is a LIAR! (#984)
try {
Lib.TEST_VID['volume'] = 0.5;
} catch (e) {
return false;
}
return !!Lib.TEST_VID.canPlayType;
};
// Add Source Handler pattern functions to this tech
Tech.withSourceHandlers(Html5);
/**
* The default native source handler.
* This simply passes the source to the video element. Nothing fancy.
* @param {Object} source The source object
* @param {vjs.Html5} tech The instance of the HTML5 tech
*/
Html5.nativeSourceHandler = {};
/**
* Check if the video element can handle the source natively
* @param {Object} source The source object
* @return {String} 'probably', 'maybe', or '' (empty string)
*/
Html5.nativeSourceHandler.canHandleSource = function(source){
var match, ext;
function canPlayType(type){
// IE9 on Windows 7 without MediaPlayer throws an error here
// https://github.com/videojs/video.js/issues/519
try {
return Lib.TEST_VID.canPlayType(type);
} catch(e) {
return '';
}
}
// If a type was provided we should rely on that
if (source.type) {
return canPlayType(source.type);
} else if (source.src) {
// If no type, fall back to checking 'video/[EXTENSION]'
ext = Lib.getFileExtension(source.src);
return canPlayType('video/'+ext);
}
return '';
};
/**
* Pass the source to the video element
* Adaptive source handlers will have more complicated workflows before passing
* video data to the video element
* @param {Object} source The source object
* @param {vjs.Html5} tech The instance of the Html5 tech
*/
Html5.nativeSourceHandler.handleSource = function(source, tech){
tech.setSrc(source.src);
};
/**
* Clean up the source handler when disposing the player or switching sources..
* (no cleanup is needed when supporting the format natively)
*/
Html5.nativeSourceHandler.dispose = function(){};
// Register the native source handler
Html5.registerSourceHandler(Html5.nativeSourceHandler);
/**
* Check if the volume can be changed in this browser/device.
* Volume cannot be changed in a lot of mobile devices.
* Specifically, it can't be changed from 1 on iOS.
* @return {Boolean}
*/
Html5.canControlVolume = function(){
var volume = Lib.TEST_VID.volume;
Lib.TEST_VID.volume = (volume / 2) + 0.1;
return volume !== Lib.TEST_VID.volume;
};
/**
* Check if playbackRate is supported in this browser/device.
* @return {[type]} [description]
*/
Html5.canControlPlaybackRate = function(){
var playbackRate = Lib.TEST_VID.playbackRate;
Lib.TEST_VID.playbackRate = (playbackRate / 2) + 0.1;
return playbackRate !== Lib.TEST_VID.playbackRate;
};
/**
* Check to see if native text tracks are supported by this browser/device
* @return {Boolean}
*/
Html5.supportsNativeTextTracks = function() {
var supportsTextTracks;
// Figure out native text track support
// If mode is a number, we cannot change it because it'll disappear from view.
// Browsers with numeric modes include IE10 and older (<=2013) samsung android models.
// Firefox isn't playing nice either with modifying the mode
// TODO: Investigate firefox: https://github.com/videojs/video.js/issues/1862
supportsTextTracks = !!Lib.TEST_VID.textTracks;
if (supportsTextTracks && Lib.TEST_VID.textTracks.length > 0) {
supportsTextTracks = typeof Lib.TEST_VID.textTracks[0]['mode'] !== 'number';
}
if (supportsTextTracks && Lib.IS_FIREFOX) {
supportsTextTracks = false;
}
return supportsTextTracks;
};
/**
* Set the tech's volume control support status
* @type {Boolean}
*/
Html5.prototype['featuresVolumeControl'] = Html5.canControlVolume();
/**
* Set the tech's playbackRate support status
* @type {Boolean}
*/
Html5.prototype['featuresPlaybackRate'] = Html5.canControlPlaybackRate();
/**
* Set the tech's status on moving the video element.
* In iOS, if you move a video element in the DOM, it breaks video playback.
* @type {Boolean}
*/
Html5.prototype['movingMediaElementInDOM'] = !Lib.IS_IOS;
/**
* Set the the tech's fullscreen resize support status.
* HTML video is able to automatically resize when going to fullscreen.
* (No longer appears to be used. Can probably be removed.)
*/
Html5.prototype['featuresFullscreenResize'] = true;
/**
* Set the tech's progress event support status
* (this disables the manual progress events of the Tech)
*/
Html5.prototype['featuresProgressEvents'] = true;
/**
* Sets the tech's status on native text track support
* @type {Boolean}
*/
Html5.prototype['featuresNativeTextTracks'] = Html5.supportsNativeTextTracks();
// HTML5 Feature detection and Device Fixes --------------------------------- //
let canPlayType;
const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
const mp4RE = /^video\/mp4/i;
Html5.patchCanPlayType = function() {
// Android 4.0 and above can play HLS to some extent but it reports being unable to do so
if (Lib.ANDROID_VERSION >= 4.0) {
if (!canPlayType) {
canPlayType = Lib.TEST_VID.constructor.prototype.canPlayType;
}
Lib.TEST_VID.constructor.prototype.canPlayType = function(type) {
if (type && mpegurlRE.test(type)) {
return 'maybe';
}
return canPlayType.call(this, type);
};
}
// Override Android 2.2 and less canPlayType method which is broken
if (Lib.IS_OLD_ANDROID) {
if (!canPlayType) {
canPlayType = Lib.TEST_VID.constructor.prototype.canPlayType;
}
Lib.TEST_VID.constructor.prototype.canPlayType = function(type){
if (type && mp4RE.test(type)) {
return 'maybe';
}
return canPlayType.call(this, type);
};
}
};
Html5.unpatchCanPlayType = function() {
var r = Lib.TEST_VID.constructor.prototype.canPlayType;
Lib.TEST_VID.constructor.prototype.canPlayType = canPlayType;
canPlayType = null;
return r;
};
// by default, patch the video element
Html5.patchCanPlayType();
// List of all HTML5 events (various uses).
Html5.Events = 'loadstart,suspend,abort,error,emptied,stalled,loadedmetadata,loadeddata,canplay,canplaythrough,playing,waiting,seeking,seeked,ended,durationchange,timeupdate,progress,play,pause,ratechange,volumechange'.split(',');
Html5.disposeMediaElement = function(el){
if (!el) { return; }
el['player'] = null;
if (el.parentNode) {
el.parentNode.removeChild(el);
}
// remove any child track or source nodes to prevent their loading
while(el.hasChildNodes()) {
el.removeChild(el.firstChild);
}
// remove any src reference. not setting `src=''` because that causes a warning
// in firefox
el.removeAttribute('src');
// force the media element to update its loading state by calling load()
// however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
if (typeof el.load === 'function') {
// wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
(function() {
try {
el.load();
} catch (e) {
// not supported
}
})();
}
};
Component.registerComponent('Html5', Html5);
export default Html5;

View File

@ -8,10 +8,10 @@ import window from 'global/window';
* *
* @constructor * @constructor
*/ */
let MediaLoader = Component.extend({ class MediaLoader extends Component {
/** @constructor */
init: function(player, options, ready){ constructor(player, options, ready){
Component.call(this, player, options, ready); super(player, options, ready);
// If there are no sources when the player is initialized, // If there are no sources when the player is initialized,
// load the first supported playback technology. // load the first supported playback technology.
@ -34,8 +34,7 @@ let MediaLoader = Component.extend({
player.src(player.options_['sources']); player.src(player.options_['sources']);
} }
} }
}); }
Component.registerComponent('MediaLoader', MediaLoader); Component.registerComponent('MediaLoader', MediaLoader);
export default MediaLoader; export default MediaLoader;

536
src/js/tech/tech.js Normal file
View File

@ -0,0 +1,536 @@
/**
* @fileoverview Media Technology Controller - Base class for media playback
* technology controllers like Flash and HTML5
*/
import Component from '../component';
import TextTrack from '../tracks/text-track';
import TextTrackList from '../tracks/text-track-list';
import * as Lib from '../lib';
import window from 'global/window';
import document from 'global/document';
/**
* Base class for media (HTML5 Video, Flash) controllers
* @param {vjs.Player|Object} player Central player instance
* @param {Object=} options Options object
* @constructor
*/
class Tech extends Component {
constructor(player, options, ready){
options = options || {};
// we don't want the tech to report user activity automatically.
// This is done manually in addControlsListeners
options.reportTouchActivity = false;
super(player, options, ready);
// Manually track progress in cases where the browser/flash player doesn't report it.
if (!this['featuresProgressEvents']) {
this.manualProgressOn();
}
// Manually track timeupdates in cases where the browser/flash player doesn't report it.
if (!this['featuresTimeupdateEvents']) {
this.manualTimeUpdatesOn();
}
this.initControlsListeners();
if (options['nativeCaptions'] === false || options['nativeTextTracks'] === false) {
this['featuresNativeTextTracks'] = false;
}
if (!this['featuresNativeTextTracks']) {
this.emulateTextTracks();
}
this.initTextTrackListeners();
}
/**
* Set up click and touch listeners for the playback element
* On desktops, a click on the video itself will toggle playback,
* on a mobile device a click on the video toggles controls.
* (toggling controls is done by toggling the user state between active and
* inactive)
*
* A tap can signal that a user has become active, or has become inactive
* e.g. a quick tap on an iPhone movie should reveal the controls. Another
* quick tap should hide them again (signaling the user is in an inactive
* viewing state)
*
* In addition to this, we still want the user to be considered inactive after
* a few seconds of inactivity.
*
* Note: the only part of iOS interaction we can't mimic with this setup
* is a touch and hold on the video element counting as activity in order to
* keep the controls showing, but that shouldn't be an issue. A touch and hold on
* any controls will still keep the user active
*/
initControlsListeners() {
let player = this.player();
let activateControls = function(){
if (player.controls() && !player.usingNativeControls()) {
this.addControlsListeners();
}
};
// Set up event listeners once the tech is ready and has an element to apply
// listeners to
this.ready(activateControls);
this.on(player, 'controlsenabled', activateControls);
this.on(player, 'controlsdisabled', this.removeControlsListeners);
// if we're loading the playback object after it has started loading or playing the
// video (often with autoplay on) then the loadstart event has already fired and we
// need to fire it manually because many things rely on it.
// Long term we might consider how we would do this for other events like 'canplay'
// that may also have fired.
this.ready(function(){
if (this.networkState && this.networkState() > 0) {
this.player().trigger('loadstart');
}
});
}
addControlsListeners() {
let userWasActive;
// Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do
// trigger mousedown/up.
// http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object
// Any touch events are set to block the mousedown event from happening
this.on('mousedown', this.onClick);
// If the controls were hidden we don't want that to change without a tap event
// so we'll check if the controls were already showing before reporting user
// activity
this.on('touchstart', function(event) {
userWasActive = this.player_.userActive();
});
this.on('touchmove', function(event) {
if (userWasActive){
this.player().reportUserActivity();
}
});
this.on('touchend', function(event) {
// Stop the mouse events from also happening
event.preventDefault();
});
// Turn on component tap events
this.emitTapEvents();
// The tap listener needs to come after the touchend listener because the tap
// listener cancels out any reportedUserActivity when setting userActive(false)
this.on('tap', this.onTap);
}
/**
* Remove the listeners used for click and tap controls. This is needed for
* toggling to controls disabled, where a tap/touch should do nothing.
*/
removeControlsListeners() {
// We don't want to just use `this.off()` because there might be other needed
// listeners added by techs that extend this.
this.off('tap');
this.off('touchstart');
this.off('touchmove');
this.off('touchleave');
this.off('touchcancel');
this.off('touchend');
this.off('click');
this.off('mousedown');
}
/**
* Handle a click on the media element. By default will play/pause the media.
*/
onClick(event) {
// We're using mousedown to detect clicks thanks to Flash, but mousedown
// will also be triggered with right-clicks, so we need to prevent that
if (event.button !== 0) return;
// When controls are disabled a click should not toggle playback because
// the click is considered a control
if (this.player().controls()) {
if (this.player().paused()) {
this.player().play();
} else {
this.player().pause();
}
}
}
/**
* Handle a tap on the media element. By default it will toggle the user
* activity state, which hides and shows the controls.
*/
onTap() {
this.player().userActive(!this.player().userActive());
}
/* Fallbacks for unsupported event types
================================================================================ */
// Manually trigger progress events based on changes to the buffered amount
// Many flash players and older HTML5 browsers don't send progress or progress-like events
manualProgressOn() {
this.manualProgress = true;
// Trigger progress watching when a source begins loading
this.trackProgress();
}
manualProgressOff() {
this.manualProgress = false;
this.stopTrackingProgress();
}
trackProgress() {
this.progressInterval = this.setInterval(function(){
// Don't trigger unless buffered amount is greater than last time
let bufferedPercent = this.player().bufferedPercent();
if (this.bufferedPercent_ != bufferedPercent) {
this.player().trigger('progress');
}
this.bufferedPercent_ = bufferedPercent;
if (bufferedPercent === 1) {
this.stopTrackingProgress();
}
}, 500);
}
stopTrackingProgress() {
this.clearInterval(this.progressInterval);
}
/*! Time Tracking -------------------------------------------------------------- */
manualTimeUpdatesOn() {
let player = this.player_;
this.manualTimeUpdates = true;
this.on(player, 'play', this.trackCurrentTime);
this.on(player, 'pause', this.stopTrackingCurrentTime);
// timeupdate is also called by .currentTime whenever current time is set
// Watch for native timeupdate event
this.one('timeupdate', function(){
// Update known progress support for this playback technology
this['featuresTimeupdateEvents'] = true;
// Turn off manual progress tracking
this.manualTimeUpdatesOff();
});
}
manualTimeUpdatesOff() {
let player = this.player_;
this.manualTimeUpdates = false;
this.stopTrackingCurrentTime();
this.off(player, 'play', this.trackCurrentTime);
this.off(player, 'pause', this.stopTrackingCurrentTime);
}
trackCurrentTime() {
if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); }
this.currentTimeInterval = this.setInterval(function(){
this.player().trigger('timeupdate');
}, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
}
// Turn off play progress tracking (when paused or dragging)
stopTrackingCurrentTime() {
this.clearInterval(this.currentTimeInterval);
// #1002 - if the video ends right before the next timeupdate would happen,
// the progress bar won't make it all the way to the end
this.player().trigger('timeupdate');
}
dispose() {
// Turn off any manual progress or timeupdate tracking
if (this.manualProgress) { this.manualProgressOff(); }
if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); }
super.dispose();
}
setCurrentTime() {
// improve the accuracy of manual timeupdates
if (this.manualTimeUpdates) { this.player().trigger('timeupdate'); }
}
// TODO: Consider looking at moving this into the text track display directly
// https://github.com/videojs/video.js/issues/1863
initTextTrackListeners() {
let player = this.player_;
let textTrackListChanges = function() {
let textTrackDisplay = player.getChild('textTrackDisplay');
if (textTrackDisplay) {
textTrackDisplay.updateDisplay();
}
};
let tracks = this.textTracks();
if (!tracks) return;
tracks.addEventListener('removetrack', textTrackListChanges);
tracks.addEventListener('addtrack', textTrackListChanges);
this.on('dispose', Lib.bind(this, function() {
tracks.removeEventListener('removetrack', textTrackListChanges);
tracks.removeEventListener('addtrack', textTrackListChanges);
}));
}
emulateTextTracks() {
let player = this.player_;
if (!window['WebVTT']) {
let script = document.createElement('script');
script.src = player.options()['vtt.js'] || '../node_modules/vtt.js/dist/vtt.js';
player.el().appendChild(script);
window['WebVTT'] = true;
}
let tracks = this.textTracks();
if (!tracks) {
return;
}
let textTracksChanges = function() {
let textTrackDisplay = player.getChild('textTrackDisplay');
textTrackDisplay.updateDisplay();
for (let i = 0; i < this.length; i++) {
let track = this[i];
track.removeEventListener('cuechange', Lib.bind(textTrackDisplay, textTrackDisplay.updateDisplay));
if (track.mode === 'showing') {
track.addEventListener('cuechange', Lib.bind(textTrackDisplay, textTrackDisplay.updateDisplay));
}
}
};
tracks.addEventListener('change', textTracksChanges);
this.on('dispose', Lib.bind(this, function() {
tracks.removeEventListener('change', textTracksChanges);
}));
}
/**
* Provide default methods for text tracks.
*
* Html5 tech overrides these.
*/
textTracks() {
this.player_.textTracks_ = this.player_.textTracks_ || new TextTrackList();
return this.player_.textTracks_;
}
remoteTextTracks() {
this.player_.remoteTextTracks_ = this.player_.remoteTextTracks_ || new TextTrackList();
return this.player_.remoteTextTracks_;
}
addTextTrack(kind, label, language) {
if (!kind) {
throw new Error('TextTrack kind is required but was not provided');
}
return createTrackHelper(this, kind, label, language);
}
addRemoteTextTrack(options) {
let track = createTrackHelper(this, options['kind'], options['label'], options['language'], options);
this.remoteTextTracks().addTrack_(track);
return {
track: track
};
}
removeRemoteTextTrack(track) {
this.textTracks().removeTrack_(track);
this.remoteTextTracks().removeTrack_(track);
}
/**
* Provide a default setPoster method for techs
*
* Poster support for techs should be optional, so we don't want techs to
* break if they don't have a way to set a poster.
*/
setPoster() {}
}
/**
* List of associated text tracks
* @type {Array}
* @private
*/
Tech.prototype.textTracks_;
var createTrackHelper = function(self, kind, label, language, options) {
let tracks = self.textTracks();
options = options || {};
options['kind'] = kind;
if (label) {
options['label'] = label;
}
if (language) {
options['language'] = language;
}
options['player'] = self.player_;
let track = new TextTrack(options);
tracks.addTrack_(track);
return track;
};
Tech.prototype['featuresVolumeControl'] = true;
// Resizing plugins using request fullscreen reloads the plugin
Tech.prototype['featuresFullscreenResize'] = false;
Tech.prototype['featuresPlaybackRate'] = false;
// Optional events that we can manually mimic with timers
// currently not triggered by video-js-swf
Tech.prototype['featuresProgressEvents'] = false;
Tech.prototype['featuresTimeupdateEvents'] = false;
Tech.prototype['featuresNativeTextTracks'] = false;
/**
* A functional mixin for techs that want to use the Source Handler pattern.
*
* ##### EXAMPLE:
*
* Tech.withSourceHandlers.call(MyTech);
*
*/
Tech.withSourceHandlers = function(_Tech){
/**
* Register a source handler
* Source handlers are scripts for handling specific formats.
* The source handler pattern is used for adaptive formats (HLS, DASH) that
* manually load video data and feed it into a Source Buffer (Media Source Extensions)
* @param {Function} handler The source handler
* @param {Boolean} first Register it before any existing handlers
*/
_Tech.registerSourceHandler = function(handler, index){
let handlers = _Tech.sourceHandlers;
if (!handlers) {
handlers = _Tech.sourceHandlers = [];
}
if (index === undefined) {
// add to the end of the list
index = handlers.length;
}
handlers.splice(index, 0, handler);
};
/**
* Return the first source handler that supports the source
* TODO: Answer question: should 'probably' be prioritized over 'maybe'
* @param {Object} source The source object
* @returns {Object} The first source handler that supports the source
* @returns {null} Null if no source handler is found
*/
_Tech.selectSourceHandler = function(source){
let handlers = _Tech.sourceHandlers || [];
let can;
for (let i = 0; i < handlers.length; i++) {
can = handlers[i].canHandleSource(source);
if (can) {
return handlers[i];
}
}
return null;
};
/**
* Check if the tech can support the given source
* @param {Object} srcObj The source object
* @return {String} 'probably', 'maybe', or '' (empty string)
*/
_Tech.canPlaySource = function(srcObj){
let sh = _Tech.selectSourceHandler(srcObj);
if (sh) {
return sh.canHandleSource(srcObj);
}
return '';
};
/**
* Create a function for setting the source using a source object
* and source handlers.
* Should never be called unless a source handler was found.
* @param {Object} source A source object with src and type keys
* @return {Tech} self
*/
_Tech.prototype.setSource = function(source){
let sh = _Tech.selectSourceHandler(source);
if (!sh) {
// Fall back to a native source hander when unsupported sources are
// deliberately set
if (_Tech.nativeSourceHandler) {
sh = _Tech.nativeSourceHandler;
} else {
Lib.log.error('No source hander found for the current source.');
}
}
// Dispose any existing source handler
this.disposeSourceHandler();
this.off('dispose', this.disposeSourceHandler);
this.currentSource_ = source;
this.sourceHandler_ = sh.handleSource(source, this);
this.on('dispose', this.disposeSourceHandler);
return this;
};
/**
* Clean up any existing source handler
*/
_Tech.prototype.disposeSourceHandler = function(){
if (this.sourceHandler_ && this.sourceHandler_.dispose) {
this.sourceHandler_.dispose();
}
};
};
Component.registerComponent('Tech', Tech);
// Old name for Tech
Component.registerComponent('MediaTechController', Tech);
export default Tech;

View File

@ -1,580 +0,0 @@
import Component from '../component';
import Menu, { MenuItem, MenuButton } from '../menu';
import * as Lib from '../lib';
import document from 'global/document';
import window from 'global/window';
/* Text Track Display
============================================================================= */
// Global container for both subtitle and captions text. Simple div container.
/**
* The component for displaying text track cues
*
* @constructor
*/
var TextTrackDisplay = Component.extend({
/** @constructor */
init: function(player, options, ready){
Component.call(this, player, options, ready);
player.on('loadstart', Lib.bind(this, this.toggleDisplay));
// This used to be called during player init, but was causing an error
// if a track should show by default and the display hadn't loaded yet.
// Should probably be moved to an external track loader when we support
// tracks that don't need a display.
player.ready(Lib.bind(this, function() {
if (player.tech && player.tech['featuresNativeTextTracks']) {
this.hide();
return;
}
player.on('fullscreenchange', Lib.bind(this, this.updateDisplay));
let tracks = player.options_['tracks'] || [];
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
this.player_.addRemoteTextTrack(track);
}
}));
}
});
Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
TextTrackDisplay.prototype.toggleDisplay = function() {
if (this.player_.tech && this.player_.tech['featuresNativeTextTracks']) {
this.hide();
} else {
this.show();
}
};
TextTrackDisplay.prototype.createEl = function(){
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-text-track-display'
});
};
TextTrackDisplay.prototype.clearDisplay = function() {
if (typeof window['WebVTT'] === 'function') {
window['WebVTT']['processCues'](window, [], this.el_);
}
};
// Add cue HTML to display
let constructColor = function(color, opacity) {
return 'rgba(' +
// color looks like "#f0e"
parseInt(color[1] + color[1], 16) + ',' +
parseInt(color[2] + color[2], 16) + ',' +
parseInt(color[3] + color[3], 16) + ',' +
opacity + ')';
};
const darkGray = '#222';
const lightGray = '#ccc';
const fontMap = {
monospace: 'monospace',
sansSerif: 'sans-serif',
serif: 'serif',
monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
monospaceSerif: '"Courier New", monospace',
proportionalSansSerif: 'sans-serif',
proportionalSerif: 'serif',
casual: '"Comic Sans MS", Impact, fantasy',
script: '"Monotype Corsiva", cursive',
smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
};
let tryUpdateStyle = function(el, style, rule) {
// some style changes will throw an error, particularly in IE8. Those should be noops.
try {
el.style[style] = rule;
} catch (e) {}
};
TextTrackDisplay.prototype.updateDisplay = function() {
var tracks = this.player_.textTracks();
this.clearDisplay();
if (!tracks) {
return;
}
for (let i=0; i < tracks.length; i++) {
let track = tracks[i];
if (track['mode'] === 'showing') {
this.updateForTrack(track);
}
}
};
TextTrackDisplay.prototype.updateForTrack = function(track) {
if (typeof window['WebVTT'] !== 'function' || !track['activeCues']) {
return;
}
let overrides = this.player_['textTrackSettings'].getValues();
let cues = [];
for (let i = 0; i < track['activeCues'].length; i++) {
cues.push(track['activeCues'][i]);
}
window['WebVTT']['processCues'](window, track['activeCues'], this.el_);
let i = cues.length;
while (i--) {
let cueDiv = cues[i].displayState;
if (overrides.color) {
cueDiv.firstChild.style.color = overrides.color;
}
if (overrides.textOpacity) {
tryUpdateStyle(cueDiv.firstChild,
'color',
constructColor(overrides.color || '#fff',
overrides.textOpacity));
}
if (overrides.backgroundColor) {
cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
}
if (overrides.backgroundOpacity) {
tryUpdateStyle(cueDiv.firstChild,
'backgroundColor',
constructColor(overrides.backgroundColor || '#000',
overrides.backgroundOpacity));
}
if (overrides.windowColor) {
if (overrides.windowOpacity) {
tryUpdateStyle(cueDiv,
'backgroundColor',
constructColor(overrides.windowColor, overrides.windowOpacity));
} else {
cueDiv.style.backgroundColor = overrides.windowColor;
}
}
if (overrides.edgeStyle) {
if (overrides.edgeStyle === 'dropshadow') {
cueDiv.firstChild.style.textShadow = '2px 2px 3px ' + darkGray + ', 2px 2px 4px ' + darkGray + ', 2px 2px 5px ' + darkGray;
} else if (overrides.edgeStyle === 'raised') {
cueDiv.firstChild.style.textShadow = '1px 1px ' + darkGray + ', 2px 2px ' + darkGray + ', 3px 3px ' + darkGray;
} else if (overrides.edgeStyle === 'depressed') {
cueDiv.firstChild.style.textShadow = '1px 1px ' + lightGray + ', 0 1px ' + lightGray + ', -1px -1px ' + darkGray + ', 0 -1px ' + darkGray;
} else if (overrides.edgeStyle === 'uniform') {
cueDiv.firstChild.style.textShadow = '0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray;
}
}
if (overrides.fontPercent && overrides.fontPercent !== 1) {
const fontSize = window.parseFloat(cueDiv.style.fontSize);
cueDiv.style.fontSize = (fontSize * overrides.fontPercent) + 'px';
cueDiv.style.height = 'auto';
cueDiv.style.top = 'auto';
cueDiv.style.bottom = '2px';
}
if (overrides.fontFamily && overrides.fontFamily !== 'default') {
if (overrides.fontFamily === 'small-caps') {
cueDiv.firstChild.style.fontVariant = 'small-caps';
} else {
cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
}
}
}
};
/**
* The specific menu item type for selecting a language within a text track kind
*
* @constructor
*/
var TextTrackMenuItem = MenuItem.extend({
/** @constructor */
init: function(player, options){
let track = this.track = options['track'];
let tracks = player.textTracks();
let changeHandler;
if (tracks) {
changeHandler = Lib.bind(this, function() {
let selected = this.track['mode'] === 'showing';
if (this instanceof OffTextTrackMenuItem) {
selected = true;
for (let i = 0, l = tracks.length; i < l; i++) {
let track = tracks[i];
if (track['kind'] === this.track['kind'] && track['mode'] === 'showing') {
selected = false;
break;
}
}
}
this.selected(selected);
});
tracks.addEventListener('change', changeHandler);
player.on('dispose', function() {
tracks.removeEventListener('change', changeHandler);
});
}
// Modify options for parent MenuItem class's init.
options['label'] = track['label'] || track['language'] || 'Unknown';
options['selected'] = track['default'] || track['mode'] === 'showing';
MenuItem.call(this, player, options);
// iOS7 doesn't dispatch change events to TextTrackLists when an
// associated track's mode changes. Without something like
// Object.observe() (also not present on iOS7), it's not
// possible to detect changes to the mode attribute and polyfill
// the change event. As a poor substitute, we manually dispatch
// change events whenever the controls modify the mode.
if (tracks && tracks.onchange === undefined) {
let event;
this.on(['tap', 'click'], function() {
if (typeof window.Event !== 'object') {
// Android 2.3 throws an Illegal Constructor error for window.Event
try {
event = new window.Event('change');
} catch(err){}
}
if (!event) {
event = document.createEvent('Event');
event.initEvent('change', true, true);
}
tracks.dispatchEvent(event);
});
}
}
});
Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
TextTrackMenuItem.prototype.onClick = function(){
let kind = this.track['kind'];
let tracks = this.player_.textTracks();
MenuItem.prototype.onClick.call(this);
if (!tracks) return;
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
if (track['kind'] !== kind) {
continue;
}
if (track === this.track) {
track['mode'] = 'showing';
} else {
track['mode'] = 'disabled';
}
}
};
/**
* A special menu item for turning of a specific type of text track
*
* @constructor
*/
var OffTextTrackMenuItem = TextTrackMenuItem.extend({
/** @constructor */
init: function(player, options){
// Create pseudo track info
// Requires options['kind']
options['track'] = {
'kind': options['kind'],
'player': player,
'label': options['kind'] + ' off',
'default': false,
'mode': 'disabled'
};
TextTrackMenuItem.call(this, player, options);
this.selected(true);
}
});
Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
let CaptionSettingsMenuItem = TextTrackMenuItem.extend({
init: function(player, options) {
options['track'] = {
'kind': options['kind'],
'player': player,
'label': options['kind'] + ' settings',
'default': false,
mode: 'disabled'
};
TextTrackMenuItem.call(this, player, options);
this.addClass('vjs-texttrack-settings');
}
});
Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
CaptionSettingsMenuItem.prototype.onClick = function() {
this.player().getChild('textTrackSettings').show();
};
/**
* The base class for buttons that toggle specific text track types (e.g. subtitles)
*
* @constructor
*/
var TextTrackButton = MenuButton.extend({
/** @constructor */
init: function(player, options){
MenuButton.call(this, player, options);
let tracks = this.player_.textTracks();
if (this.items.length <= 1) {
this.hide();
}
if (!tracks) {
return;
}
let updateHandler = Lib.bind(this, this.update);
tracks.addEventListener('removetrack', updateHandler);
tracks.addEventListener('addtrack', updateHandler);
this.player_.on('dispose', function() {
tracks.removeEventListener('removetrack', updateHandler);
tracks.removeEventListener('addtrack', updateHandler);
});
}
});
Component.registerComponent('TextTrackButton', TextTrackButton);
// Create a menu item for each text track
TextTrackButton.prototype.createItems = function(){
let items = [];
if (this instanceof CaptionsButton && !(this.player().tech && this.player().tech['featuresNativeTextTracks'])) {
items.push(new CaptionSettingsMenuItem(this.player_, { 'kind': this.kind_ }));
}
// Add an OFF menu item to turn all tracks off
items.push(new OffTextTrackMenuItem(this.player_, { 'kind': this.kind_ }));
let tracks = this.player_.textTracks();
if (!tracks) {
return items;
}
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
// only add tracks that are of the appropriate kind and have a label
if (track['kind'] === this.kind_) {
items.push(new TextTrackMenuItem(this.player_, {
'track': track
}));
}
}
return items;
};
/**
* The button component for toggling and selecting captions
*
* @constructor
*/
var CaptionsButton = TextTrackButton.extend({
/** @constructor */
init: function(player, options, ready){
TextTrackButton.call(this, player, options, ready);
this.el_.setAttribute('aria-label','Captions Menu');
}
});
Component.registerComponent('CaptionsButton', CaptionsButton);
CaptionsButton.prototype.kind_ = 'captions';
CaptionsButton.prototype.buttonText = 'Captions';
CaptionsButton.prototype.className = 'vjs-captions-button';
CaptionsButton.prototype.update = function() {
let threshold = 2;
TextTrackButton.prototype.update.call(this);
// if native, then threshold is 1 because no settings button
if (this.player().tech && this.player().tech['featuresNativeTextTracks']) {
threshold = 1;
}
if (this.items && this.items.length > threshold) {
this.show();
} else {
this.hide();
}
};
/**
* The button component for toggling and selecting subtitles
*
* @constructor
*/
var SubtitlesButton = TextTrackButton.extend({
/** @constructor */
init: function(player, options, ready){
TextTrackButton.call(this, player, options, ready);
this.el_.setAttribute('aria-label','Subtitles Menu');
}
});
Component.registerComponent('SubtitlesButton', SubtitlesButton);
SubtitlesButton.prototype.kind_ = 'subtitles';
SubtitlesButton.prototype.buttonText = 'Subtitles';
SubtitlesButton.prototype.className = 'vjs-subtitles-button';
// Chapters act much differently than other text tracks
// Cues are navigation vs. other tracks of alternative languages
/**
* The button component for toggling and selecting chapters
*
* @constructor
*/
var ChaptersButton = TextTrackButton.extend({
/** @constructor */
init: function(player, options, ready){
TextTrackButton.call(this, player, options, ready);
this.el_.setAttribute('aria-label','Chapters Menu');
}
});
Component.registerComponent('ChaptersButton', ChaptersButton);
ChaptersButton.prototype.kind_ = 'chapters';
ChaptersButton.prototype.buttonText = 'Chapters';
ChaptersButton.prototype.className = 'vjs-chapters-button';
// Create a menu item for each text track
ChaptersButton.prototype.createItems = function(){
let items = [];
let tracks = this.player_.textTracks();
if (!tracks) {
return items;
}
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
if (track['kind'] === this.kind_) {
items.push(new TextTrackMenuItem(this.player_, {
'track': track
}));
}
}
return items;
};
ChaptersButton.prototype.createMenu = function(){
let tracks = this.player_.textTracks() || [];
let chaptersTrack;
let items = this.items = [];
for (let i = 0, l = tracks.length; i < l; i++) {
let track = tracks[i];
if (track['kind'] == this.kind_) {
if (!track.cues) {
track['mode'] = 'hidden';
/* jshint loopfunc:true */
// TODO see if we can figure out a better way of doing this https://github.com/videojs/video.js/issues/1864
window.setTimeout(Lib.bind(this, function() {
this.createMenu();
}), 100);
/* jshint loopfunc:false */
} else {
chaptersTrack = track;
break;
}
}
}
let menu = this.menu;
if (menu === undefined) {
menu = new Menu(this.player_);
menu.contentEl().appendChild(Lib.createEl('li', {
className: 'vjs-menu-title',
innerHTML: Lib.capitalize(this.kind_),
tabindex: -1
}));
}
if (chaptersTrack) {
let cues = chaptersTrack['cues'], cue;
for (let i = 0, l = cues.length; i < l; i++) {
cue = cues[i];
let mi = new ChaptersTrackMenuItem(this.player_, {
'track': chaptersTrack,
'cue': cue
});
items.push(mi);
menu.addChild(mi);
}
this.addChild(menu);
}
if (this.items.length > 0) {
this.show();
}
return menu;
};
/**
* @constructor
*/
var ChaptersTrackMenuItem = MenuItem.extend({
/** @constructor */
init: function(player, options){
let track = this.track = options['track'];
let cue = this.cue = options['cue'];
let currentTime = player.currentTime();
// Modify options for parent MenuItem class's init.
options['label'] = cue.text;
options['selected'] = (cue['startTime'] <= currentTime && currentTime < cue['endTime']);
MenuItem.call(this, player, options);
track.addEventListener('cuechange', Lib.bind(this, this.update));
}
});
Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
ChaptersTrackMenuItem.prototype.onClick = function(){
MenuItem.prototype.onClick.call(this);
this.player_.currentTime(this.cue.startTime);
this.update(this.cue.startTime);
};
ChaptersTrackMenuItem.prototype.update = function(){
let cue = this.cue;
let currentTime = this.player_.currentTime();
// vjs.log(currentTime, cue.startTime);
this.selected(cue['startTime'] <= currentTime && currentTime < cue['endTime']);
};
export { TextTrackDisplay, TextTrackButton, CaptionsButton, SubtitlesButton, ChaptersButton, TextTrackMenuItem, ChaptersTrackMenuItem };

View File

@ -0,0 +1,185 @@
import Component from '../component';
import Menu from '../menu/menu.js';
import MenuItem from '../menu/menu-item.js';
import MenuButton from '../menu/menu-button.js';
import * as Lib from '../lib.js';
import document from 'global/document';
import window from 'global/window';
const darkGray = '#222';
const lightGray = '#ccc';
const fontMap = {
monospace: 'monospace',
sansSerif: 'sans-serif',
serif: 'serif',
monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
monospaceSerif: '"Courier New", monospace',
proportionalSansSerif: 'sans-serif',
proportionalSerif: 'serif',
casual: '"Comic Sans MS", Impact, fantasy',
script: '"Monotype Corsiva", cursive',
smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
};
/**
* The component for displaying text track cues
*
* @constructor
*/
class TextTrackDisplay extends Component {
constructor(player, options, ready){
super(player, options, ready);
player.on('loadstart', Lib.bind(this, this.toggleDisplay));
// This used to be called during player init, but was causing an error
// if a track should show by default and the display hadn't loaded yet.
// Should probably be moved to an external track loader when we support
// tracks that don't need a display.
player.ready(Lib.bind(this, function() {
if (player.tech && player.tech['featuresNativeTextTracks']) {
this.hide();
return;
}
player.on('fullscreenchange', Lib.bind(this, this.updateDisplay));
let tracks = player.options_['tracks'] || [];
for (let i = 0; i < tracks.length; i++) {
let track = tracks[i];
this.player_.addRemoteTextTrack(track);
}
}));
}
toggleDisplay() {
if (this.player_.tech && this.player_.tech['featuresNativeTextTracks']) {
this.hide();
} else {
this.show();
}
}
createEl() {
return super.createEl('div', {
className: 'vjs-text-track-display'
});
}
clearDisplay() {
if (typeof window['WebVTT'] === 'function') {
window['WebVTT']['processCues'](window, [], this.el_);
}
}
updateDisplay() {
var tracks = this.player_.textTracks();
this.clearDisplay();
if (!tracks) {
return;
}
for (let i=0; i < tracks.length; i++) {
let track = tracks[i];
if (track['mode'] === 'showing') {
this.updateForTrack(track);
}
}
}
updateForTrack(track) {
if (typeof window['WebVTT'] !== 'function' || !track['activeCues']) {
return;
}
let overrides = this.player_['textTrackSettings'].getValues();
let cues = [];
for (let i = 0; i < track['activeCues'].length; i++) {
cues.push(track['activeCues'][i]);
}
window['WebVTT']['processCues'](window, track['activeCues'], this.el_);
let i = cues.length;
while (i--) {
let cueDiv = cues[i].displayState;
if (overrides.color) {
cueDiv.firstChild.style.color = overrides.color;
}
if (overrides.textOpacity) {
tryUpdateStyle(cueDiv.firstChild,
'color',
constructColor(overrides.color || '#fff',
overrides.textOpacity));
}
if (overrides.backgroundColor) {
cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
}
if (overrides.backgroundOpacity) {
tryUpdateStyle(cueDiv.firstChild,
'backgroundColor',
constructColor(overrides.backgroundColor || '#000',
overrides.backgroundOpacity));
}
if (overrides.windowColor) {
if (overrides.windowOpacity) {
tryUpdateStyle(cueDiv,
'backgroundColor',
constructColor(overrides.windowColor, overrides.windowOpacity));
} else {
cueDiv.style.backgroundColor = overrides.windowColor;
}
}
if (overrides.edgeStyle) {
if (overrides.edgeStyle === 'dropshadow') {
cueDiv.firstChild.style.textShadow = '2px 2px 3px ' + darkGray + ', 2px 2px 4px ' + darkGray + ', 2px 2px 5px ' + darkGray;
} else if (overrides.edgeStyle === 'raised') {
cueDiv.firstChild.style.textShadow = '1px 1px ' + darkGray + ', 2px 2px ' + darkGray + ', 3px 3px ' + darkGray;
} else if (overrides.edgeStyle === 'depressed') {
cueDiv.firstChild.style.textShadow = '1px 1px ' + lightGray + ', 0 1px ' + lightGray + ', -1px -1px ' + darkGray + ', 0 -1px ' + darkGray;
} else if (overrides.edgeStyle === 'uniform') {
cueDiv.firstChild.style.textShadow = '0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray;
}
}
if (overrides.fontPercent && overrides.fontPercent !== 1) {
const fontSize = window.parseFloat(cueDiv.style.fontSize);
cueDiv.style.fontSize = (fontSize * overrides.fontPercent) + 'px';
cueDiv.style.height = 'auto';
cueDiv.style.top = 'auto';
cueDiv.style.bottom = '2px';
}
if (overrides.fontFamily && overrides.fontFamily !== 'default') {
if (overrides.fontFamily === 'small-caps') {
cueDiv.firstChild.style.fontVariant = 'small-caps';
} else {
cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
}
}
}
}
}
// Add cue HTML to display
function constructColor(color, opacity) {
return 'rgba(' +
// color looks like "#f0e"
parseInt(color[1] + color[1], 16) + ',' +
parseInt(color[2] + color[2], 16) + ',' +
parseInt(color[3] + color[3], 16) + ',' +
opacity + ')';
}
function tryUpdateStyle(el, style, rule) {
// some style changes will throw an error, particularly in IE8. Those should be noops.
try {
el.style[style] = rule;
} catch (e) {}
}
Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
export default TextTrackDisplay;

View File

@ -3,9 +3,10 @@ import * as Lib from '../lib';
import * as Events from '../events'; import * as Events from '../events';
import window from 'global/window'; import window from 'global/window';
let TextTrackSettings = Component.extend({ class TextTrackSettings extends Component {
init: function(player, options) {
Component.call(this, player, options); constructor(player, options) {
super(player, options);
this.hide(); this.hide();
Events.on(this.el().querySelector('.vjs-done-button'), 'click', Lib.bind(this, function() { Events.on(this.el().querySelector('.vjs-done-button'), 'click', Lib.bind(this, function() {
@ -40,18 +41,15 @@ let TextTrackSettings = Component.extend({
this.restoreSettings(); this.restoreSettings();
} }
} }
});
Component.registerComponent('TextTrackSettings', TextTrackSettings); createEl() {
return super.createEl('div', {
TextTrackSettings.prototype.createEl = function() {
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-caption-settings vjs-modal-overlay', className: 'vjs-caption-settings vjs-modal-overlay',
innerHTML: captionOptionsMenuTemplate() innerHTML: captionOptionsMenuTemplate()
}); });
}; }
TextTrackSettings.prototype.getValues = function() { getValues() {
const el = this.el(); const el = this.el();
const textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select')); const textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select'));
@ -81,9 +79,9 @@ TextTrackSettings.prototype.getValues = function() {
} }
} }
return result; return result;
}; }
TextTrackSettings.prototype.setValues = function(values) { setValues(values) {
const el = this.el(); const el = this.el();
setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle); setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle);
@ -102,9 +100,9 @@ TextTrackSettings.prototype.setValues = function(values) {
} }
setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent); setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent);
}; }
TextTrackSettings.prototype.restoreSettings = function() { restoreSettings() {
let values; let values;
try { try {
values = JSON.parse(window.localStorage.getItem('vjs-text-track-settings')); values = JSON.parse(window.localStorage.getItem('vjs-text-track-settings'));
@ -113,9 +111,9 @@ TextTrackSettings.prototype.restoreSettings = function() {
if (values) { if (values) {
this.setValues(values); this.setValues(values);
} }
}; }
TextTrackSettings.prototype.saveSettings = function() { saveSettings() {
if (!this.player_.options()['persistTextTrackSettings']) { if (!this.player_.options()['persistTextTrackSettings']) {
return; return;
} }
@ -128,14 +126,18 @@ TextTrackSettings.prototype.saveSettings = function() {
window.localStorage.removeItem('vjs-text-track-settings'); window.localStorage.removeItem('vjs-text-track-settings');
} }
} catch (e) {} } catch (e) {}
}; }
TextTrackSettings.prototype.updateDisplay = function() { updateDisplay() {
let ttDisplay = this.player_.getChild('textTrackDisplay'); let ttDisplay = this.player_.getChild('textTrackDisplay');
if (ttDisplay) { if (ttDisplay) {
ttDisplay.updateDisplay(); ttDisplay.updateDisplay();
} }
}; }
}
Component.registerComponent('TextTrackSettings', TextTrackSettings);
function getSelectedOptionValue(target) { function getSelectedOptionValue(target) {
let selectedOption; let selectedOption;

View File

@ -28,7 +28,6 @@ import XHR from '../xhr.js';
* attribute EventHandler oncuechange; * attribute EventHandler oncuechange;
* }; * };
*/ */
let TextTrack = function(options) { let TextTrack = function(options) {
options = options || {}; options = options || {};
@ -227,7 +226,7 @@ TextTrack.prototype.removeCue = function(removeCue) {
/* /*
* Downloading stuff happens below this point * Downloading stuff happens below this point
*/ */
let parseCues = function(srcContent, track) { var parseCues = function(srcContent, track) {
if (typeof window['WebVTT'] !== 'function') { if (typeof window['WebVTT'] !== 'function') {
//try again a bit later //try again a bit later
return window.setTimeout(function() { return window.setTimeout(function() {

View File

@ -1,14 +1,14 @@
import document from 'global/document'; import document from 'global/document';
import MediaLoader from './media/loader'; import MediaLoader from './tech/loader.js';
import Html5 from './media/html5'; import Html5 from './tech/html5.js';
import Flash from './media/flash'; import Flash from './tech/flash.js';
import PosterImage from './poster'; import PosterImage from './poster-image.js';
import { TextTrackDisplay } from './tracks/text-track-controls'; import TextTrackDisplay from './tracks/text-track-display.js';
import LoadingSpinner from './loading-spinner'; import LoadingSpinner from './loading-spinner.js';
import BigPlayButton from './big-play-button'; import BigPlayButton from './big-play-button.js';
import ControlBar from './control-bar/control-bar'; import ControlBar from './control-bar/control-bar.js';
import ErrorDisplay from './error-display'; import ErrorDisplay from './error-display.js';
import videojs from './core'; import videojs from './core';
import * as setup from './setup'; import * as setup from './setup';

View File

@ -73,7 +73,8 @@ test('should be able to access expected player API methods', function() {
}); });
test('should be able to access expected component API methods', function() { test('should be able to access expected component API methods', function() {
var comp = videojs.getComponent('Component').create({ id: function(){ return 1; }, reportUserActivity: function(){} }); var Component = videojs.getComponent('Component');
var comp = new Component({ id: function(){ return 1; }, reportUserActivity: function(){} });
// Component methods // Component methods
ok(comp.player, 'player exists'); ok(comp.player, 'player exists');
@ -110,7 +111,7 @@ test('should be able to access expected component API methods', function() {
}); });
test('should be able to access expected MediaTech API methods', function() { test('should be able to access expected MediaTech API methods', function() {
var media = videojs.getComponent('MediaTechController'); var media = videojs.getComponent('Tech');
var mediaProto = media.prototype; var mediaProto = media.prototype;
var html5 = videojs.getComponent('Html5'); var html5 = videojs.getComponent('Html5');
var html5Proto = html5.prototype; var html5Proto = html5.prototype;

View File

@ -442,7 +442,8 @@ test('should change the width and height of a component', function(){
test('should use a defined content el for appending children', function(){ test('should use a defined content el for appending children', function(){
var CompWithContent = Component.extend(); class CompWithContent extends Component {}
CompWithContent.prototype.createEl = function(){ CompWithContent.prototype.createEl = function(){
// Create the main componenent element // Create the main componenent element
var el = Lib.createEl('div'); var el = Lib.createEl('div');

View File

@ -1,7 +1,7 @@
import VolumeControl from '../../src/js/control-bar/volume-control.js'; import VolumeControl from '../../src/js/control-bar/volume-control/volume-control.js';
import MuteToggle from '../../src/js/control-bar/mute-toggle.js'; import MuteToggle from '../../src/js/control-bar/mute-toggle.js';
import PlaybackRateMenuButton from '../../src/js/control-bar/playback-rate-menu-button.js'; import PlaybackRateMenuButton from '../../src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js';
import Slider from '../../src/js/slider.js'; import Slider from '../../src/js/slider/slider.js';
import TestHelpers from './test-helpers.js'; import TestHelpers from './test-helpers.js';
import document from 'global/document'; import document from 'global/document';

View File

@ -1,4 +1,4 @@
import Flash from '../../src/js/media/flash.js'; import Flash from '../../src/js/tech/flash.js';
import document from 'global/document'; import document from 'global/document';
q.module('Flash'); q.module('Flash');

View File

@ -1,6 +1,6 @@
var player, tech, el; var player, tech, el;
import Html5 from '../../src/js/media/html5.js'; import Html5 from '../../src/js/tech/html5.js';
import * as Lib from '../../src/js/lib.js'; import * as Lib from '../../src/js/lib.js';
import document from 'global/document'; import document from 'global/document';

View File

@ -1,16 +1,16 @@
var noop = function() {}, clock, oldTextTracks; var noop = function() {}, clock, oldTextTracks;
import MediaTechController from '../../src/js/media/media.js'; import Tech from '../../src/js/tech/tech.js';
q.module('Media Tech', { q.module('Media Tech', {
'setup': function() { 'setup': function() {
this.noop = function() {}; this.noop = function() {};
this.clock = sinon.useFakeTimers(); this.clock = sinon.useFakeTimers();
this.featuresProgessEvents = MediaTechController.prototype['featuresProgessEvents']; this.featuresProgessEvents = Tech.prototype['featuresProgessEvents'];
MediaTechController.prototype['featuresProgressEvents'] = false; Tech.prototype['featuresProgressEvents'] = false;
MediaTechController.prototype['featuresNativeTextTracks'] = true; Tech.prototype['featuresNativeTextTracks'] = true;
oldTextTracks = MediaTechController.prototype.textTracks; oldTextTracks = Tech.prototype.textTracks;
MediaTechController.prototype.textTracks = function() { Tech.prototype.textTracks = function() {
return { return {
addEventListener: Function.prototype, addEventListener: Function.prototype,
removeEventListener: Function.prototype removeEventListener: Function.prototype
@ -19,15 +19,15 @@ q.module('Media Tech', {
}, },
'teardown': function() { 'teardown': function() {
this.clock.restore(); this.clock.restore();
MediaTechController.prototype['featuresProgessEvents'] = this.featuresProgessEvents; Tech.prototype['featuresProgessEvents'] = this.featuresProgessEvents;
MediaTechController.prototype['featuresNativeTextTracks'] = false; Tech.prototype['featuresNativeTextTracks'] = false;
MediaTechController.prototype.textTracks = oldTextTracks; Tech.prototype.textTracks = oldTextTracks;
} }
}); });
test('should synthesize timeupdate events by default', function() { test('should synthesize timeupdate events by default', function() {
var timeupdates = 0, playHandler, i, tech; var timeupdates = 0, playHandler, i, tech;
tech = new MediaTechController({ tech = new Tech({
id: this.noop, id: this.noop,
on: function(event, handler) { on: function(event, handler) {
if (event === 'play') { if (event === 'play') {
@ -51,7 +51,7 @@ test('should synthesize timeupdate events by default', function() {
test('stops timeupdates if the tech produces them natively', function() { test('stops timeupdates if the tech produces them natively', function() {
var timeupdates = 0, tech, playHandler, expected; var timeupdates = 0, tech, playHandler, expected;
tech = new MediaTechController({ tech = new Tech({
id: this.noop, id: this.noop,
off: this.noop, off: this.noop,
on: function(event, handler) { on: function(event, handler) {
@ -78,7 +78,7 @@ test('stops timeupdates if the tech produces them natively', function() {
test('stops manual timeupdates while paused', function() { test('stops manual timeupdates while paused', function() {
var timeupdates = 0, tech, playHandler, pauseHandler, expected; var timeupdates = 0, tech, playHandler, pauseHandler, expected;
tech = new MediaTechController({ tech = new Tech({
id: this.noop, id: this.noop,
on: function(event, handler) { on: function(event, handler) {
if (event === 'play') { if (event === 'play') {
@ -110,7 +110,7 @@ test('stops manual timeupdates while paused', function() {
test('should synthesize progress events by default', function() { test('should synthesize progress events by default', function() {
var progresses = 0, tech; var progresses = 0, tech;
tech = new MediaTechController({ tech = new Tech({
id: this.noop, id: this.noop,
on: this.noop, on: this.noop,
bufferedPercent: function() { bufferedPercent: function() {
@ -131,7 +131,7 @@ test('should synthesize progress events by default', function() {
}); });
test('dispose() should stop time tracking', function() { test('dispose() should stop time tracking', function() {
var tech = new MediaTechController({ var tech = new Tech({
id: this.noop, id: this.noop,
on: this.noop, on: this.noop,
off: this.noop, off: this.noop,
@ -158,17 +158,17 @@ test('should add the source hanlder interface to a tech', function(){
var sourceB = { src: 'no-support', type: 'no-support' }; var sourceB = { src: 'no-support', type: 'no-support' };
// Define a new tech class // Define a new tech class
var Tech = MediaTechController.extend(); var MyTech = Tech.extend();
// Extend Tech with source handlers // Extend Tech with source handlers
MediaTechController.withSourceHandlers(Tech); Tech.withSourceHandlers(MyTech);
// Check for the expected class methods // Check for the expected class methods
ok(Tech.registerSourceHandler, 'added a registerSourceHandler function to the Tech'); ok(MyTech.registerSourceHandler, 'added a registerSourceHandler function to the Tech');
ok(Tech.selectSourceHandler, 'added a selectSourceHandler function to the Tech'); ok(MyTech.selectSourceHandler, 'added a selectSourceHandler function to the Tech');
// Create an instance of Tech // Create an instance of Tech
var tech = new Tech(mockPlayer); var tech = new MyTech(mockPlayer);
// Check for the expected instance methods // Check for the expected instance methods
ok(tech.setSource, 'added a setSource function to the tech instance'); ok(tech.setSource, 'added a setSource function to the tech instance');
@ -208,18 +208,18 @@ test('should add the source hanlder interface to a tech', function(){
}; };
// Test registering source handlers // Test registering source handlers
Tech.registerSourceHandler(handlerOne); MyTech.registerSourceHandler(handlerOne);
strictEqual(Tech.sourceHandlers[0], handlerOne, 'handlerOne was added to the source handler array'); strictEqual(MyTech.sourceHandlers[0], handlerOne, 'handlerOne was added to the source handler array');
Tech.registerSourceHandler(handlerTwo, 0); MyTech.registerSourceHandler(handlerTwo, 0);
strictEqual(Tech.sourceHandlers[0], handlerTwo, 'handlerTwo was registered at the correct index (0)'); strictEqual(MyTech.sourceHandlers[0], handlerTwo, 'handlerTwo was registered at the correct index (0)');
// Test handler selection // Test handler selection
strictEqual(Tech.selectSourceHandler(sourceA), handlerOne, 'handlerOne was selected to handle the valid source'); strictEqual(MyTech.selectSourceHandler(sourceA), handlerOne, 'handlerOne was selected to handle the valid source');
strictEqual(Tech.selectSourceHandler(sourceB), null, 'no handler was selected to handle the invalid source'); strictEqual(MyTech.selectSourceHandler(sourceB), null, 'no handler was selected to handle the invalid source');
// Test canPlaySource return values // Test canPlaySource return values
strictEqual(Tech.canPlaySource(sourceA), 'probably', 'the Tech returned probably for the valid source'); strictEqual(MyTech.canPlaySource(sourceA), 'probably', 'the Tech returned probably for the valid source');
strictEqual(Tech.canPlaySource(sourceB), '', 'the Tech returned an empty string for the invalid source'); strictEqual(MyTech.canPlaySource(sourceB), '', 'the Tech returned an empty string for the invalid source');
// Pass a source through the source handler process of a tech instance // Pass a source through the source handler process of a tech instance
tech.setSource(sourceA); tech.setSource(sourceA);
@ -239,14 +239,14 @@ test('should handle unsupported sources with the source hanlder API', function()
}; };
// Define a new tech class // Define a new tech class
var Tech = MediaTechController.extend(); var MyTech = Tech.extend();
// Extend Tech with source handlers // Extend Tech with source handlers
MediaTechController.withSourceHandlers(Tech); Tech.withSourceHandlers(MyTech);
// Create an instance of Tech // Create an instance of Tech
var tech = new Tech(mockPlayer); var tech = new MyTech(mockPlayer);
var usedNative; var usedNative;
Tech.nativeSourceHandler = { MyTech.nativeSourceHandler = {
handleSource: function(){ usedNative = true; } handleSource: function(){ usedNative = true; }
}; };

View File

@ -1,29 +1,25 @@
// Fake a media playback tech controller so that player tests // Fake a media playback tech controller so that player tests
// can run without HTML5 or Flash, of which PhantomJS supports neither. // can run without HTML5 or Flash, of which PhantomJS supports neither.
import MediaTechController from '../../src/js/media/media.js'; import Tech from '../../src/js/tech/tech.js';
import * as Lib from '../../src/js/lib.js'; import * as Lib from '../../src/js/lib.js';
import Component from '../../src/js/component.js'; import Component from '../../src/js/component.js';
/** /**
* @constructor * @constructor
*/ */
var MediaFaker = MediaTechController.extend({ class MediaFaker extends Tech {
init: function(player, options, onReady){
MediaTechController.call(this, player, options, onReady);
constructor(player, options, onReady){
super(player, options, onReady);
this.triggerReady(); this.triggerReady();
} }
});
// Support everything except for "video/unsupported-format" createEl() {
MediaFaker.isSupported = function(){ return true; }; var el = super.createEl('div', {
MediaFaker.canPlaySource = function(srcObj){ return srcObj.type !== 'video/unsupported-format'; };
MediaFaker.prototype.createEl = function(){
var el = MediaTechController.prototype.createEl.call(this, 'div', {
className: 'vjs-tech' className: 'vjs-tech'
}); });
if (this.player().poster()) { if (this.player().poster()) {
// transfer the poster image to mimic HTML // transfer the poster image to mimic HTML
el.poster = this.player().poster(); el.poster = this.player().poster();
@ -32,27 +28,30 @@ MediaFaker.prototype.createEl = function(){
Lib.insertFirst(el, this.player_.el()); Lib.insertFirst(el, this.player_.el());
return el; return el;
}; }
// fake a poster attribute to mimic the video element // fake a poster attribute to mimic the video element
MediaFaker.prototype.poster = function(){ return this.el().poster; }; poster() { return this.el().poster; }
MediaFaker.prototype['setPoster'] = function(val){ this.el().poster = val; }; setPoster(val) { this.el().poster = val; }
MediaFaker.prototype.currentTime = function(){ return 0; }; currentTime() { return 0; }
MediaFaker.prototype.seeking = function(){ return false; }; seeking() { return false; }
MediaFaker.prototype.src = function(){ return 'movie.mp4'; }; src() { return 'movie.mp4'; }
MediaFaker.prototype.volume = function(){ return 0; }; volume() { return 0; }
MediaFaker.prototype.muted = function(){ return false; }; muted() { return false; }
MediaFaker.prototype.pause = function(){ return false; }; pause() { return false; }
MediaFaker.prototype.paused = function(){ return true; }; paused() { return true; }
MediaFaker.prototype.play = function() { play() { this.player().trigger('play'); }
this.player().trigger('play'); supportsFullScreen() { return false; }
}; buffered() { return {}; }
MediaFaker.prototype.supportsFullScreen = function(){ return false; }; duration() { return {}; }
MediaFaker.prototype.buffered = function(){ return {}; }; networkState() { return 0; }
MediaFaker.prototype.duration = function(){ return {}; }; readyState() { return 0; }
MediaFaker.prototype.networkState = function(){ return 0; };
MediaFaker.prototype.readyState = function(){ return 0; }; // Support everything except for "video/unsupported-format"
static isSupported() { return true; }
static canPlaySource(srcObj) { return srcObj.type !== 'video/unsupported-format'; }
}
Component.registerComponent('MediaFaker', MediaFaker); Component.registerComponent('MediaFaker', MediaFaker);
module.exports = MediaFaker; module.exports = MediaFaker;

View File

@ -1,4 +1,4 @@
import { MenuButton } from '../../src/js/menu.js'; import MenuButton from '../../src/js/menu/menu-button.js';
import TestHelpers from './test-helpers.js'; import TestHelpers from './test-helpers.js';
q.module('MenuButton'); q.module('MenuButton');

View File

@ -3,7 +3,7 @@ import videojs from '../../src/js/core.js';
import Options from '../../src/js/options.js'; import Options from '../../src/js/options.js';
import * as Lib from '../../src/js/lib.js'; import * as Lib from '../../src/js/lib.js';
import MediaError from '../../src/js/media-error.js'; import MediaError from '../../src/js/media-error.js';
import Html5 from '../../src/js/media/html5.js'; import Html5 from '../../src/js/tech/html5.js';
import TestHelpers from './test-helpers.js'; import TestHelpers from './test-helpers.js';
import document from 'global/document'; import document from 'global/document';

View File

@ -1,4 +1,4 @@
import PosterImage from '../../src/js/poster.js'; import PosterImage from '../../src/js/poster-image.js';
import * as Lib from '../../src/js/lib.js'; import * as Lib from '../../src/js/lib.js';
import TestHelpers from './test-helpers.js'; import TestHelpers from './test-helpers.js';
import document from 'global/document'; import document from 'global/document';

View File

@ -1,4 +1,4 @@
import { TextTrackMenuItem } from '../../../src/js/tracks/text-track-controls'; import TextTrackMenuItem from '../../../src/js/control-bar/text-track-controls/text-track-menu-item.js';
import TestHelpers from '../test-helpers.js'; import TestHelpers from '../test-helpers.js';
import * as Lib from '../../../src/js/lib.js'; import * as Lib from '../../../src/js/lib.js';

View File

@ -1,10 +1,11 @@
import { CaptionsButton } from '../../../src/js/tracks/text-track-controls.js'; import ChaptersButton from '../../../src/js/control-bar/text-track-controls/chapters-button.js';
import { SubtitlesButton } from '../../../src/js/tracks/text-track-controls.js'; import SubtitlesButton from '../../../src/js/control-bar/text-track-controls/subtitles-button.js';
import { ChaptersButton } from '../../../src/js/tracks/text-track-controls.js'; import CaptionsButton from '../../../src/js/control-bar/text-track-controls/captions-button.js';
import { TextTrackDisplay } from '../../../src/js/tracks/text-track-controls.js';
import Html5 from '../../../src/js/media/html5.js'; import TextTrackDisplay from '../../../src/js/tracks/text-track-display.js';
import Flash from '../../../src/js/media/flash.js'; import Html5 from '../../../src/js/tech/html5.js';
import MediaTechController from '../../../src/js/media/media.js'; import Flash from '../../../src/js/tech/flash.js';
import Tech from '../../../src/js/tech/tech.js';
import Component from '../../../src/js/component.js'; import Component from '../../../src/js/component.js';
import * as Lib from '../../../src/js/lib.js'; import * as Lib from '../../../src/js/lib.js';
@ -155,9 +156,9 @@ test('update texttrack buttons on removetrack or addtrack', function() {
oldChaptersUpdate.call(this); oldChaptersUpdate.call(this);
}; };
MediaTechController.prototype['featuresNativeTextTracks'] = true; Tech.prototype['featuresNativeTextTracks'] = true;
oldTextTracks = MediaTechController.prototype.textTracks; oldTextTracks = Tech.prototype.textTracks;
MediaTechController.prototype.textTracks = function() { Tech.prototype.textTracks = function() {
return { return {
length: 0, length: 0,
addEventListener: function(type, handler) { addEventListener: function(type, handler) {
@ -201,8 +202,8 @@ test('update texttrack buttons on removetrack or addtrack', function() {
equal(update, 9, 'update was called on the three buttons for remove track'); equal(update, 9, 'update was called on the three buttons for remove track');
MediaTechController.prototype.textTracks = oldTextTracks; Tech.prototype.textTracks = oldTextTracks;
MediaTechController.prototype['featuresNativeTextTracks'] = false; Tech.prototype['featuresNativeTextTracks'] = false;
CaptionsButton.prototype.update = oldCaptionsUpdate; CaptionsButton.prototype.update = oldCaptionsUpdate;
SubtitlesButton.prototype.update = oldSubsUpdate; SubtitlesButton.prototype.update = oldSubsUpdate;
ChaptersButton.prototype.update = oldChaptersUpdate; ChaptersButton.prototype.update = oldChaptersUpdate;