import Promise from 'promise-polyfill'; import utils from './utils'; import handleOption from './options'; import i18n from './i18n'; import Template from './template'; import Icons from './icons'; import Danmaku from './danmaku'; import Events from './events'; import FullScreen from './fullscreen'; import User from './user'; import Subtitle from './subtitle'; import Bar from './bar'; import Timer from './timer'; import Bezel from './bezel'; import Controller from './controller'; import Setting from './setting'; import Comment from './comment'; import HotKey from './hotkey'; import ContextMenu from './contextmenu'; import InfoPanel from './info-panel'; import tplVideo from '../template/video.art'; let index = 0; const instances = []; class DPlayer { /** * DPlayer constructor function * * @param {Object} options - See README * @constructor */ constructor (options) { this.options = handleOption({preload:options.video.type === 'webtorrent' ? 'none' : 'metadata', ...options}); if (this.options.video.quality) { this.qualityIndex = this.options.video.defaultQuality; this.quality = this.options.video.quality[this.options.video.defaultQuality]; } this.tran = new i18n(this.options.lang).tran; this.events = new Events(); this.user = new User(this); this.container = this.options.container; this.container.classList.add('dplayer'); if (!this.options.danmaku) { this.container.classList.add('dplayer-no-danmaku'); } if (this.options.live) { this.container.classList.add('dplayer-live'); } if (utils.isMobile) { this.container.classList.add('dplayer-mobile'); } this.arrow = this.container.offsetWidth <= 500; if (this.arrow) { this.container.classList.add('dplayer-arrow'); } this.template = new Template({ container: this.container, options: this.options, index: index, tran: this.tran, }); this.video = this.template.video; this.bar = new Bar(this.template); this.bezel = new Bezel(this.template.bezel); this.fullScreen = new FullScreen(this); this.controller = new Controller(this); if (this.options.danmaku) { this.danmaku = new Danmaku({ container: this.template.danmaku, opacity: this.user.get('opacity'), callback: () => { setTimeout(() => { this.template.danmakuLoading.style.display = 'none'; // autoplay if (this.options.autoplay) { this.play(); } }, 0); }, error: (msg) => { this.notice(msg); }, apiBackend: this.options.apiBackend, borderColor: this.options.theme, height: this.arrow ? 24 : 30, time: () => this.video.currentTime, unlimited: this.user.get('unlimited'), api: { id: this.options.danmaku.id, address: this.options.danmaku.api, token: this.options.danmaku.token, maximum: this.options.danmaku.maximum, addition: this.options.danmaku.addition, user: this.options.danmaku.user, }, events: this.events, tran: (msg) => this.tran(msg), }); this.comment = new Comment(this); } this.setting = new Setting(this); this.plugins = {}; document.addEventListener('click', () => { this.focus = false; }, true); this.container.addEventListener('click', () => { this.focus = true; }, true); this.paused = true; this.timer = new Timer(this); this.hotkey = new HotKey(this); this.contextmenu = new ContextMenu(this); this.initVideo(this.video, this.quality && this.quality.type || this.options.video.type); this.infoPanel = new InfoPanel(this); if (!this.danmaku && this.options.autoplay) { this.play(); } index++; instances.push(this); } /** * Seek video */ seek (time) { time = Math.max(time, 0); if (this.video.duration) { time = Math.min(time, this.video.duration); } if (this.video.currentTime < time) { this.notice(`${this.tran('FF')} ${(time - this.video.currentTime).toFixed(0)} ${this.tran('s')}`); } else if (this.video.currentTime > time) { this.notice(`${this.tran('REW')} ${(this.video.currentTime - time).toFixed(0)} ${this.tran('s')}`); } this.video.currentTime = time; if (this.danmaku) { this.danmaku.seek(); } this.bar.set('played', time / this.video.duration, 'width'); this.template.ptime.innerHTML = utils.secondToTime(time); } /** * Play video */ play () { this.paused = false; if (this.video.paused) { this.bezel.switch(Icons.play); } this.template.playButton.innerHTML = Icons.pause; const playedPromise = Promise.resolve(this.video.play()); playedPromise.catch(() => { this.pause(); }).then(() => { }); this.timer.enable('loading'); this.container.classList.remove('dplayer-paused'); this.container.classList.add('dplayer-playing'); if (this.danmaku) { this.danmaku.play(); } if (this.options.mutex) { for (let i = 0; i < instances.length; i++) { if (this !== instances[i]) { instances[i].pause(); } } } } /** * Pause video */ pause () { this.paused = true; this.container.classList.remove('dplayer-loading'); if (!this.video.paused) { this.bezel.switch(Icons.pause); } this.template.playButton.innerHTML = Icons.play; this.video.pause(); this.timer.disable('loading'); this.container.classList.remove('dplayer-playing'); this.container.classList.add('dplayer-paused'); if (this.danmaku) { this.danmaku.pause(); } } switchVolumeIcon () { if (this.volume() >= 0.95) { this.template.volumeIcon.innerHTML = Icons.volumeUp; } else if (this.volume() > 0) { this.template.volumeIcon.innerHTML = Icons.volumeDown; } else { this.template.volumeIcon.innerHTML = Icons.volumeOff; } } /** * Set volume */ volume (percentage, nostorage, nonotice) { percentage = parseFloat(percentage); if (!isNaN(percentage)) { percentage = Math.max(percentage, 0); percentage = Math.min(percentage, 1); this.bar.set('volume', percentage, 'width'); const formatPercentage = `${(percentage * 100).toFixed(0)}%`; this.template.volumeBarWrapWrap.dataset.balloon = formatPercentage; if (!nostorage) { this.user.set('volume', percentage); } if (!nonotice) { this.notice(`${this.tran('Volume')} ${(percentage * 100).toFixed(0)}%`); } this.video.volume = percentage; if (this.video.muted) { this.video.muted = false; } this.switchVolumeIcon(); } return this.video.volume; } /** * Toggle between play and pause */ toggle () { if (this.video.paused) { this.play(); } else { this.pause(); } } /** * attach event */ on (name, callback) { this.events.on(name, callback); } /** * Switch to a new video * * @param {Object} video - new video info * @param {Object} danmaku - new danmaku info */ switchVideo (video, danmakuAPI) { this.pause(); this.video.poster = video.pic ? video.pic : ''; this.video.src = video.url; this.initMSE(this.video, video.type || 'auto'); if (danmakuAPI) { this.template.danmakuLoading.style.display = 'block'; this.bar.set('played', 0, 'width'); this.bar.set('loaded', 0, 'width'); this.template.ptime.innerHTML = '00:00'; this.template.danmaku.innerHTML = ''; if (this.danmaku) { this.danmaku.reload({ id: danmakuAPI.id, address: danmakuAPI.api, token: danmakuAPI.token, maximum: danmakuAPI.maximum, addition: danmakuAPI.addition, user: danmakuAPI.user, }); } } } initMSE (video, type) { this.type = type; if (this.options.video.customType && this.options.video.customType[type]) { if (Object.prototype.toString.call(this.options.video.customType[type]) === '[object Function]') { this.options.video.customType[type](this.video, this); } else { console.error(`Illegal customType: ${type}`); } } else { if (this.type === 'auto') { if (/m3u8(#|\?|$)/i.exec(video.src)) { this.type = 'hls'; } else if (/.flv(#|\?|$)/i.exec(video.src)) { this.type = 'flv'; } else if (/.mpd(#|\?|$)/i.exec(video.src)) { this.type = 'dash'; } else { this.type = 'normal'; } } if (this.type === 'hls' && (video.canPlayType('application/x-mpegURL') || video.canPlayType('application/vnd.apple.mpegURL'))) { this.type = 'normal'; } switch (this.type) { // https://github.com/video-dev/hls.js case 'hls': if (Hls) { if (Hls.isSupported()) { const options = this.options.pluginOptions.hls; const hls = new Hls(options); this.plugins.hls = hls; hls.loadSource(video.src); hls.attachMedia(video); this.events.on('destroy', () => { hls.destroy(); delete this.plugins.hls; }); } else { this.notice('Error: Hls is not supported.'); } } else { this.notice('Error: Can\'t find Hls.'); } break; // https://github.com/Bilibili/flv.js case 'flv': if (flvjs) { if (flvjs.isSupported()) { const options = Object.assign(this.options.pluginOptions.flvjs,{ type: 'flv', url: video.src }); const flvPlayer = flvjs.createPlayer(options); this.plugins.flvjs = flvPlayer; flvPlayer.attachMediaElement(video); flvPlayer.load(); this.events.on('destroy', () => { flvPlayer.unload(); flvPlayer.detachMediaElement(); flvPlayer.destroy(); delete this.plugins.flvjs; }); } else { this.notice('Error: flvjs is not supported.'); } } else { this.notice('Error: Can\'t find flvjs.'); } break; // https://github.com/Dash-Industry-Forum/dash.js case 'dash': if (dashjs) { const dashjsPlayer = dashjs.MediaPlayer().create().initialize(video, video.src, false); const options = this.options.pluginOptions.dash; dashjsPlayer.updateSettings(options); this.plugins.dash = dashjsPlayer; this.events.on('destroy', () => { dashjs.MediaPlayer().reset(); delete this.plugins.dash; }); } else { this.notice('Error: Can\'t find dashjs.'); } break; // https://github.com/webtorrent/webtorrent case 'webtorrent': if (WebTorrent) { if (WebTorrent.WEBRTC_SUPPORT) { this.container.classList.add('dplayer-loading'); const options = this.options.pluginOptions.webtorrent; const client = new WebTorrent(options); this.plugins.webtorrent = client; const torrentId = video.src; video.src = ''; video.preload = 'metadata'; video.addEventListener('durationchange', () => this.container.classList.remove('dplayer-loading'), {once: true}); client.add(torrentId, (torrent) => { const file = torrent.files.find((file) => file.name.endsWith('.mp4')); file.renderTo(this.video, { autoplay: this.options.autoplay }); }); this.events.on('destroy', () => { client.remove(torrentId); client.destroy(); delete this.plugins.webtorrent; }); } else { this.notice('Error: Webtorrent is not supported.'); } } else { this.notice('Error: Can\'t find Webtorrent.'); } break; } } } initVideo (video, type) { this.initMSE(video, type); /** * video events */ // show video time: the metadata has loaded or changed this.on('durationchange', () => { // compatibility: Android browsers will output 1 or Infinity at first if (video.duration !== 1 && video.duration !== Infinity) { this.template.dtime.innerHTML = utils.secondToTime(video.duration); } }); // show video loaded bar: to inform interested parties of progress downloading the media this.on('progress', () => { const percentage = video.buffered.length ? video.buffered.end(video.buffered.length - 1) / video.duration : 0; this.bar.set('loaded', percentage, 'width'); }); // video download error: an error occurs this.on('error', () => { if (!this.video.error) { // Not a video load error, may be poster load failed, see #307 return; } this.tran && this.notice && this.type !== 'webtorrent' && this.notice(this.tran('Video load failed'), -1); }); // video end this.on('ended', () => { this.bar.set('played', 1, 'width'); if (!this.setting.loop) { this.pause(); } else { this.seek(0); this.play(); } if (this.danmaku) { this.danmaku.danIndex = 0; } }); this.on('play', () => { if (this.paused) { this.play(); } }); this.on('pause', () => { if (!this.paused) { this.pause(); } }); this.on('timeupdate', () => { this.bar.set('played', this.video.currentTime / this.video.duration, 'width'); const currentTime = utils.secondToTime(this.video.currentTime); if (this.template.ptime.innerHTML !== currentTime) { this.template.ptime.innerHTML = currentTime; } }); for (let i = 0; i < this.events.videoEvents.length; i++) { video.addEventListener(this.events.videoEvents[i], () => { this.events.trigger(this.events.videoEvents[i]); }); } this.volume(this.user.get('volume'), true, true); if (this.options.subtitle) { this.subtitle = new Subtitle(this.template.subtitle, this.video, this.options.subtitle, this.events); if (!this.user.get('subtitle')) { this.subtitle.hide(); } } } switchQuality (index) { index = typeof index === 'string' ? parseInt(index) : index; if (this.qualityIndex === index || this.switchingQuality) { return; } else { this.qualityIndex = index; } this.switchingQuality = true; this.quality = this.options.video.quality[index]; this.template.qualityButton.innerHTML = this.quality.name; const paused = this.video.paused; this.video.pause(); const videoHTML = tplVideo({ current: false, pic: null, screenshot: this.options.screenshot, preload: 'auto', url: this.quality.url, subtitle: this.options.subtitle }); const videoEle = new DOMParser().parseFromString(videoHTML, 'text/html').body.firstChild; this.template.videoWrap.insertBefore(videoEle, this.template.videoWrap.getElementsByTagName('div')[0]); this.prevVideo = this.video; this.video = videoEle; this.initVideo(this.video, this.quality.type || this.options.video.type); this.seek(this.prevVideo.currentTime); this.notice(`${this.tran('Switching to')} ${this.quality.name} ${this.tran('quality')}`, -1); this.events.trigger('quality_start', this.quality); this.on('canplay', () => { if (this.prevVideo) { if (this.video.currentTime !== this.prevVideo.currentTime) { this.seek(this.prevVideo.currentTime); return; } this.template.videoWrap.removeChild(this.prevVideo); this.video.classList.add('dplayer-video-current'); if (!paused) { this.video.play(); } this.prevVideo = null; this.notice(`${this.tran('Switched to')} ${this.quality.name} ${this.tran('quality')}`); this.switchingQuality = false; this.events.trigger('quality_end'); } }); } notice (text, time = 2000, opacity = 0.8) { this.template.notice.innerHTML = text; this.template.notice.style.opacity = opacity; if (this.noticeTime) { clearTimeout(this.noticeTime); } this.events.trigger('notice_show', text); if (time > 0) { this.noticeTime = setTimeout(() => { this.template.notice.style.opacity = 0; this.events.trigger('notice_hide'); }, time); } } resize () { if (this.danmaku) { this.danmaku.resize(); } if (this.controller.thumbnails) { this.controller.thumbnails.resize(160, this.video.videoHeight / this.video.videoWidth * 160, this.template.barWrap.offsetWidth); } this.events.trigger('resize'); } speed (rate) { this.video.playbackRate = rate; } destroy () { instances.splice(instances.indexOf(this), 1); this.pause(); this.controller.destroy(); this.timer.destroy(); this.video.src = ''; this.container.innerHTML = ''; this.events.trigger('destroy'); } static get version () { /* global DPLAYER_VERSION */ return DPLAYER_VERSION; } } export default DPlayer;