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

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

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

View File

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

View File

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

View File

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

View File

@ -12,13 +12,10 @@ import document from 'global/document';
* @class * @class
* @constructor * @constructor
*/ */
var Button = Component.extend({ class Button extends Component {
/**
* @constructor constructor(player, options) {
* @inheritDoc super(player, options);
*/
init: function(player, options){
Component.call(this, player, options);
this.emitTapEvents(); this.emitTapEvents();
@ -27,64 +24,65 @@ var Button = Component.extend({
this.on('focus', this.onFocus); this.on('focus', this.onFocus);
this.on('blur', this.onBlur); this.on('blur', this.onBlur);
} }
});
Component.registerComponent('Button', Button); createEl(type, props) {
// Add standard Aria and Tabindex info
props = Lib.obj.merge({
className: this.buildCSSClass(),
'role': 'button',
'aria-live': 'polite', // let the screen reader user know that the text of the button may change
tabIndex: 0
}, props);
Button.prototype.createEl = function(type, props){ let el = super.createEl(type, props);
// Add standard Aria and Tabindex info
props = Lib.obj.merge({
className: this.buildCSSClass(),
'role': 'button',
'aria-live': 'polite', // let the screen reader user know that the text of the button may change
tabIndex: 0
}, props);
let el = Component.prototype.createEl.call(this, type, props); // if innerHTML hasn't been overridden (bigPlayButton), add content elements
if (!props.innerHTML) {
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-control-content'
});
// if innerHTML hasn't been overridden (bigPlayButton), add content elements this.controlText_ = Lib.createEl('span', {
if (!props.innerHTML) { className: 'vjs-control-text',
this.contentEl_ = Lib.createEl('div', { innerHTML: this.localize(this.buttonText) || 'Need Text'
className: 'vjs-control-content' });
});
this.controlText_ = Lib.createEl('span', { this.contentEl_.appendChild(this.controlText_);
className: 'vjs-control-text', el.appendChild(this.contentEl_);
innerHTML: this.localize(this.buttonText) || 'Need Text' }
});
this.contentEl_.appendChild(this.controlText_); return el;
el.appendChild(this.contentEl_);
} }
return el; buildCSSClass() {
}; // TODO: Change vjs-control to vjs-button?
return 'vjs-control ' + super.buildCSSClass();
Button.prototype.buildCSSClass = function(){ }
// TODO: Change vjs-control to vjs-button?
return 'vjs-control ' + Component.prototype.buildCSSClass.call(this);
};
// Click - Override with specific functionality for button // Click - Override with specific functionality for button
Button.prototype.onClick = function(){}; onClick() {}
// Focus - Add keyboard functionality to element // Focus - Add keyboard functionality to element
Button.prototype.onFocus = function(){ onFocus() {
Events.on(document, 'keydown', Lib.bind(this, this.onKeyPress)); Events.on(document, 'keydown', Lib.bind(this, this.onKeyPress));
}; }
// KeyPress (document level) - Trigger click when keys are pressed // KeyPress (document level) - Trigger click when keys are pressed
Button.prototype.onKeyPress = function(event){ onKeyPress(event) {
// Check for space bar (32) or enter (13) keys // Check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) { if (event.which == 32 || event.which == 13) {
event.preventDefault(); event.preventDefault();
this.onClick(); this.onClick();
}
} }
};
// Blur - Remove keyboard triggers // Blur - Remove keyboard triggers
Button.prototype.onBlur = function(){ onBlur() {
Events.off(document, 'keydown', Lib.bind(this, this.onKeyPress)); Events.off(document, 'keydown', Lib.bind(this, this.onKeyPress));
}; }
}
Component.registerComponent('Button', Button);
export default Button; export default Button;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import Component from '../component';
import Button from '../button'; import Button from '../button';
/** /**
@ -8,33 +7,25 @@ import Button from '../button';
* @class * @class
* @extends vjs.Button * @extends vjs.Button
*/ */
var FullscreenToggle = Button.extend({ class FullscreenToggle extends Button {
/**
* @constructor
* @memberof vjs.FullscreenToggle
* @instance
*/
init: function(player, options){
Button.call(this, player, options);
}
});
Component.registerComponent('FullscreenToggle', FullscreenToggle); buildCSSClass() {
return 'vjs-fullscreen-control ' + super.buildCSSClass();
}
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.buttonText = 'Fullscreen'; FullscreenToggle.prototype.buttonText = 'Fullscreen';
FullscreenToggle.prototype.buildCSSClass = function(){ Button.registerComponent('FullscreenToggle', FullscreenToggle);
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');
}
};
export default FullscreenToggle; export default FullscreenToggle;

