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:
@ -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))
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
2221
src/js/component.js
2221
src/js/component.js
File diff suppressed because it is too large
Load Diff
@ -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'
|
|
||||||
});
|
|
||||||
};
|
|
42
src/js/control-bar/current-time-display.js
Normal file
42
src/js/control-bar/current-time-display.js
Normal 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;
|
48
src/js/control-bar/duration-display.js
Normal file
48
src/js/control-bar/duration-display.js
Normal 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;
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 };
|
|
@ -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;
|
@ -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;
|
@ -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 };
|
|
64
src/js/control-bar/progress-control/load-progress-bar.js
Normal file
64
src/js/control-bar/progress-control/load-progress-bar.js
Normal 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;
|
22
src/js/control-bar/progress-control/play-progress-bar.js
Normal file
22
src/js/control-bar/progress-control/play-progress-bar.js
Normal 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;
|
27
src/js/control-bar/progress-control/progress-control.js
Normal file
27
src/js/control-bar/progress-control/progress-control.js
Normal 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;
|
93
src/js/control-bar/progress-control/seek-bar.js
Normal file
93
src/js/control-bar/progress-control/seek-bar.js
Normal 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;
|
43
src/js/control-bar/progress-control/seek-handle.js
Normal file
43
src/js/control-bar/progress-control/seek-handle.js
Normal 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;
|
46
src/js/control-bar/remaining-time-display.js
Normal file
46
src/js/control-bar/remaining-time-display.js
Normal 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;
|
@ -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;
|
49
src/js/control-bar/text-track-controls/captions-button.js
Normal file
49
src/js/control-bar/text-track-controls/captions-button.js
Normal 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;
|
109
src/js/control-bar/text-track-controls/chapters-button.js
Normal file
109
src/js/control-bar/text-track-controls/chapters-button.js
Normal 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;
|
@ -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;
|
@ -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;
|
22
src/js/control-bar/text-track-controls/subtitles-button.js
Normal file
22
src/js/control-bar/text-track-controls/subtitles-button.js
Normal 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;
|
65
src/js/control-bar/text-track-controls/text-track-button.js
Normal file
65
src/js/control-bar/text-track-controls/text-track-button.js
Normal 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;
|
@ -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;
|
@ -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 };
|
|
24
src/js/control-bar/time-divider.js
Normal file
24
src/js/control-bar/time-divider.js
Normal 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;
|
@ -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 };
|
|
74
src/js/control-bar/volume-control/volume-bar.js
Normal file
74
src/js/control-bar/volume-control/volume-bar.js
Normal 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;
|
47
src/js/control-bar/volume-control/volume-control.js
Normal file
47
src/js/control-bar/volume-control/volume-control.js
Normal 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;
|
24
src/js/control-bar/volume-control/volume-handle.js
Normal file
24
src/js/control-bar/volume-control/volume-handle.js
Normal 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;
|
22
src/js/control-bar/volume-control/volume-level.js
Normal file
22
src/js/control-bar/volume-control/volume-level.js
Normal 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;
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
@ -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;
|
|
237
src/js/menu.js
237
src/js/menu.js
@ -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
141
src/js/menu/menu-button.js
Normal 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
51
src/js/menu/menu-item.js
Normal 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
52
src/js/menu/menu.js
Normal 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;
|
3162
src/js/player.js
3162
src/js/player.js
File diff suppressed because it is too large
Load Diff
101
src/js/poster-image.js
Normal file
101
src/js/poster-image.js
Normal 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;
|
102
src/js/poster.js
102
src/js/poster.js
@ -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;
|
|
290
src/js/slider.js
290
src/js/slider.js
@ -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 };
|
|
28
src/js/slider/slider-handle.js
Normal file
28
src/js/slider/slider-handle.js
Normal 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
257
src/js/slider/slider.js
Normal 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;
|
@ -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
682
src/js/tech/html5.js
Normal 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;
|
@ -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
536
src/js/tech/tech.js
Normal 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;
|
@ -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 };
|
|
185
src/js/tracks/text-track-display.js
Normal file
185
src/js/tracks/text-track-display.js
Normal 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;
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
6
test/unit/controls.js
vendored
6
test/unit/controls.js
vendored
@ -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';
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user