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

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

[复制链接]
发表于 前天 11:27 | 显示全部楼层 |阅读模式
本帖最后由 yywfsky 于 2026-1-6 17:06 编辑

之前做个一个php给手机用来播放的框架拿了点代码出来改了个播放器,仿抖音界面
上划返回历史,最多5个,下划获取新视频
下方进度放上面70px的范围内就可以拖动或者点取

pc端:鼠标混轮 pageup pagedown切换

移动端:支持触摸,上划或者下划切换

单一php任何支持php的服务器都可以,用手机浏览器可以看小姐姐扭腰摆臀,但让你有兴趣h5封装apk也可以。

今天发现源还没工作一天就挂了好几个小时,我稍微修改了个2.0版本
2.0版本下载
https://wwath.lanzout.com/iZiUa3ffx23c
密码:amm6
具体内容为:
  1. const API_SOURCES = [
  2.     {
  3.         name: 'xjj1',
  4.         url: 'https://v2.xxapi.cn/api/meinv',
  5.         parse: (data) => data.code === 200 && data.data ? data.data : null
  6.     },
  7.     {
  8.         name: 'xjj2',
  9.         url: 'https://api.codetabs.com/v1/敏感词XXX?quest=' + encodeURIComponent('https://api.mmp.cc/api/miss?type=json'),
  10.         parse: (data) => data.status === 'success' && data.link ? data.link : null
  11.     },
  12.     {
  13.         name: 'xjj3',
  14.         url: 'https://api.kuleu.com/api/MP4_xiaojiejie?type=json',
  15.         parse: (data) => data.code === 200 && data.mp4_video ? data.mp4_video : null
  16.     }
  17. ];
复制代码
1、我增加了3个源,如果其中一个三次尝试失败或者5秒钟没加载出视频切换到下一个,如果所有源都没成功提醒刷新页面或者稍后再试
2、有人说看到喜欢的想保存,我在右侧加了个下载按钮,点击会触发浏览的保存,保存即为当前视频
11楼贴出了源代码,但是还是会被论坛吃掉cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css导致按钮看不到,建议下载



v1.0成品php下载
https://wwath.lanzout.com/i7fRL3fbjq2h

密码:4hru



没啥科技含量,比较粗糙,弄完,我看了五分钟就腻了。当个玩意给想玩的玩下吧


想玩的朋友,还是去下载这个成品php吧,我2楼贴出来的cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css被吃了,显示不出来。直接复制的按钮看不到










发表于 前天 13:24 来自手机 | 显示全部楼层
这不来个演示?
 楼主| 发表于 前天 13:36 | 显示全部楼层
shim 发表于 2026-1-5 13:24
这不来个演示?

随便有个支持php的空间都行啊,主要是我还真没有,我测试偷拿的公司服务器  能带域名的最好了,直接h5编译apk,当app方手机上用
 楼主| 发表于 前天 13:40 | 显示全部楼层
本帖最后由 yywfsky 于 2026-1-5 15:57 编辑
  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>Ba-line</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 fullscreen-button" id="fullscreenButton">
  302.             <i class="fa fa-expand"></i>
  303.         </button>
  304.     </div>

  305. <script>
  306. const API_URL = 'https://v2.xxapi.cn/api/meinv';
  307. const MAX_HISTORY = 5;

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

  309. let videoHistory = [];
  310. let currentVideoUrl = '';
  311. let nextVideoUrl = '';
  312. let isPlaying = false;
  313. let isMuted = false;
  314. let isLoading = false;
  315. let isErrorHandling = false;
  316. let isVideoSwitching = false;
  317. let maxRetryAttempts = 3;
  318. let currentRetryCount = 0;
  319. let failedVideos = new Set();
  320. let isPreloadingNext = false;

  321. let controlsTimeout;
  322. const CONTROLS_HIDE_DELAY = 3000;
  323. const PROGRESS_STEP = 5;
  324. const VOLUME_STEP = 0.1;
  325. const TOAST_DURATION = 2000;
  326. const WHEEL_THROTTLE = 1000;
  327. let lastWheelTime = 0;

  328. let lastTapTime = 0;
  329. const DOUBLE_TAP_DELAY = 300;

  330. let savedVolume = 0.7;
  331. let savedMuted = false;

  332. let touchStartY = 0;
  333. let isDragging = false;
  334. let currentTransform = 0;
  335. const SWIPE_THRESHOLD = 50;

  336. let longPressTimer = null;

  337. const progressBar2 = document.getElementById('progressBar2');
  338. const progressBar2Filled = document.getElementById('progressBar2Filled');
  339. const progressBar2Time = document.getElementById('progressBar2Time');
  340. const bottomClickArea = document.getElementById('bottomClickArea');

  341. let isDraggingProgress = false;
  342. let touchStartCurrentTime = 0;

  343. const videoContainer = document.getElementById('videoContainer');
  344. const loadingScreen = document.getElementById('loadingScreen');
  345. const slideMask = document.getElementById('slideMask');
  346. let videoCurrent = document.getElementById('video-current');
  347. let videoNext = document.getElementById('video-next');
  348. const toastBox = document.getElementById('toastBox');
  349. const muteButton = document.getElementById('muteButton');
  350. const muteIcon = muteButton.querySelector('i');
  351. const fullscreenButton = document.getElementById('fullscreenButton');
  352. const fullscreenIcon = fullscreenButton.querySelector('i');
  353. const playPauseButton = document.getElementById('playPauseButton');
  354. const playPauseIcon = playPauseButton.querySelector('i');

  355. function showToast(message) {
  356.     if (window.toastTimer) clearTimeout(window.toastTimer);
  357.     toastBox.textContent = message;
  358.     toastBox.classList.add('visible');
  359.     window.toastTimer = setTimeout(() => {
  360.         toastBox.classList.remove('visible');
  361.     }, TOAST_DURATION);
  362. }

  363. function formatTime(seconds) {
  364.     const mins = Math.floor(seconds / 60);
  365.     const secs = Math.floor(seconds % 60);
  366.     return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  367. }

  368. function updateProgressBar2() {
  369.     if (!videoCurrent || !videoCurrent.duration) return;
  370.     const ratio = videoCurrent.currentTime / videoCurrent.duration;
  371.     progressBar2Filled.style.width = `${ratio * 100}%`;
  372.     progressBar2Time.textContent = `${formatTime(videoCurrent.currentTime)}/${formatTime(videoCurrent.duration)}`;
  373. }

  374. function handleBottomAreaClick(e) {
  375.     if (!isMobile || !videoCurrent || !videoCurrent.duration) return;
  376.     e.stopPropagation();
  377.     const rect = videoContainer.getBoundingClientRect();
  378.     const ratio = (e.clientX - rect.left) / rect.width;
  379.     const prevTime = videoCurrent.currentTime;
  380.     videoCurrent.currentTime = ratio * videoCurrent.duration;
  381.     const newTime = videoCurrent.currentTime;
  382.     const movedSeconds = Math.round(newTime - prevTime);
  383.     if (movedSeconds !== 0) {
  384.         const direction = movedSeconds > 0 ? '快进' : '快退';
  385.         showToast(`${direction} ${Math.abs(movedSeconds)}秒`);
  386.     }
  387. }

  388. async function fetchVideoUrl(retryCount = 0) {
  389.     try {
  390.         const response = await fetch(API_URL);
  391.         const data = await response.json();
  392.         
  393.         if (data.code === 200 && data.data) {
  394.             return data.data;
  395.         } else {
  396.             throw new Error('API返回数据格式错误');
  397.         }
  398.     } catch (error) {
  399.         if (retryCount < 3) {
  400.             await new Promise(resolve => setTimeout(resolve, 1000));
  401.             return fetchVideoUrl(retryCount + 1);
  402.         } else {
  403.             showToast('api无法获得视频');
  404.             throw error;
  405.         }
  406.     }
  407. }

  408. function addToHistory(videoUrl) {
  409.     if (currentVideoUrl) {
  410.         videoHistory.push(currentVideoUrl);
  411.         if (videoHistory.length > MAX_HISTORY) {
  412.             videoHistory.shift();
  413.         }
  414.     }
  415.     currentVideoUrl = videoUrl;
  416. }

  417. function getPreviousVideo() {
  418.     if (videoHistory.length > 0) {
  419.         const previousUrl = videoHistory.pop();
  420.         currentVideoUrl = previousUrl;
  421.         return previousUrl;
  422.     }
  423.     return null;
  424. }

  425. function playPreviousVideo() {
  426.     if (isLoading) {
  427.         console.log('正在加载中,跳过切换请求');
  428.         return;
  429.     }

  430.     if (isErrorHandling) {
  431.         console.log('正在处理错误,跳过切换请求');
  432.         return;
  433.     }

  434.     const previousUrl = getPreviousVideo();
  435.    
  436.     if (previousUrl) {
  437.         loadVideo(previousUrl, 'previous');
  438.         showToast('返回上一个视频');
  439.     } else {
  440.         loadVideoFromAPI('previous');
  441.         showToast('无更早历史,获取新视频');
  442.     }
  443. }

  444. function playNextVideo() {
  445.     if (isLoading) {
  446.         console.log('正在加载中,跳过切换请求');
  447.         return;
  448.     }

  449.     if (isErrorHandling) {
  450.         console.log('正在处理错误,跳过切换请求');
  451.         return;
  452.     }

  453.     if (nextVideoUrl) {
  454.         addToHistory(nextVideoUrl);
  455.         loadVideo(nextVideoUrl, 'next');
  456.         showToast('切换下一个视频');
  457.     } else {
  458.         loadVideoFromAPI('next');
  459.         showToast('获取新视频');
  460.     }
  461. }

  462. async function loadVideoFromAPI(direction = 'none') {
  463.     try {
  464.         const videoUrl = await fetchVideoUrl();
  465.         if (videoUrl) {
  466.             if (direction === 'next') {
  467.                 addToHistory(videoUrl);
  468.             }
  469.             loadVideo(videoUrl, direction);
  470.         }
  471.     } catch (error) {
  472.         console.error('获取新视频失败:', error);
  473.     }
  474. }

  475. function loadVideo(videoUrl, direction = 'none') {
  476.     if (isLoading) {
  477.         console.log('正在加载中,跳过重复请求');
  478.         return;
  479.     }

  480.     if (isErrorHandling) {
  481.         console.log('正在处理错误,跳过新的加载请求');
  482.         return;
  483.     }

  484.     if (currentRetryCount >= maxRetryAttempts) {
  485.         console.log('已达到最大重试次数:', currentRetryCount);
  486.         showToast('视频加载失败次数过多,请刷新页面重试');
  487.         loadingScreen.style.display = 'none';
  488.         return;
  489.     }

  490.     isLoading = true;
  491.     loadingScreen.style.display = 'flex';

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

  493.     if (direction === 'next') {
  494.         videoCurrent.src = videoUrl;
  495.         videoCurrent.load();

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

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

  500.             setTimeout(() => {
  501.                 videoCurrent.muted = savedMuted;
  502.                 videoCurrent.volume = savedVolume;
  503.                 videoCurrent.play();

  504.                 loadingScreen.style.display = 'none';
  505.                 currentRetryCount = 0;
  506.                 failedVideos.clear();
  507.                 isLoading = false;
  508.                 isVideoSwitching = false;

  509.                 preloadNextVideo();
  510.             }, 150);

  511.             setTimeout(() => {
  512.                 slideMask.classList.remove('slide-up-mask');
  513.             }, 300);
  514.         }, { once: true });

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

  517.             if (!videoCurrent.src) {
  518.                 console.log('忽略空src的错误(这是正常的切换操作)');
  519.                 isLoading = false;
  520.                 return;
  521.             }

  522.             if (isErrorHandling) {
  523.                 console.log('错误处理已在进行中,跳过重复处理');
  524.                 return;
  525.             }

  526.             if (isVideoSwitching) {
  527.                 console.log('正在切换视频,忽略旧的video元素的error事件');
  528.                 return;
  529.             }

  530.             isErrorHandling = true;
  531.             failedVideos.add(videoUrl);
  532.             currentRetryCount++;
  533.             isLoading = false;

  534.             if (currentRetryCount >= maxRetryAttempts) {
  535.                 showToast('视频加载失败次数过多,请刷新页面重试');
  536.                 loadingScreen.style.display = 'none';
  537.                 isErrorHandling = false;
  538.                 return;
  539.             }

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

  542.             setTimeout(() => {
  543.                 isErrorHandling = false;
  544.                 playNextVideo();
  545.             }, 500);
  546.         }, { once: true });
  547.     } else if (direction === 'previous') {
  548.         videoCurrent.src = videoUrl;
  549.         videoCurrent.load();

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

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

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

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

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

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

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

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

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

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

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

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

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

  596.             setTimeout(() => {
  597.                 isErrorHandling = false;
  598.                 playPreviousVideo();
  599.             }, 500);
  600.         }, { once: true });
  601.     } else {
  602.         videoCurrent.src = videoUrl;
  603.         videoCurrent.load();

  604.         videoCurrent.addEventListener('loadeddata', () => {
  605.             console.log('视频加载成功 (initial):', videoUrl);
  606.             videoCurrent.muted = savedMuted;
  607.             videoCurrent.volume = savedVolume;
  608.             videoCurrent.play();
  609.             loadingScreen.style.display = 'none';
  610.             currentRetryCount = 0;
  611.             failedVideos.clear();
  612.             isLoading = false;

  613.             preloadNextVideo();
  614.         }, { once: true });

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

  617.             if (!videoCurrent.src) {
  618.                 console.log('忽略空src的错误(这是正常的切换操作)');
  619.                 isLoading = false;
  620.                 return;
  621.             }

  622.             if (isErrorHandling) {
  623.                 console.log('错误处理已在进行中,跳过重复处理');
  624.                 return;
  625.             }

  626.             if (isVideoSwitching) {
  627.                 console.log('正在切换视频,忽略旧的video元素的error事件');
  628.                 return;
  629.             }

  630.             isErrorHandling = true;
  631.             failedVideos.add(videoUrl);
  632.             currentRetryCount++;
  633.             isLoading = false;

  634.             if (currentRetryCount >= maxRetryAttempts) {
  635.                 showToast('视频加载失败次数过多,请刷新页面重试');
  636.                 loadingScreen.style.display = 'none';
  637.                 isErrorHandling = false;
  638.                 return;
  639.             }

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

  642.             setTimeout(() => {
  643.                 isErrorHandling = false;
  644.                 playNextVideo();
  645.             }, 500);
  646.         }, { once: true });
  647.     }

  648.     currentVideoUrl = videoUrl;
  649. }

  650. async function preloadNextVideo() {
  651.     if (isPreloadingNext) return;

  652.     isPreloadingNext = true;
  653.     try {
  654.         nextVideoUrl = await fetchVideoUrl();
  655.         if (nextVideoUrl) {
  656.             videoNext.src = nextVideoUrl;
  657.             videoNext.load();
  658.         }
  659.     } catch (error) {
  660.         console.error('预加载视频失败:', error);
  661.     } finally {
  662.         isPreloadingNext = false;
  663.     }
  664. }

  665. function initPlayer() {
  666.     videoCurrent.muted = savedMuted;
  667.     videoCurrent.volume = savedVolume;

  668.     setTimeout(() => {
  669.         savedMuted = videoCurrent.muted;
  670.         updateMuteButtonState();
  671.     }, 100);

  672.     updateFullscreenButtonState();
  673.     updatePlayPauseButtonState();
  674.     videoContainer.classList.add(videoCurrent.paused ? 'paused' : 'playing');
  675.     videoContainer.classList.remove(videoCurrent.paused ? 'playing' : 'paused');

  676.     videoCurrent.addEventListener('loadedmetadata', () => {
  677.         loadingScreen.style.display = 'none';
  678.         updateProgressBar2();
  679.     });

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

  681.     videoCurrent.addEventListener('play', () => {
  682.         videoContainer.classList.remove('paused');
  683.         videoContainer.classList.add('playing');
  684.         updatePlayPauseButtonState();
  685.         showToast('播放中');
  686.     });

  687.     videoCurrent.addEventListener('pause', () => {
  688.         videoContainer.classList.remove('playing');
  689.         videoContainer.classList.add('paused');
  690.         updatePlayPauseButtonState();
  691.         showToast('已暂停');
  692.     });

  693.     videoCurrent.addEventListener('ended', () => {
  694.         if (!isLoading && !isErrorHandling) {
  695.             playNextVideo();
  696.         }
  697.     });

  698.     setupTouchEvents();
  699.     if (!isMobile) {
  700.         setupDesktopWheelEvent();
  701.     }
  702. }

  703. function setupDesktopWheelEvent() {
  704.     videoContainer.addEventListener('wheel', (e) => {
  705.         e.preventDefault();
  706.         const currentTime = Date.now();
  707.         if (currentTime - lastWheelTime < WHEEL_THROTTLE) {
  708.             return;
  709.         }
  710.         lastWheelTime = currentTime;
  711.         if (e.deltaY > 0) {
  712.             playNextVideo();
  713.         } else {
  714.             playPreviousVideo();
  715.         }
  716.     }, { passive: false });
  717. }

  718. function updateMuteButtonState() {
  719.     if (!videoCurrent) return;
  720.     if (videoCurrent.muted) {
  721.         muteIcon.className = 'fa fa-volume-off';
  722.         muteButton.classList.add('muted');
  723.     } else {
  724.         muteIcon.className = 'fa fa-volume-up';
  725.         muteButton.classList.remove('muted');
  726.     }
  727. }

  728. function updateFullscreenButtonState() {
  729.     if (document.fullscreenElement) {
  730.         fullscreenIcon.className = 'fa fa-compress';
  731.     } else {
  732.         fullscreenIcon.className = 'fa fa-expand';
  733.     }
  734. }

  735. function updatePlayPauseButtonState() {
  736.     if (!videoCurrent) return;
  737.     if (videoCurrent.paused) {
  738.         playPauseIcon.className = 'fa fa-play';
  739.     } else {
  740.         playPauseIcon.className = 'fa fa-pause';
  741.     }
  742. }

  743. function togglePlayPause() {
  744.     if (!videoCurrent) return;
  745.     if (videoCurrent.paused) {
  746.         videoCurrent.play();
  747.         showToast('播放');
  748.     } else {
  749.         videoCurrent.pause();
  750.         showToast('暂停');
  751.     }
  752. }

  753. function toggleMute() {
  754.     if (!videoCurrent) return;
  755.     if (videoCurrent.muted) {
  756.         videoCurrent.muted = false;
  757.         videoCurrent.volume = savedVolume;
  758.         showToast('已取消静音');
  759.     } else {
  760.         savedVolume = videoCurrent.volume;
  761.         videoCurrent.muted = true;
  762.         showToast('已静音');
  763.     }
  764.     updateMuteButtonState();
  765. }

  766. function toggleFullscreen() {
  767.     if (!document.fullscreenElement) {
  768.         if (videoContainer.requestFullscreen) videoContainer.requestFullscreen();
  769.         else if (videoContainer.webkitRequestFullscreen) videoContainer.webkitRequestFullscreen();
  770.         else if (videoContainer.msRequestFullscreen) videoContainer.msRequestFullscreen();
  771.         showToast('已进入全屏');
  772.     } else {
  773.         if (document.exitFullscreen) document.exitFullscreen();
  774.         else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
  775.         else if (document.msExitFullscreen) document.msExitFullscreen();
  776.         showToast('已退出全屏');
  777.     }
  778.     updateFullscreenButtonState();
  779. }

  780. function setupTouchEvents() {
  781.     let touchStartY = 0;
  782.     let touchStartX = 0;
  783.     let isVolumeAdjust = false;
  784.     let isProgressAdjust = false;
  785.     let isSwipeToChangeVideo = false;
  786.     const SWIPE_VIDEO_THRESHOLD = 50;
  787.     const SWIPE_PROGRESS_THRESHOLD = 20;
  788.     const VOLUME_AREA_WIDTH = window.innerWidth * 0.3;
  789.     let lastTouchX = 0;
  790.     let lastTouchY = 0;

  791.     videoContainer.addEventListener('touchstart', (e) => {
  792.         if (e.touches.length !== 1) return;
  793.         const touch = e.touches[0];
  794.         touchStartY = touch.clientY;
  795.         touchStartX = touch.clientX;
  796.         lastTouchX = touch.clientX;
  797.         lastTouchY = touch.clientY;
  798.         isVolumeAdjust = false;
  799.         isProgressAdjust = false;
  800.         isSwipeToChangeVideo = false;
  801.         touchStartCurrentTime = videoCurrent.currentTime;
  802.     }, { passive: true });

  803.     videoContainer.addEventListener('touchmove', (e) => {
  804.         if (e.touches.length !== 1) return;
  805.         e.preventDefault();
  806.         const touch = e.touches[0];
  807.         const touchY = touch.clientY;
  808.         const touchX = touch.clientX;
  809.         const deltaY = touchStartY - touchY;
  810.         const deltaX = touchX - touchStartX;

  811.         const absDeltaY = Math.abs(deltaY);
  812.         const absDeltaX = Math.abs(deltaX);

  813.         if (absDeltaY > absDeltaX) {
  814.             isSwipeToChangeVideo = true;
  815.             isVolumeAdjust = false;
  816.         } else {
  817.             if (absDeltaX > SWIPE_PROGRESS_THRESHOLD) {
  818.                 isProgressAdjust = true;
  819.                 isSwipeToChangeVideo = false;
  820.                 const swipeRatio = Math.abs(deltaX) / window.innerWidth;
  821.                 const stepCount = Math.floor(swipeRatio / 0.1);
  822.                 const totalStep = stepCount * PROGRESS_STEP;
  823.                 const direction = deltaX > 0 ? 1 : -1;
  824.                 const newTime = touchStartCurrentTime + (direction * totalStep);
  825.                 const clampedTime = Math.max(0, Math.min(videoCurrent.duration || 0, newTime));
  826.                 videoCurrent.currentTime = clampedTime;
  827.                 const movedSeconds = Math.round(clampedTime - touchStartCurrentTime);
  828.                 if (movedSeconds !== 0 && movedSeconds % PROGRESS_STEP === 0) {
  829.                     const dirText = movedSeconds > 0 ? '快进' : '快退';
  830.                     showToast(`${dirText} ${Math.abs(movedSeconds)}秒`);
  831.                 }
  832.             }
  833.         }

  834.         lastTouchX = touchX;
  835.         lastTouchY = touchY;
  836.     }, { passive: false });

  837.     videoContainer.addEventListener('touchend', (e) => {
  838.         clearTimeout(longPressTimer);
  839.         if (isSwipeToChangeVideo) {
  840.             const touchEndY = e.changedTouches[0].clientY;
  841.             const deltaY = touchStartY - touchEndY;
  842.             if (deltaY > SWIPE_VIDEO_THRESHOLD) playNextVideo();
  843.             else if (deltaY < -SWIPE_VIDEO_THRESHOLD) playPreviousVideo();
  844.         }
  845.         const currentTime = Date.now();
  846.         const tapTimeDiff = currentTime - lastTapTime;
  847.         if (tapTimeDiff < DOUBLE_TAP_DELAY && tapTimeDiff > 0) {
  848.             const touchEndX = e.changedTouches[0].clientX;
  849.             const screenWidth = window.innerWidth;
  850.             const isLeftSide = touchEndX < screenWidth * 0.4;
  851.             const isRightSide = touchEndX > screenWidth * 0.6;

  852.             if (isLeftSide) {
  853.                 videoCurrent.currentTime = Math.max(0, videoCurrent.currentTime - PROGRESS_STEP);
  854.                 showToast(`快退 ${PROGRESS_STEP}秒`);
  855.             } else if (isRightSide) {
  856.                 if (videoCurrent.duration) {
  857.                     videoCurrent.currentTime = Math.min(videoCurrent.duration, videoCurrent.currentTime + PROGRESS_STEP);
  858.                     showToast(`快进 ${PROGRESS_STEP}秒`);
  859.                 }
  860.             } else {
  861.                 toggleFullscreen();
  862.             }
  863.             lastTapTime = 0;
  864.         } else {
  865.             lastTapTime = currentTime;
  866.         }
  867.     }, { passive: true });
  868. }

  869. function setupKeyboardEvents() {
  870.     document.addEventListener('keydown', (e) => {
  871.         switch (e.key) {
  872.             case ' ':
  873.             case 'k':
  874.                 e.preventDefault();
  875.                 togglePlayPause();
  876.                 break;
  877.             case 'f':
  878.             case 'F':
  879.                 e.preventDefault();
  880.                 toggleFullscreen();
  881.                 break;
  882.             case 'm':
  883.             case 'M':
  884.                 e.preventDefault();
  885.                 toggleMute();
  886.                 break;
  887.             case 'ArrowLeft':
  888.                 e.preventDefault();
  889.                 videoCurrent.currentTime = Math.max(0, videoCurrent.currentTime - PROGRESS_STEP);
  890.                 showToast(`快退 ${PROGRESS_STEP}秒`);
  891.                 break;
  892.             case 'ArrowRight':
  893.                 e.preventDefault();
  894.                 if (videoCurrent.duration) {
  895.                     videoCurrent.currentTime = Math.min(videoCurrent.duration, videoCurrent.currentTime + PROGRESS_STEP);
  896.                     showToast(`快进 ${PROGRESS_STEP}秒`);
  897.                 }
  898.                 break;
  899.             case 'PageUp':
  900.                 e.preventDefault();
  901.                 playPreviousVideo();
  902.                 break;
  903.             case 'PageDown':
  904.                 e.preventDefault();
  905.                 playNextVideo();
  906.                 break;
  907.         }
  908.     });
  909. }

  910. function setupProgressBar2Click() {
  911.     let isDraggingProgress = false;
  912.     let touchStartX = 0;
  913.     let touchStartTime = 0;
  914.     let isTouchOperation = false;
  915.     let isMouseOperation = false;

  916.     progressBar2.addEventListener('click', (e) => {
  917.         if (isTouchOperation || isMouseOperation || !videoCurrent.duration) return;
  918.         const rect = progressBar2.getBoundingClientRect();
  919.         const ratio = (e.clientX - rect.left) / rect.width;
  920.         videoCurrent.currentTime = ratio * videoCurrent.duration;
  921.     });

  922.     progressBar2.addEventListener('touchstart', (e) => {
  923.         if (e.touches.length !== 1) return;
  924.         isTouchOperation = true;
  925.         e.stopPropagation();
  926.         const touch = e.touches[0];
  927.         touchStartX = touch.clientX;
  928.         touchStartTime = Date.now();
  929.         isDraggingProgress = false;

  930.         progressBar2.classList.add('expanded');
  931.         isDraggingProgress = true;
  932.     }, { passive: true });

  933.     progressBar2.addEventListener('touchmove', (e) => {
  934.         if (!isDraggingProgress || !videoCurrent.duration) return;
  935.         e.preventDefault();
  936.         e.stopPropagation();
  937.         const touch = e.touches[0];
  938.         const rect = progressBar2.getBoundingClientRect();
  939.         const ratio = (touch.clientX - rect.left) / rect.width;
  940.         const clampedRatio = Math.max(0, Math.min(1, ratio));
  941.         const newTime = clampedRatio * videoCurrent.duration;

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

  944.         videoCurrent.currentTime = newTime;
  945.     }, { passive: false });

  946.     progressBar2.addEventListener('touchend', (e) => {
  947.         e.stopPropagation();
  948.         setTimeout(() => {
  949.             isTouchOperation = false;
  950.         }, 100);
  951.         progressBar2.classList.remove('expanded');
  952.         isDraggingProgress = false;
  953.     });

  954.     progressBar2.addEventListener('mouseover', (e) => {
  955.         if (isTouchOperation) return;
  956.         progressBar2.classList.add('expanded');
  957.     });

  958.     progressBar2.addEventListener('mouseout', (e) => {
  959.         if (isTouchOperation || isDraggingProgress) return;
  960.         progressBar2.classList.remove('expanded');
  961.     });

  962.     progressBar2.addEventListener('mousedown', (e) => {
  963.         if (isTouchOperation || !videoCurrent.duration) return;
  964.         isMouseOperation = true;
  965.         isDraggingProgress = true;
  966.         e.preventDefault();

  967.         const rect = progressBar2.getBoundingClientRect();
  968.         const ratio = (e.clientX - rect.left) / rect.width;
  969.         const clampedRatio = Math.max(0, Math.min(1, ratio));
  970.         const newTime = clampedRatio * videoCurrent.duration;

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

  973.         videoCurrent.currentTime = newTime;
  974.     });

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

  977.         const rect = progressBar2.getBoundingClientRect();
  978.         const ratio = (e.clientX - rect.left) / rect.width;
  979.         const clampedRatio = Math.max(0, Math.min(1, ratio));
  980.         const newTime = clampedRatio * videoCurrent.duration;

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

  983.         videoCurrent.currentTime = newTime;
  984.     });

  985.     document.addEventListener('mouseup', (e) => {
  986.         if (isMouseOperation) {
  987.             setTimeout(() => {
  988.                 isMouseOperation = false;
  989.             }, 100);
  990.             isDraggingProgress = false;
  991.         }
  992.     });
  993. }

  994. function setupFullscreenEvents() {
  995.     document.addEventListener('fullscreenchange', updateFullscreenButtonState);
  996.     document.addEventListener('webkitfullscreenchange', updateFullscreenButtonState);
  997.     document.addEventListener('msfullscreenchange', updateFullscreenButtonState);
  998. }

  999. function setupControlButtons() {
  1000.     playPauseButton.addEventListener('click', togglePlayPause);
  1001.     muteButton.addEventListener('click', toggleMute);
  1002.     fullscreenButton.addEventListener('click', toggleFullscreen);

  1003.     videoContainer.addEventListener('dblclick', (e) => {
  1004.         e.preventDefault();
  1005.         toggleFullscreen();
  1006.     });
  1007. }

  1008. function initApp() {
  1009.     initPlayer();
  1010.     setupControlButtons();
  1011.     setupKeyboardEvents();
  1012.     setupProgressBar2Click();
  1013.     setupFullscreenEvents();
  1014.     loadVideoFromAPI();
  1015. }

  1016. document.addEventListener('DOMContentLoaded', initApp);
  1017. </script>
  1018. </body>
  1019. </html>
复制代码

发表于 前天 15:39 | 显示全部楼层
 楼主| 发表于 前天 15:52 | 显示全部楼层
shim 发表于 2026-1-5 15:39
用上了
http://huo.chez.com/index.html

我看了 哈哈
 楼主| 发表于 前天 15:58 | 显示全部楼层
本帖最后由 yywfsky 于 2026-1-5 16:00 编辑
cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css

这一句我贴出来的时候论坛不显示,变成了<link  rel="stylesheet">  所以按钮看不到
发表于 前天 16:11 | 显示全部楼层
不错 不错
发表于 前天 18:44 | 显示全部楼层
666
发表于 昨天 11:58 | 显示全部楼层
666
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

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

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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