|
|
本帖最后由 sunlenghua 于 2026-5-11 16:44 编辑
起因:因用 Jellyfin 自建的影院,总是出现视频播放中偶有闪退现象,更新三四个版本,调试无数次,这个现象一直有,,无耐才有了,用AI另写一个影院播放器的想法,纯网页播放,一个php文件即可。扔进电影目录,用nginx访问即可,目前,已经基本调试完美,支持手机/平板/电脑,自适应播放,支持电影播放中,滑屏切换上一个电影下一个电影,唯独多音轨电影声道切换一直调不好,(豆包挺逗的,一直检查不出问题),论里有没有熟悉video.js播放多音轨视频的朋友,帮忙看看这个能不能修复好,拜托了。
- <?php
- header("Content-Type: text/html; charset=utf-8");
- $allowExt = ['mp4'];
- $imageExt = ['jpg','jpeg','png','webp','gif','bmp','svg'];
- function getList($path) {
- global $allowExt, $imageExt;
- $list = [];
- $d = opendir($path);
- while ($f = readdir($d)) {
- if ($f === '.' || $f === '..') continue;
- $e = strtolower(pathinfo($f, PATHINFO_EXTENSION));
- if (in_array($e, $imageExt)) continue;
- if (is_dir($path.$f) || in_array($e, $allowExt)) {
- $list[] = [
- 'name' => $f,
- 'isDir' => is_dir($path.$f),
- 'src' => $path.$f
- ];
- }
- }
- closedir($d);
- return $list;
- }
- // 子目录封面:优先folder.xxx,没有自动取目录第一张图片
- function getFolderCover($folder) {
- global $imageExt;
- foreach ($imageExt as $e) {
- if (file_exists($folder . 'folder.' . $e)) {
- return $folder . 'folder.' . $e;
- }
- }
- $d = opendir($folder);
- while ($f = readdir($d)) {
- $e = strtolower(pathinfo($f, PATHINFO_EXTENSION));
- if (in_array($e, $imageExt)) {
- closedir($d);
- return $folder . $f;
- }
- }
- closedir($d);
- return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYTRmZiI+PHBhdGggZD0iTTEwLDRMMCwxNEg0VjIwSDJWSDIwVjE0SDI0TDE0LDRIMFoiIC8+PC9zdmc+';
- }
- function getMovieCover($path, $filename) {
- global $imageExt;
- $b = pathinfo($filename, PATHINFO_FILENAME);
- foreach ($imageExt as $e) {
- if (file_exists($path . $b . '-poster.' . $e)) {
- return $path . $b . '-poster.' . $e;
- }
- }
- return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTgsMXYxMmgxMkwxNiwxN2w0LTRsLTQtNEg4VjF6IiAvPjwvc3ZnPg==';
- }
- $path = isset($_GET['p']) ? $_GET['p'] : './';
- $all = getList($path);
- $videos = [];
- foreach ($all as $i) {
- if (!$i['isDir']) $videos[] = $i;
- }
- ?>
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>本地影院</title>
- <style>
- *{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei}
- body{background:#121212;color:#fff}
- /* 移除禁止右键和选择的样式 */
- .header{position:fixed;top:0;left:0;right:0;height:60px;background:#222;display:flex;align-items:center;padding:0 20px;z-index:99}
- .back{border:none;background:none;color:#fff;font-size:18px;cursor:pointer}
- .container{padding:20px;margin-top:60px}
- .title{font-size:22px;margin-bottom:15px;padding-left:10px;border-left:4px solid #09f}
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}
- .item{border-radius:12px;overflow:hidden;background:#222;cursor:pointer}
- .item:hover{transform:scale(1.04);transition:.2s}
- .cover{width:100%;height:280px;object-fit:cover}
- .info{padding:10px}
- .name{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
- /* 播放层:网页全屏 */
- #playerLayer{
- display:none;
- position:fixed;
- inset:0;
- background:#000;
- z-index:9999;
- }
- #video{
- width:100%;
- height:100%;
- object-fit:contain;
- background:#000;
- }
- /* 播放控制按钮容器 - 跟随控制条显隐 */
- .player-controls {
- position: absolute;
- bottom: 30px; /* 与原生控制条对齐 */
- left: 50%;
- transform: translateX(-50%);
- display: flex;
- gap: 15px;
- padding: 8px 20px;
- background: rgba(0,0,0,0.7);
- border-radius: 8px;
- opacity: 0;
- transition: opacity 0.3s ease;
- z-index: 10000;
- }
- /* 视频容器 hover/有操作时显示控制按钮 */
- #playerLayer:hover .player-controls,
- #playerLayer:focus-within .player-controls,
- #video:hover ~ .player-controls {
- opacity: 1;
- }
- /* 控制按钮样式 */
- .player-btn {
- border: none;
- background: #09f;
- color: #fff;
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- }
- .player-btn:disabled {
- background: #666;
- cursor: not-allowed;
- }
- /* 音轨切换按钮样式 */
- .audio-track-btn {
- border: none;
- background: #09f;
- color: #fff;
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- }
- </style>
- </head>
- <!-- 移除body的oncontextmenu事件 -->
- <body>
- <div class="header">
- <?php if($path!='./'):?>
- <button class="back" onclick="history.back()">← 返回</button>
- <?php endif?>
- <h1>本地影院</h1>
- </div>
- <div class="container" id="listPage">
- <div class="title"><?=$path=='./' ? '全部影片' : htmlspecialchars($path)?></div>
- <div class="grid">
- <?php foreach($all as $i):?>
- <?php
- $name=$i['name'];
- $isDir=$i['isDir'];
- $src=$i['src'];
- if($isDir){
- $cover=getFolderCover($src.'/');
- $click="location.href='?p=".urlencode($src.'/')."'";
- }else{
- $cover=getMovieCover($path,$name);
- $click="playVideo('".htmlspecialchars($src)."')";
- }
- ?>
- <div class="item" onclick="<?=$click?>">
- <!-- 移除图片的oncontextmenu事件 -->
- <img class="cover" src="<?=$cover?>">
- <div class="info"><div class="name"><?=htmlspecialchars($name)?></div></div>
- </div>
- <?php endforeach?>
- </div>
- </div>
- <div id="playerLayer">
- <!-- 移除视频的oncontextmenu和draggable属性 -->
- <video id="video" controls autoplay></video>
- <!-- 播放控制按钮容器 -->
- <div class="player-controls">
- <button class="player-btn" id="prevBtn" onclick="prev()">上一集</button>
- <button class="player-btn" id="closeBtn" onclick="closePlayer()">关闭播放</button>
- <button class="player-btn" id="nextBtn" onclick="next()">下一集</button>
- <button class="audio-track-btn" id="audioTrackBtn" onclick="switchAudioTrack()">切换音轨</button>
- </div>
- </div>
- <script>
- let videoList = <?=json_encode($videos)?>;
- let index = 0;
- let video = document.getElementById('video');
- let playerLayer = document.getElementById('playerLayer');
- let prevBtn = document.getElementById('prevBtn');
- let nextBtn = document.getElementById('nextBtn');
- let audioTrackBtn = document.getElementById('audioTrackBtn');
- // 网页全屏封装
- function enterWebFull() {
- if (playerLayer.requestFullscreen) {
- playerLayer.requestFullscreen();
- } else if (playerLayer.webkitRequestFullscreen) {
- playerLayer.webkitRequestFullscreen();
- }
- }
- function exitWebFull() {
- if (document.exitFullscreen) {
- document.exitFullscreen();
- } else if (document.webkitExitFullscreen) {
- document.webkitExitFullscreen();
- }
- }
- // 播放:网页全屏,不系统全屏
- function playVideo(url) {
- index = videoList.findIndex(v => v.src === url);
- document.getElementById('listPage').style.display = 'none';
- playerLayer.style.display = 'block';
- video.src = url;
-
- // 清空之前的事件监听(避免重复绑定)
- video.onloadedmetadata = null;
- video.onloadeddata = null;
-
- // 监听视频元数据加载完成(第一阶段)
- video.onloadedmetadata = function() {
- updateBtnStatus();
- // 立即检测一次音轨
- setTimeout(updateAudioTrackBtn, 500);
- };
-
- // 监听视频数据加载完成(第二阶段,更稳定)
- video.onloadeddata = function() {
- updateAudioTrackBtn();
- };
- // 监听音轨加载事件(兼容不同浏览器)
- if (video.audioTracks) {
- video.audioTracks.addEventListener('addtrack', updateAudioTrackBtn);
- video.audioTracks.addEventListener('change', updateAudioTrackBtn);
- }
- // 增加加载失败兜底
- video.onerror = function() {
- alert('视频加载失败,请检查文件路径或格式!');
- closePlayer();
- };
-
- video.play().catch(err => {
- console.error('播放失败:', err);
- alert('视频播放失败,请检查浏览器权限或文件格式!');
- });
- }
- // 更新上/下一集按钮禁用状态
- function updateBtnStatus() {
- prevBtn.disabled = index <= 0;
- nextBtn.disabled = index >= videoList.length - 1;
- }
- // 检查音轨并更新音轨按钮(优化版)
- function updateAudioTrackBtn() {
- // 兼容不同浏览器的音轨 API
- const audioTracks = video.audioTracks || video.mozAudioTracks || video.webkitAudioTracks || [];
-
- // 打印音轨详情到控制台,便于调试
- console.log('【音轨检测】当前音轨列表:', audioTracks);
- if (audioTracks.length) {
- audioTracks.forEach((track, idx) => {
- console.log(`【音轨${idx+1}】`, {
- id: track.id,
- label: track.label || '无标签',
- language: track.language || '无语言',
- enabled: track.enabled
- });
- });
- }
- // 更新按钮状态
- if (audioTracks.length <= 1) {
- audioTrackBtn.textContent = '音轨(仅1条/无)';
- audioTrackBtn.disabled = true;
- } else {
- // 找到当前激活的音轨
- const activeTrack = Array.from(audioTracks).find(t => t.enabled);
- const activeIndex = activeTrack ? Array.from(audioTracks).indexOf(activeTrack) + 1 : 1;
- audioTrackBtn.textContent = `当前音轨: ${activeIndex} (共${audioTracks.length}条)`;
- audioTrackBtn.disabled = false;
- }
- }
- // 切换音轨核心逻辑(优化版)
- function switchAudioTrack() {
- // 兼容不同浏览器的音轨 API
- const audioTracks = video.audioTracks || video.mozAudioTracks || video.webkitAudioTracks || [];
-
- if (audioTracks.length <= 1) {
- alert('当前视频仅包含1条音轨,无法切换!');
- return;
- }
-
- // 转换为数组便于操作
- const trackList = Array.from(audioTracks);
- // 找到当前激活的音轨
- const currentActiveIndex = trackList.findIndex(t => t.enabled);
-
- // 关闭所有音轨,然后激活下一条
- trackList.forEach((track, idx) => {
- track.enabled = false;
- });
-
- // 切换到下一条音轨(循环)
- const nextIndex = (currentActiveIndex + 1) % trackList.length;
- trackList[nextIndex].enabled = true;
-
- // 立即更新按钮文本
- audioTrackBtn.textContent = `当前音轨: ${nextIndex + 1} (共${trackList.length}条)`;
-
- // 提示切换结果
- console.log(`已切换到音轨${nextIndex + 1}`, trackList[nextIndex]);
- }
- // 关闭返回
- function closePlayer(){
- video.pause();
- playerLayer.style.display = 'none';
- document.getElementById('listPage').style.display = 'block';
- exitWebFull();
- }
- // 上一集
- function prev(){
- if(index>0){
- index--;
- video.src = videoList[index].src;
- video.play();
- updateBtnStatus(); // 更新按钮状态
- // 切换后重新检测音轨
- setTimeout(updateAudioTrackBtn, 500);
- }
- }
- // 下一集
- function next(){
- if(index < videoList.length-1){
- index++;
- video.src = videoList[index].src;
- video.play();
- updateBtnStatus(); // 更新按钮状态
- // 切换后重新检测音轨
- setTimeout(updateAudioTrackBtn, 500);
- }
- }
- // 手机左右滑动切换
- let touchStartX = 0;
- document.addEventListener('touchstart', e=>touchStartX=e.changedTouches[0].screenX);
- document.addEventListener('touchend', e=>{
- let delta = e.changedTouches[0].screenX - touchStartX;
- if(delta > 60) prev();
- if(delta < -60) next();
- });
- // ESC 退出
- document.addEventListener('keydown',e=>{
- if(e.key==='Escape') closePlayer();
- });
- // 监听视频控制条显隐(模拟):无操作3秒后隐藏按钮
- let hideTimer;
- playerLayer.addEventListener('mousemove', () => {
- clearTimeout(hideTimer);
- document.querySelector('.player-controls').style.opacity = 1;
- // 3秒无操作则隐藏
- hideTimer = setTimeout(() => {
- document.querySelector('.player-controls').style.opacity = 0;
- }, 3000);
- });
- </script>
- </body>
- </html>
复制代码
|
|