View File

@ -8,28 +8,25 @@ import * as Lib from '../lib';
* @param {Object=} options * @param {Object=} options
* @constructor * @constructor
*/ */
var LiveDisplay = Component.extend({ class LiveDisplay extends Component {
init: function(player, options){
Component.call(this, player, options); createEl() {
var el = super.createEl('div', {
className: 'vjs-live-controls vjs-control'
});
this.contentEl_ = Lib.createEl('div', {
className: 'vjs-live-display',
innerHTML: '<span class="vjs-control-text">' + this.localize('Stream Type') + '</span>' + this.localize('LIVE'),
'aria-live': 'off'
});
el.appendChild(this.contentEl_);
return el;
} }
});
}
Component.registerComponent('LiveDisplay', LiveDisplay); Component.registerComponent('LiveDisplay', LiveDisplay);
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: '<span class="vjs-control-text">' + this.localize('Stream Type') + '</span>' + this.localize('LIVE'),
'aria-live': 'off'
});
el.appendChild(this.contentEl_);
return el;
};
export default LiveDisplay; export default LiveDisplay;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,237 +0,0 @@
import Button from './button';
import Component from './component';
import * as Lib from './lib';
import * as Events from './events';
/* Menu
================================================================================ */
/**
* The Menu component is used to build pop up menus, including subtitle and
* captions selection menus.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
let Menu = Component.extend();
/**
* Add a menu item to the menu
* @param {Object|String} component Component or component type to add
*/
Menu.prototype.addItem = function(component){
this.addChild(component);
component.on('click', Lib.bind(this, function(){
this.unlockShowing();
}));
};
/** @inheritDoc */
Menu.prototype.createEl = function(){
let contentElType = this.options().contentElType || 'ul';
this.contentEl_ = Lib.createEl(contentElType, {
className: 'vjs-menu-content'
});
var el = Component.prototype.createEl.call(this, 'div', {
append: this.contentEl_,
className: 'vjs-menu'
});
el.appendChild(this.contentEl_);
// Prevent clicks from bubbling up. Needed for Menu Buttons,
// where a click on the parent is significant
Events.on(el, 'click', function(event){
event.preventDefault();
event.stopImmediatePropagation();
});
return el;
};
/**
* The component for a menu item. `<li>`
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
var MenuItem = Button.extend({
/** @constructor */
init: function(player, options){
Button.call(this, player, options);
this.selected(options['selected']);
}
});
/** @inheritDoc */
MenuItem.prototype.createEl = function(type, props){
return Button.prototype.createEl.call(this, 'li', Lib.obj.merge({
className: 'vjs-menu-item',
innerHTML: this.localize(this.options_['label'])
}, props));
};
/**
* Handle a click on the menu item, and set it to selected
*/
MenuItem.prototype.onClick = function(){
this.selected(true);
};
/**
* Set this menu item as selected or not
* @param {Boolean} selected
*/
MenuItem.prototype.selected = function(selected){
if (selected) {
this.addClass('vjs-selected');
this.el_.setAttribute('aria-selected',true);
} else {
this.removeClass('vjs-selected');
this.el_.setAttribute('aria-selected',false);
}
};
/**
* A button class with a popup menu
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var MenuButton = Button.extend({
/** @constructor */
init: function(player, options){
Button.call(this, player, options);
this.update();
this.on('keydown', this.onKeyPress);
this.el_.setAttribute('aria-haspopup', true);
this.el_.setAttribute('role', 'button');
}
});
MenuButton.prototype.update = function() {
let menu = this.createMenu();
if (this.menu) {
this.removeChild(this.menu);
}
this.menu = menu;
this.addChild(menu);
if (this.items && this.items.length === 0) {
this.hide();
} else if (this.items && this.items.length > 1) {
this.show();
}
};
/**
* Track the state of the menu button
* @type {Boolean}
* @private
*/
MenuButton.prototype.buttonPressed_ = false;
MenuButton.prototype.createMenu = function(){
var menu = new Menu(this.player_);
// Add a title list item to the top
if (this.options().title) {
menu.contentEl().appendChild(Lib.createEl('li', {
className: 'vjs-menu-title',
innerHTML: Lib.capitalize(this.options().title),
tabindex: -1
}));
}
this.items = this['createItems']();
if (this.items) {
// Add menu items to the menu
for (var i = 0; i < this.items.length; i++) {
menu.addItem(this.items[i]);
}
}
return menu;
};
/**
* Create the list of menu items. Specific to each subclass.
*/
MenuButton.prototype.createItems = function(){};
/** @inheritDoc */
MenuButton.prototype.buildCSSClass = function(){
return this.className + ' vjs-menu-button ' + Button.prototype.buildCSSClass.call(this);
};
// Focus - Add keyboard functionality to element
// This function is not needed anymore. Instead, the keyboard functionality is handled by
// treating the button as triggering a submenu. When the button is pressed, the submenu
// appears. Pressing the button again makes the submenu disappear.
MenuButton.prototype.onFocus = function(){};
// Can't turn off list display that we turned on with focus, because list would go away.
MenuButton.prototype.onBlur = function(){};
MenuButton.prototype.onClick = function(){
// When you click the button it adds focus, which will show the menu indefinitely.
// So we'll remove focus when the mouse leaves the button.
// Focus is needed for tab navigation.
this.one('mouseout', Lib.bind(this, function(){
this.menu.unlockShowing();
this.el_.blur();
}));
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
};
MenuButton.prototype.onKeyPress = function(event){
// Check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) {
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
event.preventDefault();
// Check for escape (27) key
} else if (event.which == 27){
if (this.buttonPressed_){
this.unpressButton();
}
event.preventDefault();
}
};
MenuButton.prototype.pressButton = function(){
this.buttonPressed_ = true;
this.menu.lockShowing();
this.el_.setAttribute('aria-pressed', true);
if (this.items && this.items.length > 0) {
this.items[0].el().focus(); // set the focus to the title of the submenu
}
};
MenuButton.prototype.unpressButton = function(){
this.buttonPressed_ = false;
this.menu.unlockShowing();
this.el_.setAttribute('aria-pressed', false);
};
Component.registerComponent('Menu', Menu);
Component.registerComponent('MenuButton', MenuButton);
Component.registerComponent('MenuItem', MenuItem);
export default Menu;
export { MenuItem, MenuButton };

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

