不知名提交

This commit is contained in:
2025-12-13 20:53:50 +08:00
parent c147502b4d
commit 1221d6faf1
120 changed files with 11005 additions and 1092 deletions

View File

@@ -0,0 +1,412 @@
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>▶️视频播放器</title>
<style>
:root{
--bg-from:#e9fbb6; /* 淡黄绿色 */
--bg-to:#bff2cf; /* 淡绿色 */
--accent:#6bbf7a; /* 主色 */
--accent-2:#4aa36b; /* 主色深 */
--text:#18412a; /* 文本深色 */
--muted:#2a6b47; /* 次要文本 */
--card:#ffffffcc; /* 半透明卡片 */
--edge:#e3f0e6; /* 边框 */
--shadow: 0 12px 24px rgba(40,120,80,.15);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
color:var(--text);
background: linear-gradient(160deg,var(--bg-from),var(--bg-to));
-webkit-tap-highlight-color: transparent;
}
.wrap{
min-height:100dvh;
display:flex;flex-direction:column;gap:16px;
padding:16px; padding-bottom:24px;
max-width: 960px; margin: 0 auto;
}
header{
display:flex; align-items:center; justify-content:space-between; gap:12px;
}
.title{
font-weight:700; letter-spacing:.2px; font-size:18px;
}
.uploader{
display:grid; grid-template-columns:1fr; gap:10px;
background:var(--card);
border:1px solid var(--edge);
box-shadow:var(--shadow);
border-radius:18px; padding:12px;
}
.row{display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
input[type="url"], .btn, input[type="file"], .speed-select, .range{
border:1px solid var(--edge); border-radius:14px;
background:#fff; color:var(--text);
padding:10px 12px; font-size:14px;
}
input[type="url"]{ flex:1; min-width:0; }
.btn{ background:linear-gradient(180deg,#ffffff,#f7fff5); cursor:pointer; user-select:none; }
.btn.primary{ background:linear-gradient(180deg,#d9fbd8,#bcf2c9); border-color:#b4e6be; }
.btn:hover{ filter:saturate(1.02); }
.btn:active{ transform:translateY(1px); }
.btn[disabled]{ opacity:.5; cursor:not-allowed; }.player-card{
background:var(--card); border:1px solid var(--edge); box-shadow:var(--shadow);
border-radius:22px; overflow:hidden;
}
.video-box{ position:relative; background:#000; aspect-ratio:16/9; }
video{ width:100%; height:auto; display:block; background:#000; }
.overlay-center{
position:absolute; inset:0; display:flex; align-items:center; justify-content:center; pointer-events:none;
}
.big-btn{
width:64px; height:64px; border-radius:50%;
background:radial-gradient(circle at 30% 30%, #ffffff, #eaffea);
display:grid; place-items:center; box-shadow:0 8px 24px rgba(0,0,0,.25);
opacity:0; transform:scale(.9); transition:.25s;
}
.overlay-center.show .big-btn{ opacity:1; transform:scale(1); }
.controls{
display:flex; flex-direction:column; gap:8px; padding:12px; background:linear-gradient(180deg,#f3fff3cc,#eafff2cc);
border-top:1px solid var(--edge);
}
.progress-row{ display:flex; align-items:center; gap:10px; }
.range{ -webkit-appearance:none; appearance:none; width:100%; height:14px; padding:0 0; background:transparent; }
.range::-webkit-slider-runnable-track{ height:6px; background:linear-gradient(90deg,#c9efcf,#aee7bb); border-radius:999px; }
.range::-moz-range-track{ height:6px; background:linear-gradient(90deg,#c9efcf,#aee7bb); border-radius:999px; }
.range::-webkit-slider-thumb{ -webkit-appearance:none; margin-top:-6px; width:18px; height:18px; border-radius:50%; background:#fff; border:1px solid #cde6d5; box-shadow:0 2px 8px rgba(51,117,84,.3); }
.range::-moz-range-thumb{ width:18px; height:18px; border:none; border-radius:50%; background:#fff; box-shadow:0 2px 8px rgba(51,117,84,.3); }
.time{ font-variant-tabular-nums:tabular-nums; font-size:12px; color:var(--muted); min-width:96px; text-align:center; }
.bar{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.icon-btn{ border:none; background:#fff; border:1px solid var(--edge); border-radius:12px; width:40px; height:40px; display:grid; place-items:center; cursor:pointer; }
.icon-btn:active{ transform:translateY(1px); }
.speed-select{ padding-right:28px; }
.vol{ display:flex; align-items:center; gap:6px; }
.vol input{ width:110px; }
.name{ font-size:12px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* 手机竖屏优先 */
@media (min-width:720px){
.controls{ padding:14px 16px; }
.icon-btn{ width:42px; height:42px; }
.vol input{ width:140px; }
}
@media (max-width:420px){
.vol input{ width:96px; }
.time{ min-width:70px; }
.uploader .row > .btn,
.uploader .row > input[type="url"] { flex: 1 1 100%; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="title">▶️视频播放器</div>
<button class="btn" id="resetBtn" title="重置播放器">重置</button>
</header><section class="uploader" aria-label="选择视频源">
<div class="row">
<input aria-label="视频链接" type="url" id="urlInput" placeholder="粘贴视频直链(.mp4 / .webm / .ogg / .m3u8后回车…" />
<button class="btn primary" id="loadUrlBtn">加载链接</button>
</div>
<div class="row">
<input aria-label="选择本地视频文件" type="file" id="fileInput" class="file-input" accept="video/*" />
<label for="fileInput" class="btn" id="fileInputLabel">选择本地文件</label>
<span class="name" id="fileName">未选择文件</span>
</div>
</section>
<section class="player-card" aria-label="视频播放器">
<div class="video-box" id="videoBox">
<video id="video" playsinline preload="metadata"></video>
<div class="overlay-center" id="overlay">
<div class="big-btn" aria-hidden="true">▶︎</div>
</div>
</div>
<div class="controls" role="group" aria-label="播放控制">
<div class="progress-row">
<span class="time" id="timeNow">00:00</span>
<input type="range" class="range" id="progress" min="0" max="1000" value="0" step="1" aria-label="进度条" />
<span class="time" id="timeTotal">--:--</span>
</div>
<div class="bar">
<button class="icon-btn" id="playBtn" title="播放/暂停 (空格)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 5v14l11-7-11-7z" fill="currentColor"/></svg>
</button>
<button class="icon-btn" id="backwardBtn" title="后退10秒 (←)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M11 5V2L6 7l5 5V9c3.86 0 7 3.14 7 7 0 1.05-.22 2.05-.62 2.95l1.48 1.48A8.96 8.96 0 0020 16c0-4.97-4.03-9-9-9z" fill="currentColor"/><text x="3" y="21" font-size="8" fill="currentColor">10</text></svg>
</button>
<button class="icon-btn" id="forwardBtn" title="快进10秒 (→)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M13 5V2l5 5-5 5V9c-3.86 0-7 3.14-7 7 0 1.05.22 2.05.62 2.95L5.14 20.4A8.96 8.96 0 014 16c0-4.97 4.03-9 9-9z" fill="currentColor"/><text x="14" y="21" font-size="8" fill="currentColor">10</text></svg>
</button>
<label class="vol" title="音量 (↑/↓)">
<button class="icon-btn" id="muteBtn" aria-label="静音切换">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/></svg>
</button>
<input type="range" class="range" id="volume" min="0" max="1" step="0.01" value="1" aria-label="音量" />
</label>
<select id="speed" class="speed-select" title="倍速 ( , / . )" aria-label="倍速">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1.0×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2.0×</option>
</select>
<button class="icon-btn" id="pipBtn" title="画中画">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor"/><rect x="12.5" y="10" width="7" height="5" rx="1" fill="currentColor"/></svg>
</button>
<button class="icon-btn" id="fsBtn" title="全屏 (F)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 3H3v5h2V5h3V3zm10 0h-5v2h3v3h2V3zM5 14H3v7h7v-2H5v-5zm16 0h-2v5h-5v2h7v-7z" fill="currentColor"/></svg>
</button>
<div class="name" id="sourceName" title="当前来源">未加载视频</div>
</div>
</div>
</section>
</div> <!-- 可选HLS 支持(仅当加载 .m3u8 时使用)。静态网页可直接引用 CDN。--> <script src="hls.min.js" integrity="sha384-N7Pzv6j4n0O3+zJVwCyO0n2A7bgb1z47Z4+Z1fH2E0KXpKj9c3n2U6xJQ8P9yq3s" crossorigin="anonymous"></script> <script>
const $ = sel => document.querySelector(sel);
const video = $('#video');
const progress = $('#progress');
const timeNow = $('#timeNow');
const timeTotal = $('#timeTotal');
const playBtn = $('#playBtn');
const backwardBtn = $('#backwardBtn');
const forwardBtn = $('#forwardBtn');
const muteBtn = $('#muteBtn');
const volume = $('#volume');
const speed = $('#speed');
const fsBtn = $('#fsBtn');
const pipBtn = $('#pipBtn');
const overlay = $('#overlay');
const fileInput = $('#fileInput');
const urlInput = $('#urlInput');
const loadUrlBtn = $('#loadUrlBtn');
const sourceName = $('#sourceName');
const fileName = $('#fileName');
const fileInputLabel = $('#fileInputLabel');
const resetBtn = $('#resetBtn');
const videoBox = $('#videoBox');
const fmt = s => {
if (!isFinite(s)) return '--:--';
const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = Math.floor(s%60);
return h>0 ? `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}` : `${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`;
};
// 恢复偏好
try{
const vol = localStorage.getItem('vp_volume');
if (vol !== null) video.volume = Number(vol);
volume.value = video.volume;
const spd = localStorage.getItem('vp_speed');
if (spd) { video.playbackRate = Number(spd); speed.value = spd; }
}catch{}
// 事件绑定
const togglePlay = () => {
if (video.paused || video.ended) { video.play(); showOverlay(); } else { video.pause(); showOverlay(); }
updatePlayIcon();
};
const updatePlayIcon = () => {
playBtn.innerHTML = video.paused ?
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 5v14l11-7-11-7z" fill="currentColor"/></svg>' :
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z" fill="currentColor"/></svg>';
};
const showOverlay = () => {
overlay.classList.add('show');
clearTimeout(showOverlay._t);
showOverlay._t = setTimeout(()=>overlay.classList.remove('show'), 350);
};
const step = (sec) => { video.currentTime = Math.max(0, Math.min(video.duration || 0, video.currentTime + sec)); showOverlay(); };
const loadFile = (file) => {
if (!file) return;
const url = URL.createObjectURL(file);
attachSource(url, {name: file.name});
sourceName.textContent = file.name;
fileName.textContent = file.name;
if (fileInputLabel) fileInputLabel.textContent = '已选择:' + file.name;
};
const isHls = (url) => /\.m3u8(\?.*)?$/i.test(url);
const isDirect = (url) => /(\.mp4|\.webm|\.ogg)(\?.*)?$/i.test(url) || url.startsWith('blob:') || url.startsWith('data:');
let hls; // hls.js 实例
const destroyHls = () => { if (hls) { hls.destroy(); hls = null; } };
function attachSource(url, {name='外部链接'}={}){
destroyHls();
video.pause();
video.removeAttribute('src');
video.load();
if (isHls(url)){
if (video.canPlayType('application/vnd.apple.mpegURL')){
video.src = url; // Safari 原生
} else if (window.Hls && window.Hls.isSupported()){
hls = new Hls({ maxBufferLength: 30, enableWorker: true });
hls.loadSource(url);
hls.attachMedia(video);
} else {
alert('该浏览器不支持 HLS 播放(.m3u8。请使用 Safari 或现代 Chromium 浏览器。');
return;
}
} else if (isDirect(url)) {
video.src = url;
} else {
alert('请提供视频“直链”地址(.mp4 / .webm / .ogg / .m3u8。普通网页链接无法直接播放。');
return;
}
sourceName.textContent = name || url;
video.playbackRate = Number(speed.value || 1);
video.volume = Number(volume.value);
updatePlayIcon();
video.addEventListener('loadedmetadata', ()=>{
timeTotal.textContent = fmt(video.duration);
progress.value = 0; timeNow.textContent = '00:00';
}, { once: true });
}
// 选择文件
fileInput.addEventListener('change', e=>{
const file = e.target.files && e.target.files[0];
loadFile(file);
});
// 通过 URL 加载
const loadFromInput = () => {
const url = (urlInput.value || '').trim();
if (!url) return;
attachSource(url, {name: url});
};
loadUrlBtn.addEventListener('click', loadFromInput);
urlInput.addEventListener('keydown', e=>{ if (e.key === 'Enter') loadFromInput(); });
// 播放控制
playBtn.addEventListener('click', togglePlay);
backwardBtn.addEventListener('click', ()=>step(-10));
forwardBtn.addEventListener('click', ()=>step(10));
// 音量与静音
volume.addEventListener('input', ()=>{ video.volume = Number(volume.value); try{localStorage.setItem('vp_volume', volume.value);}catch{} });
muteBtn.addEventListener('click', ()=>{ video.muted = !video.muted; muteBtn.style.opacity = video.muted? .6:1; });
// 倍速
speed.addEventListener('change', ()=>{ video.playbackRate = Number(speed.value); try{localStorage.setItem('vp_speed', speed.value);}catch{} });
// 画中画
pipBtn.addEventListener('click', async ()=>{
try{
if (document.pictureInPictureElement) { await document.exitPictureInPicture(); }
else if (document.pictureInPictureEnabled && !video.disablePictureInPicture) { await video.requestPictureInPicture(); }
}catch(err){ console.warn(err); }
});
// 全屏
const isFullscreen = () => document.fullscreenElement || document.webkitFullscreenElement;
fsBtn.addEventListener('click', ()=>{
if (!isFullscreen()) {
(videoBox.requestFullscreen || videoBox.webkitRequestFullscreen || videoBox.requestFullScreen)?.call(videoBox);
} else {
(document.exitFullscreen || document.webkitExitFullscreen)?.call(document);
}
});
// 进度显示 & 拖动
const syncProgress = () => {
if (!video.duration) return;
const fraction = video.currentTime / video.duration;
progress.value = Math.round(fraction * 1000);
timeNow.textContent = fmt(video.currentTime);
timeTotal.textContent = fmt(video.duration);
};
let rafId;
const loop = () => { syncProgress(); rafId = requestAnimationFrame(loop); };
video.addEventListener('play', ()=>{ updatePlayIcon(); cancelAnimationFrame(rafId); loop(); });
video.addEventListener('pause', ()=>{ updatePlayIcon(); cancelAnimationFrame(rafId); syncProgress(); });
video.addEventListener('ended', ()=>{ updatePlayIcon(); showOverlay(); });
let seeking = false;
const seekTo = (val)=>{
if (!video.duration) return;
const target = (val/1000) * video.duration;
video.currentTime = target;
syncProgress();
};
['input','change'].forEach(evt=>progress.addEventListener(evt, e=>{ seeking = evt==='input'; seekTo(Number(progress.value)); }));
// 键盘快捷键
document.addEventListener('keydown', (e)=>{
if (/input|textarea|select/i.test(document.activeElement.tagName)) return;
switch(e.key){
case ' ': e.preventDefault(); togglePlay(); break;
case 'ArrowLeft': step(-5); break;
case 'ArrowRight': step(5); break;
case 'ArrowUp': video.volume = Math.min(1, video.volume + .05); volume.value = video.volume; break;
case 'ArrowDown': video.volume = Math.max(0, video.volume - .05); volume.value = video.volume; break;
case 'f': case 'F': fsBtn.click(); break;
case 'm': case 'M': video.muted = !video.muted; muteBtn.style.opacity = video.muted? .6:1; break;
case ',': video.playbackRate = Math.max(.25, Math.round((video.playbackRate - .25)*4)/4); speed.value = String(video.playbackRate); break;
case '.': video.playbackRate = Math.min(2, Math.round((video.playbackRate + .25)*4)/4); speed.value = String(video.playbackRate); break;
default:
if (/^[0-9]$/.test(e.key) && video.duration){
const pct = Number(e.key) / 10; video.currentTime = pct * video.duration;
}
}
});
// 双击左右快进/后退(移动端友好)
let lastTap = 0;
videoBox.addEventListener('touchend', (e)=>{
const now = Date.now();
const dt = now - lastTap; lastTap = now;
if (dt>300) return; // 双击窗口
const x = e.changedTouches[0].clientX;
const rect = videoBox.getBoundingClientRect();
if (x - rect.left < rect.width/2) step(-10); else step(10);
});
// 点击视频区域也可播放/暂停
videoBox.addEventListener('click', (e)=>{
// 避免点击控制条触发
if (e.target.closest('.controls')) return;
togglePlay();
});
// 重置
resetBtn.addEventListener('click', ()=>{
try{ localStorage.removeItem('vp_volume'); localStorage.removeItem('vp_speed'); }catch{}
destroyHls();
video.pause();
video.removeAttribute('src');
video.load();
sourceName.textContent = '未加载视频';
fileName.textContent = '未选择文件';
urlInput.value = '';
progress.value = 0; timeNow.textContent = '00:00'; timeTotal.textContent = '--:--';
volume.value = 1; video.volume = 1; speed.value = '1'; video.playbackRate = 1;
updatePlayIcon();
});
// 初始图标
updatePlayIcon();
</script></body>
</html>