摘要:
比照 badminton-scoreboard 的做法,加一個 nginx 容器負責 SSL,
反向代理到原本的 Node 應用;Node 端維持純 HTTP,不碰憑證。
根本原因:
播放器原本只提供 HTTP。專案內原先想直接 symlink www/certificate 進來,
但那份憑證是 2026-04-16 從 DSM 複製出來的靜態副本,已於 2026-07-01 過期,
且 DSM 續期後不會自動更新副本。共用目錄 /volume1/docker/certs 才是
其他專案在用、且持續被續期的來源(目前效期至 2026-09-04)。
影響:
- 外網連線全程明文
- 若沿用 www/certificate,瀏覽器會直接因憑證過期而擋下連線
修法:
- 新增 docker/nginx/(Dockerfile + entrypoint.sh):啟動時把 cert.pem 與
chain.pem 正規化並串成 fullchain,依環境變數產生 nginx 設定,
再用 inotifywait 監看憑證目錄,續期後自動 reload,不需重啟容器
- docker-compose.yml 拆成 360-player(app)與 360-player-web(nginx)兩個服務,
憑證以唯讀掛載 ${SSL_CERT_DIR:-/volume1/docker/certs}
- 保留 app 的 HTTP 埠:憑證綁網域,區網用 IP 連 HTTPS 必定跳警告,
維持 區網走 http / 外網走 https 兩條路
- .env.example 補上 SSL_* 與 HTTPS_PORT 設定項
針對本專案調整(與 badminton-scoreboard 不同之處):
- proxy_buffering off + proxy_max_temp_file_size 0:影片走 HTTP Range 串流,
開著緩衝 nginx 會把整段回應先寫成暫存檔,數 GB 來源會塞爆容器磁碟
- proxy_read_timeout/send_timeout 24h:/api/events 是 SSE,轉檔動輒數小時,
預設 60 秒會被切斷導致進度停止更新
- 移除 websocket 的 Upgrade 標頭:本專案用 SSE,沒有 websocket
驗證:
以 entrypoint 相同的 awk 邏輯在本機組出 fullchain,openssl 解析得到完整三層
憑證鏈(jianmiau.tk → Let's Encrypt YR2 → ISRG Root YR),且 cert 與 privkey
的 modulus 相符。docker compose config 展開後的路徑與埠號皆正確。
註:無 docker daemon 權限,未實際 build 與啟動容器。
順帶修正 README:videoDir 範例改為 NAS 路徑、VAAPI 一節更新為已實機驗證。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.7 KiB
360 Player
在瀏覽器裡播放 360° 全景影片的本機網頁播放器。
從下拉選單挑選影片資料夾(config.json 的 videoDir,NAS 上是 /volume1/photo/Badminton)裡的影片、選擇播放畫質;低畫質版本由 ffmpeg(NVIDIA NVENC)轉出並快取在本機。
啟動
npm install # 第一次
npm start # 或 node server.js
啟動後終端機會列出網址,例如 http://localhost:8360;同一區網的其他裝置(手機、平板)可用列出的 http://192.168.x.x:8360 連線。
需求:Node.js 18+、ffmpeg / ffprobe 在 PATH 裡(轉檔用)。有 NVIDIA 顯卡時自動使用 NVDEC + NVENC,沒有就退回 CPU(libx264)。
操作
| 動作 | 滑鼠 / 觸控 | 鍵盤 |
|---|---|---|
| 環視 | 拖曳 | ← → ↑ ↓ |
| 縮放(視角) | 滾輪 / 雙指 | + - |
| 播放 / 暫停 | 點一下畫面 | 空白鍵 / K |
| 快退 / 快進 10 秒 | — | J / L |
| 逐格 | — | , / . |
| 全螢幕 | 雙擊畫面 | F |
| 靜音 | — | M |
| 重置視角 | ⌖ 按鈕 | 0 |
- 上方「360° / 平面」按鈕可手動切換投影方式(影片沒有 360 metadata 但是 2:1 比例時會自動判斷為 360)。
- 每部影片的播放位置、偏好畫質、音量都會記住。
畫質與轉檔
「畫質」下拉選單列出原始檔與 config.json 裡定義的每一級畫質:
- 已轉檔的畫質會顯示 ✓ 與檔案大小,選取後立即切換(保留目前播放進度)。
- 尚未轉檔的畫質選取後會加入轉檔佇列,完成時畫面會跳出通知與「切換」按鈕。
- 「轉檔」面板可以看進度(fps、速度、剩餘時間)、取消、刪除已轉檔檔案, 以及**「一次轉出全部畫質」**:來源只讀一次就同時輸出全部畫質。
來源放在網路磁碟(例如 RaiDrive)時,讀取速度通常才是瓶頸, 而不是 GPU。這種情況下請用「一次轉出全部畫質」,時間和只轉一種幾乎相同。
轉檔輸出:H.264 + AAC、faststart(moov 在檔頭,可立即開播、順暢拖動),
並重新寫入 Spherical Video V1 metadata,所以用 VLC / YouTube 開也會被認成 360 影片。
轉出的檔案放在 cache/,檔名 <原檔名>__<畫質id>.mp4;來源檔案變動(大小或修改時間改變)時會自動視為過期並重轉。
部署到 NAS(Docker)
兩個容器:360-player 是應用本體(node:22-alpine,內含 ffmpeg),
360-player-web 是 nginx,負責終結 TLS 再轉給前者。資料夾都用 volume 掛進容器:
| 容器 | 容器內路徑 | 用途 | 對應環境變數 |
|---|---|---|---|
360-player |
/videos |
影片資料夾(唯讀) | VIDEO_DIR |
360-player |
/cache |
轉檔輸出與 probe 快取(需可寫) | CACHE_DIR |
360-player-web |
/etc/nginx/certs |
SSL 憑證(唯讀) | SSL_CERT_DIR |
# 1. 把整個專案資料夾複製到 NAS,例如 /volume1/docker/360_player
# 2. 建立 .env,填 NAS 上的實際路徑
cp .env.example .env && vi .env
# 3. 建置並啟動
docker compose up -d --build
# 4. 瀏覽器開 https://你的網域:8443(或區網 http://NAS-IP:8360)
Synology Container Manager:「專案」→「新增」→ 選這個資料夾,它會讀取 docker-compose.yml,
環境變數可在「環境」分頁填(等同 .env)。
SSL
TLS 由 360-player-web(nginx)終結,Node 那端維持純 HTTP,所以應用程式碼裡沒有任何憑證邏輯。
憑證放在 SSL_CERT_DIR(預設 /volume1/docker/certs,和其他專案共用同一份),
唯讀掛進 nginx 容器:
cert.pem 伺服器憑證
chain.pem 中介憑證
privkey.pem 私鑰
檔名可用 SSL_CERT_FILE_NAME / SSL_CHAIN_FILE_NAME / SSL_KEY_FILE_NAME 改。
容器啟動時 entrypoint 會把 cert.pem + chain.pem 接成 fullchain(順便去掉 CRLF),
並用 inotifywait 盯著這個資料夾 —— DSM 續期後直接覆蓋檔案就好,nginx 會自己 reload,不用重開容器。
啟動 log 會印出憑證的 subject 和到期日。
兩點注意:
- 憑證是綁網域的,用區網 IP 連 HTTPS 一定會跳警告。所以 compose 同時保留了
HTTP 的
${PORT}:8360:區網走 http、外網走 https。想只留 HTTPS 就把那段ports刪掉。 - nginx 這邊關掉了
proxy_buffering並把proxy_max_temp_file_size設為 0。 影片是靠 HTTP Range 串流的,開著緩衝會讓 nginx 先把整段回應寫成暫存檔再吐出去, 幾 GB 的來源會直接塞爆容器磁碟。/api/events(SSE)另外把proxy_read_timeout拉到 24 小時,否則轉檔跑到一半進度就不再更新。
NAS 上轉檔速度
- 沒有 GPU 時走
libx264(CPU)。NAS 的 CPU 把 4K 360 影片同時轉成三種畫質大約只有個位數 fps, 82 分鐘的影片可能要跑 5–10 小時(可在背景跑,轉檔面板會顯示剩餘時間)。 CPU 很弱可把.env的X264_PRESET改成superfast或ultrafast。 - NAS 有 Intel 內顯時,
docker-compose.yml裡的devices: /dev/dri要打開, 啟動時會自動偵測並改用 VAAPI 硬體編碼,偵測失敗則自動退回 CPU。 已在 Celeron J4025(UHD Graphics 600)+ DSM 實機驗證:容器啟動 log 顯示轉檔引擎:VAAPI 硬體編碼,/api/config回傳modes: ["vaapi","cpu"]。 注意 DSM 內建的 ffmpeg 拿掉了 VAAPI 編碼器,直接在 NAS 上npm start只會走 CPU, 硬體轉檔只有在容器裡才有。 - 最快的做法:在有 NVIDIA 顯卡的電腦上先跑
npm start轉完,再把cache/裡的*.mp4與*.mp4.json複製到 NAS 的CACHE_DIR。快取檔只認來源檔的大小與修改時間(容許 2 秒誤差), 所以兩邊看到同一個檔案就能直接共用。
設定(config.json)
{
"port": 8360,
"host": "0.0.0.0",
"videoDir": "/volume1/photo/Badminton",
"cacheDir": "./cache",
"extensions": [".mp4", ".mov", ".m4v", ".webm"],
"qualities": [
{ "id": "2560", "label": "高", "width": 2560, "bitrate": "10M", "maxrate": "13M" },
{ "id": "1920", "label": "中", "width": 1920, "bitrate": "5M", "maxrate": "7M" },
{ "id": "1280", "label": "低", "width": 1280, "bitrate": "2.5M", "maxrate": "3.5M" }
]
}
qualities:每一級只要給寬度,高度依原始比例計算;比原始影片寬的級別會自動略過。 可加"nvencPreset": "p6"之類的欄位調整 NVENC 預設(預設p4)。- 環境變數:
PORT=9000覆寫埠號;CONFIG=other.json使用另一份設定;LOG_STREAM=0關閉串流請求 log。
API(給進階使用)
| 方法 | 路徑 | 說明 |
|---|---|---|
| GET | /api/videos |
影片清單(含 ffprobe 資訊、各畫質狀態) |
| GET | /stream/:name?q=original|<畫質id> |
影片串流(支援 Range) |
| POST | /api/transcode |
{ "name", "quality" } / { "name", "qualities": [] } / { "name", "all": true } |
| GET | /api/jobs |
轉檔工作 |
| DELETE | /api/jobs/:id |
取消工作 |
| DELETE | /api/jobs/finished |
清除已完成 |
| DELETE | /api/variant?name=&quality= |
刪除轉檔檔案 |
| GET | /api/events |
Server-Sent Events:jobs(進度)、finished |
專案結構
server.js Express:清單、Range 串流、轉檔 API、SSE
lib/probe.js ffprobe 包裝 + 永續快取(含 360 metadata 判斷)
lib/transcode.js ffmpeg 工作佇列(一次解碼多輸出;CUDA → NVENC → CPU 備援)
lib/spherical.js 把 Spherical Video V1 metadata 注回 MP4
public/ 前端(Three.js 球體貼圖播放器)
docker/nginx/ nginx 映像檔:終結 TLS,反向代理到 app(憑證變更自動 reload)
cache/ 轉檔輸出與 probe 快取(已 gitignore)