Halcyon: convierte Jellyfin en un videoclub de los 90
Convierte Jellyfin en un videoclub de los 90 desde el navegador. Incluye el comando Docker, el proxy inverso y las limitaciones reales que debes conocer.
Qué hace Halcyon con tu biblioteca de Jellyfin
Halcyon Video transforma tu biblioteca de Jellyfin en una tienda de alquiler de vídeos de los años 1990 que puedes recorrer desde el navegador. Cada película que tienes se convierte en una carátula colocada en una estantería. Recorres los pasillos bajo los tubos fluorescentes, sacas una caja, le das la vuelta para leer las especificaciones de la parte trasera y la llevas al mostrador para iniciar la reproducción. El estado de la reproducción, el progreso y la detención se notifican a Jellyfin, por lo que los puntos de reanudación y el historial de reproducciones se mantienen correctamente.
Halcyon lee un servidor Jellyfin existente mediante la API de Jellyfin y no mantiene una biblioteca propia. Esta guía presupone que Jellyfin ya está funcionando y que el escaneo se completa correctamente. Si no es así, configura primero Jellyfin como servidor multimedia en un VPS y vuelve cuando la biblioteca se muestre correctamente en el cliente web normal. Este es el tipo de aplicación que instalas porque la biblioteca ya existe, no porque necesites otro servicio en tu lista de servicios autoalojados.
El proyecto usa la licencia GPL-3.0 y está escrito por una sola persona. El README indica claramente que no acepta pull requests. El desarrollo avanza rápido y no hay un segundo responsable que detecte una regresión, así que fija la versión de la imagen antes de mostrar la tienda a otras personas. La última sección explica cómo hacerlo.
¿Dónde se realiza el renderizado?
En el navegador. Halcyon es una aplicación de Vite y TypeScript basada en three.js, una biblioteca de JavaScript que dibuja gráficos 3D mediante WebGL (biblioteca de gráficos web, la interfaz del navegador con la GPU). La geometría de la tienda y las imágenes de las cajas se combinan en el equipo conectado a la pantalla.
El contenedor hace muy poco. Ejecuta npm run serve, que es vite preview --port 1420 --strictPort --host, y sirve los archivos compilados junto con algunas rutas pequeñas de middleware. Halcyon no añade transcodificación ni ejecuta ningún motor en el servidor.
Por tanto, la cuestión de la GPU corresponde al cliente. Un VPS pequeño puede servir este contenido sin problemas, porque hacerlo consiste en servir archivos estáticos y algunas rutas ligeras mediante HTTP. El portátil, la tableta o el televisor que ejecuta el navegador determina si la tienda funciona con fluidez o avanza con lentitud.
Una función rompe esta regla. Las instancias privadas de Remote Play inician Chromium sin interfaz gráfica en el servidor, una por visitante, y transmiten la tienda renderizada a un teléfono o un decodificador mediante WebRTC (comunicación web en tiempo real). En esta ruta, el renderizado se realiza en el servidor, con un límite predeterminado de dos instancias que se puede ajustar mediante REMOTE_PLAY_MAX_INSTANCES. También necesita una GPU allí. La imagen publicada no incluye un renderizador por software, por lo que, sin un nodo de renderizado /dev/dri asignado, el servidor no puede iniciar una tienda privada y responde This store server has no GPU available, so it can’t render a private store.
Lo que la tienda lee de tu biblioteca
Los pasillos proceden de la estructura propia de Jellyfin. Halcyon organiza las secciones a partir de tus bibliotecas y géneros, y agrupa las secuelas según tus BoxSets. Las especificaciones impresas en la parte posterior de cada caja proceden de los metadatos de MediaStreams que Jellyfin ya conserva. Por tanto, cualquier dato que falte en Jellyfin también faltará en la estantería.
Así, la tienda refleja fielmente tus metadatos. Una biblioteca alimentada por una pila arr en Docker Compose que ya tenga el material gráfico y los géneros completos se verá mucho mejor aquí que una carpeta con archivos sueltos y nombres genéricos. Las bibliotecas de fotos dependen igualmente de la herramienta que las haya indexado. Conviene recordarlo al comparar PhotoPrism con Immich para las imágenes almacenadas en el mismo servidor.
Pruebe la demostración de la videoteca antes de instalar nada
El proyecto ofrece una demostración que funciona sin servidor y sin iniciar sesión. En el código fuente actual, esa compilación alojada llena sus estanterías con títulos en streaming, no con una biblioteca como la suya, así que úsela para conocer la tienda. Si añade ?demo=1 a la URL de su propia implementación, cargará una biblioteca sintética.
Esa biblioteca sintética sirve para probar el hardware, con una salvedad: se adapta al dispositivo. Un navegador con suficiente margen de GPU carga la tienda completa, con unos 2,000 títulos. Una GPU de gama media carga unos 800. Un teléfono, o cualquier navegador que informe de 4 GB de memoria del dispositivo o menos, carga unos 300. Una demostración que funciona con fluidez en un dispositivo limitado ya se ha reducido automáticamente, por lo que dice poco sobre cómo funcionará allí su biblioteca completa. La prueba útil se hace en un dispositivo que cargue la tienda completa. Si la reproducción se entrecorta, su propia biblioteca de tamaño similar también se entrecortará. La solución es el modo 2.5D descrito más abajo, no un VPS más grande.
Ejecutarlo con Docker
Este es el comando documentado por el proyecto.
docker run -d --name halcyon --network host --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoDespués, compruebe que se haya iniciado.
docker logs halcyon
curl -I http://127.0.0.1:1420El registro debería mostrar que el servidor de vista previa está escuchando en el puerto 1420, y curl debería responder a HTTP/1.1 200 OK. Un contenedor que se detiene al cabo de unos segundos casi siempre tiene un problema con el puerto. --strictPort indica que el servidor no puede cambiar al puerto 1421 cuando 1420 ya está ocupado, por lo que se detiene.
--network host se utiliza para Remote Play, no para la tienda. WebRTC debe anunciar la dirección real de la máquina al dispositivo que solicita la transmisión. Detrás del puente predeterminado de Docker, el contenedor sólo conoce su propia dirección 172.x, que ningún teléfono de la red puede alcanzar. Por eso, la transmisión nunca llega a conectarse. Si sólo quiere usar la tienda en un navegador, publique el puerto en su lugar.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoEs la opción predeterminada más adecuada en un VPS, porque la red del host conecta el contenedor a todas las interfaces de la máquina, incluida la interfaz pública. Ejecutar Docker en un VPS explica el resto de este compromiso. --restart unless-stopped hace que la tienda vuelva a iniciarse después de un reinicio, igual que Servicios de Compose que se inician al arrancar.
Clonar el repositorio y ejecutar docker compose up -d crea la imagen localmente. El archivo de Compose incluido crea la imagen desde el código fuente de forma predeterminada y contiene comentada la línea de image: precompilada. Quite el comentario de esa línea si quiere usar la imagen publicada con Compose.
La imagen publicada es compatible con linux/amd64 y linux/arm64. Según la comprobación realizada el 11 de octubre de 2026, la versión actual de 0.25.0 incluye un digest independiente para cada plataforma. Por tanto, un VPS arm64 se descarga con el mismo comando que cualquier otro host. La limitación exclusiva para amd64 duró poco: el registro de cambios eliminó la imagen arm64 en v0.2.1, el 8 de agosto de 2026, y la volvió a incluir, compilada en runners nativos, en v0.4.0, el 11 de agosto de 2026. Sólo un host fijado a una etiqueta de ese intervalo debe compilar la imagen desde el clon.
Indique el servidor Jellyfin
Abra http://<host>:1420 e inicie sesión con la dirección, el nombre de usuario y la contraseña de su servidor Jellyfin. El archivo .env.local.example del repositorio sólo sirve para el desarrollo local. Vite expone al código del cliente las variables que tienen el prefijo VITE_, por lo que una contraseña de Jellyfin escrita allí queda compilada en el paquete JavaScript que descarga cada visitante. En un servidor accesible para otras personas, inicie sesión mediante la interfaz.
El navegador se comunica directamente con Jellyfin. El contenedor de Halcyon no actúa como proxy de la API de Jellyfin. Esto tiene dos consecuencias que debe conocer antes de empezar a depurar.
Primero, Jellyfin debe ser accesible desde el navegador, no sólo desde el VPS que sirve Halcyon. Un Jellyfin enlazado a 127.0.0.1:8096 funciona para una prueba local, pero deja las estanterías vacías para el resto de usuarios.
Segundo, la llamada es de origen cruzado, desde la dirección de Halcyon hasta la de Jellyfin. De forma predeterminada, Jellyfin responde a las solicitudes de la API con Access-Control-Allow-Origin: *, por lo que funciona sin configuración adicional. Si ha restringido ese ajuste o ha colocado un proxy de autenticación delante de la API de Jellyfin, la consola del navegador muestra blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource y la aplicación carga las estanterías vacías.
Colóquelo detrás de un proxy inverso, con autenticación delante
vite preview es un servidor de vista previa. No termina TLS (seguridad de la capa de transporte) y no tiene control de acceso propio, por lo que debe estar detrás de nginx o Caddy en cualquier entorno público.
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;
}
}Un nombre de dominio delante del contenedor requiere un ajuste adicional. Halcyon responde a localhost, a direcciones IP sin formato y a los nombres de la máquina donde se ejecuta como protección contra el DNS rebinding. Dentro de un contenedor, la máquina donde se ejecuta es el propio contenedor, por lo que su nombre de host no es el suyo. Una petición que llega como halcyon.example.com se rechaza y la respuesta indica el host rechazado. Añada ese nombre.
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-videoEl valor se separa mediante comas. Un punto inicial, como .example.com, coincide con los subdominios, y all desactiva la comprobación. Use all sólo en una máquina a la que no pueda acceder nada externo.
Cuando el almacén se sirve mediante https://, la dirección de Jellyfin que introduce al iniciar sesión también debe ser https://. El navegador bloquea una llamada a la API mediante http:// sin cifrar cuando procede de una página HTTPS, y la consola muestra Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. El inicio de sesión simplemente falla, sin ninguna explicación dentro de Halcyon. Sirva ambos mediante TLS o mantenga ambos en HTTP sin cifrar dentro de una red privada.
Después viene la autenticación. El almacén solicita las credenciales de Jellyfin, por lo que cualquier persona que encuentre la URL verá una pantalla de inicio de sesión. Una función cambia este comportamiento. Al activar Remote Play en Settings y después en Connection, se entrega la sesión de Jellyfin al servidor. Así, los visitantes de /remote.html obtienen su propia instancia de su biblioteca real. Ese es el objetivo de la función y significa que el secreto de la URL es lo que separa sus películas de Internet. Si activa Remote Play, coloque un inicio de sesión único delante de todo el sitio mediante Authentik como gateway SSO autoalojado, o elimine el nombre de host público y acceda al almacén mediante un túnel WireGuard administrado con wg-easy.
Hay dos detalles relacionados. El proxy inverso transporta sólo el almacén: el flujo de Remote Play usa WebRTC sobre UDP y no pasa por un proxy HTTP, por lo que necesita su propia ruta en 3478/udp y en 49200 a 49260/udp cuando se utiliza el relay TURN incluido. Además, el docker run sin cifrar indicado arriba no conserva ningún volumen, por lo que la semilla de Remote Play no sobrevive a docker rm. El archivo Compose monta un volumen halcyon-data en /data y establece REMOTE_PLAY_SEED en /data/remote-play-seed.json precisamente por ese motivo.
Qué hacer cuando la tienda funciona mal
Halcyon renderiza bajo demanda. Una tienda inactiva no compone ningún fotograma y, cuando la ventana pierde el foco, se detiene el bucle de animación. Por eso, dejar una pestaña abierta no agota la batería del portátil. Esto ayuda cuando el equipo está al límite de sus capacidades. No sirve para un equipo que no puede renderizar la tienda en absoluto.
Para esos clientes existe un modo 2.5D basado únicamente en HTML y CSS, sin WebGL, diseñado para funcionar incluso en hardware tan limitado como una Raspberry Pi. Puede cambiar entre 3D y 2.5D desde los ajustes o el menú de energía, sin volver a cargar la página. Así puede probar ambos modos en el mismo dispositivo en pocos segundos. Sea realista con el resultado: en agosto de 2026, el autor describió el modo plano como rudimentario y afirmó que el proyecto apenas había comenzado. Considérelo una alternativa para clientes con pocos recursos.
Cuando un cliente no tiene capacidad suficiente para la tienda 3D, el fallo es evidente. La pestaña se vuelve a cargar o el navegador informa de que se ha perdido el contexto de WebGL, normalmente mientras las estanterías todavía se están llenando. Cambie ese dispositivo al modo 2.5D en lugar de reducir su biblioteca.
Fije la imagen y compruébela antes de descargarla
Tome esta parte en serio. Las etiquetas v0.1.0 a v0.3.1 se publicaron con pocos días de diferencia, y v0.2.1 sólo existe porque la subida de la imagen para v0.2.0 falló. El ritmo no se ha ralentizado desde entonces: v0.25.0 se publicó el 7 de octubre de 2026, tres días después de v0.23.0. Los informes de errores son bienvenidos en el proyecto original, pero no se aceptan parches, por lo que la serie de versiones refleja el estado de trabajo de una sola persona.
Ejecutar latest con la costumbre de usar docker pull significa que el almacén puede cambiar sin previo aviso cualquier martes. Fije la imagen por su digest, la única referencia que no puede cambiar.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0Esto muestra el digest asociado a la etiqueta. Úselo en lugar de la etiqueta.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67deEl 11 de octubre de 2026, ese era el digest asociado a la etiqueta 0.25.0. Consulte usted mismo el actual en lugar de copiarlo y lea las notas de la versión antes de actualizar, porque una versión de corrección puede incluir cambios en la estructura del almacén además de correcciones.
FAQ
¿Halcyon necesita una GPU en mi VPS?
No para el uso normal. La tienda se renderiza con three.js en el navegador, por lo que el equipo cliente realiza el renderizado y el contenedor sirve los archivos compilados y algunas rutas ligeras del servidor en el puerto 1420. La excepción son las instancias privadas de Remote Play, que ejecutan Chromium sin interfaz gráfica en el servidor y transmiten el resultado. Con la imagen publicada, esa ruta necesita que /dev/dri esté asignado al contenedor. Sin una GPU, el servidor se niega a iniciar una tienda privada.
¿Puedo poner Halcyon en Internet público?
Sólo detrás de autenticación. La tienda solicita las credenciales de Jellyfin, pero activar Remote Play entrega la sesión de Jellyfin al servidor, por lo que cualquiera que cargue /remote.html obtiene una instancia de tu biblioteca real sin iniciar sesión. Coloca un reverse proxy con inicio de sesión único delante, o mantén el nombre de host fuera del DNS público y accede a la tienda mediante una VPN.
¿Por qué las estanterías están vacías después de iniciar sesión?
El navegador llama directamente a la API de Jellyfin, por lo que Jellyfin debe ser accesible desde el navegador y no sólo desde el VPS. Abre la consola del navegador. blocked by CORS policy significa que Jellyfin no acepta la solicitud desde la dirección de Halcyon. Un mensaje Mixed Content significa que la página usa HTTPS, mientras que la dirección de Jellyfin introducida usa HTTP sin cifrar.
¿Necesito --network host?
Sólo para Remote Play. WebRTC debe anunciar la dirección real del equipo y, detrás del puente de Docker, el contenedor sólo puede ofrecer una dirección 172.x que ningún teléfono de tu red puede alcanzar. Para explorar la tienda en un navegador, -p 1420:1420 funciona y expone mucho menos del host.
¿Qué etiqueta de imagen debo usar?
Fija un digest en lugar de latest. Lee el digest de una versión con docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0, ejecuta ese digest y actualízalo sólo después de leer las notas de la versión. A fecha de 11 October 2026, la imagen publicada incluye tanto linux/amd64 como linux/arm64, por lo que un host arm64 la descarga como cualquier otra y no necesita compilarla desde el clon.