feature: combined-video player.
This commit is contained in:
293
lib/ui/media.js
293
lib/ui/media.js
@ -62,14 +62,24 @@ function playPcm(samples, ended) {
|
||||
}
|
||||
|
||||
function getTimeLabel(time) {
|
||||
time = Math.round(time);
|
||||
return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
|
||||
// time = Math.round(time);
|
||||
// return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
|
||||
if (isNaN(time) || time < 0) {
|
||||
return '0:00';
|
||||
}
|
||||
time = Math.floor(time);
|
||||
const m = Math.floor(time / 60);
|
||||
const h = Math.floor(m / 60);
|
||||
if (h > 0) {
|
||||
return h + ':' + String(m % 60).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
|
||||
}
|
||||
return m + ':' + String(time % 60).padStart(2, '0');
|
||||
}
|
||||
|
||||
export function createAudio(mime, url) {
|
||||
if ((mime === 'audio/amr' || mime === '.amr') && typeof AMR !== 'undefined') {
|
||||
const timestamp = createElement('span', 'ui-media-timestamp');
|
||||
timestamp.textContent = '00:00 / 00:00';
|
||||
timestamp.textContent = '0:00 / 0:00';
|
||||
let context;
|
||||
let timer;
|
||||
return createElement('div', 'ui-media-audio',
|
||||
@ -80,7 +90,7 @@ export function createAudio(mime, url) {
|
||||
clearInterval(timer);
|
||||
context.close();
|
||||
context = null;
|
||||
timestamp.textContent = '00:00 / 00:00';
|
||||
timestamp.textContent = '0:00 / 0:00';
|
||||
button.className = 'play';
|
||||
button.replaceChildren(createIcon('fa-solid', 'play'));
|
||||
return;
|
||||
@ -92,7 +102,7 @@ export function createAudio(mime, url) {
|
||||
.then(r => playPcm(r, ctx => {
|
||||
context = null;
|
||||
clearInterval(timer);
|
||||
timestamp.textContent = '00:00 / ' + getTimeLabel(ctx.duration);
|
||||
timestamp.textContent = '0:00 / ' + getTimeLabel(ctx.duration);
|
||||
button.className = 'play';
|
||||
button.replaceChildren(createIcon('fa-solid', 'play'));
|
||||
}))
|
||||
@ -139,4 +149,277 @@ export function createFile(url, icon = 'file-alt') {
|
||||
a.innerText = 'Click here to view the file';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建联动视频元素
|
||||
* @param {string[]} urls - 视频 url 数组
|
||||
* @param {any} [options] - 播放参数
|
||||
* @param {boolean} options.[autoPlay] - 是否自动播放
|
||||
* @param {boolean} options.[autoFullScreen] - 是否自动全屏
|
||||
* @param {boolean} options.[autoLoop] - 是否循环播放
|
||||
* @param {Function} [callback] - 视频元素处理回调函数
|
||||
* @returns {HTMLDivElement} 返回联动视频元素
|
||||
*/
|
||||
export function createVideoList(urls, options, callback) {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [urls];
|
||||
}
|
||||
const length = urls.length;
|
||||
const container = createElement('div', 'ui-media-video-container');
|
||||
let seekProgressBar;
|
||||
let playIcon;
|
||||
let timeLabel;
|
||||
let volumeProgressBar;
|
||||
let volumeIcon;
|
||||
|
||||
const videos = Array(length);
|
||||
const waiting = Array(length);
|
||||
let prepared = 0;
|
||||
let ended = 0;
|
||||
|
||||
let playing;
|
||||
let duration = -1;
|
||||
let durationLabel;
|
||||
let seekBarWidth;
|
||||
let mousing;
|
||||
const controller = createElement('div', 'ui-video-control active',
|
||||
createElement('div', seek => {
|
||||
seek.className = 'ui-video-bar seek-bar';
|
||||
seek.addEventListener('mousedown', e => {
|
||||
if (prepared < length || waiting.find(p => p) != null) {
|
||||
return;
|
||||
}
|
||||
const width = seekBarWidth = seek.offsetWidth;
|
||||
const currentTime = Math.min(Math.max(e.offsetX * duration / width, 0), duration);
|
||||
videos.forEach(video => typeof video.fastSeek === 'function' ?
|
||||
video.fastSeek(currentTime) :
|
||||
(video.currentTime = currentTime));
|
||||
mousing = true;
|
||||
e.stopPropagation();
|
||||
});
|
||||
seek.addEventListener('mousemove', e => {
|
||||
if (mousing) {
|
||||
if (!e.buttons) {
|
||||
mousing = false;
|
||||
} else {
|
||||
const v = Math.min(Math.max(e.offsetX / seekBarWidth, 0), 1);
|
||||
seekProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
|
||||
}
|
||||
}
|
||||
});
|
||||
seek.addEventListener('mouseup', e => {
|
||||
if (mousing) {
|
||||
mousing = false;
|
||||
const currentTime = Math.min(Math.max(e.offsetX * duration / seekBarWidth, 0), duration);
|
||||
videos.forEach(video => typeof video.fastSeek === 'function' ?
|
||||
video.fastSeek(currentTime) :
|
||||
(video.currentTime = currentTime));
|
||||
}
|
||||
});
|
||||
},
|
||||
createElement('div', 'ui-video-duration seek-duration'),
|
||||
seekProgressBar = createElement('div', 'ui-video-progress seek-progress')
|
||||
),
|
||||
playIcon = createElement('div', 'ui-video-icon play-icon',
|
||||
createIcon('fa-solid', 'play'),
|
||||
createIcon('fa-solid', 'pause')
|
||||
),
|
||||
timeLabel = createElement('div', 'ui-video-time-label'),
|
||||
createElement('div', 'ui-video-volume-container',
|
||||
createElement('div', volume => {
|
||||
volume.className = 'ui-video-bar volume-bar';
|
||||
volume.addEventListener('mousedown', e => {
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
mousing = true;
|
||||
e.stopPropagation();
|
||||
});
|
||||
volume.addEventListener('mousemove', e => {
|
||||
if (mousing) {
|
||||
if (!e.buttons) {
|
||||
mousing = false;
|
||||
} else {
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
volumeProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
}
|
||||
}
|
||||
});
|
||||
volume.addEventListener('mouseup', e => {
|
||||
if (mousing) {
|
||||
mousing = false;
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
}
|
||||
});
|
||||
},
|
||||
createElement('div', 'ui-video-duration video-duration'),
|
||||
volumeProgressBar = createElement('div', 'ui-video-progress video-progress')
|
||||
),
|
||||
volumeIcon = createElement('div', 'ui-video-icon volume-icon',
|
||||
createIcon('fa-solid', 'volume'),
|
||||
createIcon('fa-solid', 'volume-mute')
|
||||
)
|
||||
)
|
||||
);
|
||||
controller.addEventListener('mousedown', () => {
|
||||
if (prepared < length || waiting.find(p => p) != null) {
|
||||
// not prepared.
|
||||
return;
|
||||
}
|
||||
if (playing) {
|
||||
videos.forEach(video => video.pause());
|
||||
} else {
|
||||
ended = 0;
|
||||
videos.forEach(video => video.play().catch(() => { }));
|
||||
}
|
||||
});
|
||||
volumeIcon.addEventListener('mousedown', e => {
|
||||
if (volumeIcon.classList.contains('disabled')) {
|
||||
return;
|
||||
}
|
||||
const video = videos[0];
|
||||
if (video == null) {
|
||||
return;
|
||||
}
|
||||
if (volumeIcon.classList.contains('muted')) {
|
||||
video.volume = 1;
|
||||
volumeIcon.classList.remove('muted');
|
||||
} else {
|
||||
video.volume = 0;
|
||||
volumeIcon.classList.add('muted');
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
if (length === 1) {
|
||||
controller.append(createElement('div', icon => {
|
||||
icon.className = 'ui-video-icon fullscreen-icon';
|
||||
icon.addEventListener('mousedown', e => {
|
||||
if (prepared < length) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
if (document.fullscreenElement == null) {
|
||||
video.requestFullscreen().catch(() => { });
|
||||
} else if (typeof document.exitFullscreen === 'function') {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
createIcon('fa-regular', 'expand')
|
||||
));
|
||||
} else {
|
||||
controller.classList.add('no-fullscreen');
|
||||
}
|
||||
|
||||
urls.forEach((url, i) => {
|
||||
const video = createElement('video');
|
||||
videos[i] = video;
|
||||
video.src = url;
|
||||
video.addEventListener('durationchange', () => {
|
||||
const d = video.duration;
|
||||
if (d > duration) {
|
||||
duration = d;
|
||||
durationLabel = getTimeLabel(d);
|
||||
timeLabel.innerText = '0:00 / ' + durationLabel;
|
||||
}
|
||||
});
|
||||
video.addEventListener('loadeddata', () => {
|
||||
if (i === 0) {
|
||||
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
|
||||
} else {
|
||||
video.volume = 0;
|
||||
}
|
||||
prepared += 1;
|
||||
if (options?.autoPlay && prepared >= length) {
|
||||
// auto play
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) {
|
||||
video.requestFullscreen().catch(() => { });
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('canplaythrough', () => {
|
||||
const w = video.parentElement.querySelector('.ui-video-waiting');
|
||||
if (w != null) {
|
||||
w.style.opacity = 0;
|
||||
}
|
||||
if (waiting[i]) {
|
||||
waiting[i] = false;
|
||||
if (waiting.find(p => p) == null) {
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('pause', () => {
|
||||
playing = false;
|
||||
controller.classList.add('active');
|
||||
playIcon.classList.remove('pause');
|
||||
});
|
||||
video.addEventListener('playing', () => {
|
||||
playing = true;
|
||||
controller.classList.remove('active');
|
||||
playIcon.classList.add('pause');
|
||||
});
|
||||
video.addEventListener('waiting', () => {
|
||||
waiting[i] = true;
|
||||
const w = video.parentElement.querySelector('.ui-video-waiting');
|
||||
if (w != null) {
|
||||
w.style.opacity = 1;
|
||||
}
|
||||
videos.forEach(v => v.pause());
|
||||
});
|
||||
video.addEventListener('ended', () => {
|
||||
ended += 1;
|
||||
if (ended >= length) {
|
||||
if (options?.autoLoop) {
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
} else {
|
||||
playing = false;
|
||||
controller.classList.add('active');
|
||||
seekProgressBar.style.width = '0';
|
||||
playIcon.classList.remove('pause');
|
||||
timeLabel.innerText = '0:00 / ' + durationLabel;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (i === 0) {
|
||||
video.addEventListener('timeupdate', () => {
|
||||
seekProgressBar.style.width = `${(video.currentTime * 100 / duration).toFixed(2)}%`;
|
||||
timeLabel.innerText = getTimeLabel(video.currentTime) + ' / ' + durationLabel;
|
||||
});
|
||||
video.addEventListener('volumechange', () => {
|
||||
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
|
||||
});
|
||||
}
|
||||
const wrapper = createElement('div', 'ui-video-wrapper',
|
||||
video,
|
||||
createElement('div', 'ui-video-waiting',
|
||||
createIcon('fa-regular', 'spinner-third')
|
||||
)
|
||||
);
|
||||
if (typeof callback === 'function') {
|
||||
callback(wrapper);
|
||||
}
|
||||
container.append(wrapper);
|
||||
});
|
||||
container.append(controller);
|
||||
|
||||
return container;
|
||||
}
|
Reference in New Issue
Block a user