找回密码
 注册
搜索
免费空间 免费域名 免费AI 老牌主机商首月仅1美分!27美元/年!Namecheap优惠码Spaceship优惠码
楼主: yywfsky

[程序代码] php版小姐姐播放器(仿抖音逻辑和界面)

[复制链接]
 楼主| 发表于 昨天 16:58 | 显示全部楼层
  1. <?php
  2. ?>
  3. <!DOCTYPE html>
  4. <html lang="zh-CN">
  5. <head>
  6.     <meta charset="UTF-8">
  7.     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
  8.     <meta name="mobile-web-app-capable" content="yes">
  9.     <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  10.     <title>Hello Girl</title>
  11.    
  12.     <link  rel="stylesheet">
  13.    
  14.     <style>
  15. * {
  16.     margin: 0;
  17.     padding: 0;
  18.     box-sizing: border-box;
  19. }

  20. html, body {
  21.     width: 100%;
  22.     height: 100%;
  23.     min-height: 100vh;
  24.     margin: 0;
  25.     padding: 0;
  26.     display: -webkit-box;
  27.     display: -webkit-flex;
  28.     display: flex;
  29.     overflow: hidden;
  30.     background-color: #000;
  31.     color: #fff;
  32.     font-family: -apple-system, BlinkMacSystemFont, Arial, sans-serif;
  33. }

  34. .video-container {
  35.     position: relative;
  36.     width: 100vw;
  37.     height: 100vh;
  38.     overflow: hidden;
  39.     z-index: 1;
  40. }

  41. .video-element {
  42.     position: absolute;
  43.     top: 0;
  44.     left: 0;
  45.     width: 100vw;
  46.     height: 100vh;
  47.     object-fit: contain;
  48.     background-color: #000;
  49.     z-index: 1;
  50.     transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
  51.                 opacity 0.3s ease;
  52.     opacity: 0;
  53.     pointer-events: none;
  54. }

  55. .video-element.active {
  56.     opacity: 1;
  57.     pointer-events: auto;
  58.     transform: translateY(0);
  59. }

  60. .video-element.loading {
  61.     opacity: 0.6;
  62. }

  63. .video-element.slide-up {
  64.     transform: translateY(-100%);
  65. }

  66. .video-element.slide-up-enter {
  67.     transform: translateY(100%);
  68. }

  69. .video-element.slide-down {
  70.     transform: translateY(100%);
  71. }

  72. .video-element.slide-down-enter {
  73.     transform: translateY(-100%);
  74. }

  75. .slide-mask {
  76.     position: absolute;
  77.     top: 0;
  78.     left: 0;
  79.     width: 100%;
  80.     height: 100%;
  81.     background-color: #000;
  82.     z-index: 10;
  83.     pointer-events: none;
  84.     opacity: 0;
  85. }

  86. .slide-mask.slide-up-mask {
  87.     animation: slideUp 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  88. }

  89. .slide-mask.slide-down-mask {
  90.     animation: slideDown 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  91. }

  92. @keyframes slideUp {
  93.     0% {
  94.         transform: translateY(100%);
  95.         opacity: 1;
  96.     }
  97.     50% {
  98.         transform: translateY(0);
  99.         opacity: 1;
  100.     }
  101.     100% {
  102.         transform: translateY(-100%);
  103.         opacity: 1;
  104.     }
  105. }

  106. @keyframes slideDown {
  107.     0% {
  108.         transform: translateY(-100%);
  109.         opacity: 1;
  110.     }
  111.     50% {
  112.         transform: translateY(0);
  113.         opacity: 1;
  114.     }
  115.     100% {
  116.         transform: translateY(100%);
  117.         opacity: 1;
  118.     }
  119. }

  120. .toast-box {
  121.     position: fixed;
  122.     top: 1rem;
  123.     left: 50%;
  124.     transform: translateX(-50%);
  125.     padding: 0.5rem 1rem;
  126.     background-color: rgba(0, 0, 0, 0.7);
  127.     backdrop-filter: blur(8px);
  128.     border-radius: 4px;
  129.     color: white;
  130.     font-size: 0.9rem;
  131.     z-index: 15;
  132.     opacity: 0;
  133.     transition: opacity 0.3s ease;
  134.     pointer-events: none;
  135. }

  136. .toast-box.visible {
  137.     opacity: 1;
  138. }

  139. .controls-group {
  140.     position: fixed;
  141.     top: 55%;
  142.     right: 1rem;
  143.     transform: translateY(-50%);
  144.     display: flex;
  145.     flex-direction: column;
  146.     gap: 1.5rem;
  147.     z-index: 20;
  148. }

  149. .control-button {
  150.     width: 40px;
  151.     height: 40px;
  152.     border-radius: 50%;
  153.     background-color: rgba(255, 255, 255, 0.2);
  154.     backdrop-filter: blur(8px);
  155.     display: flex;
  156.     justify-content: center;
  157.     align-items: center;
  158.     color: white;
  159.     font-size: 1.2rem;
  160.     cursor: pointer;
  161.     border: none;
  162.     transition: background-color 0.3s ease;
  163. }

  164. .control-button:hover {
  165.     background-color: rgba(255, 255, 255, 0.3);
  166. }

  167. .mute-button.muted {
  168.     background-color: rgba(254, 44, 85, 0.6);
  169. }

  170. .loading-screen {
  171.     position: fixed;
  172.     top: 0;
  173.     left: 0;
  174.     width: 100%;
  175.     height: 100%;
  176.     background-color: #000;
  177.     display: flex;
  178.     justify-content: center;
  179.     align-items: center;
  180.     z-index: 100;
  181. }

  182. .loading-spinner {
  183.     width: 50px;
  184.     height: 50px;
  185.     border: 3px solid rgba(255, 255, 255, 0.3);
  186.     border-radius: 50%;
  187.     border-top-color: #fe2c55;
  188.     animation: spin 1s linear infinite;
  189. }

  190. @keyframes spin {
  191.     to { transform: rotate(360deg); }
  192. }

  193. .volume-hint {
  194.     position: absolute;
  195.     right: 10px;
  196.     top: 50%;
  197.     transform: translateY(-50%);
  198.     color: rgba(255, 255, 255, 0.3);
  199.     font-size: 12px;
  200.     writing-mode: vertical-rl;
  201.     z-index: 2;
  202.     pointer-events: none;
  203. }

  204. @media (min-width: 769px) {
  205.     .volume-hint {
  206.         display: none;
  207.     }
  208. }

  209. @media (max-width: 768px) {
  210.     .vjs-progress-control:active .vjs-progress-holder {
  211.         height: 8px !important;
  212.         transition: height 0.2s ease;
  213.     }
  214. }

  215. .progress-bar-2 {
  216.     position: fixed;
  217.     left: 0;
  218.     right: 0;
  219.     bottom: 0;
  220.     height: 3px;
  221.     background-color: rgba(255, 255, 255, 0.2);
  222.     z-index: 20;
  223.     pointer-events: auto;
  224.     transition: height 0.6s ease, background-color 0.6s ease;
  225. }

  226. .progress-bar-2::before {
  227.     content: '';
  228.     position: absolute;
  229.     top: -87px;
  230.     left: 0;
  231.     right: 0;
  232.     height: 90px;
  233.     background-color: transparent;
  234.     pointer-events: auto;
  235. }

  236. .progress-bar-2.expanded {
  237.     height: 50px;
  238.     background-color: rgba(0, 0, 0, 0.6);
  239.     backdrop-filter: blur(6px);
  240. }

  241. .progress-bar-2-filled {
  242.     height: 100%;
  243.     background-color: #fe2c55;
  244.     width: 0%;
  245. }

  246. .progress-bar-2-time {
  247.     position: absolute;
  248.     right: 10px;
  249.     top: 5px;
  250.     color: white;
  251.     font-size: 1rem;
  252.     text-shadow: 0 1px 2px rgba(0,0,0,0.8);
  253.     background-color: rgba(0,0,0,0.5);
  254.     padding: 4px 8px;
  255.     border-radius: 3px;
  256.     display: none;
  257. }

  258. .progress-bar-2.expanded .progress-bar-2-time {
  259.     display: block;
  260. }

  261. .video-container.temp-progress-visible .progress-bar-2 {
  262.     opacity: 0;
  263.     pointer-events: none;
  264. }

  265. .bottom-click-area {
  266.     position: fixed;
  267.     left: 0;
  268.     right: 0;
  269.     bottom: 0;
  270.     height: 15vh;
  271.     z-index: 15;
  272.     pointer-events: auto;
  273.     background-color: transparent;
  274. }
  275.     </style>
  276. </head>
  277. <body>
  278.     <div class="loading-screen" id="loadingScreen">
  279.         <div class="loading-spinner"></div>
  280.     </div>

  281.     <div class="toast-box" id="toastBox"></div>

  282.     <div class="video-container" id="videoContainer">
  283.         <div class="slide-mask" id="slideMask"></div>
  284.         <video id="video-current" class="video-element active" playsinline></video>
  285.         <video id="video-next" class="video-element" playsinline></video>
  286.         
  287.         <div class="progress-bar-2" id="progressBar2">
  288.             <div class="progress-bar-2-filled" id="progressBar2Filled"></div>
  289.             <div class="progress-bar-2-time" id="progressBar2Time">00:00/00:00</div>
  290.         </div>
  291.         
  292.         <div class="bottom-click-area" id="bottomClickArea"></div>
  293.     </div>

  294.     <div class="controls-group">
  295.         <button class="control-button play-pause-button" id="playPauseButton">
  296.             <i class="fa fa-pause"></i>
  297.         </button>
  298.         <button class="control-button mute-button" id="muteButton">
  299.             <i class="fa fa-volume-up"></i>
  300.         </button>
  301.         <button class="control-button download-button" id="downloadButton">
  302.             <i class="fa fa-download"></i>
  303.         </button>
  304.         <button class="control-button fullscreen-button" id="fullscreenButton">
  305.             <i class="fa fa-expand"></i>
  306.         </button>
  307.     </div>

  308. <script>
  309. const API_SOURCES = [
  310.     {
  311.         name: 'xjj1',
  312.         url: 'https://v2.xxapi.cn/api/meinv',
  313.         parse: (data) => data.code === 200 && data.data ? data.data : null
  314.     },
  315.     {
  316.         name: 'xjj2',
  317.         url: 'https://api.codetabs.com/v1/敏感词XXX?quest=' + encodeURIComponent('https://api.mmp.cc/api/miss?type=json'),
  318.         parse: (data) => data.status === 'success' && data.link ? data.link : null
  319.     },
  320.     {
  321.         name: 'xjj3',
  322.         url: 'https://api.kuleu.com/api/MP4_xiaojiejie?type=json',
  323.         parse: (data) => data.code === 200 && data.mp4_video ? data.mp4_video : null
  324.     }
  325. ];
  326. const MAX_HISTORY = 5;

  327. const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

  328. let videoHistory = [];
  329. let currentVideoUrl = '';
  330. let nextVideoUrl = '';
  331. let isPlaying = false;
  332. let isMuted = false;
  333. let isLoading = false;
  334. let isErrorHandling = false;
  335. let isVideoSwitching = false;
  336. let maxRetryAttempts = 3;
  337. let currentRetryCount = 0;
  338. let failedVideos = new Set();
  339. let isPreloadingNext = false;
  340. let currentApiIndex = 0;
  341. let allSourcesFailed = false;

  342. let controlsTimeout;
  343. const CONTROLS_HIDE_DELAY = 3000;
  344. const PROGRESS_STEP = 5;
  345. const VOLUME_STEP = 0.1;
  346. const TOAST_DURATION = 2000;
  347. const WHEEL_THROTTLE = 1000;
  348. let lastWheelTime = 0;

  349. let lastTapTime = 0;
  350. const DOUBLE_TAP_DELAY = 300;

  351. let savedVolume = 0.7;
  352. let savedMuted = false;

  353. let touchStartY = 0;
  354. let isDragging = false;
  355. let currentTransform = 0;
  356. const SWIPE_THRESHOLD = 50;

  357. let longPressTimer = null;

  358. const progressBar2 = document.getElementById('progressBar2');
  359. const progressBar2Filled = document.getElementById('progressBar2Filled');
  360. const progressBar2Time = document.getElementById('progressBar2Time');
  361. const bottomClickArea = document.getElementById('bottomClickArea');

  362. let isDraggingProgress = false;
  363. let touchStartCurrentTime = 0;

  364. const videoContainer = document.getElementById('videoContainer');
  365. const loadingScreen = document.getElementById('loadingScreen');
  366. const slideMask = document.getElementById('slideMask');
  367. let videoCurrent = document.getElementById('video-current');
  368. let videoNext = document.getElementById('video-next');
  369. const toastBox = document.getElementById('toastBox');
  370. const muteButton = document.getElementById('muteButton');
  371. const muteIcon = muteButton.querySelector('i');
  372. const downloadButton = document.getElementById('downloadButton');
  373. const fullscreenButton = document.getElementById('fullscreenButton');
  374. const fullscreenIcon = fullscreenButton.querySelector('i');
  375. const playPauseButton = document.getElementById('playPauseButton');
  376. const playPauseIcon = playPauseButton.querySelector('i');

  377. function showToast(message) {
  378.     if (window.toastTimer) clearTimeout(window.toastTimer);
  379.     toastBox.textContent = message;
  380.     toastBox.classList.add('visible');
  381.     window.toastTimer = setTimeout(() => {
  382.         toastBox.classList.remove('visible');
  383.     }, TOAST_DURATION);
  384. }

  385. function formatTime(seconds) {
  386.     const mins = Math.floor(seconds / 60);
  387.     const secs = Math.floor(seconds % 60);
  388.     return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  389. }

  390. function updateProgressBar2() {
  391.     if (!videoCurrent || !videoCurrent.duration) return;
  392.     const ratio = videoCurrent.currentTime / videoCurrent.duration;
  393.     progressBar2Filled.style.width = `${ratio * 100}%`;
  394.     progressBar2Time.textContent = `${formatTime(videoCurrent.currentTime)}/${formatTime(videoCurrent.duration)}`;
  395. }

  396. function handleBottomAreaClick(e) {
  397.     if (!isMobile || !videoCurrent || !videoCurrent.duration) return;
  398.     e.stopPropagation();
  399.     const rect = videoContainer.getBoundingClientRect();
  400.     const ratio = (e.clientX - rect.left) / rect.width;
  401.     const prevTime = videoCurrent.currentTime;
  402.     videoCurrent.currentTime = ratio * videoCurrent.duration;
  403.     const newTime = videoCurrent.currentTime;
  404.     const movedSeconds = Math.round(newTime - prevTime);
  405.     if (movedSeconds !== 0) {
  406.         const direction = movedSeconds > 0 ? '快进' : '快退';
  407.         showToast(`${direction} ${Math.abs(movedSeconds)}秒`);
  408.     }
  409. }

  410. async function fetchVideoUrl(retryCount = 0, apiIndex = 0) {
  411.     const apiSource = API_SOURCES[apiIndex];
  412.     const timeout = 5000;
  413.    
  414.     try {
  415.         const controller = new AbortController();
  416.         const timeoutId = setTimeout(() => controller.abort(), timeout);
  417.         
  418.         const response = await fetch(apiSource.url, {
  419.             signal: controller.signal
  420.         });
  421.         
  422.         clearTimeout(timeoutId);
  423.         
  424.         if (!response.ok) {
  425.             throw new Error(`HTTP error! status: ${response.status}`);
  426.         }
  427.         
  428.         const data = await response.json();
  429.         const videoUrl = apiSource.parse(data);
  430.         
  431.         if (videoUrl) {
  432.             currentApiIndex = apiIndex;
  433.             console.log(`使用API源 ${apiSource.name} 获取视频成功`);
  434.             return videoUrl;
  435.         } else {
  436.             throw new Error('API返回数据格式错误');
  437.         }
  438.     } catch (error) {
  439.         console.error(`API源 ${apiSource.name} 请求失败:`, error.message);
  440.         
  441.         if (apiIndex < API_SOURCES.length - 1) {
  442.             console.log(`切换到下一个API源: ${API_SOURCES[apiIndex + 1].name}`);
  443.             return fetchVideoUrl(retryCount, apiIndex + 1);
  444.         } else {
  445.             if (retryCount < 3) {
  446.                 console.log(`所有API源都失败,重试第 ${retryCount + 1} 次`);
  447.                 await new Promise(resolve => setTimeout(resolve, 1000));
  448.                 return fetchVideoUrl(retryCount + 1, 0);
  449.             } else {
  450.                 allSourcesFailed = true;
  451.                 showToast('所有源都不可用,尝试刷新页面或稍后访问');
  452.                 throw new Error('所有API源都不可用');
  453.             }
  454.         }
  455.     }
  456. }

  457. function addToHistory(videoUrl) {
  458.     if (currentVideoUrl) {
  459.         videoHistory.push(currentVideoUrl);
  460.         if (videoHistory.length > MAX_HISTORY) {
  461.             videoHistory.shift();
  462.         }
  463.     }
  464.     currentVideoUrl = videoUrl;
  465. }

  466. function getPreviousVideo() {
  467.     if (videoHistory.length > 0) {
  468.         const previousUrl = videoHistory.pop();
  469.         currentVideoUrl = previousUrl;
  470.         return previousUrl;
  471.     }
  472.     return null;
  473. }

  474. function playPreviousVideo() {
  475.     if (isLoading) {
  476.         console.log('正在加载中,跳过切换请求');
  477.         return;
  478.     }

  479.     if (isErrorHandling) {
  480.         console.log('正在处理错误,跳过切换请求');
  481.         return;
  482.     }

  483.     const previousUrl = getPreviousVideo();
  484.    
  485.     if (previousUrl) {
  486.         loadVideo(previousUrl, 'previous');
  487.         showToast('返回上一个视频');
  488.     } else {
  489.         loadVideoFromAPI('previous');
  490.         showToast('无更早历史,获取新视频');
  491.     }
  492. }

  493. function playNextVideo() {
  494.     if (isLoading) {
  495.         console.log('正在加载中,跳过切换请求');
  496.         return;
  497.     }

  498.     if (isErrorHandling) {
  499.         console.log('正在处理错误,跳过切换请求');
  500.         return;
  501.     }

  502.     if (nextVideoUrl) {
  503.         addToHistory(nextVideoUrl);
  504.         loadVideo(nextVideoUrl, 'next');
  505.         showToast('切换下一个视频');
  506.     } else {
  507.         loadVideoFromAPI('next');
  508.         showToast('获取新视频');
  509.     }
  510. }

  511. async function loadVideoFromAPI(direction = 'none') {
  512.     try {
  513.         allSourcesFailed = false;
  514.         const videoUrl = await fetchVideoUrl();
  515.         if (videoUrl) {
  516.             if (direction === 'next') {
  517.                 addToHistory(videoUrl);
  518.             }
  519.             loadVideo(videoUrl, direction);
  520.         }
  521.     } catch (error) {
  522.         console.error('获取新视频失败:', error);
  523.         if (allSourcesFailed) {
  524.             showToast('所有源都不可用,尝试刷新页面或稍后访问');
  525.         }
  526.     }
  527. }

  528. function loadVideo(videoUrl, direction = 'none') {
  529.     if (isLoading) {
  530.         console.log('正在加载中,跳过重复请求');
  531.         return;
  532.     }

  533.     if (isErrorHandling) {
  534.         console.log('正在处理错误,跳过新的加载请求');
  535.         return;
  536.     }

  537.     if (currentRetryCount >= maxRetryAttempts) {
  538.         console.log('已达到最大重试次数:', currentRetryCount);
  539.         showToast('视频加载失败次数过多,请刷新页面重试');
  540.         loadingScreen.style.display = 'none';
  541.         return;
  542.     }

  543.     isLoading = true;
  544.     loadingScreen.style.display = 'flex';

  545.     console.log('加载视频:', videoUrl, '重试次数:', currentRetryCount);

  546.     if (direction === 'next') {
  547.         videoCurrent.src = videoUrl;
  548.         videoCurrent.load();

  549.         videoCurrent.addEventListener('loadeddata', () => {
  550.             console.log('视频加载成功 (next):', videoUrl);
  551.             isVideoSwitching = true;

  552.             slideMask.classList.add('slide-up-mask');

  553.             setTimeout(() => {
  554.                 videoCurrent.muted = savedMuted;
  555.                 videoCurrent.volume = savedVolume;
  556.                 videoCurrent.play();

  557.                 loadingScreen.style.display = 'none';
  558.                 currentRetryCount = 0;
  559.                 failedVideos.clear();
  560.                 isLoading = false;
  561.                 isVideoSwitching = false;

  562.                 preloadNextVideo();
  563.             }, 150);

  564.             setTimeout(() => {
  565.                 slideMask.classList.remove('slide-up-mask');
  566.             }, 300);
  567.         }, { once: true });

  568.         videoCurrent.addEventListener('error', (e) => {
  569.             console.error('视频加载失败 (next):', videoUrl, e);

  570.             if (!videoCurrent.src) {
  571.                 console.log('忽略空src的错误(这是正常的切换操作)');
  572.                 isLoading = false;
  573.                 return;
  574.             }

  575.             if (isErrorHandling) {
  576.                 console.log('错误处理已在进行中,跳过重复处理');
  577.                 return;
  578.             }

  579.             if (isVideoSwitching) {
  580.                 console.log('正在切换视频,忽略旧的video元素的error事件');
  581.                 return;
  582.             }

  583.             isErrorHandling = true;
  584.             failedVideos.add(videoUrl);
  585.             currentRetryCount++;
  586.             isLoading = false;

  587.             if (currentRetryCount >= maxRetryAttempts) {
  588.                 showToast('视频加载失败次数过多,请刷新页面重试');
  589.                 loadingScreen.style.display = 'none';
  590.                 isErrorHandling = false;
  591.                 return;
  592.             }

  593.             showToast('下一个视频加载失败,尝试其他视频');
  594.             loadingScreen.style.display = 'none';

  595.             setTimeout(() => {
  596.                 isErrorHandling = false;
  597.                 playNextVideo();
  598.             }, 500);
  599.         }, { once: true });
  600.     } else if (direction === 'previous') {
  601.         videoCurrent.src = videoUrl;
  602.         videoCurrent.load();

  603.         videoCurrent.addEventListener('loadeddata', () => {
  604.             console.log('视频加载成功 (previous):', videoUrl);
  605.             isVideoSwitching = true;

  606.             slideMask.classList.add('slide-down-mask');

  607.             setTimeout(() => {
  608.                 videoCurrent.muted = savedMuted;
  609.                 videoCurrent.volume = savedVolume;
  610.                 videoCurrent.play();

  611.                 loadingScreen.style.display = 'none';
  612.                 currentRetryCount = 0;
  613.                 failedVideos.clear();
  614.                 isLoading = false;
  615.                 isVideoSwitching = false;

  616.                 preloadNextVideo();
  617.             }, 150);

  618.             setTimeout(() => {
  619.                 slideMask.classList.remove('slide-down-mask');
  620.             }, 300);
  621.         }, { once: true });

  622.         videoCurrent.addEventListener('error', (e) => {
  623.             console.error('视频加载失败 (previous):', videoUrl, e);

  624.             if (!videoCurrent.src) {
  625.                 console.log('忽略空src的错误(这是正常的切换操作)');
  626.                 isLoading = false;
  627.                 return;
  628.             }

  629.             if (isErrorHandling) {
  630.                 console.log('错误处理已在进行中,跳过重复处理');
  631.                 return;
  632.             }

  633.             if (isVideoSwitching) {
  634.                 console.log('正在切换视频,忽略旧的video元素的error事件');
  635.                 return;
  636.             }

  637.             isErrorHandling = true;
  638.             failedVideos.add(videoUrl);
  639.             currentRetryCount++;
  640.             isLoading = false;

  641.             if (currentRetryCount >= maxRetryAttempts) {
  642.                 showToast('视频加载失败次数过多,请刷新页面重试');
  643.                 loadingScreen.style.display = 'none';
  644.                 isErrorHandling = false;
  645.                 return;
  646.             }

  647.             showToast('上一个视频加载失败,尝试其他视频');
  648.             loadingScreen.style.display = 'none';

  649.             setTimeout(() => {
  650.                 isErrorHandling = false;
  651.                 playPreviousVideo();
  652.             }, 500);
  653.         }, { once: true });
  654.     } else {
  655.         videoCurrent.src = videoUrl;
  656.         videoCurrent.load();

  657.         videoCurrent.addEventListener('loadeddata', () => {
  658.             console.log('视频加载成功 (initial):', videoUrl);
  659.             videoCurrent.muted = savedMuted;
  660.             videoCurrent.volume = savedVolume;
  661.             videoCurrent.play();
  662.             loadingScreen.style.display = 'none';
  663.             currentRetryCount = 0;
  664.             failedVideos.clear();
  665.             isLoading = false;

  666.             preloadNextVideo();
  667.         }, { once: true });

  668.         videoCurrent.addEventListener('error', (e) => {
  669.             console.error('视频加载失败 (initial):', videoUrl, e);

  670.             if (!videoCurrent.src) {
  671.                 console.log('忽略空src的错误(这是正常的切换操作)');
  672.                 isLoading = false;
  673.                 return;
  674.             }

  675.             if (isErrorHandling) {
  676.                 console.log('错误处理已在进行中,跳过重复处理');
  677.                 return;
  678.             }

  679.             if (isVideoSwitching) {
  680.                 console.log('正在切换视频,忽略旧的video元素的error事件');
  681.                 return;
  682.             }

  683.             isErrorHandling = true;
  684.             failedVideos.add(videoUrl);
  685.             currentRetryCount++;
  686.             isLoading = false;

  687.             if (currentRetryCount >= maxRetryAttempts) {
  688.                 showToast('视频加载失败次数过多,请刷新页面重试');
  689.                 loadingScreen.style.display = 'none';
  690.                 isErrorHandling = false;
  691.                 return;
  692.             }

  693.             showToast('视频加载失败,尝试其他视频');
  694.             loadingScreen.style.display = 'none';

  695.             setTimeout(() => {
  696.                 isErrorHandling = false;
  697.                 playNextVideo();
  698.             }, 500);
  699.         }, { once: true });
  700.     }

  701.     currentVideoUrl = videoUrl;
  702. }

  703. async function preloadNextVideo() {
  704.     if (isPreloadingNext) return;

  705.     isPreloadingNext = true;
  706.     try {
  707.         const nextApiIndex = currentApiIndex;
  708.         nextVideoUrl = await fetchVideoUrl(0, nextApiIndex);
  709.         if (nextVideoUrl) {
  710.             videoNext.src = nextVideoUrl;
  711.             videoNext.load();
  712.         }
  713.     } catch (error) {
  714.         console.error('预加载视频失败:', error);
  715.     } finally {
  716.         isPreloadingNext = false;
  717.     }
  718. }

  719. function initPlayer() {
  720.     videoCurrent.muted = savedMuted;
  721.     videoCurrent.volume = savedVolume;

  722.     setTimeout(() => {
  723.         savedMuted = videoCurrent.muted;
  724.         updateMuteButtonState();
  725.     }, 100);

  726.     updateFullscreenButtonState();
  727.     updatePlayPauseButtonState();
  728.     videoContainer.classList.add(videoCurrent.paused ? 'paused' : 'playing');
  729.     videoContainer.classList.remove(videoCurrent.paused ? 'playing' : 'paused');

  730.     videoCurrent.addEventListener('loadedmetadata', () => {
  731.         loadingScreen.style.display = 'none';
  732.         updateProgressBar2();
  733.     });

  734.     videoCurrent.addEventListener('timeupdate', updateProgressBar2);

  735.     videoCurrent.addEventListener('play', () => {
  736.         videoContainer.classList.remove('paused');
  737.         videoContainer.classList.add('playing');
  738.         updatePlayPauseButtonState();
  739.         showToast('播放中');
  740.     });

  741.     videoCurrent.addEventListener('pause', () => {
  742.         videoContainer.classList.remove('playing');
  743.         videoContainer.classList.add('paused');
  744.         updatePlayPauseButtonState();
  745.         showToast('已暂停');
  746.     });

  747.     videoCurrent.addEventListener('ended', () => {
  748.         if (!isLoading && !isErrorHandling) {
  749.             playNextVideo();
  750.         }
  751.     });

  752.     setupTouchEvents();
  753.     if (!isMobile) {
  754.         setupDesktopWheelEvent();
  755.     }
  756. }

  757. function setupDesktopWheelEvent() {
  758.     videoContainer.addEventListener('wheel', (e) => {
  759.         e.preventDefault();
  760.         const currentTime = Date.now();
  761.         if (currentTime - lastWheelTime < WHEEL_THROTTLE) {
  762.             return;
  763.         }
  764.         lastWheelTime = currentTime;
  765.         if (e.deltaY > 0) {
  766.             playNextVideo();
  767.         } else {
  768.             playPreviousVideo();
  769.         }
  770.     }, { passive: false });
  771. }

  772. function updateMuteButtonState() {
  773.     if (!videoCurrent) return;
  774.     if (videoCurrent.muted) {
  775.         muteIcon.className = 'fa fa-volume-off';
  776.         muteButton.classList.add('muted');
  777.     } else {
  778.         muteIcon.className = 'fa fa-volume-up';
  779.         muteButton.classList.remove('muted');
  780.     }
  781. }

  782. function updateFullscreenButtonState() {
  783.     if (document.fullscreenElement) {
  784.         fullscreenIcon.className = 'fa fa-compress';
  785.     } else {
  786.         fullscreenIcon.className = 'fa fa-expand';
  787.     }
  788. }

  789. function updatePlayPauseButtonState() {
  790.     if (!videoCurrent) return;
  791.     if (videoCurrent.paused) {
  792.         playPauseIcon.className = 'fa fa-play';
  793.     } else {
  794.         playPauseIcon.className = 'fa fa-pause';
  795.     }
  796. }

  797. function togglePlayPause() {
  798.     if (!videoCurrent) return;
  799.     if (videoCurrent.paused) {
  800.         videoCurrent.play();
  801.         showToast('播放');
  802.     } else {
  803.         videoCurrent.pause();
  804.         showToast('暂停');
  805.     }
  806. }

  807. function toggleMute() {
  808.     if (!videoCurrent) return;
  809.     if (videoCurrent.muted) {
  810.         videoCurrent.muted = false;
  811.         videoCurrent.volume = savedVolume;
  812.         showToast('已取消静音');
  813.     } else {
  814.         savedVolume = videoCurrent.volume;
  815.         videoCurrent.muted = true;
  816.         showToast('已静音');
  817.     }
  818.     updateMuteButtonState();
  819. }

  820. function toggleFullscreen() {
  821.     if (!document.fullscreenElement) {
  822.         if (videoContainer.requestFullscreen) videoContainer.requestFullscreen();
  823.         else if (videoContainer.webkitRequestFullscreen) videoContainer.webkitRequestFullscreen();
  824.         else if (videoContainer.msRequestFullscreen) videoContainer.msRequestFullscreen();
  825.         showToast('已进入全屏');
  826.     } else {
  827.         if (document.exitFullscreen) document.exitFullscreen();
  828.         else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
  829.         else if (document.msExitFullscreen) document.msExitFullscreen();
  830.         showToast('已退出全屏');
  831.     }
  832.     updateFullscreenButtonState();
  833. }

  834. function downloadVideo() {
  835.     if (!currentVideoUrl) {
  836.         showToast('没有可下载的视频');
  837.         return;
  838.     }

  839.     showToast('正在下载视频...');
  840.    
  841.     fetch(currentVideoUrl)
  842.         .then(response => {
  843.             if (!response.ok) {
  844.                 throw new Error('网络请求失败');
  845.             }
  846.             return response.blob();
  847.         })
  848.         .then(blob => {
  849.             const url = window.URL.createObjectURL(blob);
  850.             const link = document.createElement('a');
  851.             link.href = url;
  852.             link.download = `video_${Date.now()}.mp4`;
  853.             document.body.appendChild(link);
  854.             link.click();
  855.             document.body.removeChild(link);
  856.             window.URL.revokeObjectURL(url);
  857.             showToast('下载成功');
  858.         })
  859.         .catch(error => {
  860.             console.error('下载失败:', error);
  861.             showToast('下载失败,请尝试右键保存');
  862.         });
  863. }

  864. function setupTouchEvents() {
  865.     let touchStartY = 0;
  866.     let touchStartX = 0;
  867.     let isVolumeAdjust = false;
  868.     let isProgressAdjust = false;
  869.     let isSwipeToChangeVideo = false;
  870.     const SWIPE_VIDEO_THRESHOLD = 50;
  871.     const SWIPE_PROGRESS_THRESHOLD = 20;
  872.     const VOLUME_AREA_WIDTH = window.innerWidth * 0.3;
  873.     let lastTouchX = 0;
  874.     let lastTouchY = 0;

  875.     videoContainer.addEventListener('touchstart', (e) => {
  876.         if (e.touches.length !== 1) return;
  877.         const touch = e.touches[0];
  878.         touchStartY = touch.clientY;
  879.         touchStartX = touch.clientX;
  880.         lastTouchX = touch.clientX;
  881.         lastTouchY = touch.clientY;
  882.         isVolumeAdjust = false;
  883.         isProgressAdjust = false;
  884.         isSwipeToChangeVideo = false;
  885.         touchStartCurrentTime = videoCurrent.currentTime;
  886.     }, { passive: true });

  887.     videoContainer.addEventListener('touchmove', (e) => {
  888.         if (e.touches.length !== 1) return;
  889.         e.preventDefault();
  890.         const touch = e.touches[0];
  891.         const touchY = touch.clientY;
  892.         const touchX = touch.clientX;
  893.         const deltaY = touchStartY - touchY;
  894.         const deltaX = touchX - touchStartX;

  895.         const absDeltaY = Math.abs(deltaY);
  896.         const absDeltaX = Math.abs(deltaX);

  897.         if (absDeltaY > absDeltaX) {
  898.             isSwipeToChangeVideo = true;
  899.             isVolumeAdjust = false;
  900.         } else {
  901.             if (absDeltaX > SWIPE_PROGRESS_THRESHOLD) {
  902.                 isProgressAdjust = true;
  903.                 isSwipeToChangeVideo = false;
  904.                 const swipeRatio = Math.abs(deltaX) / window.innerWidth;
  905.                 const stepCount = Math.floor(swipeRatio / 0.1);
  906.                 const totalStep = stepCount * PROGRESS_STEP;
  907.                 const direction = deltaX > 0 ? 1 : -1;
  908.                 const newTime = touchStartCurrentTime + (direction * totalStep);
  909.                 const clampedTime = Math.max(0, Math.min(videoCurrent.duration || 0, newTime));
  910.                 videoCurrent.currentTime = clampedTime;
  911.                 const movedSeconds = Math.round(clampedTime - touchStartCurrentTime);
  912.                 if (movedSeconds !== 0 && movedSeconds % PROGRESS_STEP === 0) {
  913.                     const dirText = movedSeconds > 0 ? '快进' : '快退';
  914.                     showToast(`${dirText} ${Math.abs(movedSeconds)}秒`);
  915.                 }
  916.             }
  917.         }

  918.         lastTouchX = touchX;
  919.         lastTouchY = touchY;
  920.     }, { passive: false });

  921.     videoContainer.addEventListener('touchend', (e) => {
  922.         clearTimeout(longPressTimer);
  923.         if (isSwipeToChangeVideo) {
  924.             const touchEndY = e.changedTouches[0].clientY;
  925.             const deltaY = touchStartY - touchEndY;
  926.             if (deltaY > SWIPE_VIDEO_THRESHOLD) playNextVideo();
  927.             else if (deltaY < -SWIPE_VIDEO_THRESHOLD) playPreviousVideo();
  928.         }
  929.         const currentTime = Date.now();
  930.         const tapTimeDiff = currentTime - lastTapTime;
  931.         if (tapTimeDiff < DOUBLE_TAP_DELAY && tapTimeDiff > 0) {
  932.             const touchEndX = e.changedTouches[0].clientX;
  933.             const screenWidth = window.innerWidth;
  934.             const isLeftSide = touchEndX < screenWidth * 0.4;
  935.             const isRightSide = touchEndX > screenWidth * 0.6;

  936.             if (isLeftSide) {
  937.                 videoCurrent.currentTime = Math.max(0, videoCurrent.currentTime - PROGRESS_STEP);
  938.                 showToast(`快退 ${PROGRESS_STEP}秒`);
  939.             } else if (isRightSide) {
  940.                 if (videoCurrent.duration) {
  941.                     videoCurrent.currentTime = Math.min(videoCurrent.duration, videoCurrent.currentTime + PROGRESS_STEP);
  942.                     showToast(`快进 ${PROGRESS_STEP}秒`);
  943.                 }
  944.             } else {
  945.                 toggleFullscreen();
  946.             }
  947.             lastTapTime = 0;
  948.         } else {
  949.             lastTapTime = currentTime;
  950.         }
  951.     }, { passive: true });
  952. }

  953. function setupKeyboardEvents() {
  954.     document.addEventListener('keydown', (e) => {
  955.         switch (e.key) {
  956.             case ' ':
  957.             case 'k':
  958.                 e.preventDefault();
  959.                 togglePlayPause();
  960.                 break;
  961.             case 'f':
  962.             case 'F':
  963.                 e.preventDefault();
  964.                 toggleFullscreen();
  965.                 break;
  966.             case 'm':
  967.             case 'M':
  968.                 e.preventDefault();
  969.                 toggleMute();
  970.                 break;
  971.             case 'ArrowLeft':
  972.                 e.preventDefault();
  973.                 videoCurrent.currentTime = Math.max(0, videoCurrent.currentTime - PROGRESS_STEP);
  974.                 showToast(`快退 ${PROGRESS_STEP}秒`);
  975.                 break;
  976.             case 'ArrowRight':
  977.                 e.preventDefault();
  978.                 if (videoCurrent.duration) {
  979.                     videoCurrent.currentTime = Math.min(videoCurrent.duration, videoCurrent.currentTime + PROGRESS_STEP);
  980.                     showToast(`快进 ${PROGRESS_STEP}秒`);
  981.                 }
  982.                 break;
  983.             case 'PageUp':
  984.                 e.preventDefault();
  985.                 playPreviousVideo();
  986.                 break;
  987.             case 'PageDown':
  988.                 e.preventDefault();
  989.                 playNextVideo();
  990.                 break;
  991.         }
  992.     });
  993. }

  994. function setupProgressBar2Click() {
  995.     let isDraggingProgress = false;
  996.     let touchStartX = 0;
  997.     let touchStartTime = 0;
  998.     let isTouchOperation = false;
  999.     let isMouseOperation = false;

  1000.     progressBar2.addEventListener('click', (e) => {
  1001.         if (isTouchOperation || isMouseOperation || !videoCurrent.duration) return;
  1002.         const rect = progressBar2.getBoundingClientRect();
  1003.         const ratio = (e.clientX - rect.left) / rect.width;
  1004.         videoCurrent.currentTime = ratio * videoCurrent.duration;
  1005.     });

  1006.     progressBar2.addEventListener('touchstart', (e) => {
  1007.         if (e.touches.length !== 1) return;
  1008.         isTouchOperation = true;
  1009.         e.stopPropagation();
  1010.         const touch = e.touches[0];
  1011.         touchStartX = touch.clientX;
  1012.         touchStartTime = Date.now();
  1013.         isDraggingProgress = false;

  1014.         progressBar2.classList.add('expanded');
  1015.         isDraggingProgress = true;
  1016.     }, { passive: true });

  1017.     progressBar2.addEventListener('touchmove', (e) => {
  1018.         if (!isDraggingProgress || !videoCurrent.duration) return;
  1019.         e.preventDefault();
  1020.         e.stopPropagation();
  1021.         const touch = e.touches[0];
  1022.         const rect = progressBar2.getBoundingClientRect();
  1023.         const ratio = (touch.clientX - rect.left) / rect.width;
  1024.         const clampedRatio = Math.max(0, Math.min(1, ratio));
  1025.         const newTime = clampedRatio * videoCurrent.duration;

  1026.         progressBar2Filled.style.width = `${clampedRatio * 100}%`;
  1027.         progressBar2Time.textContent = `${formatTime(newTime)}/${formatTime(videoCurrent.duration)}`;

  1028.         videoCurrent.currentTime = newTime;
  1029.     }, { passive: false });

  1030.     progressBar2.addEventListener('touchend', (e) => {
  1031.         e.stopPropagation();
  1032.         setTimeout(() => {
  1033.             isTouchOperation = false;
  1034.         }, 100);
  1035.         progressBar2.classList.remove('expanded');
  1036.         isDraggingProgress = false;
  1037.     });

  1038.     progressBar2.addEventListener('mouseover', (e) => {
  1039.         if (isTouchOperation) return;
  1040.         progressBar2.classList.add('expanded');
  1041.     });

  1042.     progressBar2.addEventListener('mouseout', (e) => {
  1043.         if (isTouchOperation || isDraggingProgress) return;
  1044.         progressBar2.classList.remove('expanded');
  1045.     });

  1046.     progressBar2.addEventListener('mousedown', (e) => {
  1047.         if (isTouchOperation || !videoCurrent.duration) return;
  1048.         isMouseOperation = true;
  1049.         isDraggingProgress = true;
  1050.         e.preventDefault();

  1051.         const rect = progressBar2.getBoundingClientRect();
  1052.         const ratio = (e.clientX - rect.left) / rect.width;
  1053.         const clampedRatio = Math.max(0, Math.min(1, ratio));
  1054.         const newTime = clampedRatio * videoCurrent.duration;

  1055.         progressBar2Filled.style.width = `${clampedRatio * 100}%`;
  1056.         progressBar2Time.textContent = `${formatTime(newTime)}/${formatTime(videoCurrent.duration)}`;

  1057.         videoCurrent.currentTime = newTime;
  1058.     });

  1059.     document.addEventListener('mousemove', (e) => {
  1060.         if (!isDraggingProgress || !isMouseOperation || !videoCurrent.duration) return;

  1061.         const rect = progressBar2.getBoundingClientRect();
  1062.         const ratio = (e.clientX - rect.left) / rect.width;
  1063.         const clampedRatio = Math.max(0, Math.min(1, ratio));
  1064.         const newTime = clampedRatio * videoCurrent.duration;

  1065.         progressBar2Filled.style.width = `${clampedRatio * 100}%`;
  1066.         progressBar2Time.textContent = `${formatTime(newTime)}/${formatTime(videoCurrent.duration)}`;

  1067.         videoCurrent.currentTime = newTime;
  1068.     });

  1069.     document.addEventListener('mouseup', (e) => {
  1070.         if (isMouseOperation) {
  1071.             setTimeout(() => {
  1072.                 isMouseOperation = false;
  1073.             }, 100);
  1074.             isDraggingProgress = false;
  1075.         }
  1076.     });
  1077. }

  1078. function setupFullscreenEvents() {
  1079.     document.addEventListener('fullscreenchange', updateFullscreenButtonState);
  1080.     document.addEventListener('webkitfullscreenchange', updateFullscreenButtonState);
  1081.     document.addEventListener('msfullscreenchange', updateFullscreenButtonState);
  1082. }

  1083. function setupControlButtons() {
  1084.     playPauseButton.addEventListener('click', togglePlayPause);
  1085.     muteButton.addEventListener('click', toggleMute);
  1086.     downloadButton.addEventListener('click', downloadVideo);
  1087.     fullscreenButton.addEventListener('click', toggleFullscreen);

  1088.     videoContainer.addEventListener('dblclick', (e) => {
  1089.         e.preventDefault();
  1090.         toggleFullscreen();
  1091.     });
  1092. }

  1093. function initApp() {
  1094.     initPlayer();
  1095.     setupControlButtons();
  1096.     setupKeyboardEvents();
  1097.     setupProgressBar2Click();
  1098.     setupFullscreenEvents();
  1099.     loadVideoFromAPI();
  1100. }

  1101. document.addEventListener('DOMContentLoaded', initApp);
  1102. </script>
  1103. </body>
  1104. </html>
复制代码


老规矩,贴出源码,论坛贴代码会吃掉cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css,建议还是去下载。
 楼主| 发表于 昨天 16:59 | 显示全部楼层
shim 发表于 2026-1-5 15:39
用上了
http://huo.chez.com/index.html

大佬,demo可以更新下2.0,更多源自动切换,并提供下载
发表于 昨天 19:18 | 显示全部楼层
yywfsky 发表于 2026-1-6 16:59
大佬,demo可以更新下2.0,更多源自动切换,并提供下载

第一版就够用了
发表于 昨天 22:00 | 显示全部楼层
本帖最后由 happy12 于 2026-1-6 22:03 编辑

想问一下,自己往里边加视频源可以吗? 怎么加,如果源多的话可不可以写成调用txt文本读源的方式,另外调用的源可不可以不只支持json,而能够支持更多方式
发表于 昨天 23:22 | 显示全部楼层
shim 发表于 2026-1-6 19:18
第一版就够用了

快快快 上第二版,另外搞个https 等你哟
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|小黑屋|免费吧论坛

GMT+8, 2026-1-7 07:34 , Processed in 0.089516 second(s), 3 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表