import "./css/media.scss"; import { createElement } from "../functions"; import { createIcon } from "./icon"; import { get } from "../utility/request"; export function createPicture(url) { return createElement('a', a => { a.className = 'ui-media-picture'; a.target = '_blank'; a.href = url; }, createElement('img', img => { img.src = url; }) ); } function readBlob(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => { const data = new Uint8Array(e.target.result); resolve(data); }; reader.onerror = reject; reader.readAsArrayBuffer(blob); }); } function playAmrArray(array) { return new Promise((resolve, reject) => { const samples = AMR.decode(array); if (samples != null) { resolve(samples); } else { reject(); } }); } function playPcm(samples, ended) { return new Promise(resolve => { const ctx = new AudioContext(); ctx.addEventListener('statechange', () => resolve(ctx)); const source = ctx.createBufferSource(); if (typeof ended === 'function') { source.addEventListener('ended', () => ended(ctx)); } const buffer = ctx.createBuffer(1, samples.length, 8000); if (typeof buffer.copyToChannel === 'function') { buffer.copyToChannel(samples, 0, 0); } else { const channelBuffer = buffer.getChannelData(0); channelBuffer.set(samples); } source.buffer = buffer; source.connect(ctx.destination); ctx.duration = buffer.duration; source.start(); // resolve(ctx); }); } function getTimeLabel(time) { // 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 = '0:00 / 0:00'; let context; let timer; return createElement('div', 'ui-media-audio', createElement('button', button => { button.className = 'play'; button.addEventListener('click', () => { if (context != null) { clearInterval(timer); context.close(); context = null; timestamp.textContent = '0:00 / 0:00'; button.className = 'play'; button.replaceChildren(createIcon('fa-solid', 'play')); return; } get(url, { accept: mime }) .then(r => r.blob()) .then(r => readBlob(r)) .then(r => playAmrArray(r)) .then(r => playPcm(r, ctx => { context = null; clearInterval(timer); timestamp.textContent = '0:00 / ' + getTimeLabel(ctx.duration); button.className = 'play'; button.replaceChildren(createIcon('fa-solid', 'play')); })) .then(ctx => { context = ctx; button.className = 'stop'; button.replaceChildren(createIcon('fa-solid', 'stop')); const total = getTimeLabel(ctx.duration); const refresh = () => timestamp.textContent = getTimeLabel(ctx.currentTime) + ' / ' + total; refresh(); timer = setInterval(refresh, 500); }) .catch(e => { clearInterval(timer); console.error(e); }); }); }, createIcon('fa-solid', 'play') ), timestamp ); } return createElement('audio', audio => { audio.src = url; audio.controls = true; }); } export function createVideo(url) { return createElement('video', video => { video.className = 'ui-media-video'; video.src = url; video.controls = true; }); } export function createFile(url, icon = 'file-alt') { return createElement('div', `ui-media-file ${icon}`, createIcon('fa-solid', icon), createElement('a', a => { a.target = '_blank'; a.href = url; 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} options.[onLoaded] - 视频加载完成回调 * @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 seekBufferBar; 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'), seekBufferBar = createElement('div', 'seek-buffers'), 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'); } const content = createElement('div', 'ui-video-content'); container.append(content, controller); 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 (prepared >= length) { if (options?.autoPlay) { // auto play videos.forEach(v => v.play().catch(() => { })); if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) { video.requestFullscreen().catch(() => { }); } } if (typeof options?.onLoaded === 'function') { options.onLoaded(); } } }); video.addEventListener('progress', () => { const buffered = video.buffered; for (let i = 0; i < buffered.length; ++i) { let buffer = seekBufferBar.children[i]; if (buffer == null) { seekBufferBar.append(buffer = createElement('div', 'ui-video-buffer')); } const start = buffered.start(i) * 100 / duration; const end = buffered.end(i) * 100 / duration; buffer.style.left = `${start.toFixed(2)}%`; buffer.style.width = `${(end - start).toFixed(2)}%`; } for (let i = seekBufferBar.children.length - 1; i >= buffered.length; i -= 1) { seekBufferBar.children[i].remove(); } }); 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); } content.append(wrapper); }); return container; }