@ -0,0 +1,141 @@
import Button from '../button.js';
import Menu from './menu.js';
import * as Lib from '../lib.js';
/**
* A button class with a popup menu
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class MenuButton extends Button {
constructor(player, options){
super(player, options);
this.update();
this.on('keydown', this.onKeyPress);
this.el_.setAttribute('aria-haspopup', true);
this.el_.setAttribute('role', 'button');
}
update() {
let menu = this.createMenu();
if (this.menu) {
this.removeChild(this.menu);
}
this.menu = menu;
this.addChild(menu);
/**
* Track the state of the menu button
* @type {Boolean}
* @private
*/
this.buttonPressed_ = false;
if (this.items && this.items.length === 0) {
this.hide();
} else if (this.items && this.items.length > 1) {
this.show();
}
}
createMenu() {
var menu = new Menu(this.player_);
// Add a title list item to the top
if (this.options().title) {
menu.contentEl().appendChild(Lib.createEl('li', {
className: 'vjs-menu-title',
innerHTML: Lib.capitalize(this.options().title),
tabindex: -1
}));
}
this.items = this['createItems']();
if (this.items) {
// Add menu items to the menu
for (var i = 0; i < this.items.length; i++) {
menu.addItem(this.items[i]);
}
}
return menu;
}
/**
* Create the list of menu items. Specific to each subclass.
*/
createItems(){}
/** @inheritDoc */
buildCSSClass() {
return this.className + ' vjs-menu-button ' + super.buildCSSClass();
}
// Focus - Add keyboard functionality to element
// This function is not needed anymore. Instead, the keyboard functionality is handled by
// treating the button as triggering a submenu. When the button is pressed, the submenu
// appears. Pressing the button again makes the submenu disappear.
onFocus() {}
// Can't turn off list display that we turned on with focus, because list would go away.
onBlur() {}
onClick() {
// When you click the button it adds focus, which will show the menu indefinitely.
// So we'll remove focus when the mouse leaves the button.
// Focus is needed for tab navigation.
this.one('mouseout', Lib.bind(this, function(){
this.menu.unlockShowing();
this.el_.blur();
}));
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
}
onKeyPress(event) {
// Check for space bar (32) or enter (13) keys
if (event.which == 32 || event.which == 13) {
if (this.buttonPressed_){
this.unpressButton();
} else {
this.pressButton();
}
event.preventDefault();
// Check for escape (27) key
} else if (event.which == 27){
if (this.buttonPressed_){
this.unpressButton();
}
event.preventDefault();
}
}
pressButton() {
this.buttonPressed_ = true;
this.menu.lockShowing();
this.el_.setAttribute('aria-pressed', true);
if (this.items && this.items.length > 0) {
this.items[0].el().focus(); // set the focus to the title of the submenu
}
}
unpressButton() {
this.buttonPressed_ = false;
this.menu.unlockShowing();
this.el_.setAttribute('aria-pressed', false);
}
}
Button.registerComponent('MenuButton', MenuButton);
export default MenuButton;

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

@ -0,0 +1,51 @@
import Button from '../button.js';
import * as Lib from '../lib.js';
/**
* The component for a menu item. `<li>`
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
class MenuItem extends Button {
constructor(player, options) {
super(player, options);
this.selected(options['selected']);
}
/** @inheritDoc */
createEl(type, props) {
return super.createEl('li', Lib.obj.merge({
className: 'vjs-menu-item',
innerHTML: this.localize(this.options_['label'])
}, props));
}
/**
* Handle a click on the menu item, and set it to selected
*/
onClick() {
this.selected(true);
}
/**
* Set this menu item as selected or not
* @param {Boolean} selected
*/
selected(selected) {
if (selected) {
this.addClass('vjs-selected');
this.el_.setAttribute('aria-selected',true);
} else {
this.removeClass('vjs-selected');
this.el_.setAttribute('aria-selected',false);
}
}
}
Button.registerComponent('MenuItem', MenuItem);
export default MenuItem;

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

@ -0,0 +1,52 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
import * as Events from '../events.js';
/* Menu
================================================================================ */
/**
* The Menu component is used to build pop up menus, including subtitle and
* captions selection menus.
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
class Menu extends Component {
/**
* Add a menu item to the menu
* @param {Object|String} component Component or component type to add
*/
addItem(component) {
this.addChild(component);
component.on('click', Lib.bind(this, function(){
this.unlockShowing();
}));
}
createEl() {
let contentElType = this.options().contentElType || 'ul';
this.contentEl_ = Lib.createEl(contentElType, {
className: 'vjs-menu-content'
});
var el = super.createEl('div', {
append: this.contentEl_,
className: 'vjs-menu'
});
el.appendChild(this.contentEl_);
// Prevent clicks from bubbling up. Needed for Menu Buttons,
// where a click on the parent is significant
Events.on(el, 'click', function(event){
event.preventDefault();
event.stopImmediatePropagation();
});
return el;
}
}
Component.registerComponent('Menu', Menu);
export default Menu;

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

@ -1,290 +0,0 @@
import Component from './component';
import * as Lib from './lib';
import document from 'global/document';
/* Slider
================================================================================ */
/**
* The base functionality for sliders like the volume bar and seek bar
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
let Slider = Component.extend({
/** @constructor */
init: function(player, options){
Component.call(this, player, options);
// Set property names to bar and handle to match with the child Slider class is looking for
this.bar = this.getChild(this.options_['barName']);
this.handle = this.getChild(this.options_['handleName']);
// Set a horizontal or vertical class on the slider depending on the slider type
this.vertical(!!this.options()['vertical']);
this.on('mousedown', this.onMouseDown);
this.on('touchstart', this.onMouseDown);
this.on('focus', this.onFocus);
this.on('blur', this.onBlur);
this.on('click', this.onClick);
this.on(player, 'controlsvisible', this.update);
this.on(player, this.playerEvent, this.update);
}
});
Component.registerComponent('Slider', Slider);
Slider.prototype.createEl = function(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider';
props = Lib.obj.merge({
'role': 'slider',
'aria-valuenow': 0,
'aria-valuemin': 0,
'aria-valuemax': 100,
tabIndex: 0
}, props);
return Component.prototype.createEl.call(this, type, props);
};
Slider.prototype.onMouseDown = function(event){
event.preventDefault();
Lib.blockTextSelection();
this.addClass('vjs-sliding');
this.on(document, 'mousemove', this.onMouseMove);
this.on(document, 'mouseup', this.onMouseUp);
this.on(document, 'touchmove', this.onMouseMove);
this.on(document, 'touchend', this.onMouseUp);
this.onMouseMove(event);
};
// To be overridden by a subclass
Slider.prototype.onMouseMove = function(){};
Slider.prototype.onMouseUp = function() {
Lib.unblockTextSelection();
this.removeClass('vjs-sliding');
this.off(document, 'mousemove', this.onMouseMove);
this.off(document, 'mouseup', this.onMouseUp);
this.off(document, 'touchmove', this.onMouseMove);
this.off(document, 'touchend', this.onMouseUp);
this.update();
};
Slider.prototype.update = function(){
// In VolumeBar init we have a setTimeout for update that pops and update to the end of the
// execution stack. The player is destroyed before then update will cause an error
if (!this.el_) return;
// If scrubbing, we could use a cached value to make the handle keep up with the user's mouse.
// On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later.
// var progress = (this.player_.scrubbing) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration();
let progress = this.getPercent();
let bar = this.bar;
// If there's no bar...
if (!bar) return;
// Protect against no duration and other division issues
if (typeof progress !== 'number' ||
progress !== progress ||
progress < 0 ||
progress === Infinity) {
progress = 0;
}
// If there is a handle, we need to account for the handle in our calculation for progress bar
// so that it doesn't fall short of or extend past the handle.
let barProgress = this.updateHandlePosition(progress);
// Convert to a percentage for setting
let percentage = Lib.round(barProgress * 100, 2) + '%';
// Set the new bar width or height
if (this.vertical()) {
bar.el().style.height = percentage;
} else {
bar.el().style.width = percentage;
}
};
/**
* Update the handle position.
*/
Slider.prototype.updateHandlePosition = function(progress) {
let handle = this.handle;
if (!handle) return;
let vertical = this.vertical();
let box = this.el_;
let boxSize, handleSize;
if (vertical) {
boxSize = box.offsetHeight;
handleSize = handle.el().offsetHeight;
} else {
boxSize = box.offsetWidth;
handleSize = handle.el().offsetWidth;
}
// The width of the handle in percent of the containing box
// In IE, widths may not be ready yet causing NaN
let handlePercent = (handleSize) ? handleSize / boxSize : 0;
// Get the adjusted size of the box, considering that the handle's center never touches the left or right side.
// There is a margin of half the handle's width on both sides.
let boxAdjustedPercent = 1 - handlePercent;
// Adjust the progress that we'll use to set widths to the new adjusted box width
let adjustedProgress = progress * boxAdjustedPercent;
// The bar does reach the left side, so we need to account for this in the bar's width
let barProgress = adjustedProgress + (handlePercent / 2);
let percentage = Lib.round(adjustedProgress * 100, 2) + '%';
if (vertical) {
handle.el().style.bottom = percentage;
} else {
handle.el().style.left = percentage;
}
return barProgress;
};
Slider.prototype.calculateDistance = function(event){
let el = this.el_;
let box = Lib.findPosition(el);
let boxW = el.offsetWidth;
let boxH = el.offsetHeight;
let handle = this.handle;
if (this.options()['vertical']) {
let boxY = box.top;
let pageY;
if (event.changedTouches) {
pageY = event.changedTouches[0].pageY;
} else {
pageY = event.pageY;
}
if (handle) {
var handleH = handle.el().offsetHeight;
// Adjusted X and Width, so handle doesn't go outside the bar
boxY = boxY + (handleH / 2);
boxH = boxH - handleH;
}
// Percent that the click is through the adjusted area
return Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
} else {
let boxX = box.left;
let pageX;
if (event.changedTouches) {
pageX = event.changedTouches[0].pageX;
} else {
pageX = event.pageX;
}
if (handle) {
var handleW = handle.el().offsetWidth;
// Adjusted X and Width, so handle doesn't go outside the bar
boxX = boxX + (handleW / 2);
boxW = boxW - handleW;
}
// Percent that the click is through the adjusted area
return Math.max(0, Math.min(1, (pageX - boxX) / boxW));
}
};
Slider.prototype.onFocus = function(){
this.on(document, 'keydown', this.onKeyPress);
};
Slider.prototype.onKeyPress = function(event){
if (event.which == 37 || event.which == 40) { // Left and Down Arrows
event.preventDefault();
this.stepBack();
} else if (event.which == 38 || event.which == 39) { // Up and Right Arrows
event.preventDefault();
this.stepForward();
}
};
Slider.prototype.onBlur = function(){
this.off(document, 'keydown', this.onKeyPress);
};
/**
* Listener for click events on slider, used to prevent clicks
* from bubbling up to parent elements like button menus.
* @param {Object} event Event object
*/
Slider.prototype.onClick = function(event){
event.stopImmediatePropagation();
event.preventDefault();
};
Slider.prototype.vertical_ = false;
Slider.prototype.vertical = function(bool) {
if (bool === undefined) {
return this.vertical_;
}
this.vertical_ = !!bool;
if (this.vertical_) {
this.addClass('vjs-slider-vertical');
} else {
this.addClass('vjs-slider-horizontal');
}
return this;
};
/**
* SeekBar Behavior includes play progress bar, and seek handle
* Needed so it can determine seek position based on handle position/size
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
var SliderHandle = Component.extend();
Component.registerComponent('Slider', Slider);
/**
* Default value of the slider
*
* @type {Number}
* @private
*/
SliderHandle.prototype.defaultValue = 0;
/** @inheritDoc */
SliderHandle.prototype.createEl = function(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider-handle';
props = Lib.obj.merge({
innerHTML: '<span class="vjs-control-text">'+this.defaultValue+'</span>'
}, props);
return Component.prototype.createEl.call(this, 'div', props);
};
export default Slider;
export { SliderHandle };

View File

@ -0,0 +1,28 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
/**
* SeekBar Behavior includes play progress bar, and seek handle
* Needed so it can determine seek position based on handle position/size
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class SliderHandle extends Component {
/** @inheritDoc */
createEl(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider-handle';
props = Lib.obj.merge({
innerHTML: '<span class="vjs-control-text">'+(this.defaultValue || 0)+'</span>'
}, props);
return super.createEl('div', props);
}
}
Component.registerComponent('SliderHandle', SliderHandle);
export default SliderHandle;

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

@ -0,0 +1,257 @@
import Component from '../component.js';
import * as Lib from '../lib.js';
import document from 'global/document';
/* Slider
================================================================================ */
/**
* The base functionality for sliders like the volume bar and seek bar
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
class Slider extends Component {
constructor(player, options) {
super(player, options);
// Set property names to bar and handle to match with the child Slider class is looking for
this.bar = this.getChild(this.options_['barName']);
this.handle = this.getChild(this.options_['handleName']);
// Set a horizontal or vertical class on the slider depending on the slider type
this.vertical(!!this.options()['vertical']);
this.on('mousedown', this.onMouseDown);
this.on('touchstart', this.onMouseDown);
this.on('focus', this.onFocus);
this.on('blur', this.onBlur);
this.on('click', this.onClick);
this.on(player, 'controlsvisible', this.update);
this.on(player, this.playerEvent, this.update);
}
createEl(type, props) {
props = props || {};
// Add the slider element class to all sub classes
props.className = props.className + ' vjs-slider';
props = Lib.obj.merge({
'role': 'slider',
'aria-valuenow': 0,
'aria-valuemin': 0,
'aria-valuemax': 100,
tabIndex: 0
}, props);
return super.createEl(type, props);
}
onMouseDown(event) {
event.preventDefault();
Lib.blockTextSelection();
this.addClass('vjs-sliding');
this.on(document, 'mousemove', this.onMouseMove);
this.on(document, 'mouseup', this.onMouseUp);
this.on(document, 'touchmove', this.onMouseMove);
this.on(document, 'touchend', this.onMouseUp);
this.onMouseMove(event);
}
// To be overridden by a subclass
onMouseMove() {}
onMouseUp() {
Lib.unblockTextSelection();
this.removeClass('vjs-sliding');
this.off(document, 'mousemove', this.onMouseMove);
this.off(document, 'mouseup', this.onMouseUp);
this.off(document, 'touchmove', this.onMouseMove);
this.off(document, 'touchend', this.onMouseUp);
this.update();
}
update() {
// In VolumeBar init we have a setTimeout for update that pops and update to the end of the
// execution stack. The player is destroyed before then update will cause an error
if (!this.el_) return;
// If scrubbing, we could use a cached value to make the handle keep up with the user's mouse.
// On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later.
// var progress = (this.player_.scrubbing) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration();
let progress = this.getPercent();
let bar = this.bar;
// If there's no bar...
if (!bar) return;
// Protect against no duration and other division issues
if (typeof progress !== 'number' ||
progress !== progress ||
progress < 0 ||
progress === Infinity) {
progress = 0;
}
// If there is a handle, we need to account for the handle in our calculation for progress bar
// so that it doesn't fall short of or extend past the handle.
let barProgress = this.updateHandlePosition(progress);
// Convert to a percentage for setting
let percentage = Lib.round(barProgress * 100, 2) + '%';
// Set the new bar width or height
if (this.vertical()) {
bar.el().style.height = percentage;
} else {
bar.el().style.width = percentage;
}
}
/**
* Update the handle position.
*/
updateHandlePosition(progress) {
let handle = this.handle;
if (!handle) return;
let vertical = this.vertical();
let box = this.el_;
let boxSize, handleSize;
if (vertical) {
boxSize = box.offsetHeight;
handleSize = handle.el().offsetHeight;
} else {
boxSize = box.offsetWidth;
handleSize = handle.el().offsetWidth;
}
// The width of the handle in percent of the containing box
// In IE, widths may not be ready yet causing NaN
let handlePercent = (handleSize) ? handleSize / boxSize : 0;
// Get the adjusted size of the box, considering that the handle's center never touches the left or right side.
// There is a margin of half the handle's width on both sides.
let boxAdjustedPercent = 1 - handlePercent;
// Adjust the progress that we'll use to set widths to the new adjusted box width
let adjustedProgress = progress * boxAdjustedPercent;
// The bar does reach the left side, so we need to account for this in the bar's width
let barProgress = adjustedProgress + (handlePercent / 2);
let percentage = Lib.round(adjustedProgress * 100, 2) + '%';
if (vertical) {
handle.el().style.bottom = percentage;
} else {
handle.el().style.left = percentage;
}
return barProgress;
}
calculateDistance(event){
let el = this.el_;
let box = Lib.findPosition(el);
let boxW = el.offsetWidth;
let boxH = el.offsetHeight;
let handle = this.handle;
if (this.options()['vertical']) {
let boxY = box.top;
let pageY;
if (event.changedTouches) {
pageY = event.changedTouches[0].pageY;
} else {
pageY = event.pageY;
}
if (handle) {
var handleH = handle.el().offsetHeight;
// Adjusted X and Width, so handle doesn't go outside the bar
boxY = boxY + (handleH / 2);
boxH = boxH - handleH;
}
// Percent that the click is through the adjusted area
return Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
} else {
let boxX = box.left;
let pageX;
if (event.changedTouches) {
pageX = event.changedTouches[0].pageX;
} else {
pageX = event.pageX;
}
if (handle) {
var handleW = handle.el().offsetWidth;
// Adjusted X and Width, so handle doesn't go outside the bar
boxX = boxX + (handleW / 2);
boxW = boxW - handleW;
}
// Percent that the click is through the adjusted area
return Math.max(0, Math.min(1, (pageX - boxX) / boxW));
}
}
onFocus() {
this.on(document, 'keydown', this.onKeyPress);
}
onKeyPress(event) {
if (event.which == 37 || event.which == 40) { // Left and Down Arrows
event.preventDefault();
this.stepBack();
} else if (event.which == 38 || event.which == 39) { // Up and Right Arrows
event.preventDefault();
this.stepForward();
}
}
onBlur() {
this.off(document, 'keydown', this.onKeyPress);
}
/**
* Listener for click events on slider, used to prevent clicks
* from bubbling up to parent elements like button menus.
* @param {Object} event Event object
*/
onClick(event) {
event.stopImmediatePropagation();
event.preventDefault();
}
vertical(bool) {
if (bool === undefined) {
return this.vertical_ || false;
}
this.vertical_ = !!bool;
if (this.vertical_) {
this.addClass('vjs-slider-vertical');
} else {
this.addClass('vjs-slider-horizontal');
}
return this;
}
}
Component.registerComponent('Slider', Slider);
export default Slider;

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

@ -3,9 +3,10 @@ import * as Lib from '../lib';
import * as Events from '../events'; import * as Events from '../events';
import window from 'global/window'; import window from 'global/window';
let TextTrackSettings = Component.extend({ class TextTrackSettings extends Component {
init: function(player, options) {
Component.call(this, player, options); constructor(player, options) {
super(player, options);
this.hide(); this.hide();
Events.on(this.el().querySelector('.vjs-done-button'), 'click', Lib.bind(this, function() { Events.on(this.el().querySelector('.vjs-done-button'), 'click', Lib.bind(this, function() {
@ -40,103 +41,104 @@ let TextTrackSettings = Component.extend({
this.restoreSettings(); this.restoreSettings();
} }
} }
});
createEl() {
return super.createEl('div', {
className: 'vjs-caption-settings vjs-modal-overlay',
innerHTML: captionOptionsMenuTemplate()
});
}
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')));
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;
}
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);
let fontPercent = values.fontPercent;
if (fontPercent) {
fontPercent = fontPercent.toFixed(2);
}
setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent);
}
restoreSettings() {
let values;
try {
values = JSON.parse(window.localStorage.getItem('vjs-text-track-settings'));
} catch (e) {}
if (values) {
this.setValues(values);
}
}
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) {}
}
updateDisplay() {
let ttDisplay = this.player_.getChild('textTrackDisplay');
if (ttDisplay) {
ttDisplay.updateDisplay();
}
}
}
Component.registerComponent('TextTrackSettings', TextTrackSettings); Component.registerComponent('TextTrackSettings', TextTrackSettings);
TextTrackSettings.prototype.createEl = function() {
return Component.prototype.createEl.call(this, 'div', {
className: 'vjs-caption-settings vjs-modal-overlay',
innerHTML: captionOptionsMenuTemplate()
});
};
TextTrackSettings.prototype.getValues = function() {
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')));
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;
};
TextTrackSettings.prototype.setValues = function(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);
let fontPercent = values.fontPercent;
if (fontPercent) {
fontPercent = fontPercent.toFixed(2);
}
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) {}
if (values) {
this.setValues(values);
}
};
TextTrackSettings.prototype.saveSettings = function() {
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) {}
};
TextTrackSettings.prototype.updateDisplay = function() {
let ttDisplay = this.player_.getChild('textTrackDisplay');
if (ttDisplay) {
ttDisplay.updateDisplay();
}
};
function getSelectedOptionValue(target) { function getSelectedOptionValue(target) {
let selectedOption; let selectedOption;
// not all browsers support selectedOptions, so, fallback to options // not all browsers support selectedOptions, so, fallback to options

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +1,57 @@
// Fake a media playback tech controller so that player tests // Fake a media playback tech controller so that player tests
// can run without HTML5 or Flash, of which PhantomJS supports neither. // can run without HTML5 or Flash, of which PhantomJS supports neither.
import MediaTechController from '../../src/js/media/media.js'; import Tech from '../../src/js/tech/tech.js';
import * as Lib from '../../src/js/lib.js'; import * as Lib from '../../src/js/lib.js';
import Component from '../../src/js/component.js'; import Component from '../../src/js/component.js';
/** /**
* @constructor * @constructor
*/ */
var MediaFaker = MediaTechController.extend({ class MediaFaker extends Tech {
init: function(player, options, onReady){
MediaTechController.call(this, player, options, onReady);
constructor(player, options, onReady){
super(player, options, onReady);
this.triggerReady(); this.triggerReady();
} }
});
// Support everything except for "video/unsupported-format" createEl() {
MediaFaker.isSupported = function(){ return true; }; var el = super.createEl('div', {
MediaFaker.canPlaySource = function(srcObj){ return srcObj.type !== 'video/unsupported-format'; }; className: 'vjs-tech'
});
MediaFaker.prototype.createEl = function(){ if (this.player().poster()) {
var el = MediaTechController.prototype.createEl.call(this, 'div', { // transfer the poster image to mimic HTML
className: 'vjs-tech' el.poster = this.player().poster();
}); }
if (this.player().poster()) {
// transfer the poster image to mimic HTML Lib.insertFirst(el, this.player_.el());
el.poster = this.player().poster();
return el;
} }
Lib.insertFirst(el, this.player_.el()); // fake a poster attribute to mimic the video element
poster() { return this.el().poster; }
setPoster(val) { this.el().poster = val; }
return el; 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; }
// fake a poster attribute to mimic the video element // Support everything except for "video/unsupported-format"
MediaFaker.prototype.poster = function(){ return this.el().poster; }; static isSupported() { return true; }
MediaFaker.prototype['setPoster'] = function(val){ this.el().poster = val; }; static canPlaySource(srcObj) { return srcObj.type !== 'video/unsupported-format'; }
}
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; };
Component.registerComponent('MediaFaker', MediaFaker); Component.registerComponent('MediaFaker', MediaFaker);
module.exports = MediaFaker; module.exports = MediaFaker;

View File

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

View File

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

View File

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

View File

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

View File

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