diff --git a/src/js/contextmenu.js b/src/js/contextmenu.js index fa3bf124e..85c02c159 100644 --- a/src/js/contextmenu.js +++ b/src/js/contextmenu.js @@ -12,6 +12,11 @@ class ContextMenu { } }); + this.maskHideHandler = () => { + this.hide(); + }; + this.player.template.mask.addEventListener('click', this.maskHideHandler); + this.contextmenuHandler = (e) => { if (this.shown) { this.hide(); @@ -23,10 +28,6 @@ class ContextMenu { const clientRect = this.player.container.getBoundingClientRect(); this.show(event.clientX - clientRect.left, event.clientY - clientRect.top); - - this.player.template.mask.addEventListener('click', () => { - this.hide(); - }); }; this.player.container.addEventListener('contextmenu', this.contextmenuHandler); } @@ -66,6 +67,7 @@ class ContextMenu { destroy() { this.player.container.removeEventListener('contextmenu', this.contextmenuHandler); + this.player.template.mask.removeEventListener('click', this.maskHideHandler); } } diff --git a/src/js/controller.js b/src/js/controller.js index c2b668e12..cabbace51 100644 --- a/src/js/controller.js +++ b/src/js/controller.js @@ -59,10 +59,10 @@ class Controller { } } else { this.player.template.videoWrap.addEventListener('click', () => { - this.toggle(); + this.player.toggle(); }); this.player.template.controllerMask.addEventListener('click', () => { - this.toggle(); + this.player.toggle(); }); } } @@ -82,7 +82,10 @@ class Controller { const p = document.createElement('div'); p.classList.add('dplayer-highlight'); p.style.left = (this.player.options.highlight[i].time / this.player.video.duration) * 100 + '%'; - p.innerHTML = '' + this.player.options.highlight[i].text + ''; + const span = document.createElement('span'); + span.className = 'dplayer-highlight-text'; + span.textContent = this.player.options.highlight[i].text; + p.appendChild(span); this.player.template.playedBarWrap.insertBefore(p, this.player.template.playedBarTime); } } @@ -111,7 +114,7 @@ class Controller { percentage = Math.max(percentage, 0); percentage = Math.min(percentage, 1); this.player.bar.set('played', percentage, 'width'); - this.player.template.ptime.innerHTML = utils.secondToTime(percentage * this.player.video.duration); + this.player.template.ptime.textContent = utils.secondToTime(percentage * this.player.video.duration); }; const thumbUp = (e) => { @@ -144,7 +147,7 @@ class Controller { } this.thumbnails && this.thumbnails.move(tx); this.player.template.playedBarTime.style.left = `${tx - (time >= 3600 ? 25 : 20)}px`; - this.player.template.playedBarTime.innerText = utils.secondToTime(time); + this.player.template.playedBarTime.textContent = utils.secondToTime(time); this.player.template.playedBarTime.classList.remove('hidden'); } }); @@ -257,24 +260,22 @@ class Controller { initAirplayButton() { if (this.player.options.airplay) { if (window.WebKitPlaybackTargetAvailabilityEvent) { + this.airplayClickHandler = function () { + this.video.webkitShowPlaybackTargetPicker(); + }.bind(this.player); + this.player.template.airplayButton.addEventListener('click', this.airplayClickHandler); + this.player.video.addEventListener( 'webkitplaybacktargetavailabilitychanged', function (event) { switch (event.availability) { case 'available': - this.template.airplayButton.disable = false; + this.template.airplayButton.disabled = false; break; default: - this.template.airplayButton.disable = true; + this.template.airplayButton.disabled = true; } - - this.template.airplayButton.addEventListener( - 'click', - function () { - this.video.webkitShowPlaybackTargetPicker(); - }.bind(this) - ); }.bind(this.player) ); } else { @@ -417,6 +418,9 @@ class Controller { this.player.container.removeEventListener('mousemove', this.setAutoHideHandler); this.player.container.removeEventListener('click', this.setAutoHideHandler); } + if (this.airplayClickHandler) { + this.player.template.airplayButton.removeEventListener('click', this.airplayClickHandler); + } clearTimeout(this.autoHideTimer); } } diff --git a/src/js/danmaku.js b/src/js/danmaku.js index 453bc366f..c42050843 100644 --- a/src/js/danmaku.js +++ b/src/js/danmaku.js @@ -16,6 +16,7 @@ class Danmaku { this._opacity = this.options.opacity; this.events = this.options.events; this.unlimited = this.options.unlimited; + this._rafId = null; this._measure(''); this.load(); @@ -124,7 +125,7 @@ class Danmaku { } this.draw(dan); } - window.requestAnimationFrame(() => { + this._rafId = window.requestAnimationFrame(() => { this.frame(); }); } @@ -213,9 +214,9 @@ class Danmaku { item.classList.add('dplayer-danmaku-item'); item.classList.add(`dplayer-danmaku-${dan[i].type}`); if (dan[i].border) { - item.innerHTML = `${dan[i].text}`; + item.innerHTML = `${this.htmlEncode(dan[i].text)}`; } else { - item.innerHTML = dan[i].text; + item.textContent = dan[i].text; } item.style.opacity = this._opacity; item.style.color = utils.number2Color(dan[i].color); @@ -278,9 +279,10 @@ class Danmaku { _measure(text) { if (!this.context) { - const measureStyle = getComputedStyle(this.container.getElementsByClassName('dplayer-danmaku-item')[0], null); + const existingItem = this.container.getElementsByClassName('dplayer-danmaku-item')[0]; + const font = existingItem ? getComputedStyle(existingItem, null).getPropertyValue('font') : '14px sans-serif'; this.context = document.createElement('canvas').getContext('2d'); - this.context.font = measureStyle.getPropertyValue('font'); + this.context.font = font; } return this.context.measureText(text).width; } @@ -303,7 +305,7 @@ class Danmaku { bottom: {}, }; this.danIndex = 0; - this.options.container.innerHTML = ''; + this.container.innerHTML = ''; this.events && this.events.trigger('danmaku_clear'); } @@ -354,6 +356,14 @@ class Danmaku { }; return animations[position]; } + + destroy() { + if (this._rafId) { + window.cancelAnimationFrame(this._rafId); + this._rafId = null; + } + this.clear(); + } } export default Danmaku; diff --git a/src/js/events.js b/src/js/events.js index a147f7983..9180eaab0 100644 --- a/src/js/events.js +++ b/src/js/events.js @@ -34,6 +34,8 @@ class Events { 'danmaku_show', 'danmaku_hide', 'danmaku_clear', + 'danmaku_load_start', + 'danmaku_load_end', 'danmaku_loaded', 'danmaku_send', 'danmaku_opacity', @@ -64,6 +66,12 @@ class Events { } } + off(name, callback) { + if (this.events[name]) { + this.events[name] = this.events[name].filter((fn) => fn !== callback); + } + } + trigger(name, info) { if (this.events[name] && this.events[name].length) { for (let i = 0; i < this.events[name].length; i++) { diff --git a/src/js/player.js b/src/js/player.js index b1343f26e..54761881b 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -33,7 +33,7 @@ class DPlayer { * @constructor */ constructor(options) { - this.options = handleOption({ preload: options.video.type === 'webtorrent' ? 'none' : 'metadata', ...options }); + this.options = handleOption({ preload: options?.video?.type === 'webtorrent' ? 'none' : 'metadata', ...options }); if (this.options.video.quality) { this.qualityIndex = this.options.video.defaultQuality; @@ -208,8 +208,10 @@ class DPlayer { this.danmaku.seek(); } - this.bar.set('played', time / this.video.duration, 'width'); - this.template.ptime.innerHTML = utils.secondToTime(time); + if (this.video.duration && isFinite(this.video.duration)) { + this.bar.set('played', time / this.video.duration, 'width'); + } + this.template.ptime.textContent = utils.secondToTime(time); } /** @@ -382,7 +384,7 @@ class DPlayer { this.type = 'normal'; } - const src = this.quality.url; // 真实的视频 url, 用于切换清晰度时销毁实例 + const src = (this.quality && this.quality.url) || video.src; // 真实的视频 url, 用于切换清晰度时销毁实例 switch (this.type) { // https://github.com/video-dev/hls.js case 'hls': @@ -393,16 +395,20 @@ class DPlayer { this.plugins.hls = hls; hls.loadSource(video.src); hls.attachMedia(video); - this.events.on('destroy', () => { + const onHlsDestroy = () => { hls.destroy(); delete this.plugins.hls; - }); + this.events.off('destroy', onHlsDestroy); + }; + this.events.on('destroy', onHlsDestroy); // 切换清晰度时销毁实例 - this.events.on('quality_end', () => { + const onHlsQualityEnd = () => { if (src !== this.quality.url) { hls.destroy(); } - }); + this.events.off('quality_end', onHlsQualityEnd); + }; + this.events.on('quality_end', onHlsQualityEnd); } else { this.notice('Error: Hls is not supported.'); } @@ -416,7 +422,7 @@ class DPlayer { if (window.flvjs) { if (window.flvjs.isSupported()) { const flvPlayer = window.flvjs.createPlayer( - Object.assign(this.options.pluginOptions.flv.mediaDataSource || {}, { + Object.assign({}, this.options.pluginOptions.flv.mediaDataSource || {}, { type: 'flv', url: video.src, }), @@ -425,20 +431,24 @@ class DPlayer { this.plugins.flvjs = flvPlayer; flvPlayer.attachMediaElement(video); flvPlayer.load(); - this.events.on('destroy', () => { + const onFlvDestroy = () => { flvPlayer.unload(); flvPlayer.detachMediaElement(); flvPlayer.destroy(); delete this.plugins.flvjs; - }); + this.events.off('destroy', onFlvDestroy); + }; + this.events.on('destroy', onFlvDestroy); // 切换清晰度时销毁实例 - this.events.on('quality_end', () => { + const onFlvQualityEnd = () => { if (src !== this.quality.url) { flvPlayer.unload(); flvPlayer.detachMediaElement(); flvPlayer.destroy(); } - }); + this.events.off('quality_end', onFlvQualityEnd); + }; + this.events.on('quality_end', onFlvQualityEnd); } else { this.notice('Error: flvjs is not supported.'); } @@ -455,16 +465,20 @@ class DPlayer { const options = this.options.pluginOptions.dash; dashjsPlayer.updateSettings(options); this.plugins.dash = dashjsPlayer; - this.events.on('destroy', () => { + const onDashDestroy = () => { window.dashjs.MediaPlayer().reset(); delete this.plugins.dash; - }); + this.events.off('destroy', onDashDestroy); + }; + this.events.on('destroy', onDashDestroy); // 切换清晰度时销毁实例 - this.events.on('quality_end', () => { + const onDashQualityEnd = () => { if (src !== this.quality.url) { window.dashjs.MediaPlayer().reset(); } - }); + this.events.off('quality_end', onDashQualityEnd); + }; + this.events.on('quality_end', onDashQualityEnd); } else { this.notice("Error: Can't find dashjs."); } @@ -483,24 +497,33 @@ class DPlayer { 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')); + const videoExtensions = ['.mp4', '.webm', '.mkv', '.ogg']; + const file = torrent.files.find((f) => videoExtensions.some((ext) => f.name.endsWith(ext))); + if (!file) { + this.notice('Error: No supported video file found in torrent.'); + return; + } file.renderTo(this.video, { autoplay: this.options.autoplay, controls: false, }); }); - this.events.on('destroy', () => { + const onWebtorrentDestroy = () => { client.remove(torrentId); client.destroy(); delete this.plugins.webtorrent; - }); + this.events.off('destroy', onWebtorrentDestroy); + }; + this.events.on('destroy', onWebtorrentDestroy); // 切换清晰度时销毁实例 - this.events.on('quality_end', () => { + const onWebtorrentQualityEnd = () => { if (src !== this.quality.url) { client.remove(torrentId); client.destroy(); } - }); + this.events.off('quality_end', onWebtorrentQualityEnd); + }; + this.events.on('quality_end', onWebtorrentQualityEnd); } else { this.notice('Error: Webtorrent is not supported.'); } @@ -522,13 +545,13 @@ class DPlayer { 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); + this.template.dtime.textContent = 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; + const percentage = video.buffered.length && video.duration && isFinite(video.duration) ? video.buffered.end(video.buffered.length - 1) / video.duration : 0; this.bar.set('loaded', percentage, 'width'); }); @@ -568,12 +591,12 @@ class DPlayer { }); this.on('timeupdate', () => { - if (!this.moveBar) { + if (!this.moveBar && this.video.duration && isFinite(this.video.duration)) { 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; + if (this.template.ptime.textContent !== currentTime) { + this.template.ptime.textContent = currentTime; } }); @@ -608,7 +631,7 @@ class DPlayer { } this.switchingQuality = true; this.quality = this.options.video.quality[index]; - this.template.qualityButton.innerHTML = this.quality.name; + this.template.qualityButton.textContent = this.quality.name; const paused = this.video.paused; this.video.pause(); @@ -629,7 +652,7 @@ class DPlayer { this.notice(`${this.tran('switching-quality').replace('%q', this.quality.name)}`, -1, undefined, 'switch-quality'); this.events.trigger('quality_start', this.quality); - this.on('canplay', () => { + const onCanplay = () => { if (this.prevVideo) { if (this.video.currentTime !== this.prevVideo.currentTime) { this.seek(this.prevVideo.currentTime); @@ -646,9 +669,14 @@ class DPlayer { this.events.trigger('quality_end'); } - }); + }; + if (this._onCanplay) { + this.events.off('canplay', this._onCanplay); + } + this._onCanplay = onCanplay; + this.on('canplay', onCanplay); - this.on('error', () => { + const onQualityError = () => { if (!this.video.error) { return; } @@ -667,7 +695,12 @@ class DPlayer { this.prevVideo = null; this.switchingQuality = false; } - }); + }; + if (this._onQualityError) { + this.events.off('error', this._onQualityError); + } + this._onQualityError = onQualityError; + this.on('error', onQualityError); } notice(text, time = 2000, opacity = 0.8, id) { @@ -675,7 +708,7 @@ class DPlayer { if (id) { oldNoticeEle = document.getElementById(`dplayer-notice-${id}`); if (oldNoticeEle) { - oldNoticeEle.innerHTML = text; + oldNoticeEle.textContent = text; } if (this.noticeList[id]) { clearTimeout(this.noticeList[id]); @@ -726,6 +759,9 @@ class DPlayer { this.pause(); document.removeEventListener('click', this.docClickFun, true); this.container.removeEventListener('click', this.containerClickFun, true); + if (this.danmaku) { + this.danmaku.destroy(); + } this.fullScreen.destroy(); this.hotkey.destroy(); this.contextmenu.destroy();