UNPKG

le-player

Version:

The best HTML5 video player made for Lectoriy.

1,918 lines (1,608 loc) 48.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>le-player.js - Documentation</title> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav> <li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading">Classes</li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Player.html">Player</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#.plugin">plugin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#.preset">preset</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#changeQuality">changeQuality</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#createElement">createElement</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#exitFullscreen">exitFullscreen</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#getControls">getControls</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#getData">getData</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#getWidth">getWidth</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#load">load</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#onDelView">onDelView</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#onSetView">onSetView</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#pause">pause</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#play">play</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#requestFullscreen">requestFullscreen</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#toggleFullscreen">toggleFullscreen</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Player.html#togglePlay">togglePlay</a></span></li><li class="nav-heading">Events</li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:canplay">canplay</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:canplaythrough">canplaythrough</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:dbclick">dbclick</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:durationchange">durationchange</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:ended">ended</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:error">error</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:firstplay">firstplay</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:fullscreenchange">fullscreenchange</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:inited">inited</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:loadstart">loadstart</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:pause">pause</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:play">play</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:playing">playing</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:progress">progress</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:qualitychange">qualitychange</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:rate">rate</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:sectionsinit">sectionsinit</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:seeked">seeked</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:seeking">seeking</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:timeupdate">timeupdate</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:trackschange">trackschange</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:useractive">useractive</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:volumechange">volumechange</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="Player.html#event:waiting">waiting</a></span></li> </nav> <div id="main"> <h1 class="page-title">le-player.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>'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 : $('&lt;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 &amp;&amp; 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 &lt; newSections.length; i++) { let endSection; if (i &lt; (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 &amp;&amp; '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 = $('&lt;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 = $('&lt;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 &lt;source> element with data attr 'data-quality' * Also get sources for different quality from &lt;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 &lt; this.options.volume.mutelimit) { return 'volume-off'; } else if (value &lt; 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 &amp;&amp; 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 &amp;&amp; !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 &amp;&amp; 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] &amp;&amp; this.options.controls[name][index]) { this.options.controls[name][index] = excludeArray(this.options.controls[name][index], row); } }) } if (this.options.preset &amp;&amp; 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)) &amp;&amp; (!!binding.shiftKey === e.shiftKey) &amp;&amp; (!!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 &amp;&amp; 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() &amp;&amp; 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 = $('&lt;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 &amp;&amp; this.view === 'mini') { return; } // Added empty space this.element.css('padding-top', this.videoContainer.height()); this.view ='mini'; } this.hideMiniPlayer = (force) => { if(!force &amp;&amp; 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</code></pre> </article> </section> </div> <br class="clear"> <footer> Generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Sat Aug 03 2019 16:05:24 GMT+0300 (MSK) using the Minami theme. </footer> <script>prettyPrint();</script> <script src="scripts/linenumber.js"></script> </body> </html>