UNPKG

le-player

Version:

The best HTML5 video player made for Lectoriy.

520 lines (419 loc) 10.2 kB
import $ from 'jquery'; import Component from '../components/Component'; import { IS_SAFARI, IS_IOS, IS_ANDROID } from '../utils/browser'; import Entity from './Entity'; class Html5 extends Entity { constructor (player, options) { super(player, options); this.media = this.element[0]; this.subtitles = []; this.bufferRanges = []; this.src = this.player.options.src; if(this.player.options.poster != null) { this.poster = this.player.options.poster; } if(this.getAvailableQualityLevels().length > 0) { this._playbackQuality = this.getAvailableQualityLevels()[0]; } this.element.on('loadstart', this.onLoadStart.bind(this)); this.element.on('timeupdate', this.onTimeUpdate.bind(this)); this.element.on('durationchange', this.onDurationChange.bind(this)); this.element.on('progress', this.onProgress.bind(this)); this.element.on('seeking', this.onSeeking.bind(this)); this.element.on('seeked', this.onSeeked.bind(this)); this.element.on('volumechange', this.onVolumeChange.bind(this)); this.element.on('click', this.onClick.bind(this)); this.element.on('dblclick', this.onDblclick.bind(this)); this.element.on('play', this.onPlay.bind(this)); this.element.on('pause', this.onPause.bind(this)); this.element.on('ratechange', this.onRateChange.bind(this)); this.element.on('ended', this.onEnded.bind(this)); this.element.on('canplaythrough', this.onCanplayThrough.bind(this)); this.element.on('waiting', this.onWaiting.bind(this)); this.element.on('error', this.onError.bind(this)); } onLoadStart(e) { this.trigger('loadstart'); } onTimeUpdate(e) { this.trigger('timeupdate'); } onDurationChange(e) { this.trigger('durationchange'); } onProgress(e) { this.trigger('progress'); } onSeeking(e) { this.trigger('seeking'); } onSeeked(e) { this.trigger('seeked'); } onVolumeChange(e) { this.trigger('volumechange'); } onClick() { this.trigger('click'); } onDblclick() { this.trigger('dblclick'); } onPlay() { this.trigger('play'); } onPause() { this.trigger('pause'); } onPlaying() { this.trigger('playing'); } onRateChange() { this.trigger('ratechange'); } onEnded() { this.trigger('ended'); } onCanplayThrough() { this.trigger('canplaythrough'); } onWaiting() { this.trigger('waiting'); } onError(e) { this.trigger('error', { code : e.target.error.code }); } /* TODO */ createElement() { this.element = this.options.ctx; [ // Remove controls because we dont not support native controls yet 'controls', 'poster', // It is unnecessary attrs, this functionality solve CSS 'height', 'width' ].forEach(item => { this.element.removeAttr(item); }); // Set attrs from options [ 'preload', 'autoplay', 'loop', 'muted' ].forEach(item => { if(this.player.options[item]) { this.element.attr(item, this.player.options[item]); this.element.prop(item, this.player.options[item]); } }); this.element.find('source[data-quality]').each((i, item) => { $(item).remove(); }); return this.element; } get currentTime () { return this.media.currentTime; } set currentTime (value) { let time; if (value > this.duration) { time = this.duration } else if (value < 0) { time = 0 } else { time = value; } this.player.trigger('timeupdateload', { currentTime : time }); this.media.currentTime = time; } get duration () { return this.media.duration; } get height () { return this.media.clientHeight; } get width () { return this.media.clientWidth; } get rate () { return this.media.playbackRate; } set muted(value) { this.media.muted = value; } get muted() { return this.media.muted } get rateMax() { let max = this.player.options.rate.max; if(IS_IOS || IS_ANDROID) { max = Html5.MOBILE_MAX_RATE; } if(IS_SAFARI) { max = Html5.SAFARI_MAX_RATE; } return max; } get rateMin() { let min = this.player.options.rate.min; if (IS_IOS || IS_ANDROID) { min = Html5.MOBILE_MIN_RATE; } if (IS_SAFARI) { min = Html5.SAFARI_MIN_RATE; } return min; } set rate (value) { super.rate = value; this.media.playbackRate = value; } getAvailableQualityLevels() { return this.player.options.sources.map(item => ({ name : item.title, ...item })); } set playbackQuality(name) { super.playbackQuality = name; const time = this.currentTime; const rate = this.rate; const stop = this.paused; this._playbackQuality = this.getAvailableQualityLevels().find(item => item.name === name); this.src = this._playbackQuality; this.rate = rate; this.currentTime = time; if (stop) { this.pause(); } else { this.play(); } this.trigger('qualitychange', this._playbackQuality); } get playbackQuality() { return this._playbackQuality; } set src (src) { if(src == null) return; if(this.src && this.src.url === src.url) return; this.media.src = src.url; this._source = src; } get src () { return this._source } get track () { return this._track; } set track (value) { [...this.media.textTracks].forEach(item => { if(value != null && item.language === value.language) { item.mode = 'showing' } else { item.mode = 'hidden'; } }); this._track = value; this.trigger('trackschange'); } get paused() { return this.media.paused; } get volume () { return this.media.volume; } set volume (value) { super.volume = value; const player = this.player; if (value > 1) { this.media.volume = 1; } else if (value < player.options.volume.mutelimit) { this.media.volume = 0; } else { this.media.volume = value; } this.media.mute = (value < player.options.volume.mutelimit); } get buffered () { return this.media.buffered; } /** * @return {TimeRanges} */ get seekable () { return this.media.seekable; } /** * @return {TimeRanges} */ get played() { return this.media.played; } get ended() { return this.media.ended; } get playedPercentage() { let result = 0; for (let i = 0; i < this.played.length; i++) { result += (this.played.end(i) - this.played.start(i)) } return result / this.duration * 100 } get currentSrc() { return this.media.currentSrc; } init () { super.init(); this.load(); let dfd = $.Deferred(); this._initSubtitles(); this._initVideo() .done(() => { this._initRate(); this._initVolume(); dfd.resolve(); }); return dfd.promise(); } supportsFullScreen() { if (typeof this.media.webkitEnterFullScreen === 'function') { const userAgent = window.navigator && window.navigator.userAgent || ''; // Seems to be broken in Chromium/Chrome && Safari in Leopard if ((/Android/).test(userAgent) || !(/Chrome|Mac OS X 10.5/).test(userAgent)) { return true; } } return false; } enterFullscreen() { const video = this.media; 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.play(); // playing and pausing synchronously during the transition to fullscreen // can get iOS ~6.1 devices into a play/pause loop setTimeout(() => { this.pause(); video.webkitEnterFullScreen(); }, 0); } else { video.webkitEnterFullScreen(); } } exitFullscreen() { this.media.webkitExitFullScreen(); } togglePlay () { if (!this.media.played || this.media.paused) { this.play(); } else { this.pause(); } } play () { let dfd = $.Deferred(); const playPromise = this.media.play(); if(playPromise != null) { playPromise.then(function() { dfd.resolve(); }) } else { dfd.resolve(); } return dfd.promise(); } pause () { let dfd = $.Deferred(); const pausePromise = this.media.pause(); if(pausePromise != null) { pausePromise.then(function() { dfd.resolve(); }) } else { dfd.resolve(); } return dfd.promise(); } load() { return this.media.load() } _initSubtitles () { let _self = this; this.element.children('track[kind="subtitles"]').each(function () { const language = $(this).attr('srclang'); const title = $(this).attr('label'); const src = $(this).attr('src'); if (title.length > 0 && src.length > 0) { _self.subtitles.push({ title : title, name : language, language : language }); } }); } _initVideo () { let dfd = $.Deferred(); if (this.media.readyState > HTMLMediaElement.HAVE_NOTHING) { dfd.resolve(); this._textTracksHack(); } else { $(this.media).one('loadedmetadata', (e) => { dfd.resolve() this._textTracksHack(); }); } return dfd.promise(); } _textTracksHack () { // This is generally for Firefox only // because it somehow resets track list // for video element after DOM manipulation. if (this.media.textTracks.length === 0 && this.subtitles.length > 0) { this.element.children('track[kind="subtitles"]').remove(); for (var i in this.subtitles) { if (this.subtitles.hasOwnProperty(i)) { this.element .append($('<track/>') .attr('label', this.subtitles[ i ].title) .attr('src', this.subtitles[ i ].src) .attr('srclang', this.subtitles[ i ].language) .attr('id', this.subtitles[ i ].language) .attr('kind', 'subtitles')); } } } } } /** * Min rate for android and ios */ Html5.MOBILE_MIN_RATE = 0.5; /** * Max rate for android and ios */ Html5.MOBILE_MAX_RATE = 2; Html5.SAFARI_MIN_RATE = 0.5; Html5.SAFARI_MAX_RATE = 2; Html5.TEST_VIDEO = document.createElement('video'); /** * @return {boolean} * - True if volume can be controlled * - False otherwise */ Html5.canControlVolume = function() { // IE will error if Windows Media Player not installed #3315 try { const volume = Html5.TEST_VIDEO.volume; Html5.TEST_VIDEO.volume = (volume / 2) + 0.1; return volume !== Html5.TEST_VIDEO.volume; } catch (e) { return false; } }; Html5.prototype.featureControlVolume = Html5.canControlVolume(); Component.registerComponent('Html5', Html5); export default Html5;