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,並提供建置後的檔案及幾個小型 middleware 路由。Halcyon 不會進行轉碼,也不會在伺服器上執行遊戲引擎。
因此,GPU 的問題取決於用戶端。小型 VPS 也能順利提供這項服務,因為它只需透過 HTTP 提供靜態檔案。真正決定商店能否流暢運作的,是執行瀏覽器的筆記型電腦、平板電腦或電視。
有一項功能不遵循這項規則。Remote Play 會在伺服器上啟動 headless Chromium 執行個體,並透過 WebRTC(web real time communication)將渲染後的商店串流到手機或機上盒。這條路徑會在伺服器上進行渲染,預設最多啟動 two 個執行個體,並可使用 REMOTE_PLAY_MAX_INSTANCES 調整。若未映射 /dev/dri 裝置,這些執行個體會使用 CPU 渲染,因此 two core VPS 會明顯受到每位額外觀看者的影響。
商店如何讀取媒體庫內容
貨架結構來自 Jellyfin 本身。Halcyon 會根據媒體庫與類型排列區段,並依照 BoxSets 將續集分組。每個外盒背面列出的規格,來自 Jellyfin 已儲存的 MediaStreams 中繼資料。因此,Jellyfin 缺少的內容,貨架上也不會顯示。
這讓商店成為中繼資料的如實映射。若媒體庫是由 Docker Compose 中的 arr stack 提供,且已填妥封面與類型,這裡的呈現會比一堆使用一般名稱的散落檔案美觀許多。相片媒體庫同樣取決於建立索引的工具;如果要為存放在同一台伺服器上的相片選擇 PhotoPrism 或 Immich,這點值得納入考量。
安裝任何項目之前,先試用影片商店示範
此專案提供完整的商店,並使用合成影片庫執行於託管示範。在任何 Halcyon URL 後附加 ?demo=1,即可在自有部署環境中取得相同效果。
您可以將其用作硬體測試,但有一項限制:示範會依裝置效能自行調整規模。GPU 有足夠餘裕的瀏覽器可載入約 2,000 部影片的完整商店。中階 GPU 約可載入 800 部。手機,或回報裝置記憶體為 4 GB 以下的任何瀏覽器,約可載入 300 部。能在低效能裝置上順暢執行的示範,已經自行縮減規模,因此無法充分反映完整影片庫在該裝置上的執行效果。有效的測試是使用能載入完整商店的裝置。如果畫面出現卡頓,您自己的同等規模影片庫也會卡頓。此時應改用下文所述的 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 服務 相同。
複製儲存庫並執行 docker compose up -d,則會改為在本機建立映像。提交的 Compose 檔案預設會從原始碼建置,並將預建的 image: 行註解掉;如果想在 Compose 中使用已發布的映像,請取消該行的註解。
已發布的映像同時支援 linux/amd64 和 linux/arm64。截至 2026 年 10 月 9 日,目前的 0.25.0 版本會為各平台列出不同的摘要,因此 arm64 VPS 可使用與其他主機相同的指令拉取映像。僅支援 amd64 的期間很短:變更記錄在 2026 年 8 月 8 日的 v0.2.1 中移除 arm64 映像,並在 2026 年 8 月 11 日的 v0.4.0 中使用原生 runner 建置後恢復該映像。只有將標籤固定在該期間版本的主機,才需要從複製的儲存庫進行建置。
指定 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(傳輸層安全性),也沒有自己的存取控制,因此任何公開服務都應將它置於 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 位址,以及執行它的機器名稱。在容器內,執行它的機器就是該容器,因此其主機名稱不是你的主機名稱。以 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 閘道;或者移除公開主機名稱,透過 使用 wg-easy 管理的 WireGuard 通道 存取商店。
這會牽涉到另外兩項細節。反向代理只承載商店:Remote Play 串流使用 WebRTC over UDP,不會經過 HTTP 代理,因此在 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 的映像推送失敗。此後發布速度並未放緩:v0.25.0 於 7 October 2026 發布,比 v0.23.0 晚 3 天。上游歡迎錯誤回報,但不接受修補程式,因此發布串流反映的是單一人員當下的工作狀態。
若執行 latest 時習慣使用 docker pull,映像儲存庫的內容可能在任何一個普通的星期二發生變更。請使用 digest 固定版本;這是唯一不會變動的參照方式。
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0此命令會輸出標籤所對應的 digest。請以它取代標籤。
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67de該 digest 在 9 October 2026 為 0.25.0。請自行讀取目前的值,不要直接複製這個值;移動到新版本前也請先閱讀 release notes,因為此處的修補版本除了修正問題,也可能變更儲存庫配置。
FAQ
Halcyon 需要 VPS 配備 GPU 嗎?
一般使用不需要。商店由瀏覽器中的 three.js 繪製,因此由用戶端電腦負責渲染,容器只會在 1420 埠提供靜態檔案。例外是 Remote Play。Remote Play 會在伺服器上執行無頭 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 即可,且暴露的主機資訊少得多。
我應該使用哪個映像標籤?
請固定使用摘要,而不要使用 latest。讀取包含 docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0 的版本摘要,使用該摘要執行,並在閱讀版本資訊後再升級。截至 9 October 2026,已發布的映像包含 linux/amd64 與 linux/arm64,因此 arm64 主機會像其他主機一樣拉取該映像,不需要從複製的原始碼建置。