File size: 62,965 Bytes
7eff83b |
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 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 |
<!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;
}
/* 原生全屏时,播放器容器铺满 */
.player-container:-webkit-full-screen,
.player-container:fullscreen {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
z-index: 10000;
background-color: #000;
}
.player-container:-webkit-full-screen #player,
.player-container:fullscreen #player {
width: 100%; height: 100%;
}
</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 id="playerContainer" 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>
<button id="lockToggle" onclick="toggleControlsLock()" title="锁定控制"
class="px-2 py-1 bg-[#333] hover:bg-[#444] text-white rounded-full transition">
<svg id="lockIcon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<!-- 默认状态:未锁图标 -->
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11V7a3 3 0 00-6 0v4m-3 4h12v6H6v-6z" />
</svg>
</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; // 默认开启广告过滤
let progressSaveInterval = null; // 定期保存进度的计时器
// 页面加载
document.addEventListener('DOMContentLoaded', function() {
// 解析URL参数
const urlParams = new URLSearchParams(window.location.search);
const videoUrl = urlParams.get('url');
const title = urlParams.get('title');
let index = parseInt(urlParams.get('index') || '0');
const episodesList = urlParams.get('episodes'); // 新增:从URL获取集数信息
// 从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);
});
// 优先使用URL传递的集数信息,否则从localStorage获取
try {
if (episodesList) {
// 如果URL中有集数数据,优先使用它
currentEpisodes = JSON.parse(decodeURIComponent(episodesList));
console.log('从URL恢复集数信息:', currentEpisodes.length);
} else {
// 否则从localStorage获取
currentEpisodes = JSON.parse(localStorage.getItem('currentEpisodes') || '[]');
console.log('从localStorage恢复集数信息:', currentEpisodes.length);
}
// 检查集数索引是否有效,如果无效则调整为0
if (index < 0 || (currentEpisodes.length > 0 && index >= currentEpisodes.length)) {
console.warn(`无效的剧集索引 ${index},调整为范围内的值`);
// 如果索引太大,则使用最大有效索引
if (index >= currentEpisodes.length && currentEpisodes.length > 0) {
index = currentEpisodes.length - 1;
} else {
index = 0;
}
// 更新URL以反映修正后的索引
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('index', index);
window.history.replaceState({}, '', newUrl);
}
// 更新当前索引为验证过的值
currentEpisodeIndex = index;
episodesReversed = localStorage.getItem('episodesReversed') === 'true';
} catch (e) {
console.error('获取集数信息失败:', e);
currentEpisodes = [];
currentEpisodeIndex = 0;
episodesReversed = false;
}
// 设置页面标题
document.title = currentVideoTitle + ' - LibreTV播放器';
document.getElementById('videoTitle').textContent = currentVideoTitle;
// 初始化播放器
if (videoUrl) {
initPlayer(videoUrl);
// 尝试从URL参数中恢复播放位置
const position = urlParams.get('position');
if (position) {
setTimeout(() => {
if (dp && dp.video) {
const positionNum = parseInt(position);
if (!isNaN(positionNum) && positionNum > 0) {
dp.seek(positionNum);
showPositionRestoreHint(positionNum);
}
}
}, 1500);
}
} else {
showError('无效的视频链接');
}
// 更新集数信息
updateEpisodeInfo();
// 渲染集数列表
renderEpisodes();
// 更新按钮状态
updateButtonStates();
// 更新排序按钮状态
updateOrderButton();
// 添加对进度条的监听,确保点击准确跳转
setTimeout(() => {
setupProgressBarPreciseClicks();
}, 1000);
// 添加键盘快捷键事件监听
document.addEventListener('keydown', handleKeyboardShortcuts);
// 添加页面离开事件监听,保存播放位置
window.addEventListener('beforeunload', saveCurrentProgress);
// 新增:页面隐藏(切后台/切标签)时也保存
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
saveCurrentProgress();
}
});
// 新增:视频暂停时也保存
// 需确保 dp.video 已初始化
const waitForVideo = setInterval(() => {
if (dp && dp.video) {
dp.video.addEventListener('pause', saveCurrentProgress);
// 新增:播放进度变化时节流保存
let lastSave = 0;
dp.video.addEventListener('timeupdate', function() {
const now = Date.now();
if (now - lastSave > 5000) { // 每5秒最多保存一次
saveCurrentProgress();
lastSave = now;
}
});
clearInterval(waitForVideo);
}
}, 200);
});
// 处理键盘快捷键
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';
});
// --- 体验加强版:黑屏快进跳广告 ---
let tmp_time_add = 0.1;
const tmp_max_buffer_length = hls.config.maxBufferLength;
hls.on(Hls.Events.FRAG_PARSED, (event, data) => {
if (data.frag.endList) {
const cur = hls.media.currentTime;
const dur = hls.media.duration || 0;
if (cur < dur) {
data.frag.endList = undefined;
// 根据 tmp_time_add 调整 buffer 长度
hls.config.maxBufferLength = tmp_time_add < 1
? 2
: tmp_max_buffer_length;
// 重新加载并跳转
hls.loadSource(video.src);
hls.attachMedia(video);
hls.media.currentTime = cur + tmp_time_add;
// 切换下次跳转时长:0.1 ↔ 5
tmp_time_add = tmp_time_add < 1 ? 5 : 0.1;
player.video.play().catch(() => {});
} else {
player.video.pause();
}
}
});
}
}
}
});
// 全屏模式下锁定横屏
dp.on('fullscreen', () => {
if (window.screen.orientation && window.screen.orientation.lock) {
window.screen.orientation.lock('landscape')
.then(() => {
console.log('屏幕已锁定为横向模式');
})
.catch((error) => {
console.warn('无法锁定屏幕方向,请手动旋转设备:', error);
});
} else {
console.warn('当前浏览器不支持锁定屏幕方向,请手动旋转设备。');
}
});
// 全屏取消时解锁屏幕方向
dp.on('fullscreen_cancel', () => {
if (window.screen.orientation && window.screen.orientation.unlock) {
window.screen.orientation.unlock();
}
});
dp.on('loadedmetadata', function() {
document.getElementById('loading').style.display = 'none';
videoHasEnded = false; // 视频加载时重置结束标志
// 视频加载完成后重新设置进度条点击监听
setupProgressBarPreciseClicks();
// 视频加载成功后,在稍微延迟后将其添加到观看历史
setTimeout(saveToHistory, 3000);
// 启动定期保存播放进度
startProgressSaveInterval();
});
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; // 标记视频已自然结束
// 视频已播放完,清除播放进度记录
clearVideoProgress();
// 如果启用了自动连播,并且有下一集可播放,则自动播放下一集
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);
// 绑定原生全屏:DPlayer 触发全屏时调用 requestFullscreen
(function(){
const fsContainer = document.getElementById('playerContainer');
dp.on('fullscreen', () => {
if (fsContainer.requestFullscreen) {
fsContainer.requestFullscreen().catch(err => console.warn('原生全屏失败:', err));
}
});
dp.on('fullscreen_cancel', () => {
if (document.fullscreenElement) {
document.exitFullscreen();
}
});
})();
}
// 自定义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) {
// 确保index在有效范围内
if (index < 0 || index >= currentEpisodes.length) {
console.error(`无效的剧集索引: ${index}, 当前剧集数量: ${currentEpisodes.length}`);
showToast(`无效的剧集索引: ${index + 1},当前剧集总数: ${currentEpisodes.length}`);
return;
}
// 保存当前播放进度(如果正在播放)
if (dp && dp.video && !dp.video.paused && !videoHasEnded) {
saveCurrentProgress();
}
// 清除进度保存计时器
if (progressSaveInterval) {
clearInterval(progressSaveInterval);
progressSaveInterval = null;
}
// 首先隐藏之前可能显示的错误
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参数,保留source参数
const urlParams = new URLSearchParams(window.location.search);
const sourceName = urlParams.get('source') || '';
// 更新URL,不刷新页面,保留source参数
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('index', index);
newUrl.searchParams.set('url', url);
if (sourceName) {
newUrl.searchParams.set('source', sourceName);
}
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;
// 三秒后保存到历史记录
setTimeout(() => saveToHistory(), 3000);
}
// 播放上一集
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);
}
// 在播放器初始化后添加视频到历史记录
function saveToHistory() {
// 确保 currentEpisodes 非空
if (!currentEpisodes || currentEpisodes.length === 0) {
console.warn('没有可用的剧集列表,无法保存完整的历史记录');
}
// 尝试从URL中获取参数
const urlParams = new URLSearchParams(window.location.search);
const sourceName = urlParams.get('source') || '';
// 获取当前播放进度
let currentPosition = 0;
let videoDuration = 0;
if (dp && dp.video) {
currentPosition = dp.video.currentTime;
videoDuration = dp.video.duration;
}
// 构建要保存的视频信息对象
const videoInfo = {
title: currentVideoTitle,
// 创建基础URL,使用标题作为唯一标识符
url: `player.html?title=${encodeURIComponent(currentVideoTitle)}&source=${encodeURIComponent(sourceName)}`,
episodeIndex: currentEpisodeIndex,
sourceName: sourceName,
timestamp: Date.now(),
// 添加播放进度信息
playbackPosition: currentPosition > 10 ? currentPosition : 0,
duration: videoDuration,
// 重要:保存完整的集数列表,确保进行深拷贝
episodes: currentEpisodes && currentEpisodes.length > 0 ? [...currentEpisodes] : []
};
// 如果外部定义了addToViewingHistory函数,则调用它
if (typeof addToViewingHistory === 'function') {
addToViewingHistory(videoInfo);
console.log(`已保存 "${currentVideoTitle}" 的历史记录, 集数数据: ${currentEpisodes.length}集`);
} else {
// 否则直接使用本地实现
try {
const history = JSON.parse(localStorage.getItem('viewingHistory') || '[]');
// 检查是否已经存在相同标题的记录(同一视频的不同集数)
const existingIndex = history.findIndex(item => item.title === videoInfo.title);
if (existingIndex !== -1) {
// 存在则更新现有记录的集数、时间戳和URL
history[existingIndex].episodeIndex = currentEpisodeIndex;
history[existingIndex].timestamp = Date.now();
// 更新播放进度信息
history[existingIndex].playbackPosition = currentPosition > 10 ? currentPosition : history[existingIndex].playbackPosition;
history[existingIndex].duration = videoDuration || history[existingIndex].duration;
// 同时更新URL以保存当前的集数状态
history[existingIndex].url = window.location.href;
// 更新集数列表(如果有且与当前不同)
if (currentEpisodes && currentEpisodes.length > 0) {
// 检查是否需要更新集数数据(针对不同长度的集数列表)
if (!history[existingIndex].episodes ||
!Array.isArray(history[existingIndex].episodes) ||
history[existingIndex].episodes.length !== currentEpisodes.length) {
history[existingIndex].episodes = [...currentEpisodes]; // 深拷贝
console.log(`更新 "${currentVideoTitle}" 的剧集数据: ${currentEpisodes.length}集`);
}
}
// 移到最前面
const updatedItem = history.splice(existingIndex, 1)[0];
history.unshift(updatedItem);
} else {
// 添加新记录到最前面,但保存完整URL以便能直接打开到正确的集数
videoInfo.url = window.location.href;
console.log(`创建新的历史记录: "${currentVideoTitle}", ${currentEpisodes.length}集`);
history.unshift(videoInfo);
}
// 限制历史记录数量为50条
if (history.length > 50) history.splice(50);
localStorage.setItem('viewingHistory', JSON.stringify(history));
} catch (e) {
console.error('保存观看历史失败:', e);
}
}
}
// 显示恢复位置提示
function showPositionRestoreHint(position) {
if (!position || position < 10) return;
// 创建提示元素
const hint = document.createElement('div');
hint.className = 'position-restore-hint';
hint.innerHTML = `
<div class="hint-content">
已从 ${formatTime(position)} 继续播放
</div>
`;
// 添加到播放器容器
const playerContainer = document.querySelector('.player-container');
playerContainer.appendChild(hint);
// 显示提示
setTimeout(() => {
hint.classList.add('show');
// 3秒后隐藏
setTimeout(() => {
hint.classList.remove('show');
setTimeout(() => hint.remove(), 300);
}, 3000);
}, 100);
}
// 格式化时间为 mm:ss 格式
function formatTime(seconds) {
if (isNaN(seconds)) return '00:00';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// 开始定期保存播放进度
function startProgressSaveInterval() {
// 清除可能存在的旧计时器
if (progressSaveInterval) {
clearInterval(progressSaveInterval);
}
// 每30秒保存一次播放进度
progressSaveInterval = setInterval(saveCurrentProgress, 30000);
}
// 保存当前播放进度
function saveCurrentProgress() {
if (!dp || !dp.video) return;
const currentTime = dp.video.currentTime;
const duration = dp.video.duration;
if (!duration || currentTime < 1) return;
// 在localStorage中保存进度
const progressKey = `videoProgress_${getVideoId()}`;
const progressData = {
position: currentTime,
duration: duration,
timestamp: Date.now()
};
try {
localStorage.setItem(progressKey, JSON.stringify(progressData));
// --- 新增:同步更新 viewingHistory 中的进度 ---
try {
const historyRaw = localStorage.getItem('viewingHistory');
if (historyRaw) {
const history = JSON.parse(historyRaw);
// 用 title + 集数索引唯一标识
const idx = history.findIndex(item =>
item.title === currentVideoTitle &&
(item.episodeIndex === undefined || item.episodeIndex === currentEpisodeIndex)
);
if (idx !== -1) {
// 只在进度有明显变化时才更新,减少写入
if (
Math.abs((history[idx].playbackPosition || 0) - currentTime) > 2 ||
Math.abs((history[idx].duration || 0) - duration) > 2
) {
history[idx].playbackPosition = currentTime;
history[idx].duration = duration;
history[idx].timestamp = Date.now();
localStorage.setItem('viewingHistory', JSON.stringify(history));
}
}
}
} catch (e) {
// 忽略 viewingHistory 更新错误
}
} catch (e) {
console.error('保存播放进度失败', e);
}
}
// 清除视频进度记录
function clearVideoProgress() {
const progressKey = `videoProgress_${getVideoId()}`;
try {
localStorage.removeItem(progressKey);
console.log('已清除播放进度记录');
} catch (e) {
console.error('清除播放进度记录失败', e);
}
}
// 获取视频唯一标识
function getVideoId() {
// 使用视频标题和集数索引作为唯一标识
return `${encodeURIComponent(currentVideoTitle)}_${currentEpisodeIndex}`;
}
// 简单的Toast消息提示函数
function showToast(message, type = 'error') {
// 如果已有Toast,先移除它
const existingToast = document.getElementById('custom-toast');
if (existingToast) {
document.body.removeChild(existingToast);
}
// 创建新的Toast元素
const toast = document.createElement('div');
toast.id = 'custom-toast';
// 设置Toast样式
toast.style.position = 'fixed';
toast.style.top = '20px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.backgroundColor = type === 'error' ? '#f44336' : '#4caf50';
toast.style.color = 'white';
toast.style.padding = '12px 20px';
toast.style.borderRadius = '4px';
toast.style.zIndex = '10000';
toast.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s ease-in-out';
// 设置Toast内容
toast.textContent = message;
// 添加到页面
document.body.appendChild(toast);
// 显示Toast
setTimeout(() => {
toast.style.opacity = '1';
// 3秒后隐藏并移除
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 300);
}, 3000);
}, 10);
}
let controlsLocked = false;
function toggleControlsLock() {
const container = document.getElementById('playerContainer');
controlsLocked = !controlsLocked;
container.classList.toggle('controls-locked', controlsLocked);
const icon = document.getElementById('lockIcon');
// 切换图标:锁 / 解锁
icon.innerHTML = controlsLocked
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d=\"M12 15v2m0-8V7a4 4 0 00-8 0v2m8 0H4v8h16v-8h-4z\"/>'
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d=\"M15 11V7a3 3 0 00-6 0v4m-3 4h12v6H6v-6z\"/>';
}
</script>
</body>
</html>
|