找回密码
 注册
搜索
系统gho:最纯净好用系统下载站投放广告、加入VIP会员,请联系 微信:wuyouceo
查看: 181|回复: 5

[求助] videos.js 多音轨切换 bug 修复

[复制链接]
发表于 3 天前 | 显示全部楼层 |阅读模式
本帖最后由 sunlenghua 于 2026-5-11 16:44 编辑

起因:因用 Jellyfin 自建的影院,总是出现视频播放中偶有闪退现象,更新三四个版本,调试无数次,这个现象一直有,,无耐才有了,用AI另写一个影院播放器的想法,纯网页播放,一个php文件即可。扔进电影目录,用nginx访问即可,目前,已经基本调试完美,支持手机/平板/电脑,自适应播放,支持电影播放中,滑屏切换上一个电影下一个电影,唯独多音轨电影声道切换一直调不好,(豆包挺逗的,一直检查不出问题),论里有没有熟悉video.js播放多音轨视频的朋友,帮忙看看这个能不能修复好,拜托了。

1.jpg 2.jpg


  1. <?php
  2. header("Content-Type: text/html; charset=utf-8");
  3. $allowExt = ['mp4'];
  4. $imageExt = ['jpg','jpeg','png','webp','gif','bmp','svg'];

  5. function getList($path) {
  6.     global $allowExt, $imageExt;
  7.     $list = [];
  8.     $d = opendir($path);
  9.     while ($f = readdir($d)) {
  10.         if ($f === '.' || $f === '..') continue;
  11.         $e = strtolower(pathinfo($f, PATHINFO_EXTENSION));
  12.         if (in_array($e, $imageExt)) continue;
  13.         if (is_dir($path.$f) || in_array($e, $allowExt)) {
  14.             $list[] = [
  15.                 'name' => $f,
  16.                 'isDir' => is_dir($path.$f),
  17.                 'src' => $path.$f
  18.             ];
  19.         }
  20.     }
  21.     closedir($d);
  22.     return $list;
  23. }

  24. // 子目录封面:优先folder.xxx,没有自动取目录第一张图片
  25. function getFolderCover($folder) {
  26.     global $imageExt;
  27.     foreach ($imageExt as $e) {
  28.         if (file_exists($folder . 'folder.' . $e)) {
  29.             return $folder . 'folder.' . $e;
  30.         }
  31.     }
  32.     $d = opendir($folder);
  33.     while ($f = readdir($d)) {
  34.         $e = strtolower(pathinfo($f, PATHINFO_EXTENSION));
  35.         if (in_array($e, $imageExt)) {
  36.             closedir($d);
  37.             return $folder . $f;
  38.         }
  39.     }
  40.     closedir($d);
  41.     return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYTRmZiI+PHBhdGggZD0iTTEwLDRMMCwxNEg0VjIwSDJWSDIwVjE0SDI0TDE0LDRIMFoiIC8+PC9zdmc+';
  42. }

  43. function getMovieCover($path, $filename) {
  44.     global $imageExt;
  45.     $b = pathinfo($filename, PATHINFO_FILENAME);
  46.     foreach ($imageExt as $e) {
  47.         if (file_exists($path . $b . '-poster.' . $e)) {
  48.             return $path . $b . '-poster.' . $e;
  49.         }
  50.     }
  51.     return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTgsMXYxMmgxMkwxNiwxN2w0LTRsLTQtNEg4VjF6IiAvPjwvc3ZnPg==';
  52. }

  53. $path = isset($_GET['p']) ? $_GET['p'] : './';
  54. $all = getList($path);
  55. $videos = [];
  56. foreach ($all as $i) {
  57.     if (!$i['isDir']) $videos[] = $i;
  58. }
  59. ?>

  60. <!DOCTYPE html>
  61. <html lang="zh-CN">
  62. <head>
  63. <meta charset="UTF-8">
  64. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  65. <title>本地影院</title>
  66. <style>
  67. *{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei}
  68. body{background:#121212;color:#fff}
  69. /* 移除禁止右键和选择的样式 */

  70. .header{position:fixed;top:0;left:0;right:0;height:60px;background:#222;display:flex;align-items:center;padding:0 20px;z-index:99}
  71. .back{border:none;background:none;color:#fff;font-size:18px;cursor:pointer}

  72. .container{padding:20px;margin-top:60px}
  73. .title{font-size:22px;margin-bottom:15px;padding-left:10px;border-left:4px solid #09f}
  74. .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}
  75. .item{border-radius:12px;overflow:hidden;background:#222;cursor:pointer}
  76. .item:hover{transform:scale(1.04);transition:.2s}
  77. .cover{width:100%;height:280px;object-fit:cover}
  78. .info{padding:10px}
  79. .name{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

  80. /* 播放层:网页全屏 */
  81. #playerLayer{
  82.     display:none;
  83.     position:fixed;
  84.     inset:0;
  85.     background:#000;
  86.     z-index:9999;
  87. }
  88. #video{
  89.     width:100%;
  90.     height:100%;
  91.     object-fit:contain;
  92.     background:#000;
  93. }

  94. /* 播放控制按钮容器 - 跟随控制条显隐 */
  95. .player-controls {
  96.     position: absolute;
  97.     bottom: 30px; /* 与原生控制条对齐 */
  98.     left: 50%;
  99.     transform: translateX(-50%);
  100.     display: flex;
  101.     gap: 15px;
  102.     padding: 8px 20px;
  103.     background: rgba(0,0,0,0.7);
  104.     border-radius: 8px;
  105.     opacity: 0;
  106.     transition: opacity 0.3s ease;
  107.     z-index: 10000;
  108. }
  109. /* 视频容器 hover/有操作时显示控制按钮 */
  110. #playerLayer:hover .player-controls,
  111. #playerLayer:focus-within .player-controls,
  112. #video:hover ~ .player-controls {
  113.     opacity: 1;
  114. }
  115. /* 控制按钮样式 */
  116. .player-btn {
  117.     border: none;
  118.     background: #09f;
  119.     color: #fff;
  120.     padding: 6px 12px;
  121.     border-radius: 4px;
  122.     cursor: pointer;
  123.     font-size: 14px;
  124. }
  125. .player-btn:disabled {
  126.     background: #666;
  127.     cursor: not-allowed;
  128. }
  129. /* 音轨切换按钮样式 */
  130. .audio-track-btn {
  131.     border: none;
  132.     background: #09f;
  133.     color: #fff;
  134.     padding: 6px 12px;
  135.     border-radius: 4px;
  136.     cursor: pointer;
  137.     font-size: 14px;
  138. }
  139. </style>
  140. </head>

  141. <!-- 移除body的oncontextmenu事件 -->
  142. <body>
  143. <div class="header">
  144.     <?php if($path!='./'):?>
  145.     <button class="back" onclick="history.back()">← 返回</button>
  146.     <?php endif?>
  147.     <h1>本地影院</h1>
  148. </div>

  149. <div class="container" id="listPage">
  150.     <div class="title"><?=$path=='./' ? '全部影片' : htmlspecialchars($path)?></div>
  151.     <div class="grid">
  152.         <?php foreach($all as $i):?>
  153.         <?php
  154.         $name=$i['name'];
  155.         $isDir=$i['isDir'];
  156.         $src=$i['src'];
  157.         if($isDir){
  158.             $cover=getFolderCover($src.'/');
  159.             $click="location.href='?p=".urlencode($src.'/')."'";
  160.         }else{
  161.             $cover=getMovieCover($path,$name);
  162.             $click="playVideo('".htmlspecialchars($src)."')";
  163.         }
  164.         ?>
  165.         <div class="item" onclick="<?=$click?>">
  166.             <!-- 移除图片的oncontextmenu事件 -->
  167.             <img class="cover" src="<?=$cover?>">
  168.             <div class="info"><div class="name"><?=htmlspecialchars($name)?></div></div>
  169.         </div>
  170.         <?php endforeach?>
  171.     </div>
  172. </div>

  173. <div id="playerLayer">
  174.     <!-- 移除视频的oncontextmenu和draggable属性 -->
  175.     <video id="video" controls autoplay></video>
  176.     <!-- 播放控制按钮容器 -->
  177.     <div class="player-controls">
  178.         <button class="player-btn" id="prevBtn" onclick="prev()">上一集</button>
  179.         <button class="player-btn" id="closeBtn" onclick="closePlayer()">关闭播放</button>
  180.         <button class="player-btn" id="nextBtn" onclick="next()">下一集</button>
  181.         <button class="audio-track-btn" id="audioTrackBtn" onclick="switchAudioTrack()">切换音轨</button>
  182.     </div>
  183. </div>

  184. <script>
  185. let videoList = <?=json_encode($videos)?>;
  186. let index = 0;
  187. let video = document.getElementById('video');
  188. let playerLayer = document.getElementById('playerLayer');
  189. let prevBtn = document.getElementById('prevBtn');
  190. let nextBtn = document.getElementById('nextBtn');
  191. let audioTrackBtn = document.getElementById('audioTrackBtn');

  192. // 网页全屏封装
  193. function enterWebFull() {
  194.     if (playerLayer.requestFullscreen) {
  195.         playerLayer.requestFullscreen();
  196.     } else if (playerLayer.webkitRequestFullscreen) {
  197.         playerLayer.webkitRequestFullscreen();
  198.     }
  199. }
  200. function exitWebFull() {
  201.     if (document.exitFullscreen) {
  202.         document.exitFullscreen();
  203.     } else if (document.webkitExitFullscreen) {
  204.         document.webkitExitFullscreen();
  205.     }
  206. }

  207. // 播放:网页全屏,不系统全屏
  208. function playVideo(url) {
  209.     index = videoList.findIndex(v => v.src === url);
  210.     document.getElementById('listPage').style.display = 'none';
  211.     playerLayer.style.display = 'block';
  212.     video.src = url;
  213.    
  214.     // 清空之前的事件监听(避免重复绑定)
  215.     video.onloadedmetadata = null;
  216.     video.onloadeddata = null;
  217.    
  218.     // 监听视频元数据加载完成(第一阶段)
  219.     video.onloadedmetadata = function() {
  220.         updateBtnStatus();
  221.         // 立即检测一次音轨
  222.         setTimeout(updateAudioTrackBtn, 500);
  223.     };
  224.    
  225.     // 监听视频数据加载完成(第二阶段,更稳定)
  226.     video.onloadeddata = function() {
  227.         updateAudioTrackBtn();
  228.     };

  229.     // 监听音轨加载事件(兼容不同浏览器)
  230.     if (video.audioTracks) {
  231.         video.audioTracks.addEventListener('addtrack', updateAudioTrackBtn);
  232.         video.audioTracks.addEventListener('change', updateAudioTrackBtn);
  233.     }

  234.     // 增加加载失败兜底
  235.     video.onerror = function() {
  236.         alert('视频加载失败,请检查文件路径或格式!');
  237.         closePlayer();
  238.     };
  239.    
  240.     video.play().catch(err => {
  241.         console.error('播放失败:', err);
  242.         alert('视频播放失败,请检查浏览器权限或文件格式!');
  243.     });
  244. }

  245. // 更新上/下一集按钮禁用状态
  246. function updateBtnStatus() {
  247.     prevBtn.disabled = index <= 0;
  248.     nextBtn.disabled = index >= videoList.length - 1;
  249. }

  250. // 检查音轨并更新音轨按钮(优化版)
  251. function updateAudioTrackBtn() {
  252.     // 兼容不同浏览器的音轨 API
  253.     const audioTracks = video.audioTracks || video.mozAudioTracks || video.webkitAudioTracks || [];
  254.    
  255.     // 打印音轨详情到控制台,便于调试
  256.     console.log('【音轨检测】当前音轨列表:', audioTracks);
  257.     if (audioTracks.length) {
  258.         audioTracks.forEach((track, idx) => {
  259.             console.log(`【音轨${idx+1}】`, {
  260.                 id: track.id,
  261.                 label: track.label || '无标签',
  262.                 language: track.language || '无语言',
  263.                 enabled: track.enabled
  264.             });
  265.         });
  266.     }

  267.     // 更新按钮状态
  268.     if (audioTracks.length <= 1) {
  269.         audioTrackBtn.textContent = '音轨(仅1条/无)';
  270.         audioTrackBtn.disabled = true;
  271.     } else {
  272.         // 找到当前激活的音轨
  273.         const activeTrack = Array.from(audioTracks).find(t => t.enabled);
  274.         const activeIndex = activeTrack ? Array.from(audioTracks).indexOf(activeTrack) + 1 : 1;
  275.         audioTrackBtn.textContent = `当前音轨: ${activeIndex} (共${audioTracks.length}条)`;
  276.         audioTrackBtn.disabled = false;
  277.     }
  278. }

  279. // 切换音轨核心逻辑(优化版)
  280. function switchAudioTrack() {
  281.     // 兼容不同浏览器的音轨 API
  282.     const audioTracks = video.audioTracks || video.mozAudioTracks || video.webkitAudioTracks || [];
  283.    
  284.     if (audioTracks.length <= 1) {
  285.         alert('当前视频仅包含1条音轨,无法切换!');
  286.         return;
  287.     }
  288.    
  289.     // 转换为数组便于操作
  290.     const trackList = Array.from(audioTracks);
  291.     // 找到当前激活的音轨
  292.     const currentActiveIndex = trackList.findIndex(t => t.enabled);
  293.    
  294.     // 关闭所有音轨,然后激活下一条
  295.     trackList.forEach((track, idx) => {
  296.         track.enabled = false;
  297.     });
  298.    
  299.     // 切换到下一条音轨(循环)
  300.     const nextIndex = (currentActiveIndex + 1) % trackList.length;
  301.     trackList[nextIndex].enabled = true;
  302.    
  303.     // 立即更新按钮文本
  304.     audioTrackBtn.textContent = `当前音轨: ${nextIndex + 1} (共${trackList.length}条)`;
  305.    
  306.     // 提示切换结果
  307.     console.log(`已切换到音轨${nextIndex + 1}`, trackList[nextIndex]);
  308. }

  309. // 关闭返回
  310. function closePlayer(){
  311.     video.pause();
  312.     playerLayer.style.display = 'none';
  313.     document.getElementById('listPage').style.display = 'block';
  314.     exitWebFull();
  315. }

  316. // 上一集
  317. function prev(){
  318.     if(index>0){
  319.         index--;
  320.         video.src = videoList[index].src;
  321.         video.play();
  322.         updateBtnStatus(); // 更新按钮状态
  323.         // 切换后重新检测音轨
  324.         setTimeout(updateAudioTrackBtn, 500);
  325.     }
  326. }
  327. // 下一集
  328. function next(){
  329.     if(index < videoList.length-1){
  330.         index++;
  331.         video.src = videoList[index].src;
  332.         video.play();
  333.         updateBtnStatus(); // 更新按钮状态
  334.         // 切换后重新检测音轨
  335.         setTimeout(updateAudioTrackBtn, 500);
  336.     }
  337. }

  338. // 手机左右滑动切换
  339. let touchStartX = 0;
  340. document.addEventListener('touchstart', e=>touchStartX=e.changedTouches[0].screenX);
  341. document.addEventListener('touchend', e=>{
  342.     let delta = e.changedTouches[0].screenX - touchStartX;
  343.     if(delta > 60) prev();
  344.     if(delta < -60) next();
  345. });

  346. // ESC 退出
  347. document.addEventListener('keydown',e=>{
  348.     if(e.key==='Escape') closePlayer();
  349. });

  350. // 监听视频控制条显隐(模拟):无操作3秒后隐藏按钮
  351. let hideTimer;
  352. playerLayer.addEventListener('mousemove', () => {
  353.     clearTimeout(hideTimer);
  354.     document.querySelector('.player-controls').style.opacity = 1;
  355.     // 3秒无操作则隐藏
  356.     hideTimer = setTimeout(() => {
  357.         document.querySelector('.player-controls').style.opacity = 0;
  358.     }, 3000);
  359. });
  360. </script>
  361. </body>
  362. </html>
复制代码


发表于 3 天前 | 显示全部楼层
没环境啊,问逗包也不能测试,白问,对你也没有一点帮助,纯支持观望有大佬出手或是大佬自己解决了发出源码来。
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
看不明白
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
多换几个AI进行调教
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
进来了解一下
回复

使用道具 举报

发表于 前天 05:51 | 显示全部楼层
我也不知道  帮顶  来学习下
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|Archiver|捐助支持|无忧启动 ( 闽ICP备05002490号-1|闽公网安备35020302032614号 )

GMT+8, 2026-5-14 00:00

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

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