Halcyon: convierte Jellyfin en un videoclub de los 90
Convierte tu biblioteca Jellyfin en un videoclub navegable desde el navegador. Incluye el comando Docker, el proxy inverso y las limitaciones reales del proyecto.
Qué hace Halcyon con tu biblioteca de Jellyfin
Halcyon Video transforma tu biblioteca de Jellyfin en una tienda de vídeo 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 posterior y la llevas al mostrador para iniciar la reproducción. Los eventos de inicio, progreso y detención de la reproducción se envían de vuelta a Jellyfin, por lo que los puntos de reanudación y el historial de reproducción siguen siendo correctos.
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 autohospedados.
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 mantenedor 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 imágenes de las cajas se componen 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 además de 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 que ejecuta el navegador es lo que determina si la tienda funciona con fluidez o se ralentiza.
Una función rompe esta regla. Remote Play inicia instancias de Chromium sin interfaz gráfica en el servidor y transmite la tienda renderizada a un teléfono o a un decodificador mediante WebRTC (web real time communication). En esta ruta, 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 mediante 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.
Esto hace que la tienda sea un reflejo fiel de tus metadatos. Una biblioteca alimentada por un stack de arr en Docker Compose con las ilustraciones y los géneros ya completados se ve mucho mejor aquí que una carpeta con archivos sueltos y nombres genéricos. Las bibliotecas de fotos dependen del mismo modo 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, conectada a una biblioteca sintética, en la demostración alojada. Añadir ?demo=1 a cualquier URL de Halcyon produce el mismo resultado en su propia implementación.
Úsela como prueba de hardware. La biblioteca de demostración contiene unos 2,000 títulos y requiere aproximadamente 2 GB de memoria del navegador, por lo que consume más recursos que la mayoría de las bibliotecas personales. Si la demostración funciona con interrupciones en el dispositivo desde el que piensa navegar, su propia biblioteca también funcionará con interrupciones. 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.
docker run -d --name halcyon --network host --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoA continuación, compruebe que se haya iniciado.
docker logs halcyon
curl -I http://127.0.0.1:1420El 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 cambia al puerto 1421 cuando 1420 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 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-videoEs la mejor opción predeterminada 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 esa decisión. --restart unless-stopped es lo que vuelve a iniciar la tienda después de un reinicio, igual que Servicios de Compose que se inician durante el arranque.
Clonar el repositorio y ejecutar docker compose up -d crea la imagen localmente. El archivo Compose incluido crea la imagen 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.
Hay un límite importante a fecha de agosto de 2026: la imagen publicada sólo está disponible para linux/amd64. La variante arm64 de la publicación multiarquitectura falló durante la emulación y está a la espera de runners arm nativos. En un VPS arm64, la extracción falla con no matching manifest for linux/arm64/v8 in the manifest list entries. En ese caso, la solución es compilar desde el clon.
Diríjalo a su 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 cuyo prefijo es VITE_, por lo que una contraseña de Jellyfin escrita allí queda compilada en el paquete JavaScript que descarga cada visitante. En un servidor al que puedan acceder 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 conviene conocer antes de empezar a depurar.
En primer lugar, 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.
En segundo lugar, la llamada es cross-origin: se realiza desde la dirección de Halcyon a 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 tienda se carga con las estanterías vacías.
Ponlo 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 cuando se expone públicamente.
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 resolver 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 hostname no es el tuyo. Se rechaza una petición que llegue como halcyon.example.com, y la respuesta indica el host que se rechazó. Añade 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 usa comas como separadores. Un punto inicial, como .example.com, coincide con los subdominios, y all desactiva la comprobación. Usa all sólo en una máquina a la que no pueda acceder nada externo.
Cuando el store se sirve mediante https://, la dirección de Jellyfin que introduces al iniciar sesión también debe ser https://. El navegador bloquea una llamada de la API mediante http:// realizada desde 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 falla sin más, sin ninguna explicación dentro de Halcyon. Sirve ambos mediante TLS, o mantén ambos en HTTP sin cifrar dentro de una red privada.
Después, configura la autenticación. El store solicita las credenciales de Jellyfin, por lo que un desconocido que encuentre la URL verá una pantalla de inicio de sesión. Una función cambia esta situación. Al activar Remote Play, en Settings y después en Connection, entregas tu sesión de Jellyfin al servidor, de modo que los visitantes de /remote.html obtienen su propia instancia de tu biblioteca real. Ese es el objetivo de la función y significa que el secreto de la URL es lo único que separa tus películas de Internet. Si activas Remote Play, coloca single sign on delante de todo el sitio mediante Authentik como puerta de enlace SSO autogestionada, o elimina el hostname público y accede al store mediante un túnel de WireGuard gestionado con wg-easy.
Hay otros dos detalles relacionados. El proxy inverso sólo transporta el store: 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 más 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 pierde el foco de la ventana, detiene el bucle de animación. Por eso, dejar una pestaña abierta no agota la batería de un portátil. Esto ayuda a los equipos que sólo están al límite de sus capacidades. No sirve para un equipo que no puede dibujar la tienda.
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 la configuración o el menú de energía sin recargar la página, por lo que probar ambos modos en el mismo dispositivo lleva unos segundos. Sea realista sobre el resultado: el autor describe el modo plano como rudimentario y todavía en desarrollo. Úselo como 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 recarga sola o el navegador informa de que se perdió el contexto de WebGL, normalmente mientras los estantes todavía se están cargando. Cambie ese dispositivo a 2.5D en lugar de reducir la 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 falló el envío de la imagen para v0.2.0. Los informes de errores son bienvenidos en el proyecto original, pero no se aceptan parches, por lo que la línea 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.3.1Este comando 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:747dcc821a3d2fa318b50e76024783c1835609047e84f502e23d021bc1898b20Ese digest era 0.3.1 el 10 de agosto 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 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 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 en el servidor y transmite el resultado. Esta ruta utiliza la CPU para el renderizado, salvo que asigne /dev/dri al contenedor para habilitar la aceleración por hardware.
¿Puedo poner Halcyon en Internet público?
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 del servicio, 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 que ha introducido utiliza HTTP sin cifrar.
¿Necesito --network host?
Sólo para Remote Play. WebRTC debe anunciar la dirección real del equipo. 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 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?
Fije un digest en lugar de latest. Consulte el digest de una versión con docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1, ejecute ese digest y actualícelo sólo después de leer las notas de la versión. En agosto de 2026, la imagen publicada sólo está disponible como linux/amd64, por lo que un host arm64 debe compilarla desde el clon con docker compose up -d.