Files
JianMiauandClaude Opus 5 0a3c548e0b 移除 nginx 容器,改由 Node 直接終結 TLS,合併成單一容器
摘要:
拿掉 360-player-web(nginx)服務與 docker/nginx/,改在 server.js 用
node:https 直接提供 HTTPS,部署從兩個容器變成一個。

根本原因:
先前為了 TLS 而多開一個 nginx 容器。分開的主因是「憑證續期時能 reload
而不重啟 app」——重啟會殺掉正在跑的 ffmpeg 轉檔,一部 4K 360 影片動輒數小時。
但 Node 的 https.Server 本來就有 setSecureContext(),可以熱換憑證不重啟行程,
這個理由不成立,多一個容器只是多一層維護成本。

影響:
- 需維護額外的 nginx 映像檔與 entrypoint.sh
- 影片經反向代理多一跳,且必須小心處理 proxy_buffering 與 SSE 逾時,
  設錯會讓 Range 串流被寫進暫存檔、或讓轉檔進度停止更新

修法:
- server.js 新增 TLS:讀取 SSL_CERT_DIR 的憑證,以 fs.watch 監看該資料夾,
  檔案變動時 debounce 1 秒後呼叫 setSecureContext() 熱套用
- 中介憑證串進 cert 而非 ca:Node 只送出 cert 的內容,ca 是驗證對方用的
  信任庫、不會送給瀏覽器,放錯會導致憑證鏈不完整
- 串接前正規化 PEM(去 CRLF、補結尾換行),沿用原 nginx entrypoint 的處理
- config.json 新增 httpsPort(預設 0 = 停用)與 certDir,本機開發不需憑證;
  憑證讀不到時退回只提供 HTTP 並印警告,不讓服務起不來
- docker-compose.yml 併回單一服務,憑證改掛 /certs;Dockerfile 補上
  HTTPS_PORT、SSL_CERT_DIR,EXPOSE 改為 8443
- 刪除 docker/nginx/

驗證(實機執行,非僅靜態檢查):
- 以 SSL_CERT_DIR=/volume1/docker/certs 啟動,log 顯示
  「HTTPS:jianmiau.tk — 14 天後到期」,https 的 /api/config 回 200
- openssl s_client 確認送出完整三層憑證鏈
  (jianmiau.tk → Let's Encrypt YR2 → ISRG Root YR)
- HTTPS 上的 Range 請求正常:檔頭與中段各取一段皆回 206 且長度正確
- 熱換測試:換上 CN=hotswap-test.local 的自簽憑證後,log 出現
  「憑證已重新載入」,s_client 讀到新 CN,且行程 PID 與啟動時間不變

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:09:56 +08:00

164 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 360 Player
在瀏覽器裡播放 360° 全景影片的本機網頁播放器。
從下拉選單挑選影片資料夾(`config.json``videoDir`NAS 上是 `/volume1/photo/Badminton`)裡的影片、選擇播放畫質;低畫質版本由 ffmpeg(NVIDIA NVENC)轉出並快取在本機。
## 啟動
```bash
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,沒有就退回 CPUlibx264)。
## 操作
| 動作 | 滑鼠 / 觸控 | 鍵盤 |
|---|---|---|
| 環視 | 拖曳 | ← → ↑ ↓ |
| 縮放(視角) | 滾輪 / 雙指 | `+` `-` |
| 播放 / 暫停 | 點一下畫面 | `空白鍵` / `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`;來源檔案變動(大小或修改時間改變)時會自動視為過期並重轉。
## 部署到 NASDocker
單一容器 `360-player`:以 `node:22-alpine` 為基礎,內含 ffmpegTLS 由 Node 自己終結。
資料夾都用 volume 掛進容器:
| 容器內路徑 | 用途 | 對應環境變數 |
|---|---|---|
| `/videos` | 影片資料夾(唯讀) | `VIDEO_DIR` |
| `/cache` | 轉檔輸出與 probe 快取(需可寫) | `CACHE_DIR` |
| `/certs` | SSL 憑證(唯讀) | `SSL_CERT_DIR` |
```bash
# 1. 把整個專案資料夾複製到 NAS,例如 /volume1/docker/360_player
# 2. 建立 .env,填 NAS 上的實際路徑
cp .env.example .env && vi .env
# 3. 建置並啟動
docker compose up -d --build
# 4. 瀏覽器開 https://你的網域:8443
```
Synology Container Manager:「專案」→「新增」→ 選這個資料夾,它會讀取 `docker-compose.yml`
環境變數可在「環境」分頁填(等同 `.env`)。
### SSL
TLS 由 Node 直接處理(`server.js`),沒有額外的反向代理容器。
憑證放在 `SSL_CERT_DIR`NAS 上預設 `/volume1/docker/certs`,和其他專案共用同一份),
唯讀掛進容器的 `/certs`
```
cert.pem 伺服器憑證
chain.pem 中介憑證
privkey.pem 私鑰
```
檔名可用 `SSL_CERT_FILE_NAME` / `SSL_CHAIN_FILE_NAME` / `SSL_KEY_FILE_NAME`
DSM 匯出的是 `RSA-cert.pem` 這種名字)。啟動 log 會印出憑證的網域與剩餘天數。
**續期不用重啟。** `server.js``fs.watch` 盯著憑證資料夾,檔案一被覆蓋就呼叫
`server.setSecureContext()` 熱套用新憑證 —— 這點很重要,因為重啟會殺掉正在跑的
ffmpeg 轉檔,而一部 4K 360 影片動輒轉好幾小時。
兩個實作細節:
- 中介憑證要放進 `cert` 而不是 `ca`。Node 只會把 `cert` 裡的內容送給瀏覽器,
`ca` 是用來驗證對方憑證的信任庫、不會送出;放錯位置會變成不完整的憑證鏈,
部分客戶端(尤其 Android)會驗證失敗。程式裡是把 cert 和 chain 串成 fullchain。
- 串接前會正規化 PEM(去掉 CRLF、補上結尾換行),否則兩個檔黏成一行會解析不出來。
`httpsPort` 設 0`config.json` 的預設)就完全不啟用 HTTPS,本機 `npm start`
開發時不必準備憑證。憑證讀不到時會印警告並退回只提供 HTTP,不會讓服務起不來。
**對外只有 HTTPS 一個入口**:容器內的 8360(HTTP)不對外開,只給 HEALTHCHECK 用。
代價是憑證綁網域,區網也得用網域連(`https://jianmiau.tk:8443`)而不是 IP —— 用 IP 連
會跳憑證警告。真的想要區網免警告的快速通道,在 compose 的 `ports` 補一條並綁死區網介面
`"192.168.0.15:8360:8360"`),不要開成 `0.0.0.0`,否則等於在外網開了一條明文路徑。
**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 J4025UHD 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`
```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、TLS(憑證熱換)
lib/probe.js ffprobe 包裝 + 永續快取(含 360 metadata 判斷)
lib/transcode.js ffmpeg 工作佇列(一次解碼多輸出;CUDA → NVENC → CPU 備援)
lib/spherical.js 把 Spherical Video V1 metadata 注回 MP4
public/ 前端(Three.js 球體貼圖播放器)
cache/ 轉檔輸出與 probe 快取(已 gitignore
```