Halcyon:把 Jellyfin 變成 90 年代錄影帶店
Halcyon 在瀏覽器中把 Jellyfin 媒體庫重建為可漫遊的 1990 年代出租店,本文提供 Docker 指令、反向代理設定與版本固定等注意事項。
Halcyon 對 Jellyfin 媒體庫的作用
Halcyon Video 會在瀏覽器中將 Jellyfin 媒體庫重新呈現為可漫遊的 1990 年代錄影帶出租店。您擁有的每部影片都會成為架上其中一個影帶盒。您可以在日光燈下沿著走道移動,取下影帶盒,翻面查看背面的規格,再將它帶到櫃檯開始播放。播放開始、進度與停止狀態都會回報給 Jellyfin,因此續播位置與觀看記錄會保持正確。
Halcyon 透過 Jellyfin API 讀取現有的 Jellyfin 伺服器,不會建立自己的媒體庫。本指南假設 Jellyfin 已在執行,且媒體掃描正常。如果尚未完成,請先依照 在 VPS 上設定 Jellyfin 媒體伺服器,等到媒體庫在一般 Web 用戶端中顯示正確後再回來。安裝這項服務的前提是媒體庫已經存在,而不是因為您需要在 自架服務清單 中再增加一項服務。
此專案採用 GPL-3.0 授權,由一人撰寫;README 明確表示不接受 pull request。開發速度快,且沒有第二位維護者可以攔截回歸問題,因此在向其他人展示這家商店前,請先固定映像檔版本。最後一節會說明操作方式。
渲染在哪裡進行?
在瀏覽器中。Halcyon 是以 three.js 為基礎建置的 Vite 與 TypeScript 應用程式。three.js 是透過 WebGL(web graphics library,瀏覽器與 GPU 的介面)繪製 3D 圖形的 JavaScript 函式庫。商店幾何圖形與盒裝美術圖由顯示畫面的裝置合成。
容器幾乎不需要執行任何工作。它會執行 npm run serve,也就是 vite preview --port 1420 --strictPort --host,並提供建置後的檔案與少量中介軟體路由。Halcyon 不會在伺服器上進行轉碼,也不會執行遊戲引擎。
因此,GPU 問題取決於用戶端。小型 VPS 也能順利提供這項服務,因為提供服務代表透過 HTTP 傳送靜態檔案。真正決定商店執行是否流暢的,是執行瀏覽器的筆電、平板或電視。
有一項功能不符合上述規則。Remote Play 會在伺服器上啟動無頭 Chromium 執行個體,並透過 WebRTC(web real time communication)將渲染後的商店串流至手機或機上盒。這條路徑會在伺服器上進行渲染,預設上限為 2 個執行個體,也可使用 REMOTE_PLAY_MAX_INSTANCES 調整。若未映射 /dev/dri 裝置,這些執行個體會使用 CPU 進行渲染,因此 2 核心 VPS 會明顯受到每位額外檢視者的影響。
商店會從媒體庫讀取的內容
走道結構來自 Jellyfin 本身。Halcyon 會依照媒體庫與類型配置區段,並將 BoxSets 中的續集分組。每個盒裝背面列印的規格來自 Jellyfin 已保存的 MediaStreams 中繼資料,因此 Jellyfin 中缺少的資訊,貨架上也不會有。
因此,商店會如實反映你的中繼資料。若媒體庫是由 Docker Compose 中的 arr stack 提供,且已填入封面圖與類型,在這裡的呈現會比一堆使用通用名稱的散落檔案好得多。相片媒體庫同樣取決於建立索引的工具;如果你要比較同一台伺服器上的靜態相片應使用 PhotoPrism 還是 Immich,這點值得記住。
安裝前先試用影片商店示範
此專案提供一個完整運作的商店,使用合成媒體庫,您可以在託管示範環境中試用。在您自己的部署環境中,只要在任何 Halcyon URL 後附加 ?demo=1,即可取得相同效果。
將它用作硬體測試。示範媒體庫包含約 2,000 部影片,瀏覽器約需 2 GB 記憶體,負載高於多數個人媒體庫。如果示範環境在您預計用來瀏覽的裝置上出現卡頓,您自己的媒體庫也會卡頓。此時應改用下文所述的 2.5D 模式,而不是升級至更大的 VPS。
使用 Docker 執行
這是上游文件提供的指令。
docker run -d --name halcyon --network host --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video接著確認服務是否已啟動。
docker logs halcyon
curl -I http://127.0.0.1:1420日誌應顯示預覽伺服器正在監聽連接埠 1420,且 curl 應回應 HTTP/1.1 200 OK。容器在幾秒內退出時,幾乎總是連接埠造成的。--strictPort 表示伺服器不會在 1420 被占用時改用 1421,因此會直接停止。
--network host 是供 Remote Play 使用,不是供商店使用。WebRTC 必須向要求串流的裝置公布機器的實際位址。使用預設 Docker bridge 時,容器只知道自己的 172.x 位址;網路上的手機無法連線到這個位址,因此串流永遠無法建立連線。如果只想在瀏覽器中使用商店,請改為發布連接埠。
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video這是在 VPS 上較好的預設設定,因為 host networking 會讓容器使用機器上的所有介面,包括公開介面。在 VPS 上執行 Docker 說明其餘取捨。--restart unless-stopped 可在重新開機後重新啟動商店,原理與 開機時啟動 Compose 服務 相同。
複製 repository 並執行 docker compose up -d 會改為在本機建置映像檔。提交的 Compose 檔案預設會從原始碼建置,並將預先建置的 image: 行註解掉;如果要在 Compose 中使用已發布的映像檔,請取消該行的註解。
截至 2026 年 8 月,有一項限制:已發布的映像檔僅支援 linux/amd64。多架構推送中的 arm64 部分在模擬環境下建置失敗,目前正等待原生 arm runner。在 arm64 VPS 上,拉取映像檔時會因 no matching manifest for linux/arm64/v8 in the manifest list entries 而失敗;此時可透過複製的 repository 直接建置。
指定 Jellyfin 伺服器
開啟 http://<host>:1420,並使用 Jellyfin 伺服器的位址、使用者名稱和密碼登入。儲存庫中的 .env.local.example 檔案僅供本機開發使用。Vite 會將前綴為 VITE_ 的變數公開給用戶端程式碼,因此寫入其中的 Jellyfin 密碼會被編譯到每位訪客都會下載的 JavaScript bundle 中。在其他人可以連線的伺服器上,請透過介面登入。
瀏覽器會直接與 Jellyfin 通訊。Halcyon 的容器不會代理 Jellyfin API,因此在開始除錯前,請先了解以下兩項影響。
首先,瀏覽器必須能連線到 Jellyfin,而不只是提供 Halcyon 的 VPS。將 Jellyfin 綁定至 127.0.0.1:8096 適合本機測試,但其他使用者會看到空白的媒體架。
其次,這是跨來源呼叫,來源是 Halcyon 的位址,目標是 Jellyfin 的位址。Jellyfin 預設會以 Access-Control-Allow-Origin: * 回應 API 請求,因此不需要額外設定即可運作。如果你縮限了該設定,或在 Jellyfin API 前方放置了驗證代理,瀏覽器主控台會顯示 blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource,而媒體庫會載入空白內容。
置於反向代理後方,並在前方加入驗證
vite preview 是預覽伺服器。它不會終止 TLS(transport layer security),也沒有自己的存取控制,因此只要提供公開存取,就應將它置於 nginx 或 Caddy 後方。
server {
listen 443 ssl;
server_name halcyon.example.com;
location / {
proxy_pass http://127.0.0.1:1420;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}在容器前方使用網域名稱時,還需要多設定一項。為防止 DNS rebinding,Halcyon 會接受 localhost、原始 IP 位址,以及執行所在機器的名稱。對容器而言,執行所在的機器就是容器本身,因此其 hostname 並不是你的 hostname。傳入的請求若是 halcyon.example.com,就會遭到拒絕,回應中也會列出遭拒絕的主機名稱。將該名稱加入設定。
docker run -d --name halcyon -p 127.0.0.1:1420:1420 --restart unless-stopped \
-e HALCYON_ALLOWED_HOSTS=halcyon.example.com \
ghcr.io/halcyon-video/halcyon-video值以逗號分隔。開頭的句點(例如 .example.com)會比對子網域,而 all 會停用檢查。只有在外部無法連入的機器上,才使用 all。
當商店透過 https:// 提供服務後,你在登入時輸入的 Jellyfin 位址也必須使用 https://。瀏覽器會封鎖從 HTTPS 頁面發出的純 http:// API 請求,而主控台會顯示 Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource。登入只會失敗,Halcyon 內不會提供任何說明。請讓兩者都透過 TLS 提供服務,或在私有網路內讓兩者都使用純 HTTP。
接著處理驗證。商店會要求 Jellyfin 憑證,因此找到 URL 的陌生訪客會看到登入畫面。但有一項功能會改變這個情況。在 Settings,再進入 Connection 中啟用 Remote Play,會將你的 Jellyfin 工作階段交給伺服器,讓造訪 /remote.html 的訪客取得你實際媒體庫的個別執行個體。這正是該功能的用途,也表示 URL 的保密性成了網際網路與你的影片之間唯一的阻隔。若啟用 Remote Play,請在整個網站前方加入單一登入,使用 Authentik 作為自架 SSO gateway;或移除公開 hostname,改用 使用 wg-easy 管理的 WireGuard tunnel 存取商店。
還有兩項細節。反向代理只承載商店本身:Remote Play 串流是透過 UDP 傳輸的 WebRTC,不會經過 HTTP proxy,因此在 3478/udp,以及使用內建 TURN relay 時的 49200 到 49260/udp,都需要各自的連線路徑。其次,上述純 docker run 不會保存任何 volume,因此 Remote Play seed 不會在 docker rm 後保留。Compose 檔案會在 /data 掛載 halcyon-data volume,並將 REMOTE_PLAY_SEED 設為 /data/remote-play-seed.json,正是為此而設。
商店執行不穩定時的處理方式
Halcyon 會按需渲染。閒置的商店不會合成任何畫格,而且失去視窗焦點後會停止動畫迴圈。因此,分頁保持開啟不會耗盡筆記型電腦的電池電力。這能改善勉強符合需求的裝置,但無法解決裝置完全無法繪製商店的問題。
這類用戶端可以使用 2.5D 模式。此模式只使用純 HTML 和 CSS,不使用 WebGL,適用於 Raspberry Pi 等低規格硬體。您可以從設定或電源選單在 3D 與 2.5D 之間切換,無須重新載入頁面,因此在同一部裝置上測試兩種模式只需幾秒。請對結果抱持合理預期:作者表示扁平模式仍較粗糙,也仍在開發中。請將它視為弱效能用戶端的備援方案。
當用戶端效能不足以執行 3D 商店時,故障情況通常很明顯。分頁會自行重新載入,或瀏覽器會回報 WebGL context 遺失,通常發生在貨架仍在載入內容時。請將該裝置切換至 2.5D,而不要刪減您的媒體庫。
固定映像,並在提取前檢查
請務必重視這一點。標籤 v0.1.0 到 v0.3.1 都是在相隔幾天內發布的,而 v0.2.1 之所以存在,純粹是因為 v0.2.0 的映像推送失敗。上游接受錯誤報告,但不接受修補程式,因此這個發布串流其實是某個人的工作狀態。
如果習慣以 docker pull 執行 latest,映像儲存區可能在任何一個平常的星期二發生變更。請使用摘要值固定版本;這是唯一不會變動的參照方式。
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1這會顯示標籤背後的摘要值。請使用該摘要值取代標籤。
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:747dcc821a3d2fa318b50e76024783c1835609047e84f502e23d021bc1898b20截至 10 August 2026,該摘要值是 0.3.1。請自行讀取目前的摘要值,不要直接複製這個值;在升級前也請先閱讀發布說明,因為這裡的修補版本除了修正問題,也可能變更儲存區配置。
FAQ
Halcyon 在 VPS 上需要 GPU 嗎?
一般使用不需要。商店由瀏覽器中的 three.js 繪製,因此由用戶端電腦負責轉譯,容器只會在 1420 埠提供靜態檔案。例外是 Remote Play,因為它會在伺服器上執行 headless Chromium 並串流轉譯結果。除非將 /dev/dri 映射到容器中以啟用硬體加速,否則這條路徑會使用 CPU 轉譯。
我可以將 Halcyon 放到公開網際網路上嗎?
只能放在驗證機制後方。商店會要求 Jellyfin 憑證,但啟用 Remote Play 會將你的 Jellyfin 工作階段提供給伺服器,因此任何載入 /remote.html 的人,都能在未登入的情況下取得你實際媒體庫的執行個體。在前方設定具備單一登入功能的反向代理,或不要將主機名稱加入公開 DNS,改用 VPN 存取商店。
為什麼我登入後架上的內容是空的?
瀏覽器會直接呼叫 Jellyfin API,因此瀏覽器必須能連線到 Jellyfin,不能只讓 VPS 能連線。開啟瀏覽器主控台。blocked by CORS policy 表示 Jellyfin 未接受來自 Halcyon 位址的請求。出現 Mixed Content 訊息表示頁面使用 HTTPS,但你輸入的 Jellyfin 位址是純 HTTP。
我需要 --network host 嗎?
只有 Remote Play 需要。WebRTC 必須公布機器的實際位址,而在 Docker bridge 後方,容器只能提供手機無法從網路連線的 172.x 位址。在瀏覽器中瀏覽商店時,-p 1420:1420 即可運作,而且暴露的主機資訊少得多。
我應該使用哪個映像檔標籤?
請固定使用 digest,而不要使用 latest。讀取具有 docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1 的版本 digest,執行該 digest,並在閱讀版本資訊後才升級。截至 2026 年 8 月,已發布的映像檔只有 linux/amd64,因此 arm64 主機必須從 clone 使用 docker compose up -d 建置。