| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>视频播放 - 宁德市建筑业协会</title>
- <script src="/static/js/hls.min.js"></script>
- <script src="/static/js/md5.min.js"></script>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- background: #f0f2f5;
- min-height: 100vh;
- }
- .pc-player-container {
- min-height: 100vh;
- background: #f0f2f5;
- padding: 20px;
- }
- .pc-player-wrapper {
- max-width: 1440px;
- margin: 0 auto;
- display: flex;
- gap: 20px;
- height: calc(100vh - 40px);
- }
- .pc-player-left { flex: 1; min-width: 0; display: flex; flex-direction: column; }
- .panel-toggle {
- flex-shrink: 0; width: 20px; display: flex; align-items: center; justify-content: center;
- cursor: pointer; color: #999; font-size: 14px; user-select: none;
- }
- .panel-toggle:hover { color: #409EFF; }
- .video-section {
- background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); overflow: hidden;
- display: flex; flex-direction: column; height: 100%;
- }
- .video-title {
- padding: 16px 20px; font-size: 16px; font-weight: 600; color: #1a1a2e;
- border-bottom: 1px solid #f0f0f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
- flex-shrink: 0;
- }
- .video-player-wrap {
- width: 100%; background: #000; position: relative; flex: 1; min-height: 0;
- display: flex; align-items: center; justify-content: center;
- }
- .video-player-wrap video {
- width: 100%; height: 100%; display: block; outline: none;
- max-height: calc(100vh - 200px);
- }
- .loading-overlay {
- position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
- background: rgba(0,0,0,0.5); color: #fff; font-size: 16px; z-index: 10;
- transition: opacity .3s;
- }
- .loading-overlay.hidden { opacity: 0; pointer-events: none; }
- .loading-spinner { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 12px; }
- @keyframes spin { to { transform: rotate(360deg); } }
- .dialog-footer {
- padding: 8px 16px; border-top: 1px solid #f0f0f0; background: #fafafa; flex-shrink: 0;
- }
- .media-footer { display: flex; align-items: center; line-height: 1; }
- .media-time {
- font-size: 14px; color: #666; white-space: nowrap; font-variant-numeric: tabular-nums;
- display: flex; align-items: center; gap: 4px;
- }
- .media-center { display: flex; align-items: center; justify-content: center; gap: 12px; flex:1; }
- .btn-icon {
- display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600;
- cursor: pointer; user-select: none; padding: 4px 8px; border: 1px solid #dcdfe6;
- border-radius: 4px; color: #606266; background: #fff; transition: all .2s;
- }
- .btn-icon:hover { color: #409EFF; border-color: #c6e2ff; background: #ecf5ff; }
- .btn-icon.is-active { color: #409EFF; border-color: #409EFF; background: #ecf5ff; }
- .volume-group { display: inline-flex; align-items: center; gap: 6px; }
- .volume-slider { width: 100px; height: 4px; -webkit-appearance: none; appearance: none; background: #dcdfe6; border-radius: 2px; outline: none; }
- .volume-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #409EFF; cursor: pointer; }
- .quality-select {
- height: 28px; font-size: 12px; padding: 0 8px; border: 1px solid #dcdfe6;
- border-radius: 4px; outline: none; color: #606266; background: #fff; cursor: pointer;
- }
- .quality-select:focus { border-color: #409EFF; }
- .pc-player-right {
- width: 380px; flex-shrink: 0; background: #fff; border-radius: 12px;
- box-shadow: 0 2px 12px rgba(0,0,0,0.08); display: flex; flex-direction: column; overflow: hidden;
- }
- .playlist-header {
- padding: 16px 20px 12px; border-bottom: 1px solid #f0f0f0; flex-shrink: 0;
- }
- .playlist-title { font-size: 16px; font-weight: 600; color: #1a1a2e; }
- .playlist-header-row {
- display: flex; align-items: center; justify-content: space-between; margin-top: 6px;
- }
- .playlist-count { font-size: 12px; color: #999; }
- .finished-count { color: #67C23A; }
- .hide-toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; cursor: pointer; color: #666; user-select: none; }
- .hide-toggle input { cursor: pointer; }
- .playlist-body { flex: 1; overflow-y: auto; padding: 8px 0; }
- .playlist-body::-webkit-scrollbar { width: 6px; }
- .playlist-body::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
- .playlist-item {
- display: flex; align-items: center; padding: 12px 20px; cursor: pointer;
- transition: all .2s; border-left: 3px solid transparent; gap: 12px;
- }
- .playlist-item:hover { background: #f5f7fa; }
- .playlist-item.is-active { background: #ecf5ff; border-left-color: #409EFF; }
- .playlist-item.is-finished { opacity: .65; }
- .item-index {
- width: 28px; height: 28px; border-radius: 50%; background: #f0f2f5;
- display: flex; align-items: center; justify-content: center; font-size: 12px;
- font-weight: 600; color: #666; flex-shrink: 0;
- }
- .item-index.active-index { background: #409EFF; color: #fff; }
- .finish-badge { color: #67C23A; font-size: 14px; }
- .item-info { flex: 1; min-width: 0; }
- .item-name { font-size: 14px; color: #333; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px; }
- .is-finished .item-name { color: #999; text-decoration: line-through; }
- .is-active .item-name { color: #409EFF; }
- .item-meta { display: flex; gap: 12px; font-size: 12px; color: #999; }
- .item-progress { color: #409EFF; }
- .is-finished .item-progress { color: #67C23A; }
- .item-action { flex-shrink: 0; }
- .play-btn {
- padding: 4px 10px; font-size: 12px; border: 1px solid #409EFF; color: #409EFF;
- background: transparent; border-radius: 4px; cursor: pointer; transition: all .2s;
- }
- .play-btn:hover { background: #409EFF; color: #fff; }
- .play-btn.pause-btn { background: #e6a23c; border-color: #e6a23c; color: #fff; }
- .playlist-empty { text-align: center; padding: 60px 20px; color: #ccc; font-size: 14px; }
- .toast {
- position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 9999;
- padding: 10px 24px; border-radius: 6px; font-size: 14px; color: #fff;
- transition: all .3s; opacity: 0; pointer-events: none;
- }
- .toast.show { opacity: 1; }
- .toast.success { background: #67C23A; }
- .toast.error { background: #F56C6C; }
- .toast.info { background: #909399; }
- @media (max-width: 900px) {
- .pc-player-wrapper { flex-direction: column; height: auto; }
- .pc-player-right { width: 100%; max-height: 400px; }
- .pc-player-container { padding: 12px; }
- .volume-slider { width: 60px; }
- }
- </style>
- </head>
- <body>
- <div class="pc-player-container">
- <div class="pc-player-wrapper">
- <div class="pc-player-left">
- <div class="video-section">
- <div class="video-title" id="videoTitle">加载中...</div>
- <div class="video-player-wrap">
- <video id="videoElement" playsinline preload="auto"></video>
- <div class="loading-overlay" id="loadingOverlay">
- <div class="loading-spinner"></div>
- <span>加载中...</span>
- </div>
- </div>
- <div class="dialog-footer">
- <div class="media-footer">
- <div class="media-time">
- <span id="currentTime">00:00:00</span>
- <strong>/</strong>
- <span id="totalTime">00:00:00</span>
- </div>
- <div class="media-center">
- <span class="volume-group">
- <span class="btn-icon" id="muteBtn">音量</span>
- <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.05" value="0.5">
- </span>
- <span class="btn-icon" id="pipBtn">画中画</span>
- </div>
- <div style="display:flex;align-items:center;justify-content:flex-end;flex:1;">
- <select class="quality-select" id="qualitySelect">
- <option value="ld">流畅</option>
- <option value="hls" selected>标清</option>
- </select>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="panel-toggle" id="panelToggle"><</div>
- <div class="pc-player-right" id="playlistPanel">
- <div class="playlist-header">
- <div class="playlist-title">课程列表</div>
- <div class="playlist-header-row">
- <div class="playlist-count">
- 共 <span id="totalCount">0</span> 个视频
- <span class="finished-count" id="finishedCountWrap" style="display:none;">,已完成 <span id="finishedCount">0</span> 个</span>
- </div>
- <label class="hide-toggle">
- <input type="checkbox" id="hideFinishedCheck"> 隐藏已完成
- </label>
- </div>
- </div>
- <div class="playlist-body" id="playlistBody"></div>
- </div>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- <script>
- const VERSION = '1.0';
- const SECRET = '017c0743819088468e590246464cc75f';
- const API_BASE = 'https://ndapi.hqedust.com:18443/rental';
- // ---- DOM refs ----
- const video = document.getElementById('videoElement');
- const loadingOverlay = document.getElementById('loadingOverlay');
- const videoTitle = document.getElementById('videoTitle');
- const currentTimeEl = document.getElementById('currentTime');
- const totalTimeEl = document.getElementById('totalTime');
- const volumeSlider = document.getElementById('volumeSlider');
- const muteBtn = document.getElementById('muteBtn');
- const pipBtn = document.getElementById('pipBtn');
- const qualitySelect = document.getElementById('qualitySelect');
- const panelToggle = document.getElementById('panelToggle');
- const playlistPanel = document.getElementById('playlistPanel');
- const playlistBody = document.getElementById('playlistBody');
- const totalCount = document.getElementById('totalCount');
- const finishedCount = document.getElementById('finishedCount');
- const finishedCountWrap = document.getElementById('finishedCountWrap');
- const hideFinishedCheck = document.getElementById('hideFinishedCheck');
- const toast = document.getElementById('toast');
- // ---- state ----
- let uid = 0, token = '', courseId = 0, mediaId = 0;
- let mediaList = [];
- let currentMedia = null;
- let currentMediaUrl = '';
- let isPlaying = false;
- let isMuted = false;
- let volume = 0.5;
- let curTimes = 0;
- let tickTimer = null;
- let tickNum = 0;
- let hasSeeked = false;
- let hiddenStartTime = 0;
- let hiddenBasePosition = 0;
- let isPiP = false;
- let hls = null;
- let showPanel = true;
- // ---- utils ----
- function parseTime(seconds) {
- seconds = parseInt(seconds) || 0;
- const h = String(Math.floor(seconds / 3600)).padStart(2, '0');
- const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
- const s = String(seconds % 60).padStart(2, '0');
- return h + ':' + m + ':' + s;
- }
- function getPercent(item) {
- if (item.isFinish == 1) return 100;
- if (!item.duration) return 0;
- let p = parseInt((item.position || 0) * 10000 / item.duration) / 100;
- return p >= 100 ? 99.99 : p;
- }
- function showToast(msg, type, duration) {
- toast.textContent = msg;
- toast.className = 'toast ' + type + ' show';
- clearTimeout(toast._timer);
- toast._timer = setTimeout(() => toast.classList.remove('show'), duration || 3000);
- }
- // ---- API ----
- function signRequest(method, data) {
- const body = JSON.stringify(data);
- const mtime = Math.floor(Date.now() / 1000);
- const raw = 'hall_' + VERSION + token + body + method + mtime + SECRET;
- const sign = md5(raw);
- const u = method.startsWith('Auth.') ? '0' : (uid || 0);
- return API_BASE + '/' + method + '?t=' + mtime + '&u=' + u + '&v=' + VERSION + '&s=' + sign;
- }
- async function api(method, data) {
- const url = signRequest(method, data);
- const res = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- });
- const json = await res.json();
- if (json.code === 401) {
- showToast('用户被踢下线,请重新登入', 'error', 5000);
- return json;
- }
- if (json.code !== 200) {
- showToast(json.msg || '请求失败', 'error');
- }
- return json;
- }
- function report(action, msg) {
- api('course.report', { action, msg });
- }
- // ---- player ----
- function loadVideo(src, position) {
- if (hls) {
- hls.destroy();
- hls = null;
- }
- video.removeAttribute('src');
- if (Hls.isSupported()) {
- hls = new Hls({
- enableWorker: true,
- xhrSetup: function(xhr) {
- xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
- xhr.setRequestHeader('Pragma', 'no-cache');
- }
- });
- hls.loadSource(src);
- hls.attachMedia(video);
- hls.on(Hls.Events.MANIFEST_PARSED, function() {
- loadingOverlay.classList.add('hidden');
- if (position > 0) {
- video.currentTime = position;
- }
- safePlay();
- });
- hls.on(Hls.Events.ERROR, function(e, data) {
- if (data.fatal) {
- showToast('视频加载失败,尝试切换清晰度', 'error');
- }
- });
- } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
- video.src = src;
- video.addEventListener('loadedmetadata', function() {
- loadingOverlay.classList.add('hidden');
- if (position > 0) video.currentTime = position;
- safePlay();
- }, { once: true });
- } else {
- showToast('您的浏览器不支持 HLS 播放', 'error');
- }
- }
- function safePlay() {
- const p = video.play();
- if (p && p.catch) p.catch(function() {});
- }
- function setVolume(val) {
- volume = Math.max(0, Math.min(1, val));
- video.volume = volume;
- video.muted = isMuted;
- volumeSlider.value = volume;
- muteBtn.textContent = volume === 0 || isMuted ? '静音' : '音量';
- }
- function setPosition(pos) {
- if (pos < 10) return;
- if (pos > (currentMedia ? currentMedia.duration : Infinity)) pos = currentMedia.duration;
- video.currentTime = pos;
- safePlay();
- if (currentMedia && currentMedia.isFinish) return;
- if (currentMedia && currentMedia.position >= currentMedia.duration - 10 && !currentMedia.isFinish) {
- tick(true);
- }
- }
- // ---- course loading ----
- async function loadCourse() {
- const res = await api('course.getCourse', { courseId });
- if (res.code !== 200) return;
- const { info, extra, list, tpl } = res.data;
- const courseInfo = Object.assign(info || {}, extra || {});
- mediaList = (list || []).map(function(item) {
- item.percent = getPercent(item);
- return item;
- });
- updatePlaylist();
- let target = null;
- if (mediaId) {
- target = mediaList.find(function(i) { return i.id == mediaId || i.mediaId == mediaId; });
- }
- if (!target) target = mediaList.find(function(i) { return i.isFinish != 1; });
- if (!target && mediaList.length) target = mediaList[0];
- if (target) {
- loadMedia(target);
- } else {
- videoTitle.textContent = (tpl || {}).name || '暂无视频';
- }
- }
- async function loadMedia(item) {
- currentMedia = item;
- videoTitle.textContent = item.name;
- loadingOverlay.classList.remove('hidden');
- hasSeeked = false;
- isPlaying = false;
- stopTick();
- const res = await api('course.GetMedia', { id: item.id });
- if (res.code !== 200) {
- loadingOverlay.classList.add('hidden');
- return;
- }
- const data = res.data || {};
- let mediaUrl = data.mediaUrl || '';
- const position = (data.position || 0) < 10 ? 0 : (data.position || 0);
- const duration = data.duration || 0;
- currentMediaUrl = mediaUrl;
- currentMedia.position = position;
- currentMedia.duration = duration;
- currentMedia.id = data.id || item.id;
- qualitySelect.value = mediaUrl.includes('/ld/') ? 'ld' : 'hls';
- curTimes = 0;
- loadVideo(mediaUrl, position);
- startTick();
- updatePlaylist();
- report('play', 'start');
- }
- function changeMedia(quality) {
- if (!currentMediaUrl) return;
- let url = currentMediaUrl;
- if (quality === 'ld') {
- url = url.replace('/hls/', '/ld/');
- } else {
- url = url.replace('/ld/', '/hls/');
- }
- currentMediaUrl = url;
- const pos = video.currentTime || 0;
- loadVideo(url, pos > 0 ? pos : 0);
- }
- // ---- tick system ----
- function startTick() {
- stopTick();
- tickTimer = setInterval(tryTick, 5000);
- }
- function stopTick() {
- if (tickTimer) {
- clearInterval(tickTimer);
- tickTimer = null;
- }
- }
- function getPlayerTime() {
- if (hiddenStartTime) {
- const elapsed = (Date.now() - hiddenStartTime) / 1000;
- const estimated = hiddenBasePosition + elapsed;
- const actual = video.currentTime || 0;
- if (actual >= estimated - 0.5) {
- hiddenStartTime = 0;
- return actual;
- }
- return Math.min(estimated, currentMedia ? currentMedia.duration : Infinity);
- }
- return video.currentTime || 0;
- }
- function tryTick() {
- try { doTick(); } catch (e) { report('play', '' + e.message); }
- }
- async function doTick(force) {
- if (!currentMedia || currentMedia.isFinish) return;
- tickNum++;
- const ct = Math.floor(getPlayerTime());
- curTimes = ct;
- if (navigator.webdriver) {
- report('play', 'webdriver');
- doPause();
- return;
- }
- let isFinish = force ? 1 : 0;
- if (ct >= currentMedia.duration) isFinish = 1;
- const res = await api('course.tick', {
- id: currentMedia.id,
- position: ct,
- isFinish: isFinish
- });
- if (res.code !== 200) return;
- const d = res.data || {};
- if (d.closed) {
- doPause();
- showToast(d.msg || '课程已关闭', 'error', 10000);
- return;
- }
- if (d.pause) {
- doPause();
- showToast(d.msg || '已暂停', 'info', 10000);
- setTimeout(async function() {
- await loadCourse();
- }, 30000);
- return;
- }
- if (!d.skip) {
- setPosition(d.position || 0);
- }
- Object.assign(currentMedia, d);
- if (d.isFinish == 1) {
- updatePlaylist();
- await new Promise(r => setTimeout(r, 1000));
- playNext();
- }
- updatePlaylist();
- }
- function tick(force) {
- doTick(force);
- }
- // ---- playlist ----
- function updatePlaylist() {
- const finished = mediaList.filter(function(i) { return i.isFinish == 1; }).length;
- totalCount.textContent = mediaList.length;
- if (finished > 0) {
- finishedCountWrap.style.display = 'inline';
- finishedCount.textContent = finished;
- } else {
- finishedCountWrap.style.display = 'none';
- }
- const hide = hideFinishedCheck.checked;
- const filtered = hide ? mediaList.filter(function(i) { return i.isFinish != 1; }) : mediaList;
- playlistBody.innerHTML = '';
- filtered.forEach(function(item, idx) {
- const div = document.createElement('div');
- div.className = 'playlist-item';
- if (currentMedia && currentMedia.id === item.id) div.classList.add('is-active');
- if (item.isFinish == 1) div.classList.add('is-finished');
- const isCurrent = currentMedia && currentMedia.id === item.id;
- const origIdx = mediaList.indexOf(item);
- div.innerHTML =
- '<div class="item-index' + (isCurrent ? ' active-index' : '') + '">' +
- (item.isFinish == 1 ? '<span class="finish-badge">✓</span>' : '<span>' + (origIdx + 1) + '</span>') +
- '</div>' +
- '<div class="item-info">' +
- '<div class="item-name">' + item.name + '</div>' +
- '<div class="item-meta">' +
- '<span>学时:' + (item.xs / 10) + '</span>' +
- '<span class="item-progress">' + getPercent(item) + '%</span>' +
- '</div>' +
- '</div>' +
- '<div class="item-action">' +
- (isCurrent && isPlaying
- ? '<button class="play-btn pause-btn" data-action="pause">暂停</button>'
- : isCurrent
- ? '<button class="play-btn" data-action="play">播放</button>'
- : '<button class="play-btn" data-action="load">播放</button>') +
- '</div>';
- div.addEventListener('click', function(e) {
- if (e.target.tagName === 'BUTTON') return;
- loadMedia(item);
- });
- const btn = div.querySelector('button');
- if (btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- const action = btn.dataset.action;
- if (action === 'play') doPlay();
- else if (action === 'pause') doPause();
- else loadMedia(item);
- });
- }
- playlistBody.appendChild(div);
- });
- if (filtered.length === 0) {
- playlistBody.innerHTML = '<div class="playlist-empty">暂无视频</div>';
- }
- }
- function playNext() {
- if (!currentMedia) return;
- const idx = mediaList.findIndex(function(i) { return i.id === currentMedia.id; });
- let next = null;
- for (let i = idx + 1; i < mediaList.length; i++) {
- if (mediaList[i].isFinish != 1) { next = mediaList[i]; break; }
- }
- if (!next) {
- for (let i = 0; i < idx; i++) {
- if (mediaList[i].isFinish != 1) { next = mediaList[i]; break; }
- }
- }
- if (next) {
- showToast('即将播放: ' + next.name, 'success');
- setTimeout(function() { loadMedia(next); }, 1000);
- }
- }
- function doPlay() {
- isPlaying = true;
- startTick();
- safePlay();
- tickNum = 0;
- updatePlaylist();
- }
- function doPause() {
- isPlaying = false;
- stopTick();
- video.pause();
- updatePlaylist();
- }
- // ---- PiP ----
- async function enterPiP() {
- if (!document.pictureInPictureEnabled || isPiP) return;
- try {
- await video.requestPictureInPicture();
- } catch (e) {}
- }
- async function exitPiP() {
- if (document.pictureInPictureElement) {
- try { await document.exitPictureInPicture(); } catch (e) {}
- }
- }
- function togglePiP() {
- if (document.pictureInPictureElement) {
- exitPiP();
- } else {
- enterPiP();
- }
- }
- // ---- visibility change ----
- function onVisibilityChange() {
- if (document.hidden) {
- hiddenStartTime = Date.now();
- hiddenBasePosition = video.currentTime || 0;
- } else if (hiddenStartTime) {
- const elapsed = (Date.now() - hiddenStartTime) / 1000;
- const estimated = hiddenBasePosition + elapsed;
- hiddenStartTime = 0;
- if (estimated < (currentMedia ? currentMedia.duration : Infinity)) {
- setPosition(estimated);
- }
- }
- }
- // ---- events ----
- video.addEventListener('timeupdate', function() {
- curTimes = video.currentTime || 0;
- currentTimeEl.textContent = parseTime(curTimes);
- });
- video.addEventListener('loadedmetadata', function() {
- totalTimeEl.textContent = parseTime(video.duration || currentMedia?.duration || 0);
- });
- video.addEventListener('play', function() {
- isPlaying = true;
- report('play', 'start');
- updatePlaylist();
- });
- video.addEventListener('pause', function() {
- isPlaying = false;
- report('play', 'pause');
- updatePlaylist();
- });
- video.addEventListener('ended', function() {
- report('play', 'end');
- tick(true);
- setTimeout(playNext, 500);
- });
- video.addEventListener('error', function(e) {
- showToast('视频播放出错', 'error');
- report('play', 'error');
- });
- video.addEventListener('enterpictureinpicture', function() {
- isPiP = true;
- pipBtn.textContent = '退出画中画';
- pipBtn.classList.add('is-active');
- });
- video.addEventListener('leavepictureinpicture', function() {
- isPiP = false;
- pipBtn.textContent = '画中画';
- pipBtn.classList.remove('is-active');
- });
- volumeSlider.addEventListener('input', function() {
- setVolume(this.valueAsNumber);
- });
- muteBtn.addEventListener('click', function() {
- isMuted = !isMuted;
- setVolume(volume);
- });
- pipBtn.addEventListener('click', togglePiP);
- qualitySelect.addEventListener('change', function() {
- changeMedia(this.value);
- });
- panelToggle.addEventListener('click', function() {
- showPanel = !showPanel;
- playlistPanel.style.display = showPanel ? '' : 'none';
- panelToggle.textContent = showPanel ? '>' : '<';
- });
- hideFinishedCheck.addEventListener('change', updatePlaylist);
- // ---- init ----
- function init() {
- const params = new URLSearchParams(window.location.search);
- uid = params.get('uid') || localStorage.getItem('uid') || 0;
- token = params.get('token') || localStorage.getItem('token') || '';
- courseId = parseInt(params.get('courseId') || params.get('id') || '0');
- mediaId = parseInt(params.get('mediaId') || '0');
- if (params.get('uid')) localStorage.setItem('uid', uid);
- if (params.get('token')) localStorage.setItem('token', token);
- document.addEventListener('visibilitychange', onVisibilityChange);
- window.addEventListener('beforeunload', function() {
- stopTick();
- exitPiP();
- });
- if (courseId) {
- loadCourse();
- } else {
- videoTitle.textContent = '缺少课程ID';
- }
- }
- init();
- </script>
- </body>
- </html>
|