初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
(() => {
|
||||
window.JukuPlayerMobile = ({panel, video, state, showEpisodes, showPreferences}) => {
|
||||
const node = id => document.getElementById(id);
|
||||
const mode = window.matchMedia('(max-width:700px), (max-height:500px) and (pointer:coarse)');
|
||||
const progress = node('mobilePlaybackProgress');
|
||||
const play = node('mobilePlayPauseBtn');
|
||||
const transport = node('mobileTransportBtn');
|
||||
const rate = node('playbackRate'), quality = node('playbackQuality');
|
||||
const orientation = window.JukuPlayerOrientation({panel, video, active: () => mode.matches && panel.open});
|
||||
const heading = panel.querySelector('.mobile-player-heading'), caption = panel.querySelector('.mobile-player-caption');
|
||||
let scrubbing = false, qualitySignature = '', lastTitle = '', waiting = false, hideTimer = null, previousLandscape = false;
|
||||
const time = value => {const seconds = Math.max(0, Math.floor(Number(value) || 0)); return Math.floor(seconds / 60) + ':' + String(seconds % 60).padStart(2, '0');};
|
||||
const duration = () => {const value = state().duration || video.duration; return Number.isFinite(value) && value > 0 ? value : 0;};
|
||||
function hideControls(hidden) {
|
||||
panel.classList.toggle('player-controls-hidden', hidden);
|
||||
heading.inert = hidden;
|
||||
caption.inert = hidden;
|
||||
}
|
||||
function reveal() {
|
||||
clearTimeout(hideTimer);
|
||||
hideControls(false);
|
||||
if (!orientation.landscape() || video.paused || waiting || state().loading || scrubbing || panel.classList.contains('episodes-open') || panel.classList.contains('preferences-open') || node('playbackError').textContent) return;
|
||||
hideTimer = setTimeout(() => {if (panel.open && orientation.landscape() && !video.paused && !scrubbing) hideControls(true);}, 3000);
|
||||
}
|
||||
function choices(select, container) {
|
||||
container.replaceChildren();
|
||||
for (const option of select.options) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'secondary';
|
||||
button.textContent = option.textContent;
|
||||
button.dataset.value = option.value;
|
||||
button.setAttribute('aria-pressed', String(option.value === select.value));
|
||||
button.addEventListener('click', () => {
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('change'));
|
||||
update();
|
||||
if (panel.dataset.playerSheet !== 'settings') node('closePlayerPreferencesBtn').click();
|
||||
});
|
||||
container.appendChild(button);
|
||||
}
|
||||
}
|
||||
function updateProgress() {
|
||||
if (!mode.matches || !panel.open) return;
|
||||
const total = duration();
|
||||
progress.max = String(total || 1);
|
||||
progress.disabled = !total || state().loading;
|
||||
if (!scrubbing) progress.value = String(Math.min(total, Math.max(0, Number(video.currentTime) || 0)));
|
||||
const position = scrubbing ? Number(progress.value) : Number(video.currentTime) || 0;
|
||||
const text = time(position) + ' / ' + time(total);
|
||||
node('mobilePlaybackTime').textContent = text;
|
||||
node('mobileCurrentTime').textContent = time(position);
|
||||
node('mobileDuration').textContent = time(total);
|
||||
progress.setAttribute('aria-valuetext', text);
|
||||
progress.style.setProperty('--played', (total ? position / total * 100 : 0) + '%');
|
||||
}
|
||||
function update() {
|
||||
const value = state();
|
||||
panel.classList.toggle('mobile-player', mode.matches);
|
||||
video.controls = !mode.matches;
|
||||
orientation.update();
|
||||
if (!mode.matches) {hideControls(false); clearTimeout(hideTimer); return;}
|
||||
panel.classList.toggle('playback-loading', Boolean(value.loading));
|
||||
panel.classList.toggle('playback-buffering', waiting);
|
||||
if (lastTitle !== value.title) {node('mobilePlayerTitle').textContent = node('mobileHeadingTitle').textContent = value.title || '正在加载'; lastTitle = value.title;}
|
||||
node('mobileEpisodeLabel').textContent = value.episode ? '第 ' + value.episode + ' 集' : '正在加载';
|
||||
node('mobileEpisodeSummary').textContent = [value.releaseStatus === 'finished' ? '已完结' : value.releaseStatus === 'ongoing' ? '连载中' : '', value.total ? '全 ' + value.total + ' 集' : '获取分集中'].filter(Boolean).join(' · ');
|
||||
node('mobileEpisodeVIP').hidden = !value.vip;
|
||||
node('mobileRateBtn').textContent = node('mobileLandscapeRateBtn').textContent = video.playbackRate === 1 ? '倍速' : (video.playbackRate || 1) + ' 倍';
|
||||
const selectedQuality = quality.selectedOptions[0]?.textContent || '自动';
|
||||
node('mobileLandscapeQualityBtn').textContent = selectedQuality.match(/\d+p/i)?.[0].toUpperCase() || '自动';
|
||||
node('mobileLandscapeQualityBtn').disabled = quality.disabled;
|
||||
node('mobileEpisodesBtn').disabled = !value.total;
|
||||
node('mobileNextEpisodeBtn').disabled = node('nextEpisodeBtn').disabled;
|
||||
panel.dataset.paused = String(video.paused);
|
||||
play.hidden = !video.paused || Boolean(node('playbackError').textContent);
|
||||
play.disabled = Boolean(value.loading) || !video.hasAttribute('src');
|
||||
play.setAttribute('aria-label', video.ended ? '重播本集' : '播放');
|
||||
transport.disabled = play.disabled;
|
||||
transport.setAttribute('aria-label', video.paused ? video.ended ? '重播本集' : '播放' : '暂停');
|
||||
const signature = Array.from(quality.options, item => item.value + ':' + item.textContent).join('|');
|
||||
if (signature !== qualitySignature) {qualitySignature = signature; choices(quality, node('mobileQualityChoices'));}
|
||||
for (const [select, target] of [[rate, 'mobileRateChoices'], [quality, 'mobileQualityChoices']]) {
|
||||
for (const button of node(target).children) {
|
||||
button.setAttribute('aria-pressed', String(button.dataset.value === select.value));
|
||||
button.disabled = select.disabled;
|
||||
}
|
||||
}
|
||||
node('mobileFullscreenBtn').hidden = node('playerFullscreenBtn').hidden;
|
||||
node('mobileFullscreenBtn').textContent = document.fullscreenElement === panel || document.webkitFullscreenElement === panel ? '退出全屏' : '全屏播放';
|
||||
node('mobilePreferencesTitle').textContent = panel.dataset.playerSheet === 'rate' ? '倍速' : panel.dataset.playerSheet === 'quality' ? '清晰度' : '更多';
|
||||
if (previousLandscape !== orientation.landscape() || video.paused || value.loading || waiting || panel.classList.contains('episodes-open') || panel.classList.contains('preferences-open')) reveal();
|
||||
previousLandscape = orientation.landscape();
|
||||
updateProgress();
|
||||
}
|
||||
choices(rate, node('mobileRateChoices'));
|
||||
node('mobileBackBtn').addEventListener('click', () => {
|
||||
if (orientation.landscape()) {
|
||||
orientation.portrait();
|
||||
if (document.fullscreenElement === panel) document.exitFullscreen().catch(() => {});
|
||||
else if (document.webkitFullscreenElement === panel) document.webkitExitFullscreen?.();
|
||||
} else panel.close();
|
||||
});
|
||||
node('mobileEpisodesBtn').addEventListener('click', showEpisodes);
|
||||
node('mobilePlayerSettingsBtn').addEventListener('click', () => showPreferences('settings'));
|
||||
for (const id of ['mobileRateBtn', 'mobileLandscapeRateBtn']) node(id).addEventListener('click', () => showPreferences('rate'));
|
||||
node('mobileLandscapeQualityBtn').addEventListener('click', () => showPreferences('quality'));
|
||||
node('mobileNextEpisodeBtn').addEventListener('click', () => node('nextEpisodeBtn').click());
|
||||
node('mobileFullscreenBtn').addEventListener('click', async () => {
|
||||
try {
|
||||
if (document.fullscreenElement === panel || document.webkitFullscreenElement === panel) await (document.exitFullscreen?.() || document.webkitExitFullscreen?.());
|
||||
else if (panel.requestFullscreen) await panel.requestFullscreen();
|
||||
else if (panel.webkitRequestFullscreen) await panel.webkitRequestFullscreen();
|
||||
else node('playerFullscreenBtn').click();
|
||||
} catch (_) {}
|
||||
update();
|
||||
});
|
||||
function togglePlayback() {
|
||||
if (!mode.matches || !panel.open || state().loading || !video.hasAttribute('src')) return;
|
||||
if (video.paused) video.play().catch(() => {}); else video.pause();
|
||||
}
|
||||
play.addEventListener('click', togglePlayback);
|
||||
transport.addEventListener('click', togglePlayback);
|
||||
video.addEventListener('click', event => {
|
||||
if (event.defaultPrevented) return;
|
||||
if (orientation.landscape()) {
|
||||
if (panel.classList.contains('player-controls-hidden') || video.paused) reveal();
|
||||
else {clearTimeout(hideTimer); hideControls(true);}
|
||||
} else togglePlayback();
|
||||
});
|
||||
progress.addEventListener('input', () => {scrubbing = true; reveal(); updateProgress();});
|
||||
progress.addEventListener('change', () => {
|
||||
const target = Math.min(duration(), Math.max(0, Number(progress.value) || 0));
|
||||
scrubbing = false;
|
||||
if (!state().loading && duration()) video.currentTime = target;
|
||||
reveal();
|
||||
updateProgress();
|
||||
});
|
||||
progress.addEventListener('pointercancel', () => {scrubbing = false; updateProgress();});
|
||||
progress.addEventListener('blur', () => {scrubbing = false; updateProgress();});
|
||||
video.addEventListener('waiting', () => {waiting = true; update();});
|
||||
for (const type of ['playing', 'canplay', 'emptied']) video.addEventListener(type, () => {waiting = false; update(); reveal();});
|
||||
panel.addEventListener('jukuorientationchange', () => {update(); reveal();});
|
||||
panel.addEventListener('keydown', reveal);
|
||||
panel.addEventListener('pointerdown', event => {if (event.target.closest('button,input,select')) reveal();});
|
||||
document.addEventListener('dialoglayerchange', () => {update(); reveal();});
|
||||
document.addEventListener('fullscreenchange', update);
|
||||
for (const type of ['timeupdate', 'durationchange']) video.addEventListener(type, updateProgress);
|
||||
for (const type of ['loadedmetadata', 'playing', 'pause', 'ended', 'ratechange', 'emptied', 'error']) video.addEventListener(type, update);
|
||||
rate.addEventListener('change', update);
|
||||
quality.addEventListener('change', update);
|
||||
mode.addEventListener('change', update);
|
||||
panel.addEventListener('close', () => {
|
||||
scrubbing = false;
|
||||
clearTimeout(hideTimer);
|
||||
hideControls(false);
|
||||
if (document.fullscreenElement === panel) document.exitFullscreen().catch(() => {});
|
||||
else if (document.webkitFullscreenElement === panel) document.webkitExitFullscreen?.();
|
||||
});
|
||||
update();
|
||||
return {update, active: () => mode.matches, rotation: () => orientation.rotation()};
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user