SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

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 (web graphics library, la interfaz del navegador con la GPU). La geometría de la tienda y las ilustraciones de las cajas las compone 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 servirlo consiste en entregar archivos estáticos mediante HTTP. El portátil, la tableta o el televisor donde se ejecuta el navegador es lo que determina si la tienda se mueve con fluidez o funciona con lentitud.

Una función rompe esta regla. Remote Play inicia instancias de Chromium sin interfaz en el servidor y transmite la tienda renderizada a un teléfono o a un set top box mediante WebRTC (web real time communication). En ese caso, el renderizado se realiza en el servidor. De forma predeterminada, hay un máximo de dos instancias, que se puede ajustar con REMOTE_PLAY_MAX_INSTANCES. Sin un dispositivo /dev/dri asignado, esas instancias renderizan mediante la CPU, por lo que un VPS de dos núcleos nota cada espectador adicional.

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 tienda de vídeos antes de instalar nada

El proyecto publica la tienda completa con una biblioteca sintética en la demostración alojada. Si añade ?demo=1 a cualquier URL de Halcyon, obtiene el mismo resultado en su propia implementación.

Úsela como prueba de hardware, con una salvedad: la demostración se adapta al dispositivo. Un navegador con suficiente margen de GPU obtiene la tienda completa, con unos 2,000 títulos. Una GPU de rendimiento medio obtiene unos 800. Un teléfono, o cualquier navegador que informe de 4 GB de memoria del dispositivo o menos, obtiene unos 300. Una demostración que funciona con fluidez en un dispositivo poco potente ya se ha reducido automáticamente, por lo que dice poco sobre cómo funcionará allí su biblioteca completa. La prueba útil es un dispositivo que obtenga la tienda completa. Si esa prueba presenta tirones, su propia biblioteca de tamaño similar también los presentará. 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 que documenta el proyecto original.

docker run -d --name halcyon --network host --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video

A continuación, compruebe que se haya iniciado.

docker logs halcyon
curl -I http://127.0.0.1:1420

El registro debe mostrar que el servidor de vista previa está escuchando en el puerto 1420, y curl debe responder a HTTP/1.1 200 OK. Si un contenedor se detiene a los pocos segundos, casi siempre se debe al puerto. --strictPort indica que el servidor no intenta cambiar al puerto 1421 cuando 1420 está ocupado, por lo que se detiene.

--network host se necesita 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, a la que ningún teléfono de la red puede acceder, por lo que la transmisión nunca se conecta. Si sólo quiere usar la tienda en un navegador, publique el puerto.

docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video

Es 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 esta decisión. --restart unless-stopped vuelve a iniciar la tienda después de un reinicio, con la misma idea que Servicios de Compose que se inician durante el arranque.

Clonar el repositorio y ejecutar docker compose up -d compila la imagen localmente. El archivo de Compose incluido compila desde el código fuente de forma predeterminada y contiene comentada la línea image: de la imagen 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 9 de octubre de 2026, la versión actual de 0.25.0 tiene un digest independiente para cada plataforma, por lo que un VPS arm64 se descarga con el mismo comando que cualquier otro host. El periodo en el que sólo estaba disponible amd64 fue breve: 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 periodo debe compilar la imagen a partir del 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-video

El 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 de un portátil. Esto ayuda cuando el equipo está justo de capacidad. 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, pensado para 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, por lo que probar ambos modos en el mismo dispositivo sólo lleva unos segundos. Sea realista sobre el resultado: el autor describe el modo plano como tosco y todavía en desarrollo. Trátelo como una alternativa para clientes con pocos recursos.

Cuando un cliente es demasiado limitado para la tienda 3D, el fallo es evidente. La pestaña se recarga sola 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 a 2.5D en lugar de reducir su biblioteca.

Fije la imagen y compruebe antes de descargarla

Tómese 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 el envío de la imagen correspondiente a v0.2.0 falló. El ritmo no se ha reducido 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 los parches, por lo que la serie de versiones refleja el estado de trabajo de una sola persona.

Ejecutar latest con el hábito de docker pull significa que el almacén puede cambiar mientras lo usa, cualquier martes normal. Fije la imagen mediante su digest, la única referencia que no puede cambiar.

docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0

Esto 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:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67de

Ese digest era 0.25.0 el 9 de octubre de 2026. Consulte usted mismo el valor actual en lugar de copiarlo y lea las notas de la versión antes de actualizar, porque una versión de parche 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 dibuja con three.js en el navegador, por lo que el equipo cliente realiza el renderizado y el contenedor sólo sirve archivos estáticos en el puerto 1420. La excepción es Remote Play, que ejecuta Chromium sin interfaz gráfica en el servidor y transmite el resultado. Esa ruta realiza el renderizado en la CPU, salvo que asigne /dev/dri al contenedor para habilitar la aceleración por hardware.

¿Puedo poner Halcyon en Internet pública?

Sólo detrás de autenticación. La tienda solicita las credenciales de Jellyfin, pero al activar Remote Play se entrega la sesión de Jellyfin al servidor. Por tanto, cualquiera que cargue /remote.html obtiene una instancia de su biblioteca real sin iniciar sesión. Coloque un reverse proxy con inicio de sesión único delante, o mantenga el nombre de host fuera del DNS público y acceda 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. Abra 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 de la máquina y, detrás del puente de Docker, el contenedor sólo puede ofrecer una dirección 172.x que ningún teléfono de la red pueda alcanzar. Para explorar la tienda en un navegador, -p 1420:1420 funciona y expone mucho menos del host.

¿Qué etiqueta de imagen debo usar?

Fije un digest en lugar de latest. Consulte el digest de una versión que incluya docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0, ejecute ese digest y actualícelo sólo después de leer las notas de la versión. A fecha de 9 de octubre de 2026, la imagen publicada incluye linux/amd64 y linux/arm64, por lo que un host arm64 la descarga como cualquier otra imagen y no necesita compilarla desde el clon.