File size: 41,268 Bytes
f3fcf64 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 |
<!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>
|