dy / player.html
flzta's picture
Upload 18 files
f3fcf64 verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LibreTV 播放器</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="css/styles.css">
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: #0f1622;
color: white;
}
.player-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
#player {
width: 100%;
height: 60vh; /* 视频播放器高度 */
}
.loading-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
color: white;
z-index: 100;
flex-direction: column;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-bottom: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
color: white;
z-index: 100;
flex-direction: column;
text-align: center;
padding: 1rem;
}
.error-icon {
font-size: 48px;
margin-bottom: 10px;
}
.episode-active {
background-color: #3b82f6 !important;
border-color: #60a5fa !important;
}
.episode-grid {
max-height: 30vh;
overflow-y: auto;
padding: 1rem 0;
}
.switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #333;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #00ccff;
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* 添加快捷键提示样式 */
.shortcut-hint {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 1rem 2rem;
border-radius: 0.5rem;
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.shortcut-hint.show {
opacity: 1;
}
</style>
</head>
<body>
<header class="bg-[#111] p-4 flex justify-between items-center border-b border-[#333]">
<div class="flex items-center">
<a href="index.html" class="flex items-center">
<svg class="w-8 h-8 mr-2 text-[#00ccff]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
<h1 class="text-xl font-bold gradient-text">LibreTV</h1>
</a>
</div>
<h2 id="videoTitle" class="text-xl font-semibold truncate flex-1 text-center"></h2>
<a href="index.html" class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors">
返回首页
</a>
</header>
<main class="container mx-auto px-4 py-4">
<!-- 视频播放区 -->
<div class="player-container">
<div class="relative">
<div id="player"></div>
<div class="loading-container" id="loading">
<div class="loading-spinner"></div>
<div>正在加载视频...</div>
</div>
<div class="error-container" id="error">
<div class="error-icon">⚠️</div>
<div id="error-message">视频加载失败</div>
<div style="margin-top: 10px; font-size: 14px; color: #aaa;">请尝试其他视频源或稍后重试</div>
</div>
</div>
</div>
<!-- 集数导航 -->
<div class="player-container">
<div class="flex justify-between items-center my-4">
<button onclick="playPreviousEpisode()" id="prevButton" class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors">
<svg class="w-5 h-5 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
上一集
</button>
<span class="text-gray-400" id="episodeInfo">加载中...</span>
<button onclick="playNextEpisode()" id="nextButton" class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors">
下一集
<svg class="w-5 h-5 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
<!-- 添加自动播放开关和排序按钮 -->
<div class="player-container">
<div class="flex justify-end items-center mb-4 gap-2">
<span class="text-gray-400 text-sm">自动连播</span>
<label class="switch">
<input type="checkbox" id="autoplayToggle">
<span class="slider"></span>
</label>
<button onclick="toggleEpisodeOrder()" class="ml-4 px-4 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" id="orderIcon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z" clip-rule="evenodd" />
</svg>
<span id="orderText">倒序排列</span>
</button>
</div>
</div>
<!-- 集数网格 -->
<div class="player-container">
<div class="episode-grid" id="episodesGrid">
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2" id="episodesList">
<!-- 集数将在这里动态加载 -->
<div class="col-span-full text-center text-gray-400 py-8">加载中...</div>
</div>
</div>
</div>
</main>
<!-- 添加快捷键提示元素 -->
<div class="shortcut-hint" id="shortcutHint">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" id="shortcutIcon">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
<span id="shortcutText"></span>
</div>
<script src="https://s4.zstatic.net/ajax/libs/hls.js/1.5.6/hls.min.js" integrity="sha256-X1GmLMzVcTBRiGjEau+gxGpjRK96atNczcLBg5w6hKA=" crossorigin="anonymous"></script>
<script src="https://s4.zstatic.net/ajax/libs/dplayer/1.26.0/DPlayer.min.js" integrity="sha256-OJg03lDZP0NAcl3waC9OT5jEa8XZ8SM2n081Ik953o4=" crossorigin="anonymous"></script>
<script src="js/config.js"></script>
<script>
// 全局变量
let currentVideoTitle = '';
let currentEpisodeIndex = 0;
let currentEpisodes = [];
let episodesReversed = false;
let dp = null;
let currentHls = null; // 跟踪当前HLS实例
let autoplayEnabled = true; // 默认开启自动连播
let isUserSeeking = false; // 跟踪用户是否正在拖动进度条
let videoHasEnded = false; // 跟踪视频是否已经自然结束
let userClickedPosition = null; // 记录用户点击的位置
let shortcutHintTimeout = null; // 用于控制快捷键提示显示时间
let adFilteringEnabled = true; // 默认开启广告过滤
// 页面加载
document.addEventListener('DOMContentLoaded', function() {
// 解析URL参数
const urlParams = new URLSearchParams(window.location.search);
const videoUrl = urlParams.get('url');
const title = urlParams.get('title');
const index = parseInt(urlParams.get('index') || '0');
// 从localStorage获取数据
currentVideoTitle = title || localStorage.getItem('currentVideoTitle') || '未知视频';
currentEpisodeIndex = index;
// 设置自动连播开关状态
autoplayEnabled = localStorage.getItem('autoplayEnabled') !== 'false'; // 默认为true
document.getElementById('autoplayToggle').checked = autoplayEnabled;
// 获取广告过滤设置
adFilteringEnabled = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true
// 监听自动连播开关变化
document.getElementById('autoplayToggle').addEventListener('change', function(e) {
autoplayEnabled = e.target.checked;
localStorage.setItem('autoplayEnabled', autoplayEnabled);
});
try {
currentEpisodes = JSON.parse(localStorage.getItem('currentEpisodes') || '[]');
episodesReversed = localStorage.getItem('episodesReversed') === 'true';
} catch (e) {
console.error('获取集数信息失败:', e);
currentEpisodes = [];
episodesReversed = false;
}
// 设置页面标题
document.title = currentVideoTitle + ' - LibreTV播放器';
document.getElementById('videoTitle').textContent = currentVideoTitle;
// 初始化播放器
if (videoUrl) {
initPlayer(videoUrl);
} else {
showError('无效的视频链接');
}
// 更新集数信息
updateEpisodeInfo();
// 渲染集数列表
renderEpisodes();
// 更新按钮状态
updateButtonStates();
// 更新排序按钮状态
updateOrderButton();
// 添加对进度条的监听,确保点击准确跳转
setTimeout(() => {
setupProgressBarPreciseClicks();
}, 1000);
// 添加键盘快捷键事件监听
document.addEventListener('keydown', handleKeyboardShortcuts);
});
// 处理键盘快捷键
function handleKeyboardShortcuts(e) {
// 忽略输入框中的按键事件
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Alt + 左箭头 = 上一集
if (e.altKey && e.key === 'ArrowLeft') {
if (currentEpisodeIndex > 0) {
playPreviousEpisode();
showShortcutHint('上一集', 'left');
e.preventDefault();
}
}
// Alt + 右箭头 = 下一集
if (e.altKey && e.key === 'ArrowRight') {
if (currentEpisodeIndex < currentEpisodes.length - 1) {
playNextEpisode();
showShortcutHint('下一集', 'right');
e.preventDefault();
}
}
}
// 显示快捷键提示
function showShortcutHint(text, direction) {
const hintElement = document.getElementById('shortcutHint');
const textElement = document.getElementById('shortcutText');
const iconElement = document.getElementById('shortcutIcon');
// 清除之前的超时
if (shortcutHintTimeout) {
clearTimeout(shortcutHintTimeout);
}
// 设置文本和图标方向
textElement.textContent = text;
if (direction === 'left') {
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>';
} else {
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>';
}
// 显示提示
hintElement.classList.add('show');
// 两秒后隐藏
shortcutHintTimeout = setTimeout(() => {
hintElement.classList.remove('show');
}, 2000);
}
// 初始化播放器
function initPlayer(videoUrl) {
if (!videoUrl) return;
// 配置HLS.js选项
const hlsConfig = {
debug: false,
loader: adFilteringEnabled ? CustomHlsJsLoader : Hls.DefaultConfig.loader,
enableWorker: true,
lowLatencyMode: false,
backBufferLength: 90,
maxBufferLength: 30,
maxMaxBufferLength: 60,
maxBufferSize: 30 * 1000 * 1000,
maxBufferHole: 0.5,
fragLoadingMaxRetry: 6,
fragLoadingMaxRetryTimeout: 64000,
fragLoadingRetryDelay: 1000,
manifestLoadingMaxRetry: 3,
manifestLoadingRetryDelay: 1000,
levelLoadingMaxRetry: 4,
levelLoadingRetryDelay: 1000,
startLevel: -1,
abrEwmaDefaultEstimate: 500000,
abrBandWidthFactor: 0.95,
abrBandWidthUpFactor: 0.7,
abrMaxWithRealBitrate: true,
stretchShortVideoTrack: true,
appendErrorMaxRetry: 5, // 增加尝试次数
liveSyncDurationCount: 3,
liveDurationInfinity: false
};
// 创建DPlayer实例
dp = new DPlayer({
container: document.getElementById('player'),
autoplay: true,
theme: '#00ccff',
preload: 'auto',
loop: false,
lang: 'zh-cn',
hotkey: true, // 启用键盘控制,包括空格暂停/播放、方向键控制进度和音量
mutex: true,
volume: 0.7,
screenshot: true, // 启用截图功能
preventClickToggle: false, // 允许点击视频切换播放/暂停
airplay: true, // 在Safari中启用AirPlay功能
chromecast: true, // 启用Chromecast投屏功能
contextmenu: [ // 自定义右键菜单
{
text: '关于 LibreTV',
link: 'https://github.com/bestzwei/LibreTV'
},
{
text: '问题反馈',
click: (player) => {
window.open('https://github.com/bestzwei/LibreTV/issues', '_blank');
}
}
],
video: {
url: videoUrl,
type: 'hls',
pic: 'https://img.picgo.net/2025/04/12/image362e7d38b4af4a74.png', // 设置视频封面图
customType: {
hls: function(video, player) {
// 清理之前的HLS实例
if (currentHls && currentHls.destroy) {
try {
currentHls.destroy();
} catch (e) {
console.warn('销毁旧HLS实例出错:', e);
}
}
// 创建新的HLS实例
const hls = new Hls(hlsConfig);
currentHls = hls;
// 跟踪是否已经显示错误
let errorDisplayed = false;
// 跟踪是否有错误发生
let errorCount = 0;
// 跟踪视频是否开始播放
let playbackStarted = false;
// 跟踪视频是否出现bufferAppendError
let bufferAppendErrorCount = 0;
// 监听视频播放事件
video.addEventListener('playing', function() {
playbackStarted = true;
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'none';
});
// 监听视频进度事件
video.addEventListener('timeupdate', function() {
if (video.currentTime > 1) {
// 视频进度超过1秒,隐藏错误(如果存在)
document.getElementById('error').style.display = 'none';
}
});
hls.loadSource(video.src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play().catch(e => {
console.warn('自动播放被阻止:', e);
});
});
hls.on(Hls.Events.ERROR, function(event, data) {
console.log('HLS事件:', event, '数据:', data);
// 增加错误计数
errorCount++;
// 处理bufferAppendError
if (data.details === 'bufferAppendError') {
bufferAppendErrorCount++;
console.warn(`bufferAppendError 发生 ${bufferAppendErrorCount} 次`);
// 如果视频已经开始播放,则忽略这个错误
if (playbackStarted) {
console.log('视频已在播放中,忽略bufferAppendError');
return;
}
// 如果出现多次bufferAppendError但视频未播放,尝试恢复
if (bufferAppendErrorCount >= 3) {
hls.recoverMediaError();
}
}
// 如果是致命错误,且视频未播放
if (data.fatal && !playbackStarted) {
console.error('致命HLS错误:', data);
// 尝试恢复错误
switch(data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log("尝试恢复网络错误");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log("尝试恢复媒体错误");
hls.recoverMediaError();
break;
default:
// 仅在多次恢复尝试后显示错误
if (errorCount > 3 && !errorDisplayed) {
errorDisplayed = true;
showError('视频加载失败,可能是格式不兼容或源不可用');
}
break;
}
}
});
// 监听分段加载事件
hls.on(Hls.Events.FRAG_LOADED, function() {
document.getElementById('loading').style.display = 'none';
});
// 监听级别加载事件
hls.on(Hls.Events.LEVEL_LOADED, function() {
document.getElementById('loading').style.display = 'none';
});
}
}
}
});
dp.on('loadedmetadata', function() {
document.getElementById('loading').style.display = 'none';
videoHasEnded = false; // 视频加载时重置结束标志
// 视频加载完成后重新设置进度条点击监听
setupProgressBarPreciseClicks();
});
dp.on('error', function() {
// 检查视频是否已经在播放
if (dp.video && dp.video.currentTime > 1) {
console.log('发生错误,但视频已在播放中,忽略');
return;
}
showError('视频播放失败,请检查视频源或网络连接');
});
// 添加seeking和seeked事件监听器,以检测用户是否在拖动进度条
dp.on('seeking', function() {
isUserSeeking = true;
videoHasEnded = false; // 重置视频结束标志
// 如果是用户通过点击进度条设置的位置,确保准确跳转
if (userClickedPosition !== null && dp.video) {
// 确保用户的点击位置被正确应用,避免自动跳至视频末尾
const clickedTime = userClickedPosition;
// 防止跳转到视频结尾
if (Math.abs(dp.video.duration - clickedTime) < 0.5) {
// 如果点击的位置非常接近结尾,稍微减少一点时间
dp.video.currentTime = Math.max(0, clickedTime - 0.5);
} else {
dp.video.currentTime = clickedTime;
}
// 清除记录的位置
setTimeout(() => {
userClickedPosition = null;
}, 200);
}
});
// 改进seeked事件处理
dp.on('seeked', function() {
// 如果视频跳转到了非常接近结尾的位置(小于0.3秒),且不是自然播放到此处
if (dp.video && dp.video.duration > 0) {
const timeFromEnd = dp.video.duration - dp.video.currentTime;
if (timeFromEnd < 0.3 && isUserSeeking) {
// 将播放时间往回移动一点点,避免触发结束事件
dp.video.currentTime = Math.max(0, dp.video.currentTime - 1);
}
}
// 延迟重置seeking标志,以便于区分自然播放结束和用户拖拽
setTimeout(() => {
isUserSeeking = false;
}, 200);
});
// 修改视频结束事件监听器,添加额外检查
dp.on('ended', function() {
videoHasEnded = true; // 标记视频已自然结束
// 如果启用了自动连播,并且有下一集可播放,则自动播放下一集
if (autoplayEnabled && currentEpisodeIndex < currentEpisodes.length - 1) {
console.log('视频播放结束,自动播放下一集');
// 稍长延迟以确保所有事件处理完成
setTimeout(() => {
// 确认不是因为用户拖拽导致的假结束事件
if (videoHasEnded && !isUserSeeking) {
playNextEpisode();
videoHasEnded = false; // 重置标志
}
}, 1000);
} else {
console.log('视频播放结束,无下一集或未启用自动连播');
}
});
// 添加事件监听以检测近视频末尾的点击拖动
dp.on('timeupdate', function() {
if (dp.video && dp.duration > 0) {
// 如果视频接近结尾但不是自然播放到结尾,重置自然结束标志
if (isUserSeeking && dp.video.currentTime > dp.video.duration * 0.95) {
videoHasEnded = false;
}
}
});
// 10秒后如果仍在加载,但不立即显示错误
setTimeout(function() {
// 如果视频已经播放开始,则不显示错误
if (dp && dp.video && dp.video.currentTime > 0) {
return;
}
if (document.getElementById('loading').style.display !== 'none') {
document.getElementById('loading').innerHTML = `
<div class="loading-spinner"></div>
<div>视频加载时间较长,请耐心等待...</div>
<div style="font-size: 12px; color: #aaa; margin-top: 10px;">如长时间无响应,请尝试其他视频源</div>
`;
}
}, 10000);
}
// 自定义M3U8 Loader用于过滤广告
class CustomHlsJsLoader extends Hls.DefaultConfig.loader {
constructor(config) {
super(config);
const load = this.load.bind(this);
this.load = function(context, config, callbacks) {
// 拦截manifest和level请求
if (context.type === 'manifest' || context.type === 'level') {
const onSuccess = callbacks.onSuccess;
callbacks.onSuccess = function(response, stats, context) {
// 如果是m3u8文件,处理内容以移除广告分段
if (response.data && typeof response.data === 'string') {
// 过滤掉广告段 - 实现更精确的广告过滤逻辑
response.data = filterAdsFromM3U8(response.data, true);
}
return onSuccess(response, stats, context);
};
}
// 执行原始load方法
load(context, config, callbacks);
};
}
}
// M3U8清单广告过滤函数
function filterAdsFromM3U8(m3u8Content, strictMode = false) {
if (!m3u8Content) return '';
// 按行分割M3U8内容
const lines = m3u8Content.split('\n');
const filteredLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 只过滤#EXT-X-DISCONTINUITY标识
if (!line.includes('#EXT-X-DISCONTINUITY')) {
filteredLines.push(line);
}
}
return filteredLines.join('\n');
}
// 显示错误
function showError(message) {
// 在视频已经播放的情况下不显示错误
if (dp && dp.video && dp.video.currentTime > 1) {
console.log('忽略错误:', message);
return;
}
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'flex';
document.getElementById('error-message').textContent = message;
}
// 更新集数信息
function updateEpisodeInfo() {
if (currentEpisodes.length > 0) {
document.getElementById('episodeInfo').textContent = `第 ${currentEpisodeIndex + 1}/${currentEpisodes.length} 集`;
} else {
document.getElementById('episodeInfo').textContent = '无集数信息';
}
}
// 更新按钮状态
function updateButtonStates() {
const prevButton = document.getElementById('prevButton');
const nextButton = document.getElementById('nextButton');
// 处理上一集按钮
if (currentEpisodeIndex > 0) {
prevButton.classList.remove('bg-gray-700', 'cursor-not-allowed');
prevButton.classList.add('bg-[#222]', 'hover:bg-[#333]');
prevButton.removeAttribute('disabled');
} else {
prevButton.classList.add('bg-gray-700', 'cursor-not-allowed');
prevButton.classList.remove('bg-[#222]', 'hover:bg-[#333]');
prevButton.setAttribute('disabled', '');
}
// 处理下一集按钮
if (currentEpisodeIndex < currentEpisodes.length - 1) {
nextButton.classList.remove('bg-gray-700', 'cursor-not-allowed');
nextButton.classList.add('bg-[#222]', 'hover:bg-[#333]');
nextButton.removeAttribute('disabled');
} else {
nextButton.classList.add('bg-gray-700', 'cursor-not-allowed');
nextButton.classList.remove('bg-[#222]', 'hover:bg-[#333]');
nextButton.setAttribute('disabled', '');
}
}
// 渲染集数按钮
function renderEpisodes() {
const episodesList = document.getElementById('episodesList');
if (!episodesList) return;
if (!currentEpisodes || currentEpisodes.length === 0) {
episodesList.innerHTML = '<div class="col-span-full text-center text-gray-400 py-8">没有可用的集数</div>';
return;
}
const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes;
let html = '';
episodes.forEach((episode, index) => {
// 根据倒序状态计算真实的剧集索引
const realIndex = episodesReversed ? currentEpisodes.length - 1 - index : index;
const isActive = realIndex === currentEpisodeIndex;
html += `
<button id="episode-${realIndex}"
onclick="playEpisode(${realIndex})"
class="px-4 py-2 ${isActive ? 'episode-active' : 'bg-[#222] hover:bg-[#333]'} border ${isActive ? 'border-blue-500' : 'border-[#333]'} rounded-lg transition-colors text-center episode-btn">
${realIndex + 1}
</button>
`;
});
episodesList.innerHTML = html;
}
// 播放指定集数
function playEpisode(index) {
if (index < 0 || index >= currentEpisodes.length) return;
// 首先隐藏之前可能显示的错误
document.getElementById('error').style.display = 'none';
// 显示加载指示器
document.getElementById('loading').style.display = 'flex';
document.getElementById('loading').innerHTML = `
<div class="loading-spinner"></div>
<div>正在加载视频...</div>
`;
const url = currentEpisodes[index];
currentEpisodeIndex = index;
videoHasEnded = false; // 重置视频结束标志
// 更新URL,不刷新页面
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('index', index);
newUrl.searchParams.set('url', url);
window.history.pushState({}, '', newUrl);
// 更新播放器
if (dp) {
try {
dp.switchVideo({
url: url,
type: 'hls'
});
// 确保播放开始
const playPromise = dp.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
console.warn('播放失败,尝试重新初始化:', error);
// 如果切换视频失败,重新初始化播放器
initPlayer(url);
});
}
} catch (e) {
console.error('切换视频出错,尝试重新初始化:', e);
// 如果出错,重新初始化播放器
initPlayer(url);
}
} else {
initPlayer(url);
}
// 更新UI
updateEpisodeInfo();
updateButtonStates();
renderEpisodes();
// 重置用户点击位置记录
userClickedPosition = null;
}
// 播放上一集
function playPreviousEpisode() {
if (currentEpisodeIndex > 0) {
playEpisode(currentEpisodeIndex - 1);
}
}
// 播放下一集
function playNextEpisode() {
if (currentEpisodeIndex < currentEpisodes.length - 1) {
playEpisode(currentEpisodeIndex + 1);
}
}
// 切换集数排序
function toggleEpisodeOrder() {
episodesReversed = !episodesReversed;
// 保存到localStorage
localStorage.setItem('episodesReversed', episodesReversed);
// 重新渲染集数列表
renderEpisodes();
// 更新排序按钮
updateOrderButton();
}
// 更新排序按钮状态
function updateOrderButton() {
const orderText = document.getElementById('orderText');
const orderIcon = document.getElementById('orderIcon');
if (orderText && orderIcon) {
orderText.textContent = episodesReversed ? '正序排列' : '倒序排列';
orderIcon.style.transform = episodesReversed ? 'rotate(180deg)' : '';
}
}
// 设置进度条准确点击处理
function setupProgressBarPreciseClicks() {
// 查找DPlayer的进度条元素
const progressBar = document.querySelector('.dplayer-bar-wrap');
if (!progressBar || !dp || !dp.video) return;
// 移除可能存在的旧事件监听器
progressBar.removeEventListener('mousedown', handleProgressBarClick);
// 添加新的事件监听器
progressBar.addEventListener('mousedown', handleProgressBarClick);
// 在移动端也添加触摸事件支持
progressBar.removeEventListener('touchstart', handleProgressBarTouch);
progressBar.addEventListener('touchstart', handleProgressBarTouch);
console.log('进度条精确点击监听器已设置');
}
// 处理进度条点击
function handleProgressBarClick(e) {
if (!dp || !dp.video) return;
// 计算点击位置相对于进度条的比例
const rect = e.currentTarget.getBoundingClientRect();
const percentage = (e.clientX - rect.left) / rect.width;
// 计算点击位置对应的视频时间
const duration = dp.video.duration;
let clickTime = percentage * duration;
// 处理视频接近结尾的情况
if (duration - clickTime < 1) {
// 如果点击位置非常接近结尾,稍微往前移一点
clickTime = Math.min(clickTime, duration - 1.5);
console.log(`进度条点击接近结尾,调整时间为 ${clickTime.toFixed(2)}/${duration.toFixed(2)}`);
}
// 记录用户点击的位置
userClickedPosition = clickTime;
// 输出调试信息
console.log(`进度条点击: ${percentage.toFixed(4)}, 时间: ${clickTime.toFixed(2)}/${duration.toFixed(2)}`);
// 阻止默认事件传播,避免DPlayer内部逻辑将视频跳至末尾
e.stopPropagation();
// 直接设置视频时间
dp.seek(clickTime);
}
// 处理移动端触摸事件
function handleProgressBarTouch(e) {
if (!dp || !dp.video || !e.touches[0]) return;
const touch = e.touches[0];
const rect = e.currentTarget.getBoundingClientRect();
const percentage = (touch.clientX - rect.left) / rect.width;
const duration = dp.video.duration;
let clickTime = percentage * duration;
// 处理视频接近结尾的情况
if (duration - clickTime < 1) {
clickTime = Math.min(clickTime, duration - 1.5);
}
// 记录用户点击的位置
userClickedPosition = clickTime;
console.log(`进度条触摸: ${percentage.toFixed(4)}, 时间: ${clickTime.toFixed(2)}/${duration.toFixed(2)}`);
e.stopPropagation();
dp.seek(clickTime);
}
</script>
</body>
</html>