myButton
- * // -> myButton === myComonent.children()[0];
- *
- * Pass in options for child constructors and options for children of the child
- *
- * var myButton = myComponent.addChild('MyButton', {
- * text: 'Press Me',
- * children: {
- * buttonChildExample: {
- * buttonChildOption: true
- * }
- * }
- * });
- *
- * @param {String|vjs.Component} child The class name or instance of a child to add
- * @param {Object=} options Options, including options to be passed to children of the child.
- * @return {vjs.Component} The child component (created by this process if a string was used)
- * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility}
- */
-Component.prototype.addChild = function(child, options){
- let component, componentName;
- // If child is a string, create nt with options
- if (typeof child === 'string') {
- let componentName = child;
-
- // Make sure options is at least an empty object to protect against errors
- if (!options || options === true) {
- options = {};
+ if (typeof component.id === 'function') {
+ this.childIndex_[component.id()] = component;
}
- // If no componentClass in options, assume componentClass is the name lowercased
- // (e.g. playButton)
- let componentClassName = options['componentClass'] || Lib.capitalize(componentName);
-
- // Set name through options
- options['name'] = componentName;
+ // If a name wasn't used to create the component, check if we can use the
+ // name function of the component
+ componentName = componentName || (component.name && component.name());
- // Create a new object & element for this controls set
- // If there's no .player_, this is a player
- // Closure Compiler throws an 'incomplete alias' warning if we use the vjs variable directly.
- // Every class should be exported, so this should never be a problem here.
- let componentClass = Component.getComponent(componentClassName);
+ if (componentName) {
+ this.childNameIndex_[componentName] = component;
+ }
- component = new componentClass(this.player_ || this, options);
+ // Add the UI object's element to the container div (box)
+ // Having an element is not required
+ if (typeof component.el === 'function' && component.el()) {
+ this.contentEl().appendChild(component.el());
+ }
- // child is a component instance
- } else {
- component = child;
+ // Return so it can stored on parent object if desired.
+ return component;
}
- this.children_.push(component);
-
- if (typeof component.id === 'function') {
- this.childIndex_[component.id()] = component;
- }
+ /**
+ * Remove a child component from this component's list of children, and the
+ * child component's element from this component's element
+ *
+ * @param {vjs.Component} component Component to remove
+ */
+ removeChild(component){
+ if (typeof component === 'string') {
+ component = this.getChild(component);
+ }
- // If a name wasn't used to create the component, check if we can use the
- // name function of the component
- componentName = componentName || (component.name && component.name());
+ if (!component || !this.children_) return;
- if (componentName) {
- this.childNameIndex_[componentName] = component;
- }
+ let childFound = false;
+ for (var i = this.children_.length - 1; i >= 0; i--) {
+ if (this.children_[i] === component) {
+ childFound = true;
+ this.children_.splice(i,1);
+ break;
+ }
+ }
- // Add the UI object's element to the container div (box)
- // Having an element is not required
- if (typeof component['el'] === 'function' && component['el']()) {
- this.contentEl().appendChild(component['el']());
- }
+ if (!childFound) return;
- // Return so it can stored on parent object if desired.
- return component;
-};
+ this.childIndex_[component.id()] = null;
+ this.childNameIndex_[component.name()] = null;
-/**
- * Remove a child component from this component's list of children, and the
- * child component's element from this component's element
- *
- * @param {vjs.Component} component Component to remove
- */
-Component.prototype.removeChild = function(component){
- if (typeof component === 'string') {
- component = this.getChild(component);
+ var compEl = component.el();
+ if (compEl && compEl.parentNode === this.contentEl()) {
+ this.contentEl().removeChild(component.el());
+ }
}
- if (!component || !this.children_) return;
+ /**
+ * Add and initialize default child components from options
+ *
+ * // when an instance of MyComponent is created, all children in options
+ * // will be added to the instance by their name strings and options
+ * MyComponent.prototype.options_.children = {
+ * myChildComponent: {
+ * myChildOption: true
+ * }
+ * }
+ *
+ * // Or when creating the component
+ * var myComp = new MyComponent(player, {
+ * children: {
+ * myChildComponent: {
+ * myChildOption: true
+ * }
+ * }
+ * });
+ *
+ * The children option can also be an Array of child names or
+ * child options objects (that also include a 'name' key).
+ *
+ * var myComp = new MyComponent(player, {
+ * children: [
+ * 'button',
+ * {
+ * name: 'button',
+ * someOtherOption: true
+ * }
+ * ]
+ * });
+ *
+ */
+ initChildren() {
+ let children = this.options_.children;
+
+ if (children) {
+ let parent = this;
+ let parentOptions = parent.options();
+ let handleAdd = function(name, opts){
+ // Allow options for children to be set at the parent options
+ // e.g. videojs(id, { controlBar: false });
+ // instead of videojs(id, { children: { controlBar: false });
+ if (parentOptions[name] !== undefined) {
+ opts = parentOptions[name];
+ }
- var childFound = false;
- for (var i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i] === component) {
- childFound = true;
- this.children_.splice(i,1);
- break;
+ // Allow for disabling default components
+ // e.g. vjs.options['children']['posterImage'] = false
+ if (opts === false) return;
+
+ // Create and add the child component.
+ // Add a direct reference to the child by name on the parent instance.
+ // If two of the same component are used, different names should be supplied
+ // for each
+ parent[name] = parent.addChild(name, opts);
+ };
+
+ // Allow for an array of children details to passed in the options
+ if (Lib.obj.isArray(children)) {
+ for (var i = 0; i < children.length; i++) {
+ let child = children[i];
+
+ let name, opts;
+ if (typeof child == 'string') {
+ // ['myComponent']
+ name = child;
+ opts = {};
+ } else {
+ // [{ name: 'myComponent', otherOption: true }]
+ name = child.name;
+ opts = child;
+ }
+
+ handleAdd(name, opts);
+ }
+ } else {
+ Lib.obj.each(children, handleAdd);
+ }
}
}
- if (!childFound) return;
-
- this.childIndex_[component.id()] = null;
- this.childNameIndex_[component.name()] = null;
-
- var compEl = component.el();
- if (compEl && compEl.parentNode === this.contentEl()) {
- this.contentEl().removeChild(component.el());
+ /**
+ * Allows sub components to stack CSS class names
+ *
+ * @return {String} The constructed class name
+ */
+ buildCSSClass(){
+ // Child classes can include a function that does:
+ // return 'CLASS NAME' + this._super();
+ return '';
}
-};
-
-/**
- * Add and initialize default child components from options
- *
- * // when an instance of MyComponent is created, all children in options
- * // will be added to the instance by their name strings and options
- * MyComponent.prototype.options_.children = {
- * myChildComponent: {
- * myChildOption: true
- * }
- * }
- *
- * // Or when creating the component
- * var myComp = new MyComponent(player, {
- * children: {
- * myChildComponent: {
- * myChildOption: true
- * }
- * }
- * });
- *
- * The children option can also be an Array of child names or
- * child options objects (that also include a 'name' key).
- *
- * var myComp = new MyComponent(player, {
- * children: [
- * 'button',
- * {
- * name: 'button',
- * someOtherOption: true
- * }
- * ]
- * });
- *
- */
-Component.prototype.initChildren = function(){
- let parent = this;
- let parentOptions = parent.options();
- let children = parentOptions['children'];
-
- let handleAdd;
- if (children) {
- handleAdd = function(name, opts){
- // Allow options for children to be set at the parent options
- // e.g. videojs(id, { controlBar: false });
- // instead of videojs(id, { children: { controlBar: false });
- if (parentOptions[name] !== undefined) {
- opts = parentOptions[name];
- }
- // Allow for disabling default components
- // e.g. vjs.options['children']['posterImage'] = false
- if (opts === false) return;
-
- // Create and add the child component.
- // Add a direct reference to the child by name on the parent instance.
- // If two of the same component are used, different names should be supplied
- // for each
- parent[name] = parent.addChild(name, opts);
- };
+ /**
+ * Add an event listener to this component's element
+ *
+ * var myFunc = function(){
+ * var myComponent = this;
+ * // Do something when the event is fired
+ * };
+ *
+ * myComponent.on('eventType', myFunc);
+ *
+ * The context of myFunc will be myComponent unless previously bound.
+ *
+ * Alternatively, you can add a listener to another element or component.
+ *
+ * myComponent.on(otherElement, 'eventName', myFunc);
+ * myComponent.on(otherComponent, 'eventName', myFunc);
+ *
+ * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)`
+ * and `otherComponent.on('eventName', myFunc)` is that this way the listeners
+ * will be automatically cleaned up when either component is disposed.
+ * It will also bind myComponent as the context of myFunc.
+ *
+ * **NOTE**: When using this on elements in the page other than window
+ * and document (both permanent), if you remove the element from the DOM
+ * you need to call `vjs.trigger(el, 'dispose')` on it to clean up
+ * references to it and allow the browser to garbage collect it.
+ *
+ * @param {String|vjs.Component} first The event type or other component
+ * @param {Function|String} second The event handler or event type
+ * @param {Function} third The event handler
+ * @return {vjs.Component} self
+ */
+ on(first, second, third){
+ var target, type, fn, removeOnDispose, cleanRemover, thisComponent;
- // Allow for an array of children details to passed in the options
- if (Lib.obj.isArray(children)) {
- for (var i = 0; i < children.length; i++) {
- let child = children[i];
-
- let name, opts;
- if (typeof child == 'string') {
- // ['myComponent']
- name = child;
- opts = {};
- } else {
- // [{ name: 'myComponent', otherOption: true }]
- name = child.name;
- opts = child;
- }
+ if (typeof first === 'string' || Lib.obj.isArray(first)) {
+ Events.on(this.el_, first, Lib.bind(this, second));
- handleAdd(name, opts);
- }
+ // Targeting another component or element
} else {
- Lib.obj.each(children, handleAdd);
+ target = first;
+ type = second;
+ fn = Lib.bind(this, third);
+ thisComponent = this;
+
+ // When this component is disposed, remove the listener from the other component
+ removeOnDispose = function(){
+ thisComponent.off(target, type, fn);
+ };
+ // Use the same function ID so we can remove it later it using the ID
+ // of the original listener
+ removeOnDispose.guid = fn.guid;
+ this.on('dispose', removeOnDispose);
+
+ // If the other component is disposed first we need to clean the reference
+ // to the other component in this component's removeOnDispose listener
+ // Otherwise we create a memory leak.
+ cleanRemover = function(){
+ thisComponent.off('dispose', removeOnDispose);
+ };
+ // Add the same function ID so we can easily remove it later
+ cleanRemover.guid = fn.guid;
+
+ // Check if this is a DOM node
+ if (first.nodeName) {
+ // Add the listener to the other element
+ Events.on(target, type, fn);
+ Events.on(target, 'dispose', cleanRemover);
+
+ // Should be a component
+ // Not using `instanceof vjs.Component` because it makes mock players difficult
+ } else if (typeof first.on === 'function') {
+ // Add the listener to the other component
+ target.on(type, fn);
+ target.on('dispose', cleanRemover);
+ }
}
- }
-};
-/**
- * Allows sub components to stack CSS class names
- *
- * @return {String} The constructed class name
- */
-Component.prototype.buildCSSClass = function(){
- // Child classes can include a function that does:
- // return 'CLASS NAME' + this._super();
- return '';
-};
+ return this;
+ }
-/* Events
-============================================================================= */
+ /**
+ * Remove an event listener from this component's element
+ *
+ * myComponent.off('eventType', myFunc);
+ *
+ * If myFunc is excluded, ALL listeners for the event type will be removed.
+ * If eventType is excluded, ALL listeners will be removed from the component.
+ *
+ * Alternatively you can use `off` to remove listeners that were added to other
+ * elements or components using `myComponent.on(otherComponent...`.
+ * In this case both the event type and listener function are REQUIRED.
+ *
+ * myComponent.off(otherElement, 'eventType', myFunc);
+ * myComponent.off(otherComponent, 'eventType', myFunc);
+ *
+ * @param {String=|vjs.Component} first The event type or other component
+ * @param {Function=|String} second The listener function or event type
+ * @param {Function=} third The listener for other component
+ * @return {vjs.Component}
+ */
+ off(first, second, third){
+ var target, otherComponent, type, fn, otherEl;
-/**
- * Add an event listener to this component's element
- *
- * var myFunc = function(){
- * var myComponent = this;
- * // Do something when the event is fired
- * };
- *
- * myComponent.on('eventType', myFunc);
- *
- * The context of myFunc will be myComponent unless previously bound.
- *
- * Alternatively, you can add a listener to another element or component.
- *
- * myComponent.on(otherElement, 'eventName', myFunc);
- * myComponent.on(otherComponent, 'eventName', myFunc);
- *
- * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)`
- * and `otherComponent.on('eventName', myFunc)` is that this way the listeners
- * will be automatically cleaned up when either component is disposed.
- * It will also bind myComponent as the context of myFunc.
- *
- * **NOTE**: When using this on elements in the page other than window
- * and document (both permanent), if you remove the element from the DOM
- * you need to call `vjs.trigger(el, 'dispose')` on it to clean up
- * references to it and allow the browser to garbage collect it.
- *
- * @param {String|vjs.Component} first The event type or other component
- * @param {Function|String} second The event handler or event type
- * @param {Function} third The event handler
- * @return {vjs.Component} self
- */
-Component.prototype.on = function(first, second, third){
- var target, type, fn, removeOnDispose, cleanRemover, thisComponent;
-
- if (typeof first === 'string' || Lib.obj.isArray(first)) {
- Events.on(this.el_, first, Lib.bind(this, second));
-
- // Targeting another component or element
- } else {
- target = first;
- type = second;
- fn = Lib.bind(this, third);
- thisComponent = this;
-
- // When this component is disposed, remove the listener from the other component
- removeOnDispose = function(){
- thisComponent.off(target, type, fn);
- };
- // Use the same function ID so we can remove it later it using the ID
- // of the original listener
- removeOnDispose.guid = fn.guid;
- this.on('dispose', removeOnDispose);
-
- // If the other component is disposed first we need to clean the reference
- // to the other component in this component's removeOnDispose listener
- // Otherwise we create a memory leak.
- cleanRemover = function(){
- thisComponent.off('dispose', removeOnDispose);
- };
- // Add the same function ID so we can easily remove it later
- cleanRemover.guid = fn.guid;
-
- // Check if this is a DOM node
- if (first.nodeName) {
- // Add the listener to the other element
- Events.on(target, type, fn);
- Events.on(target, 'dispose', cleanRemover);
-
- // Should be a component
- // Not using `instanceof vjs.Component` because it makes mock players difficult
- } else if (typeof first.on === 'function') {
- // Add the listener to the other component
- target.on(type, fn);
- target.on('dispose', cleanRemover);
+ if (!first || typeof first === 'string' || Lib.obj.isArray(first)) {
+ Events.off(this.el_, first, second);
+ } else {
+ target = first;
+ type = second;
+ // Ensure there's at least a guid, even if the function hasn't been used
+ fn = Lib.bind(this, third);
+
+ // Remove the dispose listener on this component,
+ // which was given the same guid as the event listener
+ this.off('dispose', fn);
+
+ if (first.nodeName) {
+ // Remove the listener
+ Events.off(target, type, fn);
+ // Remove the listener for cleaning the dispose listener
+ Events.off(target, 'dispose', fn);
+ } else {
+ target.off(type, fn);
+ target.off('dispose', fn);
+ }
}
+
+ return this;
}
- return this;
-};
+ /**
+ * Add an event listener to be triggered only once and then removed
+ *
+ * myComponent.one('eventName', myFunc);
+ *
+ * Alternatively you can add a listener to another element or component
+ * that will be triggered only once.
+ *
+ * myComponent.one(otherElement, 'eventName', myFunc);
+ * myComponent.one(otherComponent, 'eventName', myFunc);
+ *
+ * @param {String|vjs.Component} first The event type or other component
+ * @param {Function|String} second The listener function or event type
+ * @param {Function=} third The listener function for other component
+ * @return {vjs.Component}
+ */
+ one(first, second, third) {
+ var target, type, fn, thisComponent, newFunc;
-/**
- * Remove an event listener from this component's element
- *
- * myComponent.off('eventType', myFunc);
- *
- * If myFunc is excluded, ALL listeners for the event type will be removed.
- * If eventType is excluded, ALL listeners will be removed from the component.
- *
- * Alternatively you can use `off` to remove listeners that were added to other
- * elements or components using `myComponent.on(otherComponent...`.
- * In this case both the event type and listener function are REQUIRED.
- *
- * myComponent.off(otherElement, 'eventType', myFunc);
- * myComponent.off(otherComponent, 'eventType', myFunc);
- *
- * @param {String=|vjs.Component} first The event type or other component
- * @param {Function=|String} second The listener function or event type
- * @param {Function=} third The listener for other component
- * @return {vjs.Component}
- */
-Component.prototype.off = function(first, second, third){
- var target, otherComponent, type, fn, otherEl;
-
- if (!first || typeof first === 'string' || Lib.obj.isArray(first)) {
- Events.off(this.el_, first, second);
- } else {
- target = first;
- type = second;
- // Ensure there's at least a guid, even if the function hasn't been used
- fn = Lib.bind(this, third);
-
- // Remove the dispose listener on this component,
- // which was given the same guid as the event listener
- this.off('dispose', fn);
-
- if (first.nodeName) {
- // Remove the listener
- Events.off(target, type, fn);
- // Remove the listener for cleaning the dispose listener
- Events.off(target, 'dispose', fn);
+ if (typeof first === 'string' || Lib.obj.isArray(first)) {
+ Events.one(this.el_, first, Lib.bind(this, second));
} else {
- target.off(type, fn);
- target.off('dispose', fn);
+ target = first;
+ type = second;
+ fn = Lib.bind(this, third);
+ thisComponent = this;
+
+ newFunc = function(){
+ thisComponent.off(target, type, newFunc);
+ fn.apply(this, arguments);
+ };
+ // Keep the same function ID so we can remove it later
+ newFunc.guid = fn.guid;
+
+ this.on(target, type, newFunc);
}
- }
- return this;
-};
+ return this;
+ }
-/**
- * Add an event listener to be triggered only once and then removed
- *
- * myComponent.one('eventName', myFunc);
- *
- * Alternatively you can add a listener to another element or component
- * that will be triggered only once.
- *
- * myComponent.one(otherElement, 'eventName', myFunc);
- * myComponent.one(otherComponent, 'eventName', myFunc);
- *
- * @param {String|vjs.Component} first The event type or other component
- * @param {Function|String} second The listener function or event type
- * @param {Function=} third The listener function for other component
- * @return {vjs.Component}
- */
-Component.prototype.one = function(first, second, third) {
- var target, type, fn, thisComponent, newFunc;
-
- if (typeof first === 'string' || Lib.obj.isArray(first)) {
- Events.one(this.el_, first, Lib.bind(this, second));
- } else {
- target = first;
- type = second;
- fn = Lib.bind(this, third);
- thisComponent = this;
-
- newFunc = function(){
- thisComponent.off(target, type, newFunc);
- fn.apply(this, arguments);
- };
- // Keep the same function ID so we can remove it later
- newFunc.guid = fn.guid;
+ /**
+ * Trigger an event on an element
+ *
+ * myComponent.trigger('eventName');
+ * myComponent.trigger({'type':'eventName'});
+ *
+ * @param {Event|Object|String} event A string (the type) or an event object with a type attribute
+ * @return {vjs.Component} self
+ */
+ trigger(event){
+ Events.trigger(this.el_, event);
+ return this;
+ }
- this.on(target, type, newFunc);
+ /**
+ * Bind a listener to the component's ready state
+ *
+ * Different from event listeners in that if the ready event has already happened
+ * it will trigger the function immediately.
+ *
+ * @param {Function} fn Ready listener
+ * @return {vjs.Component}
+ */
+ ready(fn){
+ if (fn) {
+ if (this.isReady_) {
+ fn.call(this);
+ } else {
+ this.readyQueue_ = this.readyQueue_ || [];
+ this.readyQueue_.push(fn);
+ }
+ }
+ return this;
}
- return this;
-};
+ /**
+ * Trigger the ready listeners
+ *
+ * @return {vjs.Component}
+ */
+ triggerReady(){
+ this.isReady_ = true;
-/**
- * Trigger an event on an element
- *
- * myComponent.trigger('eventName');
- * myComponent.trigger({'type':'eventName'});
- *
- * @param {Event|Object|String} event A string (the type) or an event object with a type attribute
- * @return {vjs.Component} self
- */
-Component.prototype.trigger = function(event){
- Events.trigger(this.el_, event);
- return this;
-};
+ var readyQueue = this.readyQueue_;
-/* Ready
-================================================================================ */
-/**
- * Is the component loaded
- * This can mean different things depending on the component.
- *
- * @private
- * @type {Boolean}
- */
-Component.prototype.isReady_;
+ if (readyQueue && readyQueue.length > 0) {
-/**
- * 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;
+ for (var i = 0, j = readyQueue.length; i < j; i++) {
+ readyQueue[i].call(this);
+ }
-/**
- * List of ready listeners
- *
- * @type {Array}
- * @private
- */
-Component.prototype.readyQueue_;
+ // Reset Ready Queue
+ this.readyQueue_ = [];
-/**
- * Bind a listener to the component's ready state
- *
- * Different from event listeners in that if the ready event has already happened
- * it will trigger the function immediately.
- *
- * @param {Function} fn Ready listener
- * @return {vjs.Component}
- */
-Component.prototype.ready = function(fn){
- if (fn) {
- if (this.isReady_) {
- fn.call(this);
- } else {
- if (this.readyQueue_ === undefined) {
- this.readyQueue_ = [];
- }
- this.readyQueue_.push(fn);
+ // Allow for using event listeners also, in case you want to do something everytime a source is ready.
+ this.trigger('ready');
}
}
- return this;
-};
-/**
- * Trigger the ready listeners
- *
- * @return {vjs.Component}
- */
-Component.prototype.triggerReady = function(){
- this.isReady_ = true;
+ /**
+ * Check if a component's element has a CSS class name
+ *
+ * @param {String} classToCheck Classname to check
+ * @return {vjs.Component}
+ */
+ hasClass(classToCheck){
+ return Lib.hasClass(this.el_, classToCheck);
+ }
- var readyQueue = this.readyQueue_;
+ /**
+ * Add a CSS class name to the component's element
+ *
+ * @param {String} classToAdd Classname to add
+ * @return {vjs.Component}
+ */
+ addClass(classToAdd){
+ Lib.addClass(this.el_, classToAdd);
+ return this;
+ }
- if (readyQueue && readyQueue.length > 0) {
+ /**
+ * Remove a CSS class name from the component's element
+ *
+ * @param {String} classToRemove Classname to remove
+ * @return {vjs.Component}
+ */
+ removeClass(classToRemove){
+ Lib.removeClass(this.el_, classToRemove);
+ return this;
+ }
- for (var i = 0, j = readyQueue.length; i < j; i++) {
- readyQueue[i].call(this);
- }
+ /**
+ * Show the component element if hidden
+ *
+ * @return {vjs.Component}
+ */
+ show(){
+ this.removeClass('vjs-hidden');
+ return this;
+ }
- // Reset Ready Queue
- this.readyQueue_ = [];
+ /**
+ * Hide the component element if currently showing
+ *
+ * @return {vjs.Component}
+ */
+ hide(){
+ this.addClass('vjs-hidden');
+ return this;
+ }
- // Allow for using event listeners also, in case you want to do something everytime a source is ready.
- this.trigger('ready');
+ /**
+ * Lock an item in its visible state
+ * To be used with fadeIn/fadeOut.
+ *
+ * @return {vjs.Component}
+ * @private
+ */
+ lockShowing(){
+ this.addClass('vjs-lock-showing');
+ return this;
}
-};
-/* Display
-============================================================================= */
+ /**
+ * Unlock an item to be hidden
+ * To be used with fadeIn/fadeOut.
+ *
+ * @return {vjs.Component}
+ * @private
+ */
+ unlockShowing(){
+ this.removeClass('vjs-lock-showing');
+ return this;
+ }
-/**
- * Check if a component's element has a CSS class name
- *
- * @param {String} classToCheck Classname to check
- * @return {vjs.Component}
- */
-Component.prototype.hasClass = function(classToCheck){
- return Lib.hasClass(this.el_, classToCheck);
-};
+ /**
+ * Set or get the width of the component (CSS values)
+ *
+ * Setting the video tag dimension values only works with values in pixels.
+ * Percent values will not work.
+ * Some percents can be used, but width()/height() will return the number + %,
+ * not the actual computed width/height.
+ *
+ * @param {Number|String=} num Optional width number
+ * @param {Boolean} skipListeners Skip the 'resize' event trigger
+ * @return {vjs.Component} This component, when setting the width
+ * @return {Number|String} The width, when getting
+ */
+ width(num, skipListeners){
+ return this.dimension('width', num, skipListeners);
+ }
-/**
- * Add a CSS class name to the component's element
- *
- * @param {String} classToAdd Classname to add
- * @return {vjs.Component}
- */
-Component.prototype.addClass = function(classToAdd){
- Lib.addClass(this.el_, classToAdd);
- return this;
-};
+ /**
+ * Get or set the height of the component (CSS values)
+ *
+ * Setting the video tag dimension values only works with values in pixels.
+ * Percent values will not work.
+ * Some percents can be used, but width()/height() will return the number + %,
+ * not the actual computed width/height.
+ *
+ * @param {Number|String=} num New component height
+ * @param {Boolean=} skipListeners Skip the resize event trigger
+ * @return {vjs.Component} This component, when setting the height
+ * @return {Number|String} The height, when getting
+ */
+ height(num, skipListeners){
+ return this.dimension('height', num, skipListeners);
+ }
-/**
- * Remove a CSS class name from the component's element
- *
- * @param {String} classToRemove Classname to remove
- * @return {vjs.Component}
- */
-Component.prototype.removeClass = function(classToRemove){
- Lib.removeClass(this.el_, classToRemove);
- return this;
-};
+ /**
+ * Set both width and height at the same time
+ *
+ * @param {Number|String} width
+ * @param {Number|String} height
+ * @return {vjs.Component} The component
+ */
+ dimensions(width, height){
+ // Skip resize listeners on width for optimization
+ return this.width(width, true).height(height);
+ }
-/**
- * Show the component element if hidden
- *
- * @return {vjs.Component}
- */
-Component.prototype.show = function(){
- this.removeClass('vjs-hidden');
- return this;
-};
+ /**
+ * Get or set width or height
+ *
+ * This is the shared code for the width() and height() methods.
+ * All for an integer, integer + 'px' or integer + '%';
+ *
+ * Known issue: Hidden elements officially have a width of 0. We're defaulting
+ * to the style.width value and falling back to computedStyle which has the
+ * hidden element issue. Info, but probably not an efficient fix:
+ * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/
+ *
+ * @param {String} widthOrHeight 'width' or 'height'
+ * @param {Number|String=} num New dimension
+ * @param {Boolean=} skipListeners Skip resize event trigger
+ * @return {vjs.Component} The component if a dimension was set
+ * @return {Number|String} The dimension if nothing was set
+ * @private
+ */
+ dimension(widthOrHeight, num, skipListeners){
+ if (num !== undefined) {
+ // Set to zero if null or literally NaN (NaN !== NaN)
+ if (num === null || num !== num) {
+ num = 0;
+ }
-/**
- * Hide the component element if currently showing
- *
- * @return {vjs.Component}
- */
-Component.prototype.hide = function(){
- this.addClass('vjs-hidden');
- return this;
-};
+ // Check if using css width/height (% or px) and adjust
+ if ((''+num).indexOf('%') !== -1 || (''+num).indexOf('px') !== -1) {
+ this.el_.style[widthOrHeight] = num;
+ } else if (num === 'auto') {
+ this.el_.style[widthOrHeight] = '';
+ } else {
+ this.el_.style[widthOrHeight] = num+'px';
+ }
-/**
- * Lock an item in its visible state
- * To be used with fadeIn/fadeOut.
- *
- * @return {vjs.Component}
- * @private
- */
-Component.prototype.lockShowing = function(){
- this.addClass('vjs-lock-showing');
- return this;
-};
+ // skipListeners allows us to avoid triggering the resize event when setting both width and height
+ if (!skipListeners) { this.trigger('resize'); }
-/**
- * Unlock an item to be hidden
- * To be used with fadeIn/fadeOut.
- *
- * @return {vjs.Component}
- * @private
- */
-Component.prototype.unlockShowing = function(){
- this.removeClass('vjs-lock-showing');
- return this;
-};
+ // Return component
+ 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(){};
-};
+ // Not setting a value, so getting it
+ // Make sure element exists
+ if (!this.el_) return 0;
-/**
- * Set or get the width of the component (CSS values)
- *
- * Setting the video tag dimension values only works with values in pixels.
- * Percent values will not work.
- * Some percents can be used, but width()/height() will return the number + %,
- * not the actual computed width/height.
- *
- * @param {Number|String=} num Optional width number
- * @param {Boolean} skipListeners Skip the 'resize' event trigger
- * @return {vjs.Component} This component, when setting the width
- * @return {Number|String} The width, when getting
- */
-Component.prototype.width = function(num, skipListeners){
- return this.dimension('width', num, skipListeners);
-};
+ // Get dimension value from style
+ var val = this.el_.style[widthOrHeight];
+ var pxIndex = val.indexOf('px');
+ if (pxIndex !== -1) {
+ // Return the pixel value with no 'px'
+ return parseInt(val.slice(0,pxIndex), 10);
-/**
- * Get or set the height of the component (CSS values)
- *
- * Setting the video tag dimension values only works with values in pixels.
- * Percent values will not work.
- * Some percents can be used, but width()/height() will return the number + %,
- * not the actual computed width/height.
- *
- * @param {Number|String=} num New component height
- * @param {Boolean=} skipListeners Skip the resize event trigger
- * @return {vjs.Component} This component, when setting the height
- * @return {Number|String} The height, when getting
- */
-Component.prototype.height = function(num, skipListeners){
- return this.dimension('height', num, skipListeners);
-};
+ // No px so using % or no style was set, so falling back to offsetWidth/height
+ // If component has display:none, offset will return 0
+ // TODO: handle display:none and no dimension style using px
+ } else {
-/**
- * Set both width and height at the same time
- *
- * @param {Number|String} width
- * @param {Number|String} height
- * @return {vjs.Component} The component
- */
-Component.prototype.dimensions = function(width, height){
- // Skip resize listeners on width for optimization
- return this.width(width, true).height(height);
-};
+ return parseInt(this.el_['offset'+Lib.capitalize(widthOrHeight)], 10);
-/**
- * Get or set width or height
- *
- * This is the shared code for the width() and height() methods.
- * All for an integer, integer + 'px' or integer + '%';
- *
- * Known issue: Hidden elements officially have a width of 0. We're defaulting
- * to the style.width value and falling back to computedStyle which has the
- * hidden element issue. Info, but probably not an efficient fix:
- * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/
- *
- * @param {String} widthOrHeight 'width' or 'height'
- * @param {Number|String=} num New dimension
- * @param {Boolean=} skipListeners Skip resize event trigger
- * @return {vjs.Component} The component if a dimension was set
- * @return {Number|String} The dimension if nothing was set
- * @private
- */
-Component.prototype.dimension = function(widthOrHeight, num, skipListeners){
- if (num !== undefined) {
- // Set to zero if null or literally NaN (NaN !== NaN)
- if (num === null || num !== num) {
- num = 0;
- }
+ // ComputedStyle version.
+ // Only difference is if the element is hidden it will return
+ // the percent value (e.g. '100%'')
+ // instead of zero like offsetWidth returns.
+ // var val = vjs.getComputedStyleValue(this.el_, widthOrHeight);
+ // var pxIndex = val.indexOf('px');
- // Check if using css width/height (% or px) and adjust
- if ((''+num).indexOf('%') !== -1 || (''+num).indexOf('px') !== -1) {
- this.el_.style[widthOrHeight] = num;
- } else if (num === 'auto') {
- this.el_.style[widthOrHeight] = '';
- } else {
- this.el_.style[widthOrHeight] = num+'px';
+ // if (pxIndex !== -1) {
+ // return val.slice(0, pxIndex);
+ // } else {
+ // return val;
+ // }
}
-
- // skipListeners allows us to avoid triggering the resize event when setting both width and height
- if (!skipListeners) { this.trigger('resize'); }
-
- // Return component
- return this;
}
- // Not setting a value, so getting it
- // Make sure element exists
- if (!this.el_) return 0;
-
- // Get dimension value from style
- var val = this.el_.style[widthOrHeight];
- var pxIndex = val.indexOf('px');
- if (pxIndex !== -1) {
- // Return the pixel value with no 'px'
- return parseInt(val.slice(0,pxIndex), 10);
-
- // No px so using % or no style was set, so falling back to offsetWidth/height
- // If component has display:none, offset will return 0
- // TODO: handle display:none and no dimension style using px
- } else {
-
- return parseInt(this.el_['offset'+Lib.capitalize(widthOrHeight)], 10);
-
- // ComputedStyle version.
- // Only difference is if the element is hidden it will return
- // the percent value (e.g. '100%'')
- // instead of zero like offsetWidth returns.
- // var val = vjs.getComputedStyleValue(this.el_, widthOrHeight);
- // var pxIndex = val.indexOf('px');
-
- // if (pxIndex !== -1) {
- // return val.slice(0, pxIndex);
- // } else {
- // return val;
- // }
- }
-};
+ /**
+ * Emit 'tap' events when touch events are supported
+ *
+ * This is used to support toggling the controls through a tap on the video.
+ *
+ * We're requiring them to be enabled because otherwise every component would
+ * have this extra overhead unnecessarily, on mobile devices where extra
+ * overhead is especially bad.
+ * @private
+ */
+ emitTapEvents(){
+ var touchStart, firstTouch, touchTime, couldBeTap, noTap,
+ xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold;
-/**
- * Fired when the width and/or height of the component changes
- * @event resize
- */
-Component.prototype.onResize;
+ // Track the start time so we can determine how long the touch lasted
+ touchStart = 0;
+ firstTouch = null;
-/**
- * Emit 'tap' events when touch events are supported
- *
- * This is used to support toggling the controls through a tap on the video.
- *
- * We're requiring them to be enabled because otherwise every component would
- * have this extra overhead unnecessarily, on mobile devices where extra
- * overhead is especially bad.
- * @private
- */
-Component.prototype.emitTapEvents = function(){
- var touchStart, firstTouch, touchTime, couldBeTap, noTap,
- xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold;
-
- // Track the start time so we can determine how long the touch lasted
- touchStart = 0;
- firstTouch = null;
-
- // Maximum movement allowed during a touch event to still be considered a tap
- // Other popular libs use anywhere from 2 (hammer.js) to 15, so 10 seems like a nice, round number.
- tapMovementThreshold = 10;
-
- // The maximum length a touch can be while still being considered a tap
- touchTimeThreshold = 200;
-
- this.on('touchstart', function(event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length === 1) {
- firstTouch = Lib.obj.copy(event.touches[0]);
- // Record start time so we can detect a tap vs. "touch and hold"
- touchStart = new Date().getTime();
- // Reset couldBeTap tracking
- couldBeTap = true;
- }
- });
+ // Maximum movement allowed during a touch event to still be considered a tap
+ // Other popular libs use anywhere from 2 (hammer.js) to 15, so 10 seems like a nice, round number.
+ tapMovementThreshold = 10;
+
+ // The maximum length a touch can be while still being considered a tap
+ touchTimeThreshold = 200;
+
+ this.on('touchstart', function(event) {
+ // If more than one finger, don't consider treating this as a click
+ if (event.touches.length === 1) {
+ firstTouch = Lib.obj.copy(event.touches[0]);
+ // Record start time so we can detect a tap vs. "touch and hold"
+ touchStart = new Date().getTime();
+ // Reset couldBeTap tracking
+ couldBeTap = true;
+ }
+ });
- this.on('touchmove', function(event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length > 1) {
- couldBeTap = false;
- } else if (firstTouch) {
- // Some devices will throw touchmoves for all but the slightest of taps.
- // So, if we moved only a small distance, this could still be a tap
- xdiff = event.touches[0].pageX - firstTouch.pageX;
- ydiff = event.touches[0].pageY - firstTouch.pageY;
- touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
- if (touchDistance > tapMovementThreshold) {
+ this.on('touchmove', function(event) {
+ // If more than one finger, don't consider treating this as a click
+ if (event.touches.length > 1) {
couldBeTap = false;
+ } else if (firstTouch) {
+ // Some devices will throw touchmoves for all but the slightest of taps.
+ // So, if we moved only a small distance, this could still be a tap
+ xdiff = event.touches[0].pageX - firstTouch.pageX;
+ ydiff = event.touches[0].pageY - firstTouch.pageY;
+ touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
+ if (touchDistance > tapMovementThreshold) {
+ couldBeTap = false;
+ }
}
- }
- });
-
- noTap = function(){
- couldBeTap = false;
- };
- // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
- this.on('touchleave', noTap);
- this.on('touchcancel', noTap);
-
- // When the touch ends, measure how long it took and trigger the appropriate
- // event
- this.on('touchend', function(event) {
- firstTouch = null;
- // Proceed only if the touchmove/leave/cancel event didn't happen
- if (couldBeTap === true) {
- // Measure how long the touch lasted
- touchTime = new Date().getTime() - touchStart;
- // Make sure the touch was less than the threshold to be considered a tap
- if (touchTime < touchTimeThreshold) {
- event.preventDefault(); // Don't let browser turn this into a click
- this.trigger('tap');
- // It may be good to copy the touchend event object and change the
- // type to tap, if the other event properties aren't exact after
- // vjs.fixEvent runs (e.g. event.target)
+ });
+
+ noTap = function(){
+ couldBeTap = false;
+ };
+ // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
+ this.on('touchleave', noTap);
+ this.on('touchcancel', noTap);
+
+ // When the touch ends, measure how long it took and trigger the appropriate
+ // event
+ this.on('touchend', function(event) {
+ firstTouch = null;
+ // Proceed only if the touchmove/leave/cancel event didn't happen
+ if (couldBeTap === true) {
+ // Measure how long the touch lasted
+ touchTime = new Date().getTime() - touchStart;
+ // Make sure the touch was less than the threshold to be considered a tap
+ if (touchTime < touchTimeThreshold) {
+ event.preventDefault(); // Don't let browser turn this into a click
+ this.trigger('tap');
+ // It may be good to copy the touchend event object and change the
+ // type to tap, if the other event properties aren't exact after
+ // vjs.fixEvent runs (e.g. event.target)
+ }
}
+ });
+ }
+
+ /**
+ * Report user touch activity when touch events occur
+ *
+ * User activity is used to determine when controls should show/hide. It's
+ * relatively simple when it comes to mouse events, because any mouse event
+ * should show the controls. So we capture mouse events that bubble up to the
+ * player and report activity when that happens.
+ *
+ * With touch events it isn't as easy. We can't rely on touch events at the
+ * player level, because a tap (touchstart + touchend) on the video itself on
+ * mobile devices is meant to turn controls off (and on). User activity is
+ * checked asynchronously, so what could happen is a tap event on the video
+ * turns the controls off, then the touchend event bubbles up to the player,
+ * which if it reported user activity, would turn the controls right back on.
+ * (We also don't want to completely block touch events from bubbling up)
+ *
+ * Also a touchmove, touch+hold, and anything other than a tap is not supposed
+ * to turn the controls back on on a mobile device.
+ *
+ * Here we're setting the default component behavior to report user activity
+ * whenever touch events happen, and this can be turned off by components that
+ * want touch events to act differently.
+ */
+ enableTouchActivity() {
+ var report, touchHolding, touchEnd;
+
+ // Don't continue if the root player doesn't support reporting user activity
+ if (!this.player().reportUserActivity) {
+ return;
}
- });
-};
-/**
- * Report user touch activity when touch events occur
- *
- * User activity is used to determine when controls should show/hide. It's
- * relatively simple when it comes to mouse events, because any mouse event
- * should show the controls. So we capture mouse events that bubble up to the
- * player and report activity when that happens.
- *
- * With touch events it isn't as easy. We can't rely on touch events at the
- * player level, because a tap (touchstart + touchend) on the video itself on
- * mobile devices is meant to turn controls off (and on). User activity is
- * checked asynchronously, so what could happen is a tap event on the video
- * turns the controls off, then the touchend event bubbles up to the player,
- * which if it reported user activity, would turn the controls right back on.
- * (We also don't want to completely block touch events from bubbling up)
- *
- * Also a touchmove, touch+hold, and anything other than a tap is not supposed
- * to turn the controls back on on a mobile device.
- *
- * Here we're setting the default component behavior to report user activity
- * whenever touch events happen, and this can be turned off by components that
- * want touch events to act differently.
- */
-Component.prototype.enableTouchActivity = function() {
- var report, touchHolding, touchEnd;
+ // listener for reporting that the user is active
+ report = Lib.bind(this.player(), this.player().reportUserActivity);
+
+ this.on('touchstart', function() {
+ report();
+ // For as long as the they are touching the device or have their mouse down,
+ // we consider them active even if they're not moving their finger or mouse.
+ // So we want to continue to update that they are active
+ this.clearInterval(touchHolding);
+ // report at the same interval as activityCheck
+ touchHolding = this.setInterval(report, 250);
+ });
+
+ touchEnd = function(event) {
+ report();
+ // stop the interval that maintains activity if the touch is holding
+ this.clearInterval(touchHolding);
+ };
- // Don't continue if the root player doesn't support reporting user activity
- if (!this.player().reportUserActivity) {
- return;
+ this.on('touchmove', report);
+ this.on('touchend', touchEnd);
+ this.on('touchcancel', touchEnd);
}
- // listener for reporting that the user is active
- report = Lib.bind(this.player(), this.player().reportUserActivity);
-
- this.on('touchstart', function() {
- report();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(touchHolding);
- // report at the same interval as activityCheck
- touchHolding = this.setInterval(report, 250);
- });
-
- touchEnd = function(event) {
- report();
- // stop the interval that maintains activity if the touch is holding
- this.clearInterval(touchHolding);
- };
-
- this.on('touchmove', report);
- this.on('touchend', touchEnd);
- this.on('touchcancel', touchEnd);
-};
+ /**
+ * Creates timeout and sets up disposal automatically.
+ * @param {Function} fn The function to run after the timeout.
+ * @param {Number} timeout Number of ms to delay before executing specified function.
+ * @return {Number} Returns the timeout ID
+ */
+ setTimeout(fn, timeout) {
+ fn = Lib.bind(this, fn);
-/**
- * Creates timeout and sets up disposal automatically.
- * @param {Function} fn The function to run after the timeout.
- * @param {Number} timeout Number of ms to delay before executing specified function.
- * @return {Number} Returns the timeout ID
- */
-Component.prototype.setTimeout = function(fn, timeout) {
- 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.
+ var timeoutId = setTimeout(fn, timeout);
- // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't.
- var timeoutId = setTimeout(fn, timeout);
+ var disposeFn = function() {
+ this.clearTimeout(timeoutId);
+ };
- var disposeFn = function() {
- this.clearTimeout(timeoutId);
- };
+ disposeFn.guid = 'vjs-timeout-'+ timeoutId;
- disposeFn.guid = 'vjs-timeout-'+ timeoutId;
+ this.on('dispose', disposeFn);
- this.on('dispose', disposeFn);
+ return timeoutId;
+ }
- return timeoutId;
-};
+ /**
+ * Clears a timeout and removes the associated dispose listener
+ * @param {Number} timeoutId The id of the timeout to clear
+ * @return {Number} Returns the timeout ID
+ */
+ clearTimeout(timeoutId) {
+ clearTimeout(timeoutId);
-/**
- * Clears a timeout and removes the associated dispose listener
- * @param {Number} timeoutId The id of the timeout to clear
- * @return {Number} Returns the timeout ID
- */
-Component.prototype.clearTimeout = function(timeoutId) {
- clearTimeout(timeoutId);
+ var disposeFn = function(){};
+ disposeFn.guid = 'vjs-timeout-'+ timeoutId;
- var disposeFn = function(){};
- disposeFn.guid = 'vjs-timeout-'+ timeoutId;
+ this.off('dispose', disposeFn);
- this.off('dispose', disposeFn);
+ return timeoutId;
+ }
- return timeoutId;
-};
+ /**
+ * Creates an interval and sets up disposal automatically.
+ * @param {Function} fn The function to run every N seconds.
+ * @param {Number} interval Number of ms to delay before executing specified function.
+ * @return {Number} Returns the interval ID
+ */
+ setInterval(fn, interval) {
+ fn = Lib.bind(this, fn);
-/**
- * Creates an interval and sets up disposal automatically.
- * @param {Function} fn The function to run every N seconds.
- * @param {Number} interval Number of ms to delay before executing specified function.
- * @return {Number} Returns the interval ID
- */
-Component.prototype.setInterval = function(fn, interval) {
- fn = Lib.bind(this, fn);
+ var intervalId = setInterval(fn, interval);
- var intervalId = setInterval(fn, interval);
+ var disposeFn = function() {
+ this.clearInterval(intervalId);
+ };
- var disposeFn = function() {
- this.clearInterval(intervalId);
- };
+ disposeFn.guid = 'vjs-interval-'+ intervalId;
- disposeFn.guid = 'vjs-interval-'+ intervalId;
+ this.on('dispose', disposeFn);
- this.on('dispose', disposeFn);
+ return intervalId;
+ }
- return intervalId;
-};
+ /**
+ * Clears an interval and removes the associated dispose listener
+ * @param {Number} intervalId The id of the interval to clear
+ * @return {Number} Returns the interval ID
+ */
+ clearInterval(intervalId) {
+ clearInterval(intervalId);
-/**
- * Clears an interval and removes the associated dispose listener
- * @param {Number} intervalId The id of the interval to clear
- * @return {Number} Returns the interval ID
- */
-Component.prototype.clearInterval = function(intervalId) {
- clearInterval(intervalId);
+ var disposeFn = function(){};
+ disposeFn.guid = 'vjs-interval-'+ intervalId;
- var disposeFn = function(){};
- disposeFn.guid = 'vjs-interval-'+ intervalId;
+ this.off('dispose', disposeFn);
- this.off('dispose', disposeFn);
+ return intervalId;
+ }
- return intervalId;
-};
+ static registerComponent(name, comp){
+ if (!Component.components_) {
+ Component.components_ = {};
+ }
-Component.components = {};
+ Component.components_[name] = comp;
+ return comp;
+ }
-Component.registerComponent = function(name, comp){
- Component.components[name] = comp;
- return comp;
-};
+ static getComponent(name){
+ if (Component.components_ && Component.components_[name]) {
+ return Component.components_[name];
+ }
-Component.getComponent = function(name){
- if (Component.components[name]) {
- return Component.components[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(name, component)');
+ return 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');
- 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);
export default Component;
diff --git a/src/js/control-bar/control-bar.js b/src/js/control-bar/control-bar.js
index 0ac3ba830b..80f41f1ff0 100644
--- a/src/js/control-bar/control-bar.js
+++ b/src/js/control-bar/control-bar.js
@@ -1,15 +1,22 @@
-import Component from '../component';
-import * as Lib from '../lib';
+import Component from '../component.js';
+import * as Lib from '../lib.js';
-import PlayToggle from './play-toggle';
-import CurrentTimeDisplay from './time-display';
-import LiveDisplay from './live-display';
-import ProgressControl from './progress-control';
-import FullscreenToggle from './fullscreen-toggle';
-import VolumeControl from './volume-control';
-import VolumeMenuButton from './volume-menu-button';
-import MuteToggle from './mute-toggle';
-import PlaybackRateMenuButton from './playback-rate-menu-button';
+// Required children
+import PlayToggle from './play-toggle.js';
+import CurrentTimeDisplay from './current-time-display.js';
+import DurationDisplay from './duration-display.js';
+import TimeDivider from './time-divider.js';
+import RemainingTimeDisplay from './remaining-time-display.js';
+import LiveDisplay from './live-display.js';
+import ProgressControl from './progress-control/progress-control.js';
+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
@@ -19,9 +26,13 @@ import PlaybackRateMenuButton from './playback-rate-menu-button';
* @constructor
* @extends vjs.Component
*/
-var ControlBar = Component.extend();
-
-Component.registerComponent('ControlBar', ControlBar);
+class ControlBar extends Component {
+ createEl() {
+ return Lib.createEl('div', {
+ className: 'vjs-control-bar'
+ });
+ }
+}
ControlBar.prototype.options_ = {
loadEvent: 'play',
@@ -44,8 +55,5 @@ ControlBar.prototype.options_ = {
}
};
-ControlBar.prototype.createEl = function(){
- return Lib.createEl('div', {
- className: 'vjs-control-bar'
- });
-};
+Component.registerComponent('ControlBar', ControlBar);
+export default ControlBar;
\ No newline at end of file
diff --git a/src/js/control-bar/current-time-display.js b/src/js/control-bar/current-time-display.js
new file mode 100644
index 0000000000..72786f31ac
--- /dev/null
+++ b/src/js/control-bar/current-time-display.js
@@ -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: '
Current Time ' + '0:00', // label the current time for screen reader users
+ 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
+ });
+
+ el.appendChild(this.contentEl_);
+ return el;
+ }
+
+ 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 = '
' + this.localize('Current Time') + ' ' + Lib.formatTime(time, this.player_.duration());
+ }
+
+}
+
+Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
+export default CurrentTimeDisplay;
diff --git a/src/js/control-bar/duration-display.js b/src/js/control-bar/duration-display.js
new file mode 100644
index 0000000000..ade6b7b2c4
--- /dev/null
+++ b/src/js/control-bar/duration-display.js
@@ -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: '
' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users
+ 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
+ });
+
+ el.appendChild(this.contentEl_);
+ return el;
+ }
+
+ updateContent() {
+ let duration = this.player_.duration();
+ if (duration) {
+ this.contentEl_.innerHTML = '
' + this.localize('Duration Time') + ' ' + Lib.formatTime(duration); // label the duration time for screen reader users
+ }
+ }
+
+}
+
+Component.registerComponent('DurationDisplay', DurationDisplay);
+export default DurationDisplay;
diff --git a/src/js/control-bar/fullscreen-toggle.js b/src/js/control-bar/fullscreen-toggle.js
index 30c0b39435..6a63276fa8 100644
--- a/src/js/control-bar/fullscreen-toggle.js
+++ b/src/js/control-bar/fullscreen-toggle.js
@@ -1,4 +1,3 @@
-import Component from '../component';
import Button from '../button';
/**
@@ -8,33 +7,25 @@ import Button from '../button';
* @class
* @extends vjs.Button
*/
-var FullscreenToggle = Button.extend({
- /**
- * @constructor
- * @memberof vjs.FullscreenToggle
- * @instance
- */
- init: function(player, options){
- Button.call(this, player, options);
- }
-});
+class FullscreenToggle extends Button {
-Component.registerComponent('FullscreenToggle', FullscreenToggle);
+ buildCSSClass() {
+ return 'vjs-fullscreen-control ' + super.buildCSSClass();
+ }
-FullscreenToggle.prototype.buttonText = 'Fullscreen';
+ onClick() {
+ if (!this.player_.isFullscreen()) {
+ this.player_.requestFullscreen();
+ this.controlText_.innerHTML = this.localize('Non-Fullscreen');
+ } else {
+ this.player_.exitFullscreen();
+ this.controlText_.innerHTML = this.localize('Fullscreen');
+ }
+ }
-FullscreenToggle.prototype.buildCSSClass = function(){
- return 'vjs-fullscreen-control ' + Button.prototype.buildCSSClass.call(this);
-};
+}
-FullscreenToggle.prototype.onClick = function(){
- if (!this.player_.isFullscreen()) {
- this.player_.requestFullscreen();
- this.controlText_.innerHTML = this.localize('Non-Fullscreen');
- } else {
- this.player_.exitFullscreen();
- this.controlText_.innerHTML = this.localize('Fullscreen');
- }
-};
+FullscreenToggle.prototype.buttonText = 'Fullscreen';
+Button.registerComponent('FullscreenToggle', FullscreenToggle);
export default FullscreenToggle;
diff --git a/src/js/control-bar/live-display.js b/src/js/control-bar/live-display.js
index 723ab522a8..46a85ea624 100644
--- a/src/js/control-bar/live-display.js
+++ b/src/js/control-bar/live-display.js
@@ -8,28 +8,25 @@ import * as Lib from '../lib';
* @param {Object=} options
* @constructor
*/
-var LiveDisplay = Component.extend({
- init: function(player, options){
- Component.call(this, player, options);
- }
-});
+class LiveDisplay extends Component {
-Component.registerComponent('LiveDisplay', LiveDisplay);
+ createEl() {
+ var el = super.createEl('div', {
+ className: 'vjs-live-controls vjs-control'
+ });
-LiveDisplay.prototype.createEl = function(){
- var el = Component.prototype.createEl.call(this, 'div', {
- className: 'vjs-live-controls vjs-control'
- });
+ this.contentEl_ = Lib.createEl('div', {
+ className: 'vjs-live-display',
+ innerHTML: '
' + this.localize('Stream Type') + '' + this.localize('LIVE'),
+ 'aria-live': 'off'
+ });
- this.contentEl_ = Lib.createEl('div', {
- className: 'vjs-live-display',
- innerHTML: '
' + this.localize('Stream Type') + '' + this.localize('LIVE'),
- 'aria-live': 'off'
- });
+ el.appendChild(this.contentEl_);
- el.appendChild(this.contentEl_);
+ return el;
+ }
- return el;
-};
+}
+Component.registerComponent('LiveDisplay', LiveDisplay);
export default LiveDisplay;
diff --git a/src/js/control-bar/mute-toggle.js b/src/js/control-bar/mute-toggle.js
index 0ba52fae37..b49dbb91f0 100644
--- a/src/js/control-bar/mute-toggle.js
+++ b/src/js/control-bar/mute-toggle.js
@@ -9,10 +9,10 @@ import * as Lib from '../lib';
* @param {Object=} options
* @constructor
*/
-var MuteToggle = Button.extend({
- /** @constructor */
- init: function(player, options){
- Button.call(this, player, options);
+class MuteToggle extends Button {
+
+ constructor(player, options) {
+ super(player, options);
this.on(player, 'volumechange', this.update);
@@ -29,50 +29,47 @@ var MuteToggle = Button.extend({
}
});
}
-});
-MuteToggle.prototype.createEl = function(){
- return Button.prototype.createEl.call(this, 'div', {
- className: 'vjs-mute-control vjs-control',
- innerHTML: '
' + this.localize('Mute') + '
'
- });
-};
+ createEl() {
+ return super.createEl('div', {
+ className: 'vjs-mute-control vjs-control',
+ innerHTML: '
' + this.localize('Mute') + '
'
+ });
+ }
-MuteToggle.prototype.onClick = function(){
- this.player_.muted( this.player_.muted() ? false : true );
-};
+ onClick() {
+ this.player_.muted( this.player_.muted() ? false : true );
+ }
-MuteToggle.prototype.update = function(){
- var vol = this.player_.volume(),
- level = 3;
+ update() {
+ var vol = this.player_.volume(),
+ level = 3;
- if (vol === 0 || this.player_.muted()) {
- level = 0;
- } else if (vol < 0.33) {
- level = 1;
- } else if (vol < 0.67) {
- level = 2;
- }
+ if (vol === 0 || this.player_.muted()) {
+ level = 0;
+ } else if (vol < 0.33) {
+ level = 1;
+ } else if (vol < 0.67) {
+ level = 2;
+ }
- // Don't rewrite the button text if the actual text doesn't change.
- // This causes unnecessary and confusing information for screen reader users.
- // This check is needed because this function gets called every time the volume level is changed.
- if(this.player_.muted()){
- if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){
- this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute"
- }
- } else {
- if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){
- this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute"
- }
- }
+ // Don't rewrite the button text if the actual text doesn't change.
+ // This causes unnecessary and confusing information for screen reader users.
+ // This check is needed because this function gets called every time the volume level is changed.
+ let toMute = this.player_.muted() ? 'Unmute' : 'Mute';
+ let localizedMute = this.localize(toMute);
+ if (this.el_.children[0].children[0].innerHTML !== localizedMute) {
+ this.el_.children[0].children[0].innerHTML = localizedMute;
+ }
- /* TODO improve muted icon classes */
- for (var i = 0; i < 4; i++) {
- Lib.removeClass(this.el_, 'vjs-vol-'+i);
+ /* TODO improve muted icon classes */
+ for (var i = 0; i < 4; 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);
export default MuteToggle;
diff --git a/src/js/control-bar/play-toggle.js b/src/js/control-bar/play-toggle.js
index b67036a98e..796c123918 100644
--- a/src/js/control-bar/play-toggle.js
+++ b/src/js/control-bar/play-toggle.js
@@ -1,5 +1,4 @@
import Button from '../button';
-import Component from '../component';
import * as Lib from '../lib';
/**
@@ -9,45 +8,45 @@ import * as Lib from '../lib';
* @class
* @constructor
*/
-var PlayToggle = Button.extend({
- /** @constructor */
- init: function(player, options){
- Button.call(this, player, options);
+class PlayToggle extends Button {
+
+ constructor(player, options){
+ super(player, options);
this.on(player, 'play', this.onPlay);
this.on(player, 'pause', this.onPause);
}
-});
-
-Component.registerComponent('PlayToggle', PlayToggle);
-PlayToggle.prototype.buttonText = 'Play';
-
-PlayToggle.prototype.buildCSSClass = function(){
- return 'vjs-play-control ' + Button.prototype.buildCSSClass.call(this);
-};
+ buildCSSClass() {
+ return 'vjs-play-control ' + super.buildCSSClass();
+ }
-// OnClick - Toggle between play and pause
-PlayToggle.prototype.onClick = function(){
- if (this.player_.paused()) {
- this.player_.play();
- } else {
- this.player_.pause();
+ // OnClick - Toggle between play and pause
+ onClick() {
+ if (this.player_.paused()) {
+ this.player_.play();
+ } else {
+ this.player_.pause();
+ }
}
-};
// OnPlay - Add the vjs-playing class to the element so it can change appearance
-PlayToggle.prototype.onPlay = function(){
- this.removeClass('vjs-paused');
- this.addClass('vjs-playing');
- this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause"
-};
+ onPlay() {
+ this.removeClass('vjs-paused');
+ this.addClass('vjs-playing');
+ this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause"
+ }
// OnPause - Add the vjs-paused class to the element so it can change appearance
-PlayToggle.prototype.onPause = function(){
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play"
-};
+ onPause() {
+ this.removeClass('vjs-playing');
+ this.addClass('vjs-paused');
+ 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;
diff --git a/src/js/control-bar/playback-rate-menu-button.js b/src/js/control-bar/playback-rate-menu-button.js
deleted file mode 100644
index 2ca3dce12f..0000000000
--- a/src/js/control-bar/playback-rate-menu-button.js
+++ /dev/null
@@ -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
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 };
diff --git a/src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js b/src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js
new file mode 100644
index 0000000000..3802e91494
--- /dev/null
+++ b/src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js
@@ -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 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;
diff --git a/src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js b/src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js
new file mode 100644
index 0000000000..572203f02f
--- /dev/null
+++ b/src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/progress-control.js b/src/js/control-bar/progress-control.js
deleted file mode 100644
index f2afa48145..0000000000
--- a/src/js/control-bar/progress-control.js
+++ /dev/null
@@ -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: '' + this.localize('Loaded') + ': 0%'
- });
-};
-
-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: '' + this.localize('Progress') + ': 0%'
- });
-};
-
-/**
- * The Seek Handle shows the current position of the playhead during playback,
- * and can be dragged to adjust the playhead.
- *
- * @param {vjs.Player|Object} player
- * @param {Object=} options
- * @constructor
- */
-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 = '' + Lib.formatTime(time, this.player_.duration()) + '';
-};
-
-export default ProgressControl;
-export { SeekBar, LoadProgressBar, PlayProgressBar, SeekHandle };
diff --git a/src/js/control-bar/progress-control/load-progress-bar.js b/src/js/control-bar/progress-control/load-progress-bar.js
new file mode 100644
index 0000000000..589c2003e5
--- /dev/null
+++ b/src/js/control-bar/progress-control/load-progress-bar.js
@@ -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: '' + this.localize('Loaded') + ': 0%'
+ });
+ }
+
+ 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;
\ No newline at end of file
diff --git a/src/js/control-bar/progress-control/play-progress-bar.js b/src/js/control-bar/progress-control/play-progress-bar.js
new file mode 100644
index 0000000000..f78c7a3859
--- /dev/null
+++ b/src/js/control-bar/progress-control/play-progress-bar.js
@@ -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: '' + this.localize('Progress') + ': 0%'
+ });
+ }
+
+}
+
+Component.registerComponent('PlayProgressBar', PlayProgressBar);
+export default PlayProgressBar;
\ No newline at end of file
diff --git a/src/js/control-bar/progress-control/progress-control.js b/src/js/control-bar/progress-control/progress-control.js
new file mode 100644
index 0000000000..7afe41e234
--- /dev/null
+++ b/src/js/control-bar/progress-control/progress-control.js
@@ -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;
diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js
new file mode 100644
index 0000000000..6fcfed8732
--- /dev/null
+++ b/src/js/control-bar/progress-control/seek-bar.js
@@ -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;
diff --git a/src/js/control-bar/progress-control/seek-handle.js b/src/js/control-bar/progress-control/seek-handle.js
new file mode 100644
index 0000000000..ff7c4ae3e6
--- /dev/null
+++ b/src/js/control-bar/progress-control/seek-handle.js
@@ -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 = '' + Lib.formatTime(time, this.player_.duration()) + '';
+ }
+
+}
+
+/**
+ * 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;
\ No newline at end of file
diff --git a/src/js/control-bar/remaining-time-display.js b/src/js/control-bar/remaining-time-display.js
new file mode 100644
index 0000000000..d3842aefbf
--- /dev/null
+++ b/src/js/control-bar/remaining-time-display.js
@@ -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: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users
+ 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
+ });
+
+ el.appendChild(this.contentEl_);
+ return el;
+ }
+
+ updateContent() {
+ if (this.player_.duration()) {
+ this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ 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;
diff --git a/src/js/control-bar/text-track-controls/caption-settings-menu-item.js b/src/js/control-bar/text-track-controls/caption-settings-menu-item.js
new file mode 100644
index 0000000000..037c6c4cbf
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/caption-settings-menu-item.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/text-track-controls/captions-button.js b/src/js/control-bar/text-track-controls/captions-button.js
new file mode 100644
index 0000000000..d4ee86820e
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/captions-button.js
@@ -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;
diff --git a/src/js/control-bar/text-track-controls/chapters-button.js b/src/js/control-bar/text-track-controls/chapters-button.js
new file mode 100644
index 0000000000..b92995e21d
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/chapters-button.js
@@ -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;
diff --git a/src/js/control-bar/text-track-controls/chapters-track-menu-item.js b/src/js/control-bar/text-track-controls/chapters-track-menu-item.js
new file mode 100644
index 0000000000..a7a397b0d3
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/chapters-track-menu-item.js
@@ -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;
diff --git a/src/js/control-bar/text-track-controls/off-text-track-menu-item.js b/src/js/control-bar/text-track-controls/off-text-track-menu-item.js
new file mode 100644
index 0000000000..1b02bf77a2
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/off-text-track-menu-item.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/text-track-controls/subtitles-button.js b/src/js/control-bar/text-track-controls/subtitles-button.js
new file mode 100644
index 0000000000..59bcc701c6
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/subtitles-button.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/text-track-controls/text-track-button.js b/src/js/control-bar/text-track-controls/text-track-button.js
new file mode 100644
index 0000000000..d0d097ddef
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/text-track-button.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/text-track-controls/text-track-menu-item.js b/src/js/control-bar/text-track-controls/text-track-menu-item.js
new file mode 100644
index 0000000000..41a74e262c
--- /dev/null
+++ b/src/js/control-bar/text-track-controls/text-track-menu-item.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/time-display.js b/src/js/control-bar/time-display.js
deleted file mode 100644
index 297df4202a..0000000000
--- a/src/js/control-bar/time-display.js
+++ /dev/null
@@ -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: 'Current Time ' + '0:00', // label the current time for screen reader users
- 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
- });
-
- el.appendChild(this.contentEl_);
- return el;
-};
-
-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 = '' + this.localize('Current Time') + ' ' + 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: '' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users
- 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
- });
-
- el.appendChild(this.contentEl_);
- return el;
-};
-
-DurationDisplay.prototype.updateContent = function(){
- let duration = this.player_.duration();
- if (duration) {
- this.contentEl_.innerHTML = '' + this.localize('Duration Time') + ' ' + 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: '/
'
- });
-};
-
-/**
- * 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: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users
- 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes
- });
-
- el.appendChild(this.contentEl_);
- return el;
-};
-
-RemainingTimeDisplay.prototype.updateContent = function(){
- if (this.player_.duration()) {
- this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ 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 };
diff --git a/src/js/control-bar/time-divider.js b/src/js/control-bar/time-divider.js
new file mode 100644
index 0000000000..e3c4996f4a
--- /dev/null
+++ b/src/js/control-bar/time-divider.js
@@ -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: '/
'
+ });
+ }
+
+}
+
+Component.registerComponent('TimeDivider', TimeDivider);
+export default TimeDivider;
\ No newline at end of file
diff --git a/src/js/control-bar/volume-control.js b/src/js/control-bar/volume-control.js
deleted file mode 100644
index 7950948f31..0000000000
--- a/src/js/control-bar/volume-control.js
+++ /dev/null
@@ -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: ''
- });
-};
-
-/**
- * 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 };
diff --git a/src/js/control-bar/volume-control/volume-bar.js b/src/js/control-bar/volume-control/volume-bar.js
new file mode 100644
index 0000000000..b12d154727
--- /dev/null
+++ b/src/js/control-bar/volume-control/volume-bar.js
@@ -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;
diff --git a/src/js/control-bar/volume-control/volume-control.js b/src/js/control-bar/volume-control/volume-control.js
new file mode 100644
index 0000000000..bfcd2cb39f
--- /dev/null
+++ b/src/js/control-bar/volume-control/volume-control.js
@@ -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;
diff --git a/src/js/control-bar/volume-control/volume-handle.js b/src/js/control-bar/volume-control/volume-handle.js
new file mode 100644
index 0000000000..9382574fbd
--- /dev/null
+++ b/src/js/control-bar/volume-control/volume-handle.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/control-bar/volume-control/volume-level.js b/src/js/control-bar/volume-control/volume-level.js
new file mode 100644
index 0000000000..de25e95d32
--- /dev/null
+++ b/src/js/control-bar/volume-control/volume-level.js
@@ -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: ''
+ });
+ }
+
+}
+
+Component.registerComponent('VolumeLevel', VolumeLevel);
+export default VolumeLevel;
\ No newline at end of file
diff --git a/src/js/control-bar/volume-menu-button.js b/src/js/control-bar/volume-menu-button.js
index 34f078d8cf..3d67b2dcb7 100644
--- a/src/js/control-bar/volume-menu-button.js
+++ b/src/js/control-bar/volume-menu-button.js
@@ -1,18 +1,18 @@
-import Button from '../button';
-import Component from '../component';
-import Menu, { MenuButton } from '../menu';
-import MuteToggle from './mute-toggle';
-import * as Lib from '../lib';
-import { VolumeBar } from './volume-control';
+import Button from '../button.js';
+import Menu from '../menu/menu.js';
+import MenuButton from '../menu/menu-button.js';
+import MuteToggle from './mute-toggle.js';
+import * as Lib from '../lib.js';
+import VolumeBar from './volume-control/volume-bar.js';
/**
* Menu button with a popup for showing the volume slider.
* @constructor
*/
-let VolumeMenuButton = MenuButton.extend({
- /** @constructor */
- init: function(player, options){
- MenuButton.call(this, player, options);
+class VolumeMenuButton extends MenuButton {
+
+ constructor(player, options){
+ super(player, options);
// Same listeners as MuteToggle
this.on(player, 'volumechange', this.volumeUpdate);
@@ -30,36 +30,37 @@ let VolumeMenuButton = MenuButton.extend({
});
this.addClass('vjs-menu-button');
}
-});
-VolumeMenuButton.prototype.createMenu = function(){
- let menu = new Menu(this.player_, {
- contentElType: 'div'
- });
- let vc = new VolumeBar(this.player_, this.options_['volumeBar']);
- vc.on('focus', function() {
- menu.lockShowing();
- });
- vc.on('blur', function() {
- menu.unlockShowing();
- });
- menu.addChild(vc);
- return menu;
-};
+ createMenu() {
+ let menu = new Menu(this.player_, {
+ contentElType: 'div'
+ });
+ let vc = new VolumeBar(this.player_, this.options_['volumeBar']);
+ vc.on('focus', function() {
+ menu.lockShowing();
+ });
+ vc.on('blur', function() {
+ menu.unlockShowing();
+ });
+ menu.addChild(vc);
+ return menu;
+ }
-VolumeMenuButton.prototype.onClick = function(){
- MuteToggle.prototype.onClick.call(this);
- MenuButton.prototype.onClick.call(this);
-};
+ onClick() {
+ MuteToggle.prototype.onClick.call(this);
+ super.onClick();
+ }
+
+ createEl() {
+ return super.createEl('div', {
+ className: 'vjs-volume-menu-button vjs-menu-button vjs-control',
+ innerHTML: '' + this.localize('Mute') + '
'
+ });
+ }
-VolumeMenuButton.prototype.createEl = function(){
- return Button.prototype.createEl.call(this, 'div', {
- className: 'vjs-volume-menu-button vjs-menu-button vjs-control',
- innerHTML: '' + this.localize('Mute') + '
'
- });
-};
+}
VolumeMenuButton.prototype.volumeUpdate = MuteToggle.prototype.update;
-Component.registerComponent('VolumeMenuButton', VolumeMenuButton);
+Button.registerComponent('VolumeMenuButton', VolumeMenuButton);
export default VolumeMenuButton;
diff --git a/src/js/error-display.js b/src/js/error-display.js
index dc954f8908..597fcfdbfa 100644
--- a/src/js/error-display.js
+++ b/src/js/error-display.js
@@ -7,32 +7,32 @@ import * as Lib from './lib';
* @param {Object=} options
* @constructor
*/
-let ErrorDisplay = Component.extend({
- init: function(player, options){
- Component.call(this, player, options);
+class ErrorDisplay extends Component {
+
+ constructor(player, options) {
+ super(player, options);
this.update();
this.on(player, 'error', this.update);
}
-});
-
-Component.registerComponent('ErrorDisplay', ErrorDisplay);
-ErrorDisplay.prototype.createEl = function(){
- var el = Component.prototype.createEl.call(this, 'div', {
- className: 'vjs-error-display'
- });
+ createEl() {
+ var el = super.createEl('div', {
+ className: 'vjs-error-display'
+ });
- this.contentEl_ = Lib.createEl('div');
- el.appendChild(this.contentEl_);
+ this.contentEl_ = Lib.createEl('div');
+ el.appendChild(this.contentEl_);
- return el;
-};
+ return el;
+ }
-ErrorDisplay.prototype.update = function(){
- if (this.player().error()) {
- this.contentEl_.innerHTML = this.localize(this.player().error().message);
+ update() {
+ if (this.player().error()) {
+ this.contentEl_.innerHTML = this.localize(this.player().error().message);
+ }
}
-};
+}
+Component.registerComponent('ErrorDisplay', ErrorDisplay);
export default ErrorDisplay;
diff --git a/src/js/loading-spinner.js b/src/js/loading-spinner.js
index f3812f19a0..3f70b084f0 100644
--- a/src/js/loading-spinner.js
+++ b/src/js/loading-spinner.js
@@ -9,39 +9,13 @@ import Component from './component';
* @class
* @constructor
*/
-let LoadingSpinner = Component.extend({
- /** @constructor */
- init: function(player, options){
- 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));
+class LoadingSpinner extends Component {
+ createEl() {
+ return super.createEl('div', {
+ className: 'vjs-loading-spinner'
+ });
}
-});
+}
Component.registerComponent('LoadingSpinner', LoadingSpinner);
-
-LoadingSpinner.prototype.createEl = function(){
- return Component.prototype.createEl.call(this, 'div', {
- className: 'vjs-loading-spinner'
- });
-};
-
export default LoadingSpinner;
diff --git a/src/js/media/html5.js b/src/js/media/html5.js
deleted file mode 100644
index 5664758f0e..0000000000
--- a/src/js/media/html5.js
+++ /dev/null
@@ -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= 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;
diff --git a/src/js/media/media.js b/src/js/media/media.js
deleted file mode 100644
index fc4e03d367..0000000000
--- a/src/js/media/media.js
+++ /dev/null
@@ -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;
diff --git a/src/js/menu.js b/src/js/menu.js
deleted file mode 100644
index 68304ee8be..0000000000
--- a/src/js/menu.js
+++ /dev/null
@@ -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. ``
- *
- * @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 };
diff --git a/src/js/menu/menu-button.js b/src/js/menu/menu-button.js
new file mode 100644
index 0000000000..5442c84f3e
--- /dev/null
+++ b/src/js/menu/menu-button.js
@@ -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;
\ No newline at end of file
diff --git a/src/js/menu/menu-item.js b/src/js/menu/menu-item.js
new file mode 100644
index 0000000000..c583755253
--- /dev/null
+++ b/src/js/menu/menu-item.js
@@ -0,0 +1,51 @@
+import Button from '../button.js';
+import * as Lib from '../lib.js';
+
+/**
+ * The component for a menu item. ``
+ *
+ * @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;
\ No newline at end of file
diff --git a/src/js/menu/menu.js b/src/js/menu/menu.js
new file mode 100644
index 0000000000..041789db66
--- /dev/null
+++ b/src/js/menu/menu.js
@@ -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;
diff --git a/src/js/player.js b/src/js/player.js
index 54c14aacc0..8aefa7cabc 100644
--- a/src/js/player.js
+++ b/src/js/player.js
@@ -9,16 +9,16 @@ import window from 'global/window';
import document from 'global/document';
// Include required child components
-import MediaLoader from './media/loader.js';
-import Poster from './poster.js';
-import TextTrackDisplay from './tracks/text-track-controls.js';
+import MediaLoader from './tech/loader.js';
+import Poster from './poster-image.js';
+import TextTrackDisplay from './tracks/text-track-display.js';
import LoadingSpinner from './loading-spinner.js';
import BigPlayButton from './big-play-button.js';
import controlBar from './control-bar/control-bar.js';
import ErrorDisplay from './error-display.js';
import TextTrackSettings from './tracks/text-track-settings.js';
// Require html5 for disposing the original video tag
-import Html5 from './media/html5.js';
+import Html5 from './tech/html5.js';
/**
* An instance of the `vjs.Player` class is created when any of the Video.js setup methods are used to initialize a video.
@@ -40,7 +40,7 @@ import Html5 from './media/html5.js';
* @class
* @extends vjs.Component
*/
-let Player = Component.extend({
+class Player extends Component {
/**
* player's constructor function
@@ -51,21 +51,35 @@ let Player = Component.extend({
* @param {Object=} options Player options
* @param {Function=} ready Ready callback function
*/
- init: function(tag, options, ready){
- this.tag = tag; // Store the original tag used to set options
-
+ constructor(tag, options, ready){
// Make sure tag ID exists
tag.id = tag.id || 'vjs_video_' + Lib.guid++;
- // Store the tag attributes used to restore html5 element
- this.tagAttributes = tag && Lib.getElementAttributes(tag);
-
// Set Options
// The options argument overrides options set in the video tag
// which overrides globally set options.
// This latter part coincides with the load order
// (tag must exist before Player)
- options = Lib.obj.merge(this.getTagSettings(tag), options);
+ options = Lib.obj.merge(Player.getTagSettings(tag), options);
+
+ // Delay the initialization of children because we need to set up
+ // player properties first, and can't use `this` before `super()`
+ options.initChildren = false;
+
+ // Same with creating the element
+ options.createEl = false;
+
+ // we don't want the player to report touch activity on itself
+ // see enableTouchActivity in Component
+ options.reportTouchActivity = false;
+
+ // Run base component initializing with new options
+ super(null, options, ready);
+
+ this.tag = tag; // Store the original tag used to set options
+
+ // Store the tag attributes used to restore html5 element
+ this.tagAttributes = tag && Lib.getElementAttributes(tag);
// Update Current Language
this.language_ = options['language'] || Options['language'];
@@ -86,17 +100,11 @@ let Player = Component.extend({
// May be turned back on by HTML5 tech if nativeControlsForTouch is true
tag.controls = false;
- // we don't want the player to report touch activity on itself
- // see enableTouchActivity in Component
- options.reportTouchActivity = false;
+ this.el_ = this.createEl();
+ this.initChildren();
// Set isAudio based on whether or not an audio tag was used
- this.isAudio(this.tag.nodeName.toLowerCase() === 'audio');
-
- // Run base component initializing with new options.
- // Builds the element through createEl()
- // Inits and embeds any child components in opts
- Component.call(this, this, options, ready);
+ this.isAudio(tag.nodeName.toLowerCase() === 'audio');
// Update controls className. Can't do this when the controls are initially
// set because the element doesn't exist yet.
@@ -125,1670 +133,1594 @@ let Player = Component.extend({
}, this);
}
+ // When the player is first initialized, trigger activity so components
+ // like the control bar show themselves if needed
+ this.userActive_ = true;
+ this.reportUserActivity();
this.listenForUserActivity();
}
-});
-Component.registerComponent('Player', Player);
+ /**
+ * Destroys the video player and does any necessary cleanup
+ *
+ * myPlayer.dispose();
+ *
+ * This is especially helpful if you are dynamically adding and removing videos
+ * to/from the DOM.
+ */
+ dispose() {
+ this.trigger('dispose');
+ // prevent dispose from being called twice
+ this.off('dispose');
-/**
- * Global player list
- * @type {Object}
- */
-Player.players = {};
+ // Kill reference to this player
+ Player.players[this.id_] = null;
+ if (this.tag && this.tag['player']) { this.tag['player'] = null; }
+ if (this.el_ && this.el_['player']) { this.el_['player'] = null; }
-/**
- * The player's stored language code
- *
- * @type {String}
- * @private
- */
-Player.prototype.language_;
+ if (this.tech) { this.tech.dispose(); }
-/**
- * The player's language code
- * @param {String} languageCode The locale string
- * @return {String} The locale string when getting
- * @return {vjs.Player} self, when setting
- */
-Player.prototype.language = function (languageCode) {
- if (languageCode === undefined) {
- return this.language_;
+ super.dispose();
}
- this.language_ = languageCode;
- return this;
-};
-
-/**
- * The player's stored language dictionary
- *
- * @type {Object}
- * @private
- */
-Player.prototype.languages_;
-
-Player.prototype.languages = function(){
- return this.languages_;
-};
-
-/**
- * Player instance options, surfaced using vjs.options
- * vjs.options = vjs.Player.prototype.options_
- * Make changes in vjs.options, not here.
- * All options should use string keys so they avoid
- * renaming by closure compiler
- * @type {Object}
- * @private
- */
-Player.prototype.options_ = Options;
-
-/**
- * Destroys the video player and does any necessary cleanup
- *
- * myPlayer.dispose();
- *
- * This is especially helpful if you are dynamically adding and removing videos
- * to/from the DOM.
- */
-Player.prototype.dispose = function(){
- this.trigger('dispose');
- // prevent dispose from being called twice
- this.off('dispose');
-
- // Kill reference to this player
- Player.players[this.id_] = null;
- if (this.tag && this.tag['player']) { this.tag['player'] = null; }
- if (this.el_ && this.el_['player']) { this.el_['player'] = null; }
-
- if (this.tech) { this.tech.dispose(); }
-
- // Component dispose
- Component.prototype.dispose.call(this);
-};
-
-Player.prototype.getTagSettings = function(tag){
- let baseOptions = {
- 'sources': [],
- 'tracks': []
- };
-
- const tagOptions = Lib.getElementAttributes(tag);
- const dataSetup = tagOptions['data-setup'];
-
- // Check if data-setup attr exists.
- if (dataSetup !== null){
- // Parse options JSON
- // If empty string, make it a parsable json object.
- Lib.obj.merge(tagOptions, JSON.parse(dataSetup || '{}'));
- }
-
- Lib.obj.merge(baseOptions, tagOptions);
-
- // Get tag children settings
- if (tag.hasChildNodes()) {
- const children = tag.childNodes;
-
- for (let i=0, j=children.length; i 0) {
- techOptions['startTime'] = this.cache_.currentTime;
+ // Pause and remove current playback technology
+ if (this.tech) {
+ this.unloadTech();
}
- this.cache_.src = source.src;
- }
+ // get rid of the HTML5 video tag as soon as we are using another tech
+ if (techName !== 'Html5' && this.tag) {
+ Component.getComponent('Html5').disposeMediaElement(this.tag);
+ this.tag = null;
+ }
- // Initialize tech instance
- let techComponent = Component.getComponent(techName);
- this.tech = new techComponent(this, techOptions);
+ this.techName = techName;
- this.tech.ready(techReady);
-};
+ // Turn off API access because we're loading a new tech that might load asynchronously
+ this.isReady_ = false;
-Player.prototype.unloadTech = function(){
- this.isReady_ = false;
+ var techReady = function(){
+ this.player_.triggerReady();
+ };
- this.tech.dispose();
+ // Grab tech-specific options from player options and add source and parent element to use.
+ var techOptions = Lib.obj.merge({ 'source': source, 'parentEl': this.el_ }, this.options_[techName.toLowerCase()]);
- this.tech = false;
-};
+ if (source) {
+ this.currentType_ = source.type;
+ if (source.src == this.cache_.src && this.cache_.currentTime > 0) {
+ techOptions['startTime'] = this.cache_.currentTime;
+ }
-// There's many issues around changing the size of a Flash (or other plugin) object.
-// First is a plugin reload issue in Firefox that has been around for 11 years: https://bugzilla.mozilla.org/show_bug.cgi?id=90268
-// Then with the new fullscreen API, Mozilla and webkit browsers will reload the flash object after going to fullscreen.
-// To get around this, we're unloading the tech, caching source and currentTime values, and reloading the tech once the plugin is resized.
-// reloadTech: function(betweenFn){
-// vjs.log('unloadingTech')
-// this.unloadTech();
-// vjs.log('unloadedTech')
-// if (betweenFn) { betweenFn.call(); }
-// vjs.log('LoadingTech')
-// this.loadTech(this.techName, { src: this.cache_.src })
-// vjs.log('loadedTech')
-// },
+ this.cache_.src = source.src;
+ }
-// /* Player event handlers (how the player reacts to certain events)
-// ================================================================================ */
+ // Initialize tech instance
+ let techComponent = Component.getComponent(techName);
+ this.tech = new techComponent(this, techOptions);
-/**
- * Fired when the user agent begins looking for media data
- * @event loadstart
- */
-Player.prototype.onLoadStart = function() {
- // TODO: Update to use `emptied` event instead. See #1277.
-
- this.removeClass('vjs-ended');
-
- // reset the error state
- this.error(null);
-
- // If it's already playing we want to trigger a firstplay event now.
- // The firstplay event relies on both the play and loadstart events
- // which can happen in any order for a new source
- if (!this.paused()) {
- this.trigger('firstplay');
- } else {
- // reset the hasStarted state
- this.hasStarted(false);
- }
-};
-
-Player.prototype.hasStarted_ = false;
-
-Player.prototype.hasStarted = function(hasStarted){
- if (hasStarted !== undefined) {
- // only update if this is a new value
- if (this.hasStarted_ !== hasStarted) {
- this.hasStarted_ = hasStarted;
- if (hasStarted) {
- this.addClass('vjs-has-started');
- // trigger the firstplay event if this newly has played
- this.trigger('firstplay');
- } else {
- this.removeClass('vjs-has-started');
- }
- }
- return this;
+ this.tech.ready(techReady);
}
- return this.hasStarted_;
-};
-/**
- * Fired when the player has initial duration and dimension information
- * @event loadedmetadata
- */
-Player.prototype.onLoadedMetaData;
-
-/**
- * Fired when the player has downloaded data at the current playback position
- * @event loadeddata
- */
-Player.prototype.onLoadedData;
+ unloadTech() {
+ this.isReady_ = false;
-/**
- * Fired when the player has finished downloading the source data
- * @event loadedalldata
- */
-Player.prototype.onLoadedAllData;
+ this.tech.dispose();
-/**
- * Fired when the user is active, e.g. moves the mouse over the player
- * @event useractive
- */
-Player.prototype.onUserActive;
+ this.tech = false;
+ }
-/**
- * Fired when the user is inactive, e.g. a short delay after the last mouse move or control interaction
- * @event userinactive
- */
-Player.prototype.onUserInactive;
+ /**
+ * Fired when the user agent begins looking for media data
+ * @event loadstart
+ */
+ onLoadStart() {
+ // TODO: Update to use `emptied` event instead. See #1277.
-/**
- * Fired whenever the media begins or resumes playback
- * @event play
- */
-Player.prototype.onPlay = function(){
- this.removeClass('vjs-ended');
- this.removeClass('vjs-paused');
- this.addClass('vjs-playing');
+ this.removeClass('vjs-ended');
- // hide the poster when the user hits play
- // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play
- this.hasStarted(true);
-};
+ // reset the error state
+ this.error(null);
-/**
- * Fired whenever the media begins waiting
- * @event waiting
- */
-Player.prototype.onWaiting = function(){
- this.addClass('vjs-waiting');
-};
+ // If it's already playing we want to trigger a firstplay event now.
+ // The firstplay event relies on both the play and loadstart events
+ // which can happen in any order for a new source
+ if (!this.paused()) {
+ this.trigger('firstplay');
+ } else {
+ // reset the hasStarted state
+ this.hasStarted(false);
+ }
+ }
-/**
- * A handler for events that signal that waiting has ended
- * which is not consistent between browsers. See #1351
- * @private
- */
-Player.prototype.onWaitEnd = function(){
- this.removeClass('vjs-waiting');
-};
+ hasStarted(hasStarted) {
+ if (hasStarted !== undefined) {
+ // only update if this is a new value
+ if (this.hasStarted_ !== hasStarted) {
+ this.hasStarted_ = hasStarted;
+ if (hasStarted) {
+ this.addClass('vjs-has-started');
+ // trigger the firstplay event if this newly has played
+ this.trigger('firstplay');
+ } else {
+ this.removeClass('vjs-has-started');
+ }
+ }
+ return this;
+ }
+ return !!this.hasStarted_;
+ }
-/**
- * Fired whenever the player is jumping to a new time
- * @event seeking
- */
-Player.prototype.onSeeking = function(){
- this.addClass('vjs-seeking');
-};
+ /**
+ * Fired whenever the media begins or resumes playback
+ * @event play
+ */
+ onPlay() {
+ this.removeClass('vjs-ended');
+ this.removeClass('vjs-paused');
+ this.addClass('vjs-playing');
+
+ // hide the poster when the user hits play
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play
+ this.hasStarted(true);
+ }
-/**
- * Fired when the player has finished jumping to a new time
- * @event seeked
- */
-Player.prototype.onSeeked = function(){
- this.removeClass('vjs-seeking');
-};
+ /**
+ * Fired whenever the media begins waiting
+ * @event waiting
+ */
+ onWaiting() {
+ this.addClass('vjs-waiting');
+ }
-/**
- * Fired the first time a video is played
- *
- * Not part of the HLS spec, and we're not sure if this is the best
- * implementation yet, so use sparingly. If you don't have a reason to
- * prevent playback, use `myPlayer.one('play');` instead.
- *
- * @event firstplay
- */
-Player.prototype.onFirstPlay = function(){
- //If the first starttime attribute is specified
- //then we will start at the given offset in seconds
- if(this.options_['starttime']){
- this.currentTime(this.options_['starttime']);
+ /**
+ * A handler for events that signal that waiting has ended
+ * which is not consistent between browsers. See #1351
+ * @private
+ */
+ onWaitEnd() {
+ this.removeClass('vjs-waiting');
}
- this.addClass('vjs-has-started');
-};
+ /**
+ * Fired whenever the player is jumping to a new time
+ * @event seeking
+ */
+ onSeeking() {
+ this.addClass('vjs-seeking');
+ }
-/**
- * Fired whenever the media has been paused
- * @event pause
- */
-Player.prototype.onPause = function(){
- this.addClass('vjs-paused');
-};
+ /**
+ * Fired when the player has finished jumping to a new time
+ * @event seeked
+ */
+ onSeeked() {
+ this.removeClass('vjs-seeking');
+ }
-/**
- * Fired when the current playback position has changed
- *
- * During playback this is fired every 15-250 milliseconds, depending on the
- * playback technology in use.
- * @event timeupdate
- */
-Player.prototype.onTimeUpdate;
+ /**
+ * Fired the first time a video is played
+ *
+ * Not part of the HLS spec, and we're not sure if this is the best
+ * implementation yet, so use sparingly. If you don't have a reason to
+ * prevent playback, use `myPlayer.one('play');` instead.
+ *
+ * @event firstplay
+ */
+ onFirstPlay() {
+ //If the first starttime attribute is specified
+ //then we will start at the given offset in seconds
+ if(this.options_['starttime']){
+ this.currentTime(this.options_['starttime']);
+ }
-/**
- * Fired while the user agent is downloading media data
- * @event progress
- */
-Player.prototype.onProgress = function(){
- // Add custom event for when source is finished downloading.
- if (this.bufferedPercent() == 1) {
- this.trigger('loadedalldata');
+ this.addClass('vjs-has-started');
}
-};
-/**
- * Fired when the end of the media resource is reached (currentTime == duration)
- * @event ended
- */
-Player.prototype.onEnded = function(){
- this.addClass('vjs-ended');
- if (this.options_['loop']) {
- this.currentTime(0);
- this.play();
- } else if (!this.paused()) {
- this.pause();
+ /**
+ * Fired whenever the media has been paused
+ * @event pause
+ */
+ onPause() {
+ this.addClass('vjs-paused');
}
-};
-/**
- * Fired when the duration of the media resource is first known or changed
- * @event durationchange
- */
-Player.prototype.onDurationChange = function(){
- // Allows for caching value instead of asking player each time.
- // We need to get the techGet response and check for a value so we don't
- // accidentally cause the stack to blow up.
- var duration = this.techGet('duration');
- if (duration) {
- if (duration < 0) {
- duration = Infinity;
- }
- this.duration(duration);
- // Determine if the stream is live and propagate styles down to UI.
- if (duration === Infinity) {
- this.addClass('vjs-live');
- } else {
- this.removeClass('vjs-live');
+ /**
+ * Fired while the user agent is downloading media data
+ * @event progress
+ */
+ onProgress() {
+ // Add custom event for when source is finished downloading.
+ if (this.bufferedPercent() == 1) {
+ this.trigger('loadedalldata');
}
}
-};
-/**
- * Fired when the volume changes
- * @event volumechange
- */
-Player.prototype.onVolumeChange;
+ /**
+ * Fired when the end of the media resource is reached (currentTime == duration)
+ * @event ended
+ */
+ onEnded() {
+ this.addClass('vjs-ended');
+ if (this.options_['loop']) {
+ this.currentTime(0);
+ this.play();
+ } else if (!this.paused()) {
+ this.pause();
+ }
+ }
-/**
- * Fired when the player switches in or out of fullscreen mode
- * @event fullscreenchange
- */
-Player.prototype.onFullscreenChange = function() {
- if (this.isFullscreen()) {
- this.addClass('vjs-fullscreen');
- } else {
- this.removeClass('vjs-fullscreen');
+ /**
+ * Fired when the duration of the media resource is first known or changed
+ * @event durationchange
+ */
+ onDurationChange() {
+ // Allows for caching value instead of asking player each time.
+ // We need to get the techGet response and check for a value so we don't
+ // accidentally cause the stack to blow up.
+ var duration = this.techGet('duration');
+ if (duration) {
+ if (duration < 0) {
+ duration = Infinity;
+ }
+ this.duration(duration);
+ // Determine if the stream is live and propagate styles down to UI.
+ if (duration === Infinity) {
+ this.addClass('vjs-live');
+ } else {
+ this.removeClass('vjs-live');
+ }
+ }
}
-};
-/**
- * Fired when an error occurs
- * @event error
- */
-Player.prototype.onError;
+ /**
+ * Fired when the player switches in or out of fullscreen mode
+ * @event fullscreenchange
+ */
+ onFullscreenChange() {
+ if (this.isFullscreen()) {
+ this.addClass('vjs-fullscreen');
+ } else {
+ this.removeClass('vjs-fullscreen');
+ }
+ }
-// /* Player API
-// ================================================================================ */
+ /**
+ * Object for cached values.
+ */
+ getCache() {
+ return this.cache_;
+ }
-/**
- * Object for cached values.
- * @private
- */
-Player.prototype.cache_;
-
-Player.prototype.getCache = function(){
- return this.cache_;
-};
-
-// Pass values to the playback tech
-Player.prototype.techCall = function(method, arg){
- // If it's not ready yet, call method when it is
- if (this.tech && !this.tech.isReady_) {
- this.tech.ready(function(){
- this[method](arg);
- });
+ // Pass values to the playback tech
+ techCall(method, arg) {
+ // If it's not ready yet, call method when it is
+ if (this.tech && !this.tech.isReady_) {
+ this.tech.ready(function(){
+ this[method](arg);
+ });
- // Otherwise call method now
- } else {
- try {
- this.tech[method](arg);
- } catch(e) {
- Lib.log(e);
- throw e;
+ // Otherwise call method now
+ } else {
+ try {
+ this.tech[method](arg);
+ } catch(e) {
+ Lib.log(e);
+ throw e;
+ }
}
}
-};
-
-// Get calls can't wait for the tech, and sometimes don't need to.
-Player.prototype.techGet = function(method){
- if (this.tech && this.tech.isReady_) {
-
- // Flash likes to die and reload when you hide or reposition it.
- // In these cases the object methods go away and we get errors.
- // When that happens we'll catch the errors and inform tech that it's not ready any more.
- try {
- return this.tech[method]();
- } catch(e) {
- // When building additional tech libs, an expected method may not be defined yet
- if (this.tech[method] === undefined) {
- Lib.log('Video.js: ' + method + ' method not defined for '+this.techName+' playback technology.', e);
- } else {
- // When a method isn't available on the object it throws a TypeError
- if (e.name == 'TypeError') {
- Lib.log('Video.js: ' + method + ' unavailable on '+this.techName+' playback technology element.', e);
- this.tech.isReady_ = false;
+
+ // Get calls can't wait for the tech, and sometimes don't need to.
+ techGet(method) {
+ if (this.tech && this.tech.isReady_) {
+
+ // Flash likes to die and reload when you hide or reposition it.
+ // In these cases the object methods go away and we get errors.
+ // When that happens we'll catch the errors and inform tech that it's not ready any more.
+ try {
+ return this.tech[method]();
+ } catch(e) {
+ // When building additional tech libs, an expected method may not be defined yet
+ if (this.tech[method] === undefined) {
+ Lib.log('Video.js: ' + method + ' method not defined for '+this.techName+' playback technology.', e);
} else {
- Lib.log(e);
+ // When a method isn't available on the object it throws a TypeError
+ if (e.name == 'TypeError') {
+ Lib.log('Video.js: ' + method + ' unavailable on '+this.techName+' playback technology element.', e);
+ this.tech.isReady_ = false;
+ } else {
+ Lib.log(e);
+ }
}
+ throw e;
}
- throw e;
}
+
+ return;
}
- return;
-};
+ /**
+ * start media playback
+ *
+ * myPlayer.play();
+ *
+ * @return {vjs.Player} self
+ */
+ play() {
+ this.techCall('play');
+ return this;
+ }
-/**
- * start media playback
- *
- * myPlayer.play();
- *
- * @return {vjs.Player} self
- */
-Player.prototype.play = function(){
- this.techCall('play');
- return this;
-};
+ /**
+ * Pause the video playback
+ *
+ * myPlayer.pause();
+ *
+ * @return {vjs.Player} self
+ */
+ pause() {
+ this.techCall('pause');
+ return this;
+ }
-/**
- * Pause the video playback
- *
- * myPlayer.pause();
- *
- * @return {vjs.Player} self
- */
-Player.prototype.pause = function(){
- this.techCall('pause');
- return this;
-};
+ /**
+ * Check if the player is paused
+ *
+ * var isPaused = myPlayer.paused();
+ * var isPlaying = !myPlayer.paused();
+ *
+ * @return {Boolean} false if the media is currently playing, or true otherwise
+ */
+ paused() {
+ // The initial state of paused should be true (in Safari it's actually false)
+ return (this.techGet('paused') === false) ? false : true;
+ }
-/**
- * Check if the player is paused
- *
- * var isPaused = myPlayer.paused();
- * var isPlaying = !myPlayer.paused();
- *
- * @return {Boolean} false if the media is currently playing, or true otherwise
- */
-Player.prototype.paused = function(){
- // The initial state of paused should be true (in Safari it's actually false)
- return (this.techGet('paused') === false) ? false : true;
-};
+ /**
+ * Get or set the current time (in seconds)
+ *
+ * // get
+ * var whereYouAt = myPlayer.currentTime();
+ *
+ * // set
+ * myPlayer.currentTime(120); // 2 minutes into the video
+ *
+ * @param {Number|String=} seconds The time to seek to
+ * @return {Number} The time in seconds, when not setting
+ * @return {vjs.Player} self, when the current time is set
+ */
+ currentTime(seconds) {
+ if (seconds !== undefined) {
-/**
- * Get or set the current time (in seconds)
- *
- * // get
- * var whereYouAt = myPlayer.currentTime();
- *
- * // set
- * myPlayer.currentTime(120); // 2 minutes into the video
- *
- * @param {Number|String=} seconds The time to seek to
- * @return {Number} The time in seconds, when not setting
- * @return {vjs.Player} self, when the current time is set
- */
-Player.prototype.currentTime = function(seconds){
- if (seconds !== undefined) {
+ this.techCall('setCurrentTime', seconds);
- this.techCall('setCurrentTime', seconds);
+ return this;
+ }
- return this;
+ // cache last currentTime and return. default to 0 seconds
+ //
+ // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
+ // currentTime when scrubbing, but may not provide much performance benefit afterall.
+ // Should be tested. Also something has to read the actual current time or the cache will
+ // never get updated.
+ return this.cache_.currentTime = (this.techGet('currentTime') || 0);
}
- // cache last currentTime and return. default to 0 seconds
- //
- // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
- // currentTime when scrubbing, but may not provide much performance benefit afterall.
- // Should be tested. Also something has to read the actual current time or the cache will
- // never get updated.
- return this.cache_.currentTime = (this.techGet('currentTime') || 0);
-};
-
-/**
- * Get the length in time of the video in seconds
- *
- * var lengthOfVideo = myPlayer.duration();
- *
- * **NOTE**: The video must have started loading before the duration can be
- * known, and in the case of Flash, may not be known until the video starts
- * playing.
- *
- * @return {Number} The duration of the video in seconds
- */
-Player.prototype.duration = function(seconds){
- if (seconds !== undefined) {
+ /**
+ * Get the length in time of the video in seconds
+ *
+ * var lengthOfVideo = myPlayer.duration();
+ *
+ * **NOTE**: The video must have started loading before the duration can be
+ * known, and in the case of Flash, may not be known until the video starts
+ * playing.
+ *
+ * @return {Number} The duration of the video in seconds
+ */
+ duration(seconds) {
+ if (seconds !== undefined) {
- // cache the last set value for optimized scrubbing (esp. Flash)
- this.cache_.duration = parseFloat(seconds);
+ // cache the last set value for optimized scrubbing (esp. Flash)
+ this.cache_.duration = parseFloat(seconds);
- return this;
- }
+ return this;
+ }
- if (this.cache_.duration === undefined) {
- this.onDurationChange();
+ if (this.cache_.duration === undefined) {
+ this.onDurationChange();
+ }
+
+ return this.cache_.duration || 0;
}
- return this.cache_.duration || 0;
-};
+ /**
+ * Calculates how much time is left.
+ *
+ * var timeLeft = myPlayer.remainingTime();
+ *
+ * Not a native video element function, but useful
+ * @return {Number} The time remaining in seconds
+ */
+ remainingTime() {
+ return this.duration() - this.currentTime();
+ }
-/**
- * Calculates how much time is left.
- *
- * var timeLeft = myPlayer.remainingTime();
- *
- * Not a native video element function, but useful
- * @return {Number} The time remaining in seconds
- */
-Player.prototype.remainingTime = function(){
- return this.duration() - this.currentTime();
-};
+ // http://dev.w3.org/html5/spec/video.html#dom-media-buffered
+ // Buffered returns a timerange object.
+ // Kind of like an array of portions of the video that have been downloaded.
-// http://dev.w3.org/html5/spec/video.html#dom-media-buffered
-// Buffered returns a timerange object.
-// Kind of like an array of portions of the video that have been downloaded.
+ /**
+ * Get a TimeRange object with the times of the video that have been downloaded
+ *
+ * If you just want the percent of the video that's been downloaded,
+ * use bufferedPercent.
+ *
+ * // Number of different ranges of time have been buffered. Usually 1.
+ * numberOfRanges = bufferedTimeRange.length,
+ *
+ * // Time in seconds when the first range starts. Usually 0.
+ * firstRangeStart = bufferedTimeRange.start(0),
+ *
+ * // Time in seconds when the first range ends
+ * firstRangeEnd = bufferedTimeRange.end(0),
+ *
+ * // Length in seconds of the first time range
+ * firstRangeLength = firstRangeEnd - firstRangeStart;
+ *
+ * @return {Object} A mock TimeRange object (following HTML spec)
+ */
+ buffered() {
+ var buffered = this.techGet('buffered');
-/**
- * Get a TimeRange object with the times of the video that have been downloaded
- *
- * If you just want the percent of the video that's been downloaded,
- * use bufferedPercent.
- *
- * // Number of different ranges of time have been buffered. Usually 1.
- * numberOfRanges = bufferedTimeRange.length,
- *
- * // Time in seconds when the first range starts. Usually 0.
- * firstRangeStart = bufferedTimeRange.start(0),
- *
- * // Time in seconds when the first range ends
- * firstRangeEnd = bufferedTimeRange.end(0),
- *
- * // Length in seconds of the first time range
- * firstRangeLength = firstRangeEnd - firstRangeStart;
- *
- * @return {Object} A mock TimeRange object (following HTML spec)
- */
-Player.prototype.buffered = function(){
- var buffered = this.techGet('buffered');
+ if (!buffered || !buffered.length) {
+ buffered = Lib.createTimeRange(0,0);
+ }
- if (!buffered || !buffered.length) {
- buffered = Lib.createTimeRange(0,0);
+ return buffered;
}
- return buffered;
-};
+ /**
+ * Get the percent (as a decimal) of the video that's been downloaded
+ *
+ * var howMuchIsDownloaded = myPlayer.bufferedPercent();
+ *
+ * 0 means none, 1 means all.
+ * (This method isn't in the HTML5 spec, but it's very convenient)
+ *
+ * @return {Number} A decimal between 0 and 1 representing the percent
+ */
+ bufferedPercent() {
+ var duration = this.duration(),
+ buffered = this.buffered(),
+ bufferedDuration = 0,
+ start, end;
+
+ if (!duration) {
+ return 0;
+ }
-/**
- * Get the percent (as a decimal) of the video that's been downloaded
- *
- * var howMuchIsDownloaded = myPlayer.bufferedPercent();
- *
- * 0 means none, 1 means all.
- * (This method isn't in the HTML5 spec, but it's very convenient)
- *
- * @return {Number} A decimal between 0 and 1 representing the percent
- */
-Player.prototype.bufferedPercent = function(){
- var duration = this.duration(),
- buffered = this.buffered(),
- bufferedDuration = 0,
- start, end;
+ for (var i=0; i duration) {
+ end = duration;
+ }
+
+ bufferedDuration += end - start;
+ }
+
+ return bufferedDuration / duration;
}
- for (var i=0; i duration) {
end = duration;
}
- bufferedDuration += end - start;
+ return end;
}
- return bufferedDuration / duration;
-};
+ /**
+ * Get or set the current volume of the media
+ *
+ * // get
+ * var howLoudIsIt = myPlayer.volume();
+ *
+ * // set
+ * myPlayer.volume(0.5); // Set volume to half
+ *
+ * 0 is off (muted), 1.0 is all the way up, 0.5 is half way.
+ *
+ * @param {Number} percentAsDecimal The new volume as a decimal percent
+ * @return {Number} The current volume, when getting
+ * @return {vjs.Player} self, when setting
+ */
+ volume(percentAsDecimal) {
+ let vol;
+
+ if (percentAsDecimal !== undefined) {
+ vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1
+ this.cache_.volume = vol;
+ this.techCall('setVolume', vol);
+ Lib.setLocalStorage('volume', vol);
+ return this;
+ }
+
+ // Default to 1 when returning current volume.
+ vol = parseFloat(this.techGet('volume'));
+ return (isNaN(vol)) ? 1 : vol;
+ }
-/**
- * Get the ending time of the last buffered time range
- *
- * This is used in the progress bar to encapsulate all time ranges.
- * @return {Number} The end of the last buffered time range
- */
-Player.prototype.bufferedEnd = function(){
- var buffered = this.buffered(),
- duration = this.duration(),
- end = buffered.end(buffered.length-1);
- if (end > duration) {
- end = duration;
+ /**
+ * Get the current muted state, or turn mute on or off
+ *
+ * // get
+ * var isVolumeMuted = myPlayer.muted();
+ *
+ * // set
+ * myPlayer.muted(true); // mute the volume
+ *
+ * @param {Boolean=} muted True to mute, false to unmute
+ * @return {Boolean} True if mute is on, false if not, when getting
+ * @return {vjs.Player} self, when setting mute
+ */
+ muted(muted) {
+ if (muted !== undefined) {
+ this.techCall('setMuted', muted);
+ return this;
+ }
+ return this.techGet('muted') || false; // Default to false
}
- return end;
-};
+ // Check if current tech can support native fullscreen
+ // (e.g. with built in controls like iOS, so not our flash swf)
+ supportsFullScreen() {
+ return this.techGet('supportsFullScreen') || false;
+ }
-/**
- * Get or set the current volume of the media
- *
- * // get
- * var howLoudIsIt = myPlayer.volume();
- *
- * // set
- * myPlayer.volume(0.5); // Set volume to half
- *
- * 0 is off (muted), 1.0 is all the way up, 0.5 is half way.
- *
- * @param {Number} percentAsDecimal The new volume as a decimal percent
- * @return {Number} The current volume, when getting
- * @return {vjs.Player} self, when setting
- */
-Player.prototype.volume = function(percentAsDecimal){
- let vol;
-
- if (percentAsDecimal !== undefined) {
- vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1
- this.cache_.volume = vol;
- this.techCall('setVolume', vol);
- Lib.setLocalStorage('volume', vol);
- return this;
+ /**
+ * Check if the player is in fullscreen mode
+ *
+ * // get
+ * var fullscreenOrNot = myPlayer.isFullscreen();
+ *
+ * // set
+ * myPlayer.isFullscreen(true); // tell the player it's in fullscreen
+ *
+ * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
+ * property and instead document.fullscreenElement is used. But isFullscreen is
+ * still a valuable property for internal player workings.
+ *
+ * @param {Boolean=} isFS Update the player's fullscreen state
+ * @return {Boolean} true if fullscreen, false if not
+ * @return {vjs.Player} self, when setting
+ */
+ isFullscreen(isFS) {
+ if (isFS !== undefined) {
+ this.isFullscreen_ = !!isFS;
+ return this;
+ }
+ return !!this.isFullscreen_;
}
- // Default to 1 when returning current volume.
- vol = parseFloat(this.techGet('volume'));
- return (isNaN(vol)) ? 1 : vol;
-};
+ /**
+ * Old naming for isFullscreen()
+ * @deprecated for lowercase 's' version
+ */
+ isFullScreen(isFS) {
+ Lib.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")');
+ return this.isFullscreen(isFS);
+ }
+ /**
+ * Increase the size of the video to full screen
+ *
+ * myPlayer.requestFullscreen();
+ *
+ * In some browsers, full screen is not supported natively, so it enters
+ * "full window mode", where the video fills the browser window.
+ * In browsers and devices that support native full screen, sometimes the
+ * browser's default controls will be shown, and not the Video.js custom skin.
+ * This includes most mobile devices (iOS, Android) and older versions of
+ * Safari.
+ *
+ * @return {vjs.Player} self
+ */
+ requestFullscreen() {
+ var fsApi = FullscreenApi;
+
+ this.isFullscreen(true);
+
+ if (fsApi) {
+ // the browser supports going fullscreen at the element level so we can
+ // take the controls fullscreen as well as the video
+
+ // Trigger fullscreenchange event after change
+ // We have to specifically add this each time, and remove
+ // when canceling fullscreen. Otherwise if there's multiple
+ // players on a page, they would all be reacting to the same fullscreen
+ // events
+ Events.on(document, fsApi['fullscreenchange'], Lib.bind(this, function documentFullscreenChange(e){
+ this.isFullscreen(document[fsApi.fullscreenElement]);
+
+ // If cancelling fullscreen, remove event listener.
+ if (this.isFullscreen() === false) {
+ Events.off(document, fsApi['fullscreenchange'], documentFullscreenChange);
+ }
+
+ this.trigger('fullscreenchange');
+ }));
+
+ this.el_[fsApi.requestFullscreen]();
+
+ } else if (this.tech.supportsFullScreen()) {
+ // we can't take the video.js controls fullscreen but we can go fullscreen
+ // with native controls
+ this.techCall('enterFullScreen');
+ } else {
+ // fullscreen isn't supported so we'll just stretch the video element to
+ // fill the viewport
+ this.enterFullWindow();
+ this.trigger('fullscreenchange');
+ }
-/**
- * Get the current muted state, or turn mute on or off
- *
- * // get
- * var isVolumeMuted = myPlayer.muted();
- *
- * // set
- * myPlayer.muted(true); // mute the volume
- *
- * @param {Boolean=} muted True to mute, false to unmute
- * @return {Boolean} True if mute is on, false if not, when getting
- * @return {vjs.Player} self, when setting mute
- */
-Player.prototype.muted = function(muted){
- if (muted !== undefined) {
- this.techCall('setMuted', muted);
return this;
}
- return this.techGet('muted') || false; // Default to false
-};
-// Check if current tech can support native fullscreen
-// (e.g. with built in controls like iOS, so not our flash swf)
-Player.prototype.supportsFullScreen = function(){
- return this.techGet('supportsFullScreen') || false;
-};
+ /**
+ * Old naming for requestFullscreen
+ * @deprecated for lower case 's' version
+ */
+ requestFullScreen() {
+ Lib.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")');
+ return this.requestFullscreen();
+ }
-/**
- * is the player in fullscreen
- * @type {Boolean}
- * @private
- */
-Player.prototype.isFullscreen_ = false;
+ /**
+ * Return the video to its normal size after having been in full screen mode
+ *
+ * myPlayer.exitFullscreen();
+ *
+ * @return {vjs.Player} self
+ */
+ exitFullscreen() {
+ var fsApi = FullscreenApi;
+ this.isFullscreen(false);
+
+ // Check for browser element fullscreen support
+ if (fsApi) {
+ document[fsApi.exitFullscreen]();
+ } else if (this.tech.supportsFullScreen()) {
+ this.techCall('exitFullScreen');
+ } else {
+ this.exitFullWindow();
+ this.trigger('fullscreenchange');
+ }
-/**
- * Check if the player is in fullscreen mode
- *
- * // get
- * var fullscreenOrNot = myPlayer.isFullscreen();
- *
- * // set
- * myPlayer.isFullscreen(true); // tell the player it's in fullscreen
- *
- * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
- * property and instead document.fullscreenElement is used. But isFullscreen is
- * still a valuable property for internal player workings.
- *
- * @param {Boolean=} isFS Update the player's fullscreen state
- * @return {Boolean} true if fullscreen, false if not
- * @return {vjs.Player} self, when setting
- */
-Player.prototype.isFullscreen = function(isFS){
- if (isFS !== undefined) {
- this.isFullscreen_ = !!isFS;
return this;
}
- return this.isFullscreen_;
-};
-/**
- * Old naming for isFullscreen()
- * @deprecated for lowercase 's' version
- */
-Player.prototype.isFullScreen = function(isFS){
- Lib.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")');
- return this.isFullscreen(isFS);
-};
+ /**
+ * Old naming for exitFullscreen
+ * @deprecated for exitFullscreen
+ */
+ cancelFullScreen() {
+ Lib.log.warn('player.cancelFullScreen() has been deprecated, use player.exitFullscreen()');
+ return this.exitFullscreen();
+ }
-/**
- * Increase the size of the video to full screen
- *
- * myPlayer.requestFullscreen();
- *
- * In some browsers, full screen is not supported natively, so it enters
- * "full window mode", where the video fills the browser window.
- * In browsers and devices that support native full screen, sometimes the
- * browser's default controls will be shown, and not the Video.js custom skin.
- * This includes most mobile devices (iOS, Android) and older versions of
- * Safari.
- *
- * @return {vjs.Player} self
- */
-Player.prototype.requestFullscreen = function(){
- var fsApi = FullscreenApi;
-
- this.isFullscreen(true);
-
- if (fsApi) {
- // the browser supports going fullscreen at the element level so we can
- // take the controls fullscreen as well as the video
-
- // Trigger fullscreenchange event after change
- // We have to specifically add this each time, and remove
- // when canceling fullscreen. Otherwise if there's multiple
- // players on a page, they would all be reacting to the same fullscreen
- // events
- Events.on(document, fsApi['fullscreenchange'], Lib.bind(this, function documentFullscreenChange(e){
- this.isFullscreen(document[fsApi.fullscreenElement]);
-
- // If cancelling fullscreen, remove event listener.
- if (this.isFullscreen() === false) {
- Events.off(document, fsApi['fullscreenchange'], documentFullscreenChange);
- }
+ // When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us.
+ enterFullWindow() {
+ this.isFullWindow = true;
- this.trigger('fullscreenchange');
- }));
+ // Storing original doc overflow value to return to when fullscreen is off
+ this.docOrigOverflow = document.documentElement.style.overflow;
+
+ // Add listener for esc key to exit fullscreen
+ Events.on(document, 'keydown', Lib.bind(this, this.fullWindowOnEscKey));
+
+ // Hide any scroll bars
+ document.documentElement.style.overflow = 'hidden';
+
+ // Apply fullscreen styles
+ Lib.addClass(document.body, 'vjs-full-window');
- this.el_[fsApi.requestFullscreen]();
+ this.trigger('enterFullWindow');
+ }
- } else if (this.tech.supportsFullScreen()) {
- // we can't take the video.js controls fullscreen but we can go fullscreen
- // with native controls
- this.techCall('enterFullScreen');
- } else {
- // fullscreen isn't supported so we'll just stretch the video element to
- // fill the viewport
- this.enterFullWindow();
- this.trigger('fullscreenchange');
+ fullWindowOnEscKey(event) {
+ if (event.keyCode === 27) {
+ if (this.isFullscreen() === true) {
+ this.exitFullscreen();
+ } else {
+ this.exitFullWindow();
+ }
+ }
}
- return this;
-};
+ exitFullWindow() {
+ this.isFullWindow = false;
+ Events.off(document, 'keydown', this.fullWindowOnEscKey);
-/**
- * Old naming for requestFullscreen
- * @deprecated for lower case 's' version
- */
-Player.prototype.requestFullScreen = function(){
- Lib.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")');
- return this.requestFullscreen();
-};
+ // Unhide scroll bars.
+ document.documentElement.style.overflow = this.docOrigOverflow;
-/**
- * Return the video to its normal size after having been in full screen mode
- *
- * myPlayer.exitFullscreen();
- *
- * @return {vjs.Player} self
- */
-Player.prototype.exitFullscreen = function(){
- var fsApi = FullscreenApi;
- this.isFullscreen(false);
+ // Remove fullscreen styles
+ Lib.removeClass(document.body, 'vjs-full-window');
- // Check for browser element fullscreen support
- if (fsApi) {
- document[fsApi.exitFullscreen]();
- } else if (this.tech.supportsFullScreen()) {
- this.techCall('exitFullScreen');
- } else {
- this.exitFullWindow();
- this.trigger('fullscreenchange');
+ // Resize the box, controller, and poster to original sizes
+ // this.positionAll();
+ this.trigger('exitFullWindow');
}
- return this;
-};
+ selectSource(sources) {
+ // Loop through each playback technology in the options order
+ for (var i=0,j=this.options_['techOrder'];i 0) {
+ // In milliseconds, if no more activity has occurred the
+ // user will be considered inactive
+ inactivityTimeout = this.setTimeout(function () {
+ // Protect against the case where the inactivityTimeout can trigger just
+ // before the next user activity is picked up by the activityCheck loop
+ // causing a flicker
+ if (!this.userActivity_) {
+ this.userActive(false);
+ }
+ }, timeout);
+ }
}
- }
- return this;
+ }, 250);
}
- return this.controls_;
-};
-Player.prototype.usingNativeControls_;
+ /**
+ * Gets or sets the current playback rate. A playback rate of
+ * 1.0 represents normal speed and 0.5 would indicate half-speed
+ * playback, for instance.
+ * @param {Number} rate New playback rate to set.
+ * @return {Number} Returns the new playback rate when setting
+ * @return {Number} Returns the current playback rate when getting
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
+ */
+ playbackRate(rate) {
+ if (rate !== undefined) {
+ this.techCall('setPlaybackRate', rate);
+ return this;
+ }
-/**
- * Toggle native controls on/off. Native controls are the controls built into
- * devices (e.g. default iPhone controls), Flash, or other techs
- * (e.g. Vimeo Controls)
- *
- * **This should only be set by the current tech, because only the tech knows
- * if it can support native controls**
- *
- * @param {Boolean} bool True signals that native controls are on
- * @return {vjs.Player} Returns the player
- * @private
- */
-Player.prototype.usingNativeControls = function(bool){
- if (bool !== undefined) {
- bool = !!bool; // force boolean
- // Don't trigger a change event unless it actually changed
- if (this.usingNativeControls_ !== bool) {
- this.usingNativeControls_ = bool;
- if (bool) {
- this.addClass('vjs-using-native-controls');
-
- /**
- * player is using the native device controls
- *
- * @event usingnativecontrols
- * @memberof vjs.Player
- * @instance
- * @private
- */
- this.trigger('usingnativecontrols');
- } else {
- this.removeClass('vjs-using-native-controls');
-
- /**
- * player is using the custom HTML controls
- *
- * @event usingcustomcontrols
- * @memberof vjs.Player
- * @instance
- * @private
- */
- this.trigger('usingcustomcontrols');
- }
+ if (this.tech && this.tech['featuresPlaybackRate']) {
+ return this.techGet('playbackRate');
+ } else {
+ return 1.0;
}
- return this;
}
- return this.usingNativeControls_;
-};
-/**
- * Store the current media error
- * @type {Object}
- * @private
- */
-Player.prototype.error_ = null;
+ /**
+ * Gets or sets the audio flag
+ *
+ * @param {Boolean} bool True signals that this is an audio player.
+ * @return {Boolean} Returns true if player is audio, false if not when getting
+ * @return {vjs.Player} Returns the player if setting
+ * @private
+ */
+ isAudio(bool) {
+ if (bool !== undefined) {
+ this.isAudio_ = !!bool;
+ return this;
+ }
-/**
- * Set or get the current MediaError
- * @param {*} err A MediaError or a String/Number to be turned into a MediaError
- * @return {vjs.MediaError|null} when getting
- * @return {vjs.Player} when setting
- */
-Player.prototype.error = function(err){
- if (err === undefined) {
- return this.error_;
+ return !!this.isAudio_;
}
- // restoring to default
- if (err === null) {
- this.error_ = err;
- this.removeClass('vjs-error');
- return this;
+ /**
+ * Returns the current state of network activity for the element, from
+ * the codes in the list below.
+ * - NETWORK_EMPTY (numeric value 0)
+ * The element has not yet been initialised. All attributes are in
+ * their initial states.
+ * - NETWORK_IDLE (numeric value 1)
+ * The element's resource selection algorithm is active and has
+ * selected a resource, but it is not actually using the network at
+ * this time.
+ * - NETWORK_LOADING (numeric value 2)
+ * The user agent is actively trying to download data.
+ * - NETWORK_NO_SOURCE (numeric value 3)
+ * The element's resource selection algorithm is active, but it has
+ * not yet found a resource to use.
+ * @return {Number} the current network activity state
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
+ */
+ networkState() {
+ return this.techGet('networkState');
}
- // error instance
- if (err instanceof MediaError) {
- this.error_ = err;
- } else {
- this.error_ = new MediaError(err);
+ /**
+ * Returns a value that expresses the current state of the element
+ * with respect to rendering the current playback position, from the
+ * codes in the list below.
+ * - HAVE_NOTHING (numeric value 0)
+ * No information regarding the media resource is available.
+ * - HAVE_METADATA (numeric value 1)
+ * Enough of the resource has been obtained that the duration of the
+ * resource is available.
+ * - HAVE_CURRENT_DATA (numeric value 2)
+ * Data for the immediate current playback position is available.
+ * - HAVE_FUTURE_DATA (numeric value 3)
+ * Data for the immediate current playback position is available, as
+ * well as enough data for the user agent to advance the current
+ * playback position in the direction of playback.
+ * - HAVE_ENOUGH_DATA (numeric value 4)
+ * The user agent estimates that enough data is available for
+ * playback to proceed uninterrupted.
+ * @return {Number} the current playback rendering state
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
+ */
+ readyState() {
+ return this.techGet('readyState');
}
- // fire an error event on the player
- this.trigger('error');
+ /**
+ * Text tracks are tracks of timed text events.
+ * Captions - text displayed over the video for the hearing impaired
+ * Subtitles - text displayed over the video for those who don't understand language in the video
+ * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video
+ * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device
+ */
- // add the vjs-error classname to the player
- this.addClass('vjs-error');
+ /**
+ * Get an array of associated text tracks. captions, subtitles, chapters, descriptions
+ * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
+ * @return {Array} Array of track objects
+ */
+ textTracks() {
+ // cannot use techGet directly because it checks to see whether the tech is ready.
+ // Flash is unlikely to be ready in time but textTracks should still work.
+ return this.tech && this.tech['textTracks']();
+ }
- // log the name of the error type and any message
- // ie8 just logs "[object object]" if you just log the error object
- Lib.log.error('(CODE:'+this.error_.code+' '+MediaError.errorTypes[this.error_.code]+')', this.error_.message, this.error_);
+ remoteTextTracks() {
+ return this.tech && this.tech['remoteTextTracks']();
+ }
- return this;
-};
+ /**
+ * Add a text track
+ * In addition to the W3C settings we allow adding additional info through options.
+ * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
+ * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata
+ * @param {String=} label Optional label
+ * @param {String=} language Optional language
+ */
+ addTextTrack(kind, label, language) {
+ return this.tech && this.tech['addTextTrack'](kind, label, language);
+ }
-/**
- * Returns whether or not the player is in the "ended" state.
- * @return {Boolean} True if the player is in the ended state, false if not.
- */
-Player.prototype.ended = function(){ return this.techGet('ended'); };
+ addRemoteTextTrack(options) {
+ return this.tech && this.tech['addRemoteTextTrack'](options);
+ }
-/**
- * Returns whether or not the player is in the "seeking" state.
- * @return {Boolean} True if the player is in the seeking state, false if not.
- */
-Player.prototype.seeking = function(){ return this.techGet('seeking'); };
-
-// When the player is first initialized, trigger activity so components
-// like the control bar show themselves if needed
-Player.prototype.userActivity_ = true;
-Player.prototype.reportUserActivity = function(event){
- this.userActivity_ = true;
-};
-
-Player.prototype.userActive_ = true;
-Player.prototype.userActive = function(bool){
- if (bool !== undefined) {
- bool = !!bool;
- if (bool !== this.userActive_) {
- this.userActive_ = bool;
- if (bool) {
- // If the user was inactive and is now active we want to reset the
- // inactivity timer
- this.userActivity_ = true;
- this.removeClass('vjs-user-inactive');
- this.addClass('vjs-user-active');
- this.trigger('useractive');
- } else {
- // We're switching the state to inactive manually, so erase any other
- // activity
- this.userActivity_ = false;
+ removeRemoteTextTrack(track) {
+ this.tech && this.tech['removeRemoteTextTrack'](track);
+ }
- // Chrome/Safari/IE have bugs where when you change the cursor it can
- // trigger a mousemove event. This causes an issue when you're hiding
- // the cursor when the user is inactive, and a mousemove signals user
- // activity. Making it impossible to go into inactive mode. Specifically
- // this happens in fullscreen when we really need to hide the cursor.
- //
- // When this gets resolved in ALL browsers it can be removed
- // https://code.google.com/p/chromium/issues/detail?id=103041
- if(this.tech) {
- this.tech.one('mousemove', function(e){
- e.stopPropagation();
- e.preventDefault();
- });
- }
+ // Methods to add support for
+ // initialTime: function(){ return this.techCall('initialTime'); },
+ // startOffsetTime: function(){ return this.techCall('startOffsetTime'); },
+ // played: function(){ return this.techCall('played'); },
+ // seekable: function(){ return this.techCall('seekable'); },
+ // videoTracks: function(){ return this.techCall('videoTracks'); },
+ // audioTracks: function(){ return this.techCall('audioTracks'); },
+ // videoWidth: function(){ return this.techCall('videoWidth'); },
+ // videoHeight: function(){ return this.techCall('videoHeight'); },
+ // defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); },
+ // mediaGroup: function(){ return this.techCall('mediaGroup'); },
+ // controller: function(){ return this.techCall('controller'); },
+ // defaultMuted: function(){ return this.techCall('defaultMuted'); }
+
+ // TODO
+ // currentSrcList: the array of sources including other formats and bitrates
+ // playList: array of source lists in order of playback
- this.removeClass('vjs-user-active');
- this.addClass('vjs-user-inactive');
- this.trigger('userinactive');
- }
+ /**
+ * The player's language code
+ * @param {String} languageCode The locale string
+ * @return {String} The locale string when getting
+ * @return {vjs.Player} self, when setting
+ */
+ language(languageCode) {
+ if (languageCode === undefined) {
+ return this.language_;
}
+
+ this.language_ = languageCode;
return this;
}
- return this.userActive_;
-};
-Player.prototype.listenForUserActivity = function(){
- let mouseInProgress, lastMoveX, lastMoveY;
+ /**
+ * Get the player's language dictionary
+ */
+ languages() {
+ return this.languages_;
+ }
- let onActivity = Lib.bind(this, this.reportUserActivity);
+ static getTagSettings(tag) {
+ let baseOptions = {
+ 'sources': [],
+ 'tracks': []
+ };
- let onMouseMove = function(e) {
- // #1068 - Prevent mousemove spamming
- // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
- if(e.screenX != lastMoveX || e.screenY != lastMoveY) {
- lastMoveX = e.screenX;
- lastMoveY = e.screenY;
- onActivity();
+ const tagOptions = Lib.getElementAttributes(tag);
+ const dataSetup = tagOptions['data-setup'];
+
+ // Check if data-setup attr exists.
+ if (dataSetup !== null){
+ // Parse options JSON
+ // If empty string, make it a parsable json object.
+ Lib.obj.merge(tagOptions, JSON.parse(dataSetup || '{}'));
}
- };
-
- let onMouseDown = function() {
- onActivity();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(mouseInProgress);
- // Setting userActivity=true now and setting the interval to the same time
- // as the activityCheck interval (250) should ensure we never miss the
- // next activityCheck
- mouseInProgress = this.setInterval(onActivity, 250);
- };
-
- let onMouseUp = function(event) {
- onActivity();
- // Stop the interval that maintains activity if the mouse/touch is down
- this.clearInterval(mouseInProgress);
- };
-
- // Any mouse movement will be considered user activity
- this.on('mousedown', onMouseDown);
- this.on('mousemove', onMouseMove);
- this.on('mouseup', onMouseUp);
-
- // Listen for keyboard navigation
- // Shouldn't need to use inProgress interval because of key repeat
- this.on('keydown', onActivity);
- this.on('keyup', onActivity);
-
- // Run an interval every 250 milliseconds instead of stuffing everything into
- // the mousemove/touchmove function itself, to prevent performance degradation.
- // `this.reportUserActivity` simply sets this.userActivity_ to true, which
- // then gets picked up by this loop
- // http://ejohn.org/blog/learning-from-twitter/
- let activityCheck = this.setInterval(function() {
- let inactivityTimeout;
-
- // Check to see if mouse/touch activity has happened
- if (this.userActivity_) {
- // Reset the activity tracker
- this.userActivity_ = false;
-
- // If the user state was inactive, set the state to active
- this.userActive(true);
-
- // Clear any existing inactivity timeout to start the timer over
- this.clearTimeout(inactivityTimeout);
-
- var timeout = this.options()['inactivityTimeout'];
- if (timeout > 0) {
- // In milliseconds, if no more activity has occurred the
- // user will be considered inactive
- inactivityTimeout = this.setTimeout(function () {
- // Protect against the case where the inactivityTimeout can trigger just
- // before the next user activity is picked up by the activityCheck loop
- // causing a flicker
- if (!this.userActivity_) {
- this.userActive(false);
- }
- }, timeout);
+
+ Lib.obj.merge(baseOptions, tagOptions);
+
+ // Get tag children settings
+ if (tag.hasChildNodes()) {
+ const children = tag.childNodes;
+
+ for (let i=0, j=children.length; i'+this.defaultValue+''
- }, props);
-
- return Component.prototype.createEl.call(this, 'div', props);
-};
-
-export default Slider;
-export { SliderHandle };
diff --git a/src/js/slider/slider-handle.js b/src/js/slider/slider-handle.js
new file mode 100644
index 0000000000..edde4f5c9e
--- /dev/null
+++ b/src/js/slider/slider-handle.js
@@ -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: ''+(this.defaultValue || 0)+''
+ }, props);
+
+ return super.createEl('div', props);
+ }
+
+}
+
+Component.registerComponent('SliderHandle', SliderHandle);
+export default SliderHandle;
diff --git a/src/js/slider/slider.js b/src/js/slider/slider.js
new file mode 100644
index 0000000000..39ddad4f30
--- /dev/null
+++ b/src/js/slider/slider.js
@@ -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;
diff --git a/src/js/media/flash-rtmp.js b/src/js/tech/flash-rtmp.js
similarity index 100%
rename from src/js/media/flash-rtmp.js
rename to src/js/tech/flash-rtmp.js
diff --git a/src/js/media/flash.js b/src/js/tech/flash.js
similarity index 82%
rename from src/js/media/flash.js
rename to src/js/tech/flash.js
index 2b05a77613..8821618bcb 100644
--- a/src/js/media/flash.js
+++ b/src/js/tech/flash.js
@@ -4,7 +4,7 @@
* Not using setupTriggers. Using global onEvent func to distribute events
*/
-import MediaTechController from './media';
+import Tech from './tech';
import * as Lib from '../lib';
import FlashRtmpDecorator from './flash-rtmp';
import Component from '../component';
@@ -19,10 +19,10 @@ let navigator = window.navigator;
* @param {Function=} ready
* @constructor
*/
-var Flash = MediaTechController.extend({
- /** @constructor */
- init: function(player, options, ready){
- MediaTechController.call(this, player, options, ready);
+class Flash extends Tech {
+
+ constructor(player, options, ready){
+ super(player, options, ready);
let { source, parentEl } = options;
@@ -103,89 +103,85 @@ var Flash = MediaTechController.extend({
this.el_ = Flash.embed(options['swf'], placeHolder, flashVars, params, attributes);
}
-});
-
-Component.registerComponent('Flash', Flash);
-Flash.prototype.dispose = function(){
- MediaTechController.prototype.dispose.call(this);
-};
+ play() {
+ this.el_.vjs_play();
+ }
-Flash.prototype.play = function(){
- this.el_.vjs_play();
-};
+ pause() {
+ this.el_.vjs_pause();
+ }
-Flash.prototype.pause = function(){
- this.el_.vjs_pause();
-};
+ src(src) {
+ if (src === undefined) {
+ return this['currentSrc']();
+ }
-Flash.prototype.src = function(src){
- if (src === undefined) {
- return this['currentSrc']();
+ // Setting src through `src` not `setSrc` will be deprecated
+ return this.setSrc(src);
}
- // Setting src through `src` not `setSrc` will be deprecated
- return this.setSrc(src);
-};
+ setSrc(src) {
+ // Make sure source URL is absolute.
+ src = Lib.getAbsoluteURL(src);
+ this.el_.vjs_src(src);
-Flash.prototype.setSrc = function(src){
- // Make sure source URL is absolute.
- src = Lib.getAbsoluteURL(src);
- this.el_.vjs_src(src);
+ // Currently the SWF doesn't autoplay if you load a source later.
+ // e.g. Load player w/ no source, wait 2s, set src.
+ if (this.player_.autoplay()) {
+ var tech = this;
+ this.setTimeout(function(){ tech.play(); }, 0);
+ }
+ }
- // Currently the SWF doesn't autoplay if you load a source later.
- // e.g. Load player w/ no source, wait 2s, set src.
- if (this.player_.autoplay()) {
- var tech = this;
- this.setTimeout(function(){ tech.play(); }, 0);
+ setCurrentTime(time) {
+ this.lastSeekTarget_ = time;
+ this.el_.vjs_setProperty('currentTime', time);
+ super.setCurrentTime();
}
-};
-Flash.prototype['setCurrentTime'] = function(time){
- this.lastSeekTarget_ = time;
- this.el_.vjs_setProperty('currentTime', time);
- MediaTechController.prototype.setCurrentTime.call(this);
-};
+ currentTime(time) {
+ // when seeking make the reported time keep up with the requested time
+ // by reading the time we're seeking to
+ if (this.seeking()) {
+ return this.lastSeekTarget_ || 0;
+ }
+ return this.el_.vjs_getProperty('currentTime');
+ }
-Flash.prototype['currentTime'] = function(time){
- // when seeking make the reported time keep up with the requested time
- // by reading the time we're seeking to
- if (this.seeking()) {
- return this.lastSeekTarget_ || 0;
+ currentSrc() {
+ if (this.currentSource_) {
+ return this.currentSource_.src;
+ } else {
+ return this.el_.vjs_getProperty('currentSrc');
+ }
}
- return this.el_.vjs_getProperty('currentTime');
-};
-Flash.prototype['currentSrc'] = function(){
- if (this.currentSource_) {
- return this.currentSource_.src;
- } else {
- return this.el_.vjs_getProperty('currentSrc');
+ load() {
+ this.el_.vjs_load();
}
-};
-Flash.prototype.load = function(){
- this.el_.vjs_load();
-};
+ poster() {
+ this.el_.vjs_getProperty('poster');
+ }
-Flash.prototype.poster = function(){
- this.el_.vjs_getProperty('poster');
-};
-Flash.prototype['setPoster'] = function(){
// poster images are not handled by the Flash tech so make this a no-op
-};
+ setPoster() {}
-Flash.prototype.buffered = function(){
- return Lib.createTimeRange(0, this.el_.vjs_getProperty('buffered'));
-};
+ buffered() {
+ return Lib.createTimeRange(0, this.el_.vjs_getProperty('buffered'));
+ }
-Flash.prototype.supportsFullScreen = function(){
- return false; // Flash does not allow fullscreen through javascript
-};
+ supportsFullScreen() {
+ return false; // Flash does not allow fullscreen through javascript
+ }
+
+ enterFullScreen() {
+ return false;
+ }
+
+}
-Flash.prototype.enterFullScreen = function(){
- return false;
-};
// Create setters and getters for attributes
const _api = Flash.prototype;
@@ -219,7 +215,7 @@ Flash.isSupported = function(){
};
// Add Source Handler pattern functions to this tech
-MediaTechController.withSourceHandlers(Flash);
+Tech.withSourceHandlers(Flash);
/**
* The default native source handler.
@@ -421,4 +417,5 @@ Flash.getEmbedCode = function(swf, flashVars, params, attributes){
// Run Flash through the RTMP decorator
FlashRtmpDecorator(Flash);
+Tech.registerComponent('Flash', Flash);
export default Flash;
diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js
new file mode 100644
index 0000000000..8d32bd37b2
--- /dev/null
+++ b/src/js/tech/html5.js
@@ -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= 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;
diff --git a/src/js/media/loader.js b/src/js/tech/loader.js
similarity index 88%
rename from src/js/media/loader.js
rename to src/js/tech/loader.js
index 15b2e25aa0..c3c578ad97 100644
--- a/src/js/media/loader.js
+++ b/src/js/tech/loader.js
@@ -8,10 +8,10 @@ import window from 'global/window';
*
* @constructor
*/
-let MediaLoader = Component.extend({
- /** @constructor */
- init: function(player, options, ready){
- Component.call(this, player, options, ready);
+class MediaLoader extends Component {
+
+ constructor(player, options, ready){
+ super(player, options, ready);
// If there are no sources when the player is initialized,
// load the first supported playback technology.
@@ -34,8 +34,7 @@ let MediaLoader = Component.extend({
player.src(player.options_['sources']);
}
}
-});
+}
Component.registerComponent('MediaLoader', MediaLoader);
-
export default MediaLoader;
diff --git a/src/js/tech/tech.js b/src/js/tech/tech.js
new file mode 100644
index 0000000000..edc7cd8778
--- /dev/null
+++ b/src/js/tech/tech.js
@@ -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;
diff --git a/src/js/tracks/text-track-controls.js b/src/js/tracks/text-track-controls.js
deleted file mode 100644
index 4a5260535a..0000000000
--- a/src/js/tracks/text-track-controls.js
+++ /dev/null
@@ -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 };
diff --git a/src/js/tracks/text-track-display.js b/src/js/tracks/text-track-display.js
new file mode 100644
index 0000000000..f2dcbddbbd
--- /dev/null
+++ b/src/js/tracks/text-track-display.js
@@ -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;
diff --git a/src/js/tracks/text-track-settings.js b/src/js/tracks/text-track-settings.js
index c3de1910ba..fed7111bb4 100644
--- a/src/js/tracks/text-track-settings.js
+++ b/src/js/tracks/text-track-settings.js
@@ -3,9 +3,10 @@ import * as Lib from '../lib';
import * as Events from '../events';
import window from 'global/window';
-let TextTrackSettings = Component.extend({
- init: function(player, options) {
- Component.call(this, player, options);
+class TextTrackSettings extends Component {
+
+ constructor(player, options) {
+ super(player, options);
this.hide();
Events.on(this.el().querySelector('.vjs-done-button'), 'click', Lib.bind(this, function() {
@@ -40,102 +41,103 @@ let TextTrackSettings = Component.extend({
this.restoreSettings();
}
}
-});
-
-Component.registerComponent('TextTrackSettings', TextTrackSettings);
-TextTrackSettings.prototype.createEl = function() {
- return Component.prototype.createEl.call(this, 'div', {
- className: 'vjs-caption-settings vjs-modal-overlay',
- innerHTML: captionOptionsMenuTemplate()
- });
-};
+ createEl() {
+ return super.createEl('div', {
+ className: 'vjs-caption-settings vjs-modal-overlay',
+ innerHTML: captionOptionsMenuTemplate()
+ });
+ }
-TextTrackSettings.prototype.getValues = function() {
- const el = this.el();
+ getValues() {
+ const el = this.el();
- const textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select'));
- const fontFamily = getSelectedOptionValue(el.querySelector('.vjs-font-family select'));
- const fgColor = getSelectedOptionValue(el.querySelector('.vjs-fg-color > select'));
- const textOpacity = getSelectedOptionValue(el.querySelector('.vjs-text-opacity > select'));
- const bgColor = getSelectedOptionValue(el.querySelector('.vjs-bg-color > select'));
- const bgOpacity = getSelectedOptionValue(el.querySelector('.vjs-bg-opacity > select'));
- const windowColor = getSelectedOptionValue(el.querySelector('.window-color > select'));
- const windowOpacity = getSelectedOptionValue(el.querySelector('.vjs-window-opacity > select'));
- const fontPercent = window['parseFloat'](getSelectedOptionValue(el.querySelector('.vjs-font-percent > select')));
+ const textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select'));
+ const fontFamily = getSelectedOptionValue(el.querySelector('.vjs-font-family select'));
+ const fgColor = getSelectedOptionValue(el.querySelector('.vjs-fg-color > select'));
+ const textOpacity = getSelectedOptionValue(el.querySelector('.vjs-text-opacity > select'));
+ const bgColor = getSelectedOptionValue(el.querySelector('.vjs-bg-color > select'));
+ const bgOpacity = getSelectedOptionValue(el.querySelector('.vjs-bg-opacity > select'));
+ const windowColor = getSelectedOptionValue(el.querySelector('.window-color > select'));
+ const windowOpacity = getSelectedOptionValue(el.querySelector('.vjs-window-opacity > select'));
+ const fontPercent = window['parseFloat'](getSelectedOptionValue(el.querySelector('.vjs-font-percent > select')));
- let result = {
- 'backgroundOpacity': bgOpacity,
- 'textOpacity': textOpacity,
- 'windowOpacity': windowOpacity,
- 'edgeStyle': textEdge,
- 'fontFamily': fontFamily,
- 'color': fgColor,
- 'backgroundColor': bgColor,
- 'windowColor': windowColor,
- 'fontPercent': fontPercent
- };
- for (let name in result) {
- if (result[name] === '' || result[name] === 'none' || (name === 'fontPercent' && result[name] === 1.00)) {
- delete result[name];
+ let result = {
+ 'backgroundOpacity': bgOpacity,
+ 'textOpacity': textOpacity,
+ 'windowOpacity': windowOpacity,
+ 'edgeStyle': textEdge,
+ 'fontFamily': fontFamily,
+ 'color': fgColor,
+ 'backgroundColor': bgColor,
+ 'windowColor': windowColor,
+ 'fontPercent': fontPercent
+ };
+ for (let name in result) {
+ if (result[name] === '' || result[name] === 'none' || (name === 'fontPercent' && result[name] === 1.00)) {
+ delete result[name];
+ }
}
+ return result;
}
- return result;
-};
-TextTrackSettings.prototype.setValues = function(values) {
- const el = this.el();
+ setValues(values) {
+ const el = this.el();
- setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle);
- setSelectedOption(el.querySelector('.vjs-font-family select'), values.fontFamily);
- setSelectedOption(el.querySelector('.vjs-fg-color > select'), values.color);
- setSelectedOption(el.querySelector('.vjs-text-opacity > select'), values.textOpacity);
- setSelectedOption(el.querySelector('.vjs-bg-color > select'), values.backgroundColor);
- setSelectedOption(el.querySelector('.vjs-bg-opacity > select'), values.backgroundOpacity);
- setSelectedOption(el.querySelector('.window-color > select'), values.windowColor);
- setSelectedOption(el.querySelector('.vjs-window-opacity > select'), values.windowOpacity);
+ setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle);
+ setSelectedOption(el.querySelector('.vjs-font-family select'), values.fontFamily);
+ setSelectedOption(el.querySelector('.vjs-fg-color > select'), values.color);
+ setSelectedOption(el.querySelector('.vjs-text-opacity > select'), values.textOpacity);
+ setSelectedOption(el.querySelector('.vjs-bg-color > select'), values.backgroundColor);
+ setSelectedOption(el.querySelector('.vjs-bg-opacity > select'), values.backgroundOpacity);
+ setSelectedOption(el.querySelector('.window-color > select'), values.windowColor);
+ setSelectedOption(el.querySelector('.vjs-window-opacity > select'), values.windowOpacity);
- let fontPercent = values.fontPercent;
+ let fontPercent = values.fontPercent;
- if (fontPercent) {
- fontPercent = fontPercent.toFixed(2);
- }
+ if (fontPercent) {
+ fontPercent = fontPercent.toFixed(2);
+ }
- setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent);
-};
+ setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent);
+ }
-TextTrackSettings.prototype.restoreSettings = function() {
- let values;
- try {
- values = JSON.parse(window.localStorage.getItem('vjs-text-track-settings'));
- } catch (e) {}
+ restoreSettings() {
+ let values;
+ try {
+ values = JSON.parse(window.localStorage.getItem('vjs-text-track-settings'));
+ } catch (e) {}
- if (values) {
- this.setValues(values);
+ if (values) {
+ this.setValues(values);
+ }
}
-};
-TextTrackSettings.prototype.saveSettings = function() {
- if (!this.player_.options()['persistTextTrackSettings']) {
- return;
+ saveSettings() {
+ if (!this.player_.options()['persistTextTrackSettings']) {
+ return;
+ }
+
+ let values = this.getValues();
+ try {
+ if (!Lib.isEmpty(values)) {
+ window.localStorage.setItem('vjs-text-track-settings', JSON.stringify(values));
+ } else {
+ window.localStorage.removeItem('vjs-text-track-settings');
+ }
+ } catch (e) {}
}
- let values = this.getValues();
- try {
- if (!Lib.isEmpty(values)) {
- window.localStorage.setItem('vjs-text-track-settings', JSON.stringify(values));
- } else {
- window.localStorage.removeItem('vjs-text-track-settings');
+ updateDisplay() {
+ let ttDisplay = this.player_.getChild('textTrackDisplay');
+ if (ttDisplay) {
+ ttDisplay.updateDisplay();
}
- } catch (e) {}
-};
-
-TextTrackSettings.prototype.updateDisplay = function() {
- let ttDisplay = this.player_.getChild('textTrackDisplay');
- if (ttDisplay) {
- ttDisplay.updateDisplay();
}
-};
+
+}
+
+Component.registerComponent('TextTrackSettings', TextTrackSettings);
function getSelectedOptionValue(target) {
let selectedOption;
diff --git a/src/js/tracks/text-track.js b/src/js/tracks/text-track.js
index ea453e5631..25c9741008 100644
--- a/src/js/tracks/text-track.js
+++ b/src/js/tracks/text-track.js
@@ -28,7 +28,6 @@ import XHR from '../xhr.js';
* attribute EventHandler oncuechange;
* };
*/
-
let TextTrack = function(options) {
options = options || {};
@@ -227,7 +226,7 @@ TextTrack.prototype.removeCue = function(removeCue) {
/*
* Downloading stuff happens below this point
*/
-let parseCues = function(srcContent, track) {
+var parseCues = function(srcContent, track) {
if (typeof window['WebVTT'] !== 'function') {
//try again a bit later
return window.setTimeout(function() {
diff --git a/src/js/video.js b/src/js/video.js
index dc87a89b6a..bdbec35044 100644
--- a/src/js/video.js
+++ b/src/js/video.js
@@ -1,14 +1,14 @@
import document from 'global/document';
-import MediaLoader from './media/loader';
-import Html5 from './media/html5';
-import Flash from './media/flash';
-import PosterImage from './poster';
-import { TextTrackDisplay } from './tracks/text-track-controls';
-import LoadingSpinner from './loading-spinner';
-import BigPlayButton from './big-play-button';
-import ControlBar from './control-bar/control-bar';
-import ErrorDisplay from './error-display';
+import MediaLoader from './tech/loader.js';
+import Html5 from './tech/html5.js';
+import Flash from './tech/flash.js';
+import PosterImage from './poster-image.js';
+import TextTrackDisplay from './tracks/text-track-display.js';
+import LoadingSpinner from './loading-spinner.js';
+import BigPlayButton from './big-play-button.js';
+import ControlBar from './control-bar/control-bar.js';
+import ErrorDisplay from './error-display.js';
import videojs from './core';
import * as setup from './setup';
diff --git a/test/api/api.js b/test/api/api.js
index ff3bf717b7..483f6ad96b 100644
--- a/test/api/api.js
+++ b/test/api/api.js
@@ -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() {
- 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
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() {
- var media = videojs.getComponent('MediaTechController');
+ var media = videojs.getComponent('Tech');
var mediaProto = media.prototype;
var html5 = videojs.getComponent('Html5');
var html5Proto = html5.prototype;
diff --git a/test/unit/component.js b/test/unit/component.js
index 4fde5c5088..82b0016214 100644
--- a/test/unit/component.js
+++ b/test/unit/component.js
@@ -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(){
- var CompWithContent = Component.extend();
+ class CompWithContent extends Component {}
+
CompWithContent.prototype.createEl = function(){
// Create the main componenent element
var el = Lib.createEl('div');
diff --git a/test/unit/controls.js b/test/unit/controls.js
index 1ada41fda3..562ec86bb1 100644
--- a/test/unit/controls.js
+++ b/test/unit/controls.js
@@ -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 PlaybackRateMenuButton from '../../src/js/control-bar/playback-rate-menu-button.js';
-import Slider from '../../src/js/slider.js';
+import PlaybackRateMenuButton from '../../src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js';
+import Slider from '../../src/js/slider/slider.js';
import TestHelpers from './test-helpers.js';
import document from 'global/document';
diff --git a/test/unit/flash.js b/test/unit/flash.js
index 66c1b69c2d..8bbd07d601 100644
--- a/test/unit/flash.js
+++ b/test/unit/flash.js
@@ -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';
q.module('Flash');
diff --git a/test/unit/media.html5.js b/test/unit/media.html5.js
index d3c2541698..2f5de2269c 100644
--- a/test/unit/media.html5.js
+++ b/test/unit/media.html5.js
@@ -1,6 +1,6 @@
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 document from 'global/document';
diff --git a/test/unit/media.js b/test/unit/media.js
index 647386ec44..9944029c8a 100644
--- a/test/unit/media.js
+++ b/test/unit/media.js
@@ -1,16 +1,16 @@
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', {
'setup': function() {
this.noop = function() {};
this.clock = sinon.useFakeTimers();
- this.featuresProgessEvents = MediaTechController.prototype['featuresProgessEvents'];
- MediaTechController.prototype['featuresProgressEvents'] = false;
- MediaTechController.prototype['featuresNativeTextTracks'] = true;
- oldTextTracks = MediaTechController.prototype.textTracks;
- MediaTechController.prototype.textTracks = function() {
+ this.featuresProgessEvents = Tech.prototype['featuresProgessEvents'];
+ Tech.prototype['featuresProgressEvents'] = false;
+ Tech.prototype['featuresNativeTextTracks'] = true;
+ oldTextTracks = Tech.prototype.textTracks;
+ Tech.prototype.textTracks = function() {
return {
addEventListener: Function.prototype,
removeEventListener: Function.prototype
@@ -19,15 +19,15 @@ q.module('Media Tech', {
},
'teardown': function() {
this.clock.restore();
- MediaTechController.prototype['featuresProgessEvents'] = this.featuresProgessEvents;
- MediaTechController.prototype['featuresNativeTextTracks'] = false;
- MediaTechController.prototype.textTracks = oldTextTracks;
+ Tech.prototype['featuresProgessEvents'] = this.featuresProgessEvents;
+ Tech.prototype['featuresNativeTextTracks'] = false;
+ Tech.prototype.textTracks = oldTextTracks;
}
});
test('should synthesize timeupdate events by default', function() {
var timeupdates = 0, playHandler, i, tech;
- tech = new MediaTechController({
+ tech = new Tech({
id: this.noop,
on: function(event, handler) {
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() {
var timeupdates = 0, tech, playHandler, expected;
- tech = new MediaTechController({
+ tech = new Tech({
id: this.noop,
off: this.noop,
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() {
var timeupdates = 0, tech, playHandler, pauseHandler, expected;
- tech = new MediaTechController({
+ tech = new Tech({
id: this.noop,
on: function(event, handler) {
if (event === 'play') {
@@ -110,7 +110,7 @@ test('stops manual timeupdates while paused', function() {
test('should synthesize progress events by default', function() {
var progresses = 0, tech;
- tech = new MediaTechController({
+ tech = new Tech({
id: this.noop,
on: this.noop,
bufferedPercent: function() {
@@ -131,7 +131,7 @@ test('should synthesize progress events by default', function() {
});
test('dispose() should stop time tracking', function() {
- var tech = new MediaTechController({
+ var tech = new Tech({
id: this.noop,
on: 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' };
// Define a new tech class
- var Tech = MediaTechController.extend();
+ var MyTech = Tech.extend();
// Extend Tech with source handlers
- MediaTechController.withSourceHandlers(Tech);
+ Tech.withSourceHandlers(MyTech);
// Check for the expected class methods
- ok(Tech.registerSourceHandler, 'added a registerSourceHandler function to the Tech');
- ok(Tech.selectSourceHandler, 'added a selectSourceHandler function to the Tech');
+ ok(MyTech.registerSourceHandler, 'added a registerSourceHandler function to the Tech');
+ ok(MyTech.selectSourceHandler, 'added a selectSourceHandler function to the Tech');
// Create an instance of Tech
- var tech = new Tech(mockPlayer);
+ var tech = new MyTech(mockPlayer);
// Check for the expected instance methods
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
- Tech.registerSourceHandler(handlerOne);
- strictEqual(Tech.sourceHandlers[0], handlerOne, 'handlerOne was added to the source handler array');
- Tech.registerSourceHandler(handlerTwo, 0);
- strictEqual(Tech.sourceHandlers[0], handlerTwo, 'handlerTwo was registered at the correct index (0)');
+ MyTech.registerSourceHandler(handlerOne);
+ strictEqual(MyTech.sourceHandlers[0], handlerOne, 'handlerOne was added to the source handler array');
+ MyTech.registerSourceHandler(handlerTwo, 0);
+ strictEqual(MyTech.sourceHandlers[0], handlerTwo, 'handlerTwo was registered at the correct index (0)');
// Test handler selection
- strictEqual(Tech.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(sourceA), handlerOne, 'handlerOne was selected to handle the valid source');
+ strictEqual(MyTech.selectSourceHandler(sourceB), null, 'no handler was selected to handle the invalid source');
// Test canPlaySource return values
- strictEqual(Tech.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(sourceA), 'probably', 'the Tech returned probably for the valid 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
tech.setSource(sourceA);
@@ -239,14 +239,14 @@ test('should handle unsupported sources with the source hanlder API', function()
};
// Define a new tech class
- var Tech = MediaTechController.extend();
+ var MyTech = Tech.extend();
// Extend Tech with source handlers
- MediaTechController.withSourceHandlers(Tech);
+ Tech.withSourceHandlers(MyTech);
// Create an instance of Tech
- var tech = new Tech(mockPlayer);
+ var tech = new MyTech(mockPlayer);
var usedNative;
- Tech.nativeSourceHandler = {
+ MyTech.nativeSourceHandler = {
handleSource: function(){ usedNative = true; }
};
diff --git a/test/unit/mediafaker.js b/test/unit/mediafaker.js
index 408a7948aa..f0b0b4501c 100644
--- a/test/unit/mediafaker.js
+++ b/test/unit/mediafaker.js
@@ -1,58 +1,57 @@
// Fake a media playback tech controller so that player tests
// 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 Component from '../../src/js/component.js';
/**
* @constructor
*/
-var MediaFaker = MediaTechController.extend({
- init: function(player, options, onReady){
- MediaTechController.call(this, player, options, onReady);
+class MediaFaker extends Tech {
+ constructor(player, options, onReady){
+ super(player, options, onReady);
this.triggerReady();
}
-});
-
-// Support everything except for "video/unsupported-format"
-MediaFaker.isSupported = function(){ return true; };
-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'
- });
- if (this.player().poster()) {
- // transfer the poster image to mimic HTML
- el.poster = this.player().poster();
+
+ createEl() {
+ var el = super.createEl('div', {
+ className: 'vjs-tech'
+ });
+
+ if (this.player().poster()) {
+ // transfer the poster image to mimic HTML
+ el.poster = this.player().poster();
+ }
+
+ Lib.insertFirst(el, this.player_.el());
+
+ return el;
}
- Lib.insertFirst(el, this.player_.el());
-
- return el;
-};
-
-// fake a poster attribute to mimic the video element
-MediaFaker.prototype.poster = function(){ return this.el().poster; };
-MediaFaker.prototype['setPoster'] = function(val){ this.el().poster = val; };
-
-MediaFaker.prototype.currentTime = function(){ return 0; };
-MediaFaker.prototype.seeking = function(){ return false; };
-MediaFaker.prototype.src = function(){ return 'movie.mp4'; };
-MediaFaker.prototype.volume = function(){ return 0; };
-MediaFaker.prototype.muted = function(){ return false; };
-MediaFaker.prototype.pause = function(){ return false; };
-MediaFaker.prototype.paused = function(){ return true; };
-MediaFaker.prototype.play = function() {
- this.player().trigger('play');
-};
-MediaFaker.prototype.supportsFullScreen = function(){ return false; };
-MediaFaker.prototype.buffered = function(){ return {}; };
-MediaFaker.prototype.duration = function(){ return {}; };
-MediaFaker.prototype.networkState = function(){ return 0; };
-MediaFaker.prototype.readyState = function(){ return 0; };
+ // fake a poster attribute to mimic the video element
+ poster() { return this.el().poster; }
+ setPoster(val) { this.el().poster = val; }
+
+ currentTime() { return 0; }
+ seeking() { return false; }
+ src() { return 'movie.mp4'; }
+ volume() { return 0; }
+ muted() { return false; }
+ pause() { return false; }
+ paused() { return true; }
+ play() { this.player().trigger('play'); }
+ supportsFullScreen() { return false; }
+ buffered() { return {}; }
+ duration() { return {}; }
+ networkState() { return 0; }
+ readyState() { 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);
-module.exports = MediaFaker;
\ No newline at end of file
+module.exports = MediaFaker;
diff --git a/test/unit/menu.js b/test/unit/menu.js
index 19ceca5207..5fbde6afcc 100644
--- a/test/unit/menu.js
+++ b/test/unit/menu.js
@@ -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';
q.module('MenuButton');
diff --git a/test/unit/player.js b/test/unit/player.js
index 3a84b79c8a..f18105760c 100644
--- a/test/unit/player.js
+++ b/test/unit/player.js
@@ -3,7 +3,7 @@ import videojs from '../../src/js/core.js';
import Options from '../../src/js/options.js';
import * as Lib from '../../src/js/lib.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 document from 'global/document';
diff --git a/test/unit/poster.js b/test/unit/poster.js
index a0a0091803..04f82d0640 100644
--- a/test/unit/poster.js
+++ b/test/unit/poster.js
@@ -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 TestHelpers from './test-helpers.js';
import document from 'global/document';
diff --git a/test/unit/tracks/text-track-controls.js b/test/unit/tracks/text-track-controls.js
index a39baf4155..f8fcde9f00 100644
--- a/test/unit/tracks/text-track-controls.js
+++ b/test/unit/tracks/text-track-controls.js
@@ -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 * as Lib from '../../../src/js/lib.js';
diff --git a/test/unit/tracks/tracks.js b/test/unit/tracks/tracks.js
index a3cd07e7c6..41a95f1800 100644
--- a/test/unit/tracks/tracks.js
+++ b/test/unit/tracks/tracks.js
@@ -1,10 +1,11 @@
-import { CaptionsButton } from '../../../src/js/tracks/text-track-controls.js';
-import { SubtitlesButton } from '../../../src/js/tracks/text-track-controls.js';
-import { ChaptersButton } from '../../../src/js/tracks/text-track-controls.js';
-import { TextTrackDisplay } from '../../../src/js/tracks/text-track-controls.js';
-import Html5 from '../../../src/js/media/html5.js';
-import Flash from '../../../src/js/media/flash.js';
-import MediaTechController from '../../../src/js/media/media.js';
+import ChaptersButton from '../../../src/js/control-bar/text-track-controls/chapters-button.js';
+import SubtitlesButton from '../../../src/js/control-bar/text-track-controls/subtitles-button.js';
+import CaptionsButton from '../../../src/js/control-bar/text-track-controls/captions-button.js';
+
+import TextTrackDisplay from '../../../src/js/tracks/text-track-display.js';
+import Html5 from '../../../src/js/tech/html5.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 * as Lib from '../../../src/js/lib.js';
@@ -155,9 +156,9 @@ test('update texttrack buttons on removetrack or addtrack', function() {
oldChaptersUpdate.call(this);
};
- MediaTechController.prototype['featuresNativeTextTracks'] = true;
- oldTextTracks = MediaTechController.prototype.textTracks;
- MediaTechController.prototype.textTracks = function() {
+ Tech.prototype['featuresNativeTextTracks'] = true;
+ oldTextTracks = Tech.prototype.textTracks;
+ Tech.prototype.textTracks = function() {
return {
length: 0,
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');
- MediaTechController.prototype.textTracks = oldTextTracks;
- MediaTechController.prototype['featuresNativeTextTracks'] = false;
+ Tech.prototype.textTracks = oldTextTracks;
+ Tech.prototype['featuresNativeTextTracks'] = false;
CaptionsButton.prototype.update = oldCaptionsUpdate;
SubtitlesButton.prototype.update = oldSubsUpdate;
ChaptersButton.prototype.update = oldChaptersUpdate;