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();