初始化 360 全景影片播放器專案並完成 NAS 部署設定

摘要:
將本機開發的 360° 全景影片播放器納入版控,同時把執行環境從 Windows
切換到 Synology NAS,改以 Docker 部署。

根本原因:
專案原本只在有 NVIDIA 顯卡的 Windows 機器上跑,config.json 寫死了
Windows 磁碟機路徑 W:/photo/Badminton,NAS 上無法直接啟動。
另外 DSM 內建的 ffmpeg 拿掉了 VAAPI 編碼器,裸跑只能走 libx264,
Celeron J4025 雙核轉 4K 360 影片的速度無法接受。

影響:
- 在 NAS 上 npm start 會因為找不到影片資料夾而列不出任何影片
- 即使把路徑改對,轉檔仍只能用 CPU,82 分鐘的 4K 360 影片要跑十幾小時

修法:
- config.json 的 videoDir 改為 NAS 實際路徑 /volume1/photo/Badminton
- docker-compose.yml 啟用 devices: /dev/dri,讓容器取得 Intel UHD 600
  的 render node;容器內 Alpine 版 ffmpeg 保有完整 VAAPI 支援,啟動時
  會自動偵測成 vaapi 模式,並保留 libx264 當備援
- .env(不進版控)提供 VIDEO_DIR / CACHE_DIR 等 NAS 路徑給 compose 使用

驗證:
容器啟動 log 顯示「轉檔引擎:VAAPI 硬體編碼(Intel/AMD,/dev/dri)」,
/api/config 回傳 modes: ["vaapi","cpu"],/api/videos 正確辨識來源影片為
3840x1920 equirectangular。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 10:57:17 +08:00
co-authored by Claude Opus 5
commit 2a9e59357e
17 changed files with 2966 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>360 Player</title>
<link rel="stylesheet" href="/style.css">
<script type="importmap">{ "imports": { "three": "/vendor/three/three.module.js" } }</script>
</head>
<body>
<div id="app">
<header id="topbar">
<div class="brand">360<span>Player</span></div>
<label class="field">
<span class="field-label">影片</span>
<select id="videoSelect" title="選擇影片"></select>
<button id="refreshBtn" class="icon-btn" title="重新掃描資料夾"></button>
</label>
<label class="field">
<span class="field-label">畫質</span>
<select id="qualitySelect" title="選擇播放畫質"></select>
</label>
<button id="projBtn" class="pill" title="切換 360° / 平面 顯示">360°</button>
<button id="panelBtn" class="pill" title="轉檔與畫質管理">轉檔 <span id="panelBadge" class="badge" hidden></span></button>
<div id="info" class="info"></div>
</header>
<main id="stage" tabindex="0">
<canvas id="gl"></canvas>
<video id="video" playsinline preload="metadata"></video>
<div id="hint" class="hint">拖曳環視 · 滾輪/雙指縮放 · 空白鍵播放 · F 全螢幕</div>
<div id="spinner" class="spinner" hidden></div>
<button id="bigPlay" class="big-play" title="播放"></button>
<div id="toast" class="toast" hidden></div>
<div id="controls" class="controls">
<input id="seek" type="range" min="0" max="1000" value="0" step="1" aria-label="進度">
<div class="row">
<button id="playBtn" class="ctl" title="播放/暫停 (空白鍵)"></button>
<span id="time" class="time">0:00 / 0:00</span>
<span class="spacer"></span>
<button id="muteBtn" class="ctl" title="靜音 (M)">🔊</button>
<input id="vol" class="vol" type="range" min="0" max="1" step="0.02" value="1" aria-label="音量">
<select id="rate" class="ctl-select" title="播放速度">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2×</option>
</select>
<button id="resetBtn" class="ctl" title="重置視角 (0)"></button>
<button id="fsBtn" class="ctl" title="全螢幕 (F)"></button>
</div>
</div>
</main>
<aside id="panel" class="panel" hidden>
<div class="panel-head">
<h2>轉檔 / 畫質</h2>
<button id="panelClose" class="icon-btn" title="關閉"></button>
</div>
<div class="panel-body">
<p class="muted" id="encoderInfo"></p>
<h3 id="panelVideoName"></h3>
<div id="panelVariants" class="variants"></div>
<div class="panel-section-head">
<h3>轉檔佇列</h3>
<button id="clearJobsBtn" class="link-btn">清除已完成</button>
</div>
<div id="panelJobs" class="jobs"></div>
</div>
</aside>
</div>
<script type="module" src="/app.js"></script>
</body>
</html>