SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-22

Halcyon:把 Jellyfin 变成90年代录像带店

Halcyon 在浏览器中把 Jellyfin 媒体库重建为可步行浏览的 1990 年代录像带店。本文提供 Docker 命令、反向代理配置,并说明单人维护、固定镜像版本及 Remote Play 的实际限制。

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 技术栈提供内容,并且已经补全封面和流派信息的媒体库,在这里的效果会远好于一组名称通用的散乱文件。照片库同样依赖为其建立索引的组件。对于存放在同一台服务器上的照片,比较 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 上更合适的默认配置,因为主机网络会让容器使用计算机上的所有网络接口,包括公网接口。在 VPS 上运行 Docker 介绍了这一取舍的其他内容。--restart unless-stopped 会在重启后重新启动商店,原理与 启动时自动启动的 Compose 服务 相同。

克隆仓库并运行 docker compose up -d 会改为在本地构建镜像。已提交的 Compose 文件默认从源代码构建,并将预构建的 image: 行注释掉。如果希望在 Compose 中使用已发布的镜像,请取消该行的注释。

截至 August 2026,有一个明确限制:已发布的镜像仅支持 linux/amd64。多架构推送中的 arm64 部分在仿真环境下失败,目前正在等待原生 arm runner。在 arm64 VPS 上,拉取镜像会因 no matching manifest for linux/arm64/v8 in the manifest list entries 失败;此时应从克隆的仓库构建镜像。

将其指向您的 Jellyfin 服务器

打开 http://<host>:1420,使用 Jellyfin 服务器地址、用户名和密码登录。仓库中的 .env.local.example 文件仅用于本地开发。Vite 会将以 VITE_ 开头的变量暴露给客户端代码,因此写入其中的 Jellyfin 密码会被编译进每位访问者都会下载的 JavaScript 包。在其他人可以访问的服务器上,请通过界面登录。

浏览器会直接与 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 重绑定,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 流使用基于 UDP 的 WebRTC,不会经过 HTTP 代理。因此,在使用内置 TURN 中继时,需要为其在 3478/udp 以及 49200 到 49260/udp 上配置独立路径。上面的普通 docker run 不保存任何卷,因此 Remote Play 种子不会在 docker rm 后保留。Compose 文件会将 halcyon-data 卷挂载到 /data,并将 REMOTE_PLAY_SEED 设置为 /data/remote-play-seed.json,原因正是如此。

商店运行不佳时的处理方法

Halcyon 按需渲染。空闲商店不会合成任何帧,窗口失去焦点后也会停止动画循环。因此,保持标签页打开不会耗尽笔记本电池。这能帮助性能勉强够用的设备,但无法解决设备完全无法渲染商店的问题。

对于这类客户端,可以使用 2.5D 模式。该模式仅使用 HTML 和 CSS,不使用 WebGL,适用于配置低至 Raspberry Pi 的硬件。您可以在设置或电源菜单中切换 3D 和 2.5D,无需重新加载页面,因此在同一设备上测试两种模式只需几秒。请对实际效果保持合理预期:作者称扁平模式仍较粗糙,且在持续开发中。应将其视为低性能客户端的备用方案。

当客户端性能不足以运行 3D 商店时,故障通常很明显。标签页会自行重新加载,或者浏览器报告 WebGL 上下文丢失;这种情况通常发生在货架仍在加载时。请将该设备切换到 2.5D,而不是删减您的媒体库。

固定镜像,并在拉取前检查

请认真对待这一点。v0.1.0v0.3.1 这些标签在几天内相继发布,而 v0.2.1 存在的唯一原因是 v0.2.0 的镜像推送失败。上游欢迎提交错误报告,但不接受补丁,因此发布流反映的是一个人的工作状态。

如果习惯使用 latest 并执行 docker pull,镜像仓库中的内容可能在任何一个普通的周二发生变化。请使用摘要固定镜像,因为摘要是唯一不会变动的引用方式。

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 除外,它会在服务器上运行无头 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.3.1 运行该摘要;只有阅读发行说明后再升级。截至 August 2026,发布的镜像只有 linux/amd64,因此 arm64 主机必须从克隆的源代码构建,并使用 docker compose up -d