'use strict';
import $ from 'jquery';
import Control from './components/Control';
import Component from './components/Component';
import PlayButton from './components/PlayButton';
import SplashIcon from './components/SplashIcon';
import Icon from './components/Icon';
import Time from './components/Timeline/Time';
import ControlCollection from './components/ControlCollection';
import Sections from './components/Sections';
import ErrorDisplay from './components/ErrorDisplay';
import Poster from './components/Poster';
import FullscreenApi from './FullscreenApi';
import { createEl, secondsToTime, noop } from './utils';
import {
IS_ANDROID_PHONE,
IS_ANDROID,
IS_IPOD,
IS_IPHONE,
IS_MOBILE,
IS_TOUCH
} from './utils/browser';
import Cookie from './utils/cookie';
import MediaError from './MediaError';
// Register common controls
import './components/PlayControl';
import './components/VolumeControl';
import './components/Timeline/TimelineControl';
import './components/SectionControl';
import './components/FullscreenControl';
import './components/RateControl';
import './components/BackwardControl';
import './components/SourceControl';
import './components/SubtitleControl';
import './components/DownloadControl';
import './components/KeybindingInfoControl';
import './components/TimeInfoControl';
import './entity/Html5';
import 'array.prototype.find';
Control.registerControl('divider', function() {
return {
element : $('<div/>').addClass('divider')
};
});
/**
* Return array with excluded dist's items from source array
*
* @access private
* @param {Array} source
* @param {Array} dist
* @return {Array}
*/
function excludeArray(source, dist) {
const result = [].concat(source);
dist.forEach(item => {
const index = result.indexOf(item);
if (index > -1) {
result.splice(index, 1);
return
}
})
return result;
}
const defaultOptions = {
entity : 'Html5',
autoplay : false,
height : 'auto',
loop : false,
muted : false,
preload : 'metadata',
poster : null,
svgPath : '',
innactivityTimeout : 5000,
rate : {
step : 0.25,
min : 0.5,
max : 4.0,
default : 1
},
playback : {
step : {
short : 5,
medium : 10,
long : 30
}
},
controls : {
common : [
['play', 'volume', 'divider', 'timeline', 'divider', 'section', 'divider', 'fullscreen'],
['rate', 'divider', 'backward', 'divider', 'source', 'divider', 'subtitle', 'divider', 'download', 'divider', 'keybinding info']
],
fullscreen : [
['play', 'volume', 'divider', 'timeline', 'divider', 'rate', 'divider', 'keybinding info', 'divider', 'backward', 'divider', 'source', 'divider', 'subtitle', 'divider', 'download', 'divider', 'section', 'divider', 'fullscreen']
],
mini : [
['play', 'volume', 'divider', 'fullscreen', 'divider', 'timeinfo']
],
'common:android' : [
['play', 'timeline', 'fullscreen'],
['rate', 'source', 'section']
],
'fullscreen:mobile' : [
['play', 'timeline', 'fullscreen'],
['rate', 'source', 'section']
],
'common:ios' : [
['play', 'rate', 'timeline', 'source']
],
},
controlsOptions : {
common : {
align : ['justify', 'left'],
// mobile : true
},
fullscreen : {
align : 'justify'
},
'common:android' : {
align : ['justify', 'right']
},
'fullscreen:mobile' : {
align : ['justify', 'right']
}
},
volume : {
default : 0.4,
mutelimit : 0.05,
step : 0.1
},
keyBinding : [
{
key : 32,
info : ['Space'],
description : 'Начать проигрывание / поставить на паузу',
fn : (player) => {
player.video.togglePlay();
}
},
{
key : 37,
info : ['←'],
description : `Перемотать на 10 секунд назад`,
fn : (player) => {
player.video.currentTime -= player.options.playback.step.medium;
player.splashIcon.show('undo');
}
},
{
key : 39,
info : ['→'],
description : `Перемотать на 10 секунд вперёд`,
fn : (player) => {
player.video.currentTime += player.options.playback.step.medium;
player.splashIcon.show('redo');
}
},
{
shiftKey : true,
info : ['Shift', '←'],
description : 'Перейти на предыдущую секцию',
key : 37,
fn : (player) => {
if(player.sections == null) {
return;
}
player.sections.prev();
}
},
{
shiftKey : true,
key : 39,
info : ['Shift', '→'],
description : 'Перейти на следующую секцию',
fn : (player) => {
if(player.sections == null) {
return;
}
player.sections.next()
}
},
{
key : 38,
info : ['↑'],
description : 'Увеличить громкость',
fn : (player) => {
player.video.volume += player.options.volume.step;
player.splashIcon.show(player.calcVolumeIcon(player.video.volume));
}
},
{
key : 40,
info : ['↓'],
description : 'Уменьшить громкость',
fn : (player) => {
player.video.volume -= player.options.volume.step;
player.splashIcon.show(player.calcVolumeIcon(player.video.volume));
}
},
{
key : 70,
info : ['f'],
description : 'Открыть/закрыть полноэкраный режим',
fn : (player) => {
player.toggleFullscreen();
}
}
],
plugins : {
miniplayer : {}
},
onPlayerInited : noop
};
/**
* @class Player
* @extends Component
* @param {jQuery} element Element when player will init
* @param {Object} [options]
* @param {Boolean} [options.autoplay=false]
* When present, the video will automatically start playing as soon as it can do so without stopping.
* @param {String|Number} [options.height='auto'] Height of video container
* @param {String} [options.width] Width of video container
* @param {Boolean} [options.loop=false]
* When present, it specifies that the video will start over again, every time it is finished.
* @param {Boolean} [options.muted=false]
* When present, it specifies that the audio output of the video should be muted.
* @param {String} [options.preload='metadata'] Can be ('auto'|'metadata'|'none')
* @param {String} [options.poster] Path to poster of video
* @param {String} [options.svgPath] Path to svg sprite for icons
* @param {Object} [options.rate] Rate options
* @param {Number} [options.rate.step=0.25] Step of increase/decrease by rate control
* @param {Number} [options.rate.min=0.5] Min of rate
* @param {Number} [options.rate.max=4.0] Max of rate
* @param {Number} [options.rate.default=1]
* @param {Object} [options.playback] Playback options
* @param {Object} [options.playback.step]
* @param {Nubmer} [options.playback.step.short=5]
* @param {Nubmer} [options.playback.step.medium=30]
* @param {Nubmer} [options.playback.step.long=60]
* @param {Obejct} [options.controls] Object of controls
* @param {String[]} [options.controls.common] Array of controls for default view
* @param {String[]} [options.controls.fullscreen] Array of control for fullsreen view
* @param {String[]} [options.controls.mini] Array of control for miniplayer
* @param {Object} [options.excludeControls] Object of exclude controls. Structure is the same as that of options.controls
* @param {Object} [options.volume] Volume's options
* @param {Number} [options.volume.default=0.4] Default volume
* @param {Number} [options.volume.mutelimit=0.05] Delta when volume is muted
* @param {Number} [options.volume.step=0.05]
* @param {Object[]} [options.keybinding]
* Object with keybinding options, when key it's name of key binding, and value it's key binding settings
* @param {Number} [options.keybinding[].key] Code of key binding (for example 32 it's space)
* @param {String[]} [options.keybinding[].info] Array of keystrokes order
* @param {String} options.keybinding[].description] Description of key binding
* @param {Function} options.keybinding[].fn] Callback
* @param {Object|Boolean} [options.miniplayer=false]
* @param {String} [options.miniplayer.width] Width of miniplayer container
* @param {String} [options.miniplayer.width] MiniPlayer's width
* @param {String} [options.sectionContainer] Selector for sections
* @param {Object} [options.plugins] Keys of objects are name of plugin, value - plugin options
* @param {String|Object} [options.data] Url or JSON with data for player
* @param {Array} [options.data.sections] Sections array
*/
class Player extends Component {
constructor(element, options) {
options.createElement = false;
super(null, options);
this._element = element;
/**
* DOM container to hold inner of player
*
* @memberof! Player#
* @type {jQuery}
*/
this.innerElement = createEl('div');
// Users options
this._userOptions = options;
this._initOptions();
if(this.options.svgPath === '') {
Player._loadSVGSprite(Player.defaultSprite);
}
this._view = 'common';
/**
* DOM container to hold all player
*
* @memberof! Player#
* @type {jQuery}
*/
this.element = this.createElement();
this.loadEntity(this.options.entity, { ctx : element });
/**
* Video html5 component
*
* @memberof! Player#
* @type {Entity}
*/
this.video = this.entity;
// Create controls
// TODO: move this action to the createElement
this.controls = {};
this._initControls();
/**
* @access private
*/
this._dblclickTimeout = null;
this._initSections().then((data) => {
/**
* Sections init event
*
* @event Player#sectionsinit
* @example
* const player = new Player($('#video'), options);
* player.on('sectionsinit', (e, data) => cosnole.log(data));
*
*/
this.trigger('sectionsinit', data);
});
this._initPlugins();
this._listenHotKeys();
this._userActivity = false;
this._listenUserActivity();
this._waitingTimeouts = [];
/* Retrigger {@link Entity} Events */
[
/**
* durationchange player event
*
* @event Player#durationchange
*/
'durationchange',
/**
* progress html5 media event
*
* @event Player#progress
*/
'progress',
/**
* dblclick
*
* @event Player#dbclick
*/
'dblclick',
/**
* dblclick
*
* @event Player#dbclick
*/
'click',
/**
* canplay html5 media event
*
* @event Player#canplay
*/
'canplay',
/**
* qualitychange html5
*
* @event Player#qualitychange
*/
'qualitychange',
/**
* qualitychange html5
*
* @event Player#trackschange
*/
'trackschange',
].forEach(eventName => {
this.video.on(eventName, () => {
this.trigger(eventName);
})
});
this.video.one('play', () => {
/**
* First play event
*
* @event Player#firstplay
*/
this.trigger('firstplay');
this.removeClass('leplayer--virgin');
});
this.video.on('timeupdate', () => {
if (this.video.currentTime > 0) {
this.removeClass('leplayer--virgin');
}
/**
* timeupdate html5 media event
*
* @event Player#timeupdate
*/
this.trigger('timeupdate', { time : this.video.currentTime, duration : this.video.duration });
})
this.video.on('loadstart', () => {
this.removeClass('leplayer--ended');
this.error = null;
/**
* loadstart player event
*
* @event Player#loadstart
*/
this.trigger('loadstart');
});
this.video.on('seeking', () => {
this._startWaiting();
/**
* seeking html5 media event
*
* @event Player#seeking
*/
this.trigger('seeking');
});
this.video.on('seeked', () => {
this._stopWayting();
/**
* seeked html5 media event
*
* @event Player#seeked
*/
this.trigger('seeked');
});
this.video.on('volumechange', () => {
/**
* volumechange html5 media event
*
* @event Player#volumechange
*/
this.trigger('volumechange', { volume : this.video.volume });
});
this.video.on('posterchange', (e, data) => {
const url = data.url;
this.poster.url = url;
this.trigger('posterchange');
});
this.video.on('play', (e) => {
this.removeClass('leplayer--ended');
this.removeClass('leplayer--paused');
this.addClass('leplayer--playing');
/**
* play html5 media event
*
* @event Player#play
*/
this.trigger('play');
});
this.video.on('pause', () => {
this.removeClass('leplayer--playing');
this.addClass('leplayer--paused');
/**
* pause html5 media event
*
* @event Player#pause
*/
this.trigger('pause');
});
this.video.on('playing', () => {
this._stopWayting();
/**
* playing html5 media event
*
* @event Player#playing
*/
this.trigger('playing');
});
this.video.on('ratechange', () => {
/**
* rate html5 media event
*
* @event Player#rate
*/
this.trigger('ratechange', { rate : this.video.rate });
});
this.video.on('ended', () => {
this.addClass('leplayer--ended');
if(this.options.loop) {
this.currentTime = 0;
this.video.play();
} else if (!this.video.paused) {
this.video.pause();
}
/**
* ended html5 media event
*
* @event Player#ended
*/
this.trigger('ended');
});
this.video.on('canplaythrough', () => {
this._stopWayting();
/**
* canplaythrough html5 media event
*
* @event Player#canplaythrough
*/
this.trigger('canplaythrough');
});
this.video.on('waiting', () => {
this._startWaiting();
this.video.one('timeupdate', () => this._stopWayting());
/**
* waiting html5 media event
*
* @event Player#waiting
*/
this.trigger('waiting');
});
this.video.on('error', (e, data) => {
this.error = new MediaError(data.code);
});
this.video.init().then(() => {
/**
* Player init event
*
* @event Player#inited
*/
this.trigger('inited');
if(this.options.time) {
this.currentTime = this.options.time;
}
if(this.video.src != null && this.options.autoplay) {
this.play();
}
});
this.on('fullscreenchange', this._onFullscreenChange.bind(this));
this.on('click', this._onClick.bind(this));
this.on('dblclick', this._onDbclick.bind(this));
this.on('inited', this._onInited.bind(this));
this.on('play', this._onPlay.bind(this));
this.on('pause', this._onPause.bind(this));
$(document).on(FullscreenApi.fullscreenchange, this._onEntityFullscrenChange.bind(this));
}
get entity() {
return this._entity;
}
loadEntity(name, options) {
const Entity = Player.getComponent(name);
this._entity = new Entity(this, options);
}
/**
* Starts playing the video
*
*
* @access public
* @example
* const player = new Player($("#video"),options);
* $('.some-button').on('click', () => player.play());
*/
play() {
return this.video.play();
}
/**
* Pauses the currently playing video
*
* @access public
*/
pause() {
return this.video.pause();
}
/**
* Toggle the currently playing video
*
* @access public
*/
togglePlay() {
return this.video.togglePlay();
}
/**
* Begin loading the src data
*
* @access public
*/
load() {
return this.video.load();
}
/**
* On set view callback
*
* @access public
* @param {String} view View name
* @returns {Player} this
* @example
* const player = new Player($('#video'), options);
* player.onSetView('mini', () => console.log('Miniplayer yeah!')
* .onSetView('fullscreen', () => console.log('Fullscreen boom!')
* .onSetView('common', () => console.log('Common view - lol');
*/
onSetView(view, ...args) {
this.on(`setview.${view}`, ...args);
return this;
}
/**
* Change source and save time, rate
*
* @access public
* @param {Object} quality
* @param {String} [quality.title] The name of qualitut e.x SD or HD
* @param {String} quality.url
*/
changeQuality(quality) {
const video = this.video;
if(quality == null) return;
const time = this.currentTime;
const rate = this.rate;
const isPaused = this.paused;
video.src = quality;
this.playbackRate = rate;
this.currentTime = time;
if(isPaused) {
this.pause()
} else {
this.play()
}
}
/**
* On del view callback
*
* @access public
* @param {String} view View name
* @returns {Player} this
* @example
* const player = new Player($('#video'), options);
* player.onDelView('mini', () => console.log('Exit miniplayer')
*/
onDelView(view, ...args) {
this.on(`delview.${view}`, ...args);
return this;
}
/**
* Get some data for player
*
* @access public
* @returns {jQuery.promise} Promise
*/
getData() {
const dfd = new $.Deferred();
if (this._data !== undefined) {
dfd.resolve(this._data);
return dfd.promise()
}
if (typeof this.options.data === 'string') {
return $.ajax({
url : this.options.data,
method : 'GET',
dataType : 'json'
}).promise();
} else if (typeof this.options.data === 'object') {
dfd.resolve(this.options.data);
return dfd.promise()
}
}
getSectionData() {
return this.getData()
.then(data => {
return data.sections
})
}
/**
* Request fullscreen
*
* @access public
* @fires Player#fullscreenchange
*/
requestFullscreen() {
const fsApi = FullscreenApi;
if(fsApi.requestFullscreen) {
// Call HTML5 Video api requestFullscreen
this.element[0][fsApi.requestFullscreen]();
/**
* fullscreenchange html5 media event
*
* @event Player#fullscreenchange
*/
this.trigger('fullscreenchange', true);
} else if (this.video.supportsFullScreen()) {
this.video.enterFullscreen();
}
}
/**
* Exit fullscreen
*
* @access public
* @fires Player#fullscreenchange
*/
exitFullscreen() {
const fsApi = FullscreenApi;
if(fsApi.exitFullscreen) {
document[fsApi.exitFullscreen]();
} else if (this.video.supportsFullScreen()) {
this.video.exitFullscreen();
}
this.trigger('fullscreenchange', false);
}
/**
* Toggle fullscreen
*
* @access public
* @fires Player#fullscreenchange
*/
toggleFullscreen() {
if(this.view === 'fullscreen') {
this.exitFullscreen()
} else {
this.requestFullscreen()
}
}
/**
* Get ControlCollection of Player by name (e.x 'common', 'fullscreen')
*
* @access public
* @param {String} name - Name of ControlCollection
* @returns {ControlCollection}
*/
getControls(name) {
return this.controls[name];
}
/**
* Return the width of player.
*
* @access public
* @returns {Number} Width in px
*/
getWidth() {
return this.element.width()
}
/**
* Complete the sections, by the additional field 'end' in each section
*
* @access private
* @param {Object} sections - Sections
* @returns {Object} New sections
*/
_completeSections(sections) {
if (sections == null || sections.length === 0) {
return
}
let newSections = [].concat(sections)
for (let i = 0; i < newSections.length; i++) {
let endSection;
if (i < (newSections.length - 1)) {
endSection = newSections[i+1].begin
} else {
endSection = this.video.duration;
}
newSections[i].end = endSection;
}
return newSections;
}
/**
* Get and set the current playback position in the audio/video (in seconds)
* Getter and setter
*
* @access public
* @memberof! Player#
* @type {Nubmer}
*/
get currentTime() {
return this.video.currentTime;
}
set currentTime(value) {
this.video.currentTime = value;
}
/**
* Returns the length of the current audio/video (in seconds)
* Getter
*
* @access public
* @memberof! Player#
* @type {Nubmer}
*/
get duration() {
return this.video.duration;
}
/**
* Returns whether the playback of the audio/video has ended or not
* Getter
*
* @memberof! Player#
* @type {Boolean}
*/
get ended() {
return this.video.ended;
}
/**
* Returns and set whether the playback of the audio/video has ended or not
* Getter and setter
*
* @access public
* @memberof! Player#
* @type {MediaError|String}
* @fires Player#error
*/
get error() {
return this._error || null;
}
set error(value) {
if (value === null) {
this._error = null;
this.removeClass('leplayer--error');
if(this.errorDisplay) {
this.errorDisplay.element.hide()
}
return this;
}
this._error = new MediaError(value);
this.addClass('leplayer--error');
/**
* error event
*
* @event Player#error
* @property {MediaError} error
* @example
* const player = new Player($('#video'), options);
* player.on('error', (e, data) => console.error(data.error));
*/
this.trigger('error', { error : this._error});
return this;
}
get rate() {
return this.video.rate;
}
set rate(value) {
this.video.rate = value;
}
get paused() {
return this.video.paused;
}
/**
* Return the height of player. If you want get height only of video element, use this.video.height or whatever
*
* @access public
* @type {Number}
* @memberof! Player#
*/
get height() {
return this.element.height()
}
/**
* Return unnecessary video heigth
* @access public
* @type {Number}
* @memberof! Player#
*/
get videoHeight() {
return this.video.height;
}
/**
* @access public
* @type {Boolean}
* @mebmerof! Player#
*/
get userActive() {
return this._userActive || false;
}
set userActive(value) {
if(value !== this.getUserActive) {
this._userActive = value;
this.toggleClass('leplayer--user-active', value);
/**
* User active event
*
* @event Player#useractive
*/
this.trigger('useractive');
}
}
/**
* Set and get player view. View Can be 'common', 'fullscreen', 'mini'w
*
* @access public
* @type {String}
* @memberof! Player#
*/
get view() {
return this._view;
}
set view(view) {
if(this.view != null) {
this.removeClass(`leplayer--${this.view}`);
this.trigger(`delview.${this.view}`);
}
this._view = view;
this.element.addClass(`leplayer--${view}`)
this.trigger(`setview.${view}`);
return this;
}
/**
* Remove unnecessary attributes, and set some attrs from options (loop, poster etc...). Create main DOM objects
*
* @override
*/
createElement() {
const options = this.options;
const element = this._element;
this.element = createEl('div');
this.element = this.element
.addClass('leplayer')
.attr('tabindex', 0)
.css('width', options.width && '100%')
.css('max-width', options.width)
/**
* Error display component.
*
* @type {ErrorDisplay}
* @memberof! Player#
*/
this.errorDisplay = new ErrorDisplay(this);
/**
* Play button component.
*
* @type {PlayButton}
* @memberof! Player#
*/
this.playButton = new PlayButton(this);
// TODO: Вынести это в отдельнеый компонент
this.loader = $('<div />')
.addClass('leplayer-loader-container')
.append(new Icon(this, {
iconName : 'refresh',
className : 'leplayer-loader-container__icon'
}).element);
/**
* Splash icon component.
*
* @type {SplashIcon}
* @memberof! Player#
*/
this.splashIcon = new SplashIcon(this);
this.videoContainer = createEl('div', {
className : 'leplayer-video'
})
.append(this.errorDisplay.element)
.append(this.playButton.element)
.append(this.loader)
.append(this.splashIcon.element)
this.poster = new Poster(this);
this.videoContainer.append(this.poster.element);
const lastTimer = new Time(this, {
fn : (player) => {
const video = player.video;
return secondsToTime(video.duration - video.currentTime);
}
})
if(this.options.videoInfo) {
console.warn('options.videoInfo is deprecated, please use istead options.description');
}
this.infoElement = createEl('div', {
className : 'leplayer__info'
})
.append(createEl('div', {
className : 'leplayer__title',
html : this.options.title || ""
}))
.append(createEl('div', {
className : 'leplayer__video-info',
html : this.options.description || this.options.videoInfo || ""
}))
.append(createEl('div', {
className : 'leplayer__last',
html : `Видео закончится через `,
}).append(lastTimer.element))
this.innerElement = $('<div />')
.addClass('leplayer__inner')
.append(this.videoContainer)
.append(this.infoElement)
this.element = this.element
.append(this.innerElement)
this.addClass('leplayer--paused');
this.addClass('leplayer--virgin');
if(IS_IPHONE) {
this.addClass('leplayer--iphone');
}
if(IS_ANDROID) {
this.addClass('leplayer--android');
}
if(IS_MOBILE) {
this.addClass('leplayer--mobile');
}
if(options.sectionContainer) {
this.sectionsContainer = $(options.sectionContainer);
}
element.before(this.element);
this.videoContainer.append(element);
return this.element;
}
/**
* Get options from video's attribute ( height, width, poster, preload etc...)
* Get source video from src attr or <source> element with data attr 'data-quality'
* Also get sources for different quality from <source> element with data attr 'data-quality'
*
* @access private
* @returns {Object} options
*/
_optionsFromElement() {
// Copy video attrs to the opitons
const element = this._element;
if (element == null || element.length === 0) {
return {}
}
let attrOptions = [
'height',
'width',
'poster',
'autoplay',
'loop',
'muted',
'preload',
]
.reduce((obj, item) => {
const val = element.attr(item);
if(val != null) {
obj[item] = element.attr(item);
}
return obj;
}, {});
attrOptions.sources = [];
// Src it is main source, that will be load
if(element.attr('src')) {
attrOptions.src = {
url : element.attr('src'),
title : element.attr('data-quality') || element.attr('title') || 'default'
}
}
// Copy sources from HTML5 source element with data-quality attr
// If data-quality attr does not exist - no
element.find('source').each((i, item) => {
item = $(item);
if(!item.attr('data-quality')) {
return
}
attrOptions.sources = attrOptions.sources.concat({
url : item.attr('src'),
title : item.attr('data-quality') || item.attr('title') || 'default'
})
});
return attrOptions;
}
/**
* Return a name of icon. If less then 0.1 return volume-off,
* if less then 0.5 return volume down, else return volume-up
*
* @access private
* @param {Number} value Volume value
* @returns {String} Icon name
*/
calcVolumeIcon(value) {
if(value == null) {
value = this.video.volume;
}
const volume = value;
if (volume < this.options.volume.mutelimit) {
return 'volume-off';
} else if (value < 0.5) {
return 'volume-down';
} else {
return 'volume-up';
}
}
toggleSections(flag) {
if(this.sections) {
this.sections.visible = flag;
}
if(this.outsideSections) {
this.outsideSections.visible = flag;
}
}
/**
* Merge defaultOptions, presetOptions with attrOptions and user's options;
*
* And complement two objects: controls and excludeControls
*
* @access private
*/
_initOptions() {
const attrOptions = this._optionsFromElement();
let presetOptions = {};
if (this._userOptions.preset && Player.getPreset(this._userOptions.preset)) {
presetOptions = Player.getPreset(this._userOptions.preset).options;
}
// Merge default options + preset options + video attributts+ user options
this.options = $.extend(true, {}, defaultOptions, presetOptions, attrOptions, this._userOptions);
if(this.options.sources && !Array.isArray(this.options.sources)) {
this.options.sources = [this.options.sources]
}
if(typeof this.options.src === 'string') {
this.options.src = { url : this.options.src }
}
if(this.options.src == null && this.options.sources.length > 0) {
this.options.src = this.options.sources[0]
}
// Generate android:fullscreen, android:common and etc controls options
// Merge correctly controls, without deep merge
this.options.controls = $.extend({}, defaultOptions.controls, presetOptions.controls, this._userOptions.controls);
// exclude controls option
// TODO(adinvadim):
// Set depreceted flag for this option;
for (const name in this.options.excludeControls) {
if (!this.options.excludeControls.hasOwnProperty(name)) return;
const controlCollection = this.options.excludeControls[name];
controlCollection.forEach((row, index) => {
if (this.options.controls[name] && this.options.controls[name][index]) {
this.options.controls[name][index] = excludeArray(this.options.controls[name][index], row);
}
})
}
if (this.options.preset && Player.getPreset(this.options.preset)) {
Player.getPreset(this.options.preset).initOptions();
}
}
/**
* Create and init all controls
*
* @access private
*/
_initControls() {
for (const name of ['common', 'fullscreen']) {
if (!this.options.controls.hasOwnProperty(name)) return;
const controlCollection = new ControlCollection(this, { name });
this.element.append(controlCollection.element);
}
if (this.controls.common != null) {
this.controls.common.active = true;
}
}
_listenHotKeys() {
const isKeyBinding = (e, binding) => {
return ((e.which === binding.key) || (e.key === binding.key)) &&
(!!binding.shiftKey === e.shiftKey) &&
(!!binding.ctrlKey === e.ctrlKey)
}
this.element.on('keydown.leplayer.hotkey', (e) => {
this.options.keyBinding.forEach(binding => {
if(isKeyBinding(e, binding)) {
e.preventDefault();
binding.fn(this);
return false;
}
})
})
}
/**
* Init sections, get ajax or json with sections data and create Sections object and added them to the DOM
*
* @access private
* @returns {jqPromise} jQuery promise
*/
_initSections() {
const dfd = $.Deferred();
if (this.options.data == null) {
dfd.reject(null)
} else {
this.getSectionData().done(sections => {
sections = [...sections];
const isSectionOutside = (this.sectionsContainer && this.sectionsContainer.length > 0);
if (sections == null || sections.length === 0) {
dfd.reject(null);
return;
}
sections = this._completeSections(sections);
this.sections = new Sections(this, {
items : sections,
fullscreenOnly : isSectionOutside,
hideScroll : true
});
this.innerElement.append(this.sections.element);
if (isSectionOutside) {
this.outsideSections = new Sections(this, {
items : sections
});
this.sectionsContainer.append(this.outsideSections.element);
}
dfd.resolve({ items : sections });
})
}
return dfd.promise()
}
/**
* Function, than init all plugins from player options.
* If plugin doesn't exist throw an error
*
* @access private
* @returns {Player} this
*/
_initPlugins() {
if (this.options.plugins) {
for (const name in this.options.plugins) {
if(!this.options.plugins.hasOwnProperty(name)) return;
const pluginOptions = this.options.plugins[name];
if(this[name]) {
if(pluginOptions) {
this[name](pluginOptions);
}
} else {
console.error(`Plugin '${name}' doesn't exist`);
}
}
}
return this;
}
/**
* @access private
*/
_listenUserActivity() {
let mouseInProgress;
let lastMoveX;
let lastMoveY;
const onMouseMove = (e) => {
if(e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
lastMoveX = e.screenX;
lastMoveY = e.screenY;
this._userActivity = true
}
}
const onMouseDown = (e) => {
this._userActivity = true
// While user is pressing mouse or touch, dispatch user activity
clearInterval(mouseInProgress);
mouseInProgress = setInterval(() => {
this._userActivity = true
}, 250);
}
const onMouseUp = (e) => {
this._userActivity = true
clearInterval(mouseInProgress);
}
this.element.on('mousemove', onMouseMove);
this.element.on('mousedown', onMouseDown);
this.element.on('mouseup', onMouseUp);
this.element.on('keydown', (e) => this._userActivity = true);
this.element.on('keyup', (e) => this._userActivity = true);
// See http://ejohn.org/blog/learning-from-twitter/
let inactivityTimeout;
const delay = this.options.innactivityTimeout;
setInterval(() => {
if (this._userActivity) {
// Reset user activuty tracker
this._userActivity = false;
this.userActive = true;
clearTimeout(inactivityTimeout);
if (delay > 0) {
inactivityTimeout = setTimeout(() => {
if (!this._userActivity) {
this.userActive = false;
}
}, delay);
}
}
}, 250)
}
/**
* Stop showing spinner and clear delay of showing spinner
*
* @access private
*/
_stopWayting() {
this._waitingTimeouts.forEach(item => clearTimeout(item));
this._waitingTimeouts = [];
this.removeClass('leplayer--waiting');
}
/**
* Show spinner with delay in 300ms
*
* @access private
*/
_startWaiting() {
this._waitingTimeouts.push(setTimeout(() => {
this.addClass('leplayer--waiting');
}, 300));
}
/**
* On inited player event handler
*
* @access private
* @param {PlayerEvent} e
*/
_onInited(e) {
this.addClass('leplayer--inited');
this.options.onPlayerInited.call(this, e);
}
/**
* On click video event handler. Focus on video and togglePlay
*
* @access private
* @param {PlayerEvent} e
*/
_onClick(e) {
clearTimeout(this._dblclickTimeout);
const togglePlay = () => {
this._dblclickTimeout = setTimeout(() => {
this.video.element.focus()
this.togglePlay();
}, 300);
}
/**
* See LPLR-290
* On touch devices in fullscreen if user not active we don't should toggle
* At first we show him a controls
*/
if(IS_TOUCH() && this.view === 'fullscreen') {
if(this.player.userActive) {
togglePlay()
}
} else {
togglePlay()
}
}
/**
* On dblclick on the video player event handler
*
* @access private
* @param {PlayerEvent} e
*/
_onDbclick(e) {
clearTimeout(this._dblclickTimeout);
this.toggleFullscreen();
}
/**
* On fullscreen change player event handler
*
* @access private
* @param {PlayerEvent} e
*/
_onFullscreenChange(e, isFs) {
if(isFs) {
this.view = 'fullscreen';
// Hide sections by default on mobile fullscreen
if(IS_ANDROID) {
this._lastSectionsValue = this.sections.visible;
this.sections.visible = false;
}
this.focus();
} else {
this.view = 'common';
if(IS_ANDROID) {
this.sections.visible = this._lastSectionsValue;
}
// Pause video on exit fullscreeen on mobile
if(IS_ANDROID_PHONE || IS_IPHONE || IS_IPOD) {
this.pause();
}
}
}
/**
* On play event handler
*
* @access private
* @param {PlayerEvent} e
*/
_onPlay() {
this.splashIcon.show('play');
}
/**
* On pause player event handler
* Show pause icon in the center of video
*
* @access private
*/
_onPause() {
this.splashIcon.show('pause');
}
_onEntityFullscrenChange() {
const fsApi = FullscreenApi;
const isFs = !!document[fsApi.fullscreenElement];
this.trigger('fullscreenchange', isFs);
}
}
/**
* Static helper for creating a plugins for leplayer
*
* @access public
* @static
* @param {String} name The name of plugin
* @param {Function} fn Plugin init function
*
* @example
* Player.plugin('helloWorld', function(pluginOptions) {
* const player = this;
* player.on('click', () => console.log('Hello world'));
* })
*
*/
Player.plugin = function(name, fn) {
Player.prototype[name] = fn;
}
/**
* Get by name registered component
*
* @param {String} name - Name of component
* @return {Component}
*/
Player.getComponent = Component.getComponent;
/**
* Register component
*
* @access public
* @static
* @param {String} name
* @param {Component} component
*
* @example
* Player.registerComponent('ErrorDisplay', ErrorDisplay);
*/
Player.registerComponent = Component.registerComponent;
/**
* Register control
*
* @access public
* @static
* @param {String} name
* @param {Control} control
*/
Player.getControl = Control.getControl;
/**
* Get by name registered control
*
* @access public
* @static
* @param {String} name
* @returns {Control}
*
* @example
* Player.registerControl('backward', BackwardControl);
*/
Player.registerControl = Control.registerControl;
/**
* Convert seconds to format string 'hh?:mm:ss'
*
* @access public
* @param {Number} seconds Seconds
* @param {Boolean} showHours convert to format 'hh:mm:ss'
* @returns {String}
*/
Player.secondsToTime = secondsToTime;
/**
* Static helper for creating a plugins for leplayer
*
* @access public
* @static
* @param {String} name The name of plugin
* @param {Function|Object} fn Plugin init function
*
* @example
* Player.preset('common', {
* width : '100%',
* plugins : {
* miniplayer : true
* }
* });
*/
Player.preset = function(name, obj) {
if(typeof obj === 'object') {
Player._presets[name] = $.extend({}, {
options : {},
initOptions : noop
}, obj);
} else if (typeof obj === 'function') {
Player._presets[name] = obj();
}
};
Player.getPreset = function(name) {
if(Player._presets[name]) {
return Player._presets[name];
} else {
console.error(`preset ${name} doesn't exist`);
return null;
}
}
Player._presets = {};
Player.Cookie = Cookie;
Player._loadSVGSprite = function(svg) {
const hiddenElement = $('<div/>').hide();
$('body').prepend(hiddenElement.append(svg));
return hiddenElement;
}
Player.defaultSprite = require('../../dist/svg/svg-defs.svg');
/* global VERSION */
Player.version = VERSION;
window.$.fn.lePlayer = function (options) {
return this.each(function () {
return new Player($(this), options);
});
};
window.$.lePlayer = Player;
window.lePlayer = Player;
/**
* Mini Player plugin
*
* @plugin
*/
Player.plugin('miniplayer', function(pluginOptions) {
const player = this;
// Мержим с this.options.miniplayer, чтобы не сломать обратную совместимось, так как раньше
// миниплеер не был плагином плеера.
const options = $.extend({}, {
width : '100%',
offsetShow : (player) => {
const offset = player.element.offset().top
+ player.element.outerHeight()
- player.getControls('common').element.height();
return offset;
}
}, this.options.miniplayer, pluginOptions);
const controls = new ControlCollection(this, {
name : 'mini',
controls : options.controls,
controlOptions : {
control : {
disable : false
}
}
});
// Вставляем в infoElement под title и description
this.infoElement.append(controls.element);
/**
* Return offset on oY , when miniplayer should showing or hiding
*
* @returns {Number}
*/
const offsetShow = () => {
if ($.isFunction(options.offsetShow)) {
return options.offsetShow(player);
}
return options.offsetShow
}
const getWidth = () => {
return options.width || this.element.width();
}
this._updateMiniPlayer = (e, force) => {
const scrollTop = $(window).scrollTop();
// Because in force update, for normally count height and padding
// miniplayer before the show must first be hidden
if(force) {
this.hideMiniPlayer(force);
}
if(scrollTop > offsetShow()) {
this.showMiniPlayer(force);
} else {
this.hideMiniPlayer();
}
}
this.showMiniPlayer = (force) => {
if (!force && this.view === 'mini') {
return;
}
// Added empty space
this.element.css('padding-top', this.videoContainer.height());
this.view ='mini';
}
this.hideMiniPlayer = (force) => {
if(!force && this.view !== 'mini') {
return;
}
this.view = 'common';
}
$(window).on('scroll', this._updateMiniPlayer.bind(this));
$(window).on('resize', this._updateMiniPlayer.bind(this));
this.on('inited', (e) => this._updateMiniPlayer(e, true));
this.onSetView('mini', () => {
this.innerElement.css('max-width', getWidth());
this.innerElement.css('height', this.video.height);
});
this.onDelView('mini', () => {
this.innerElement.css('max-width', '')
this.innerElement.css('height', '')
this.element.css('padding-top', '');
});
this._updateMiniPlayer();
});
Player.preset('vps', require('./presets/vps.js').preset);
Player.preset('simple', require('./presets/simple.js').preset);
Player.preset('sms', require('./presets/sms.js').preset);
Player.preset('compressed', require('./presets/compressed.js').preset);
Player.preset('2035', require('./presets/2035.js').preset);
module.exports = Player