Halcyon transforme Jellyfin en vidéoclub des années 1990
Découvrez comment installer Halcyon avec Docker, le relier à Jellyfin derrière un reverse proxy et connaître ses limites avant de transformer votre bibliothèque en vidéoclub.
Ce que Halcyon fait de votre bibliothèque Jellyfin
Halcyon Video transforme votre bibliothèque Jellyfin en vidéoclub des années 1990 que vous pouvez parcourir dans le navigateur. Chaque film que vous possédez devient une jaquette sur une étagère. Vous parcourez les allées sous les néons, prenez une jaquette, la retournez pour lire les informations techniques au dos, puis l’apportez au comptoir pour démarrer la lecture. Les événements de début, de progression et d’arrêt de la lecture sont transmis à Jellyfin. Les points de reprise et l’historique de visionnage restent donc corrects.
Halcyon lit un serveur Jellyfin existant via l’API Jellyfin et ne conserve aucune bibliothèque qui lui soit propre. Ce guide suppose que Jellyfin est déjà en fonctionnement et que l’analyse de la bibliothèque se déroule correctement. Si ce n’est pas le cas, installez d’abord Jellyfin comme serveur multimédia sur un VPS, puis revenez une fois que votre bibliothèque s’affiche correctement dans le client web habituel. C’est le type de service que l’on installe parce que la bibliothèque existe déjà, et non parce qu’il vous fallait un autre service dans votre liste d’outils auto-hébergés.
Le projet est distribué sous licence GPL-3.0 et développé par une seule personne. Le README indique clairement que les pull requests ne sont pas acceptées. Le développement évolue rapidement et aucun second mainteneur ne peut détecter une régression. Épinglez donc la version de l’image avant de montrer le vidéoclub à d’autres personnes. La dernière section explique comment procéder.
Où le rendu est-il effectué ?
Dans le navigateur. Halcyon est une application Vite et TypeScript basée sur three.js, une bibliothèque JavaScript qui dessine des graphismes 3D avec WebGL (web graphics library, l’interface du navigateur avec le GPU). La géométrie du magasin et les visuels des boîtes sont composés par la machine à laquelle l’écran est connecté.
Le conteneur fait très peu de choses. Il exécute npm run serve, qui est vite preview --port 1420 --strictPort --host, et sert les fichiers générés ainsi que quelques routes middleware légères. Halcyon n’effectue aucun transcodage et n’exécute aucun moteur sur le serveur.
La question du GPU concerne donc le client. Un petit VPS suffit largement, car le service consiste à fournir des fichiers statiques et quelques routes légères via HTTP. C’est l’ordinateur portable, la tablette ou le téléviseur qui exécute le navigateur qui détermine si le magasin s’affiche fluidement ou saccade.
Une fonctionnalité fait exception à cette règle. Les instances privées de Remote Play lancent Chromium headless sur le serveur, une instance par visiteur, puis diffusent le magasin rendu vers un téléphone ou un boîtier TV via WebRTC (web real time communication). Dans ce cas, le rendu est effectué sur le serveur. Le nombre d’instances est limité à deux par défaut et peut être modifié avec REMOTE_PLAY_MAX_INSTANCES. Le serveur doit également disposer d’un GPU. L’image publiée ne contient aucun render software, donc, sans nœud de rendu /dev/dri mappé, le serveur refuse de démarrer un magasin privé et répond This store server has no GPU available, so it can’t render a private store.
Ce que le magasin lit dans votre bibliothèque
Les rayons reprennent la structure propre à Jellyfin. Halcyon crée les sections à partir de vos bibliothèques et de vos genres, puis regroupe les suites à partir de vos BoxSets. Les caractéristiques imprimées au dos de chaque boîtier proviennent des métadonnées MediaStreams déjà détenues par Jellyfin. Toute information absente de Jellyfin le sera donc aussi dans les rayons.
Le magasin constitue ainsi un reflet fidèle de vos métadonnées. Une bibliothèque alimentée par une stack arr dans Docker Compose, avec les illustrations et les genres déjà renseignés, donne ici un bien meilleur résultat qu’un dossier contenant des fichiers isolés aux noms génériques. Les photothèques dépendent de la même manière du logiciel qui les a indexées. Gardez ce point à l’esprit lorsque vous comparez PhotoPrism et Immich pour les photos stockées sur le même serveur.
Tester la démo du magasin vidéo avant toute installation
Le projet propose une démo qui fonctionne sans serveur ni connexion. Dans le code source actuel, cette version hébergée remplit ses rayons avec des titres en streaming, et non avec une bibliothèque organisée comme la vôtre. Considérez-la donc comme un aperçu du magasin. Ajouter ?demo=1 à l’URL de votre propre déploiement charge à la place une bibliothèque synthétique.
Cette bibliothèque synthétique sert de test matériel, avec une particularité : elle s’adapte à l’appareil. Un navigateur disposant de suffisamment de ressources GPU charge le magasin complet, soit environ 2,000 titres. Un GPU moyen en charge environ 800. Un téléphone, ou tout navigateur qui signale 4 GB de mémoire appareil ou moins, en charge environ 300. Une démo qui fonctionne correctement sur un appareil peu puissant s’est déjà réduite elle-même. Elle vous renseigne donc peu sur le fonctionnement de votre bibliothèque complète sur cet appareil. Le test utile consiste à utiliser un appareil qui charge le magasin complet. Si l’affichage saccade, votre propre bibliothèque d’une taille similaire saccadera aussi. La solution est alors d’activer le mode 2.5D décrit ci-dessous, et non de prendre un VPS plus puissant.
Lancer le service avec Docker
Voici la commande documentée par le projet.
docker run -d --name halcyon --network host --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoVérifiez ensuite que le service a bien démarré.
docker logs halcyon
curl -I http://127.0.0.1:1420Le journal doit indiquer que le serveur de prévisualisation écoute sur le port 1420, et curl doit répondre sur HTTP/1.1 200 OK. Un conteneur qui s’arrête au bout de quelques secondes est presque toujours confronté à un problème de port. --strictPort signifie que le serveur refuse de passer au port 1421 lorsque 1420 est déjà utilisé ; il s’arrête donc.
--network host est nécessaire pour Remote Play, pas pour le store. WebRTC doit annoncer l’adresse réelle de la machine au périphérique qui demande le flux. Derrière le bridge Docker par défaut, le conteneur ne connaît que sa propre adresse 172.x, qu’aucun téléphone de votre réseau ne peut atteindre. Le flux ne peut donc jamais se connecter. Si vous voulez uniquement utiliser le store dans un navigateur, publiez plutôt le port.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoC’est le meilleur choix par défaut sur un VPS, car le networking de l’hôte place le conteneur sur toutes les interfaces de la machine, y compris l’interface publique. La page Exécuter Docker sur un VPS présente les autres aspects de ce choix. --restart unless-stopped permet de relancer le store après un redémarrage, selon le même principe que les services Compose lancés au démarrage.
Cloner le dépôt et exécuter docker compose up -d construit l’image localement. Le fichier Compose versionné construit l’image depuis les sources par défaut et contient la ligne image: de l’image préconstruite, commentée. Décommentez cette ligne si vous voulez utiliser l’image publiée avec Compose.
L’image publiée prend en charge linux/amd64 et linux/arm64. Vérifiée le 11 octobre 2026, la version 0.25.0 actuelle publie un digest distinct pour chaque plateforme. Un VPS arm64 utilise donc la même commande que n’importe quel autre hôte. La période limitée à amd64 a été brève : le changelog a supprimé l’image arm64 dans v0.2.1 le 8 août 2026, puis l’a réintroduite, construite sur des runners natifs, dans v0.4.0 le 11 août 2026. Seul un hôte verrouillé sur un tag datant de cette période doit construire l’image depuis le clone.
Pointez-le vers votre serveur Jellyfin
Ouvrez http://<host>:1420 et connectez-vous avec l’adresse de votre serveur Jellyfin, votre nom d’utilisateur et votre mot de passe. Le fichier .env.local.example du dépôt est réservé au développement local. Vite expose les variables préfixées par VITE_ au code côté client. Un mot de passe Jellyfin écrit à cet endroit est donc compilé dans le bundle JavaScript téléchargé par chaque visiteur. Sur un serveur accessible à d’autres personnes, connectez-vous via l’interface.
Le navigateur communique directement avec Jellyfin. Le conteneur Halcyon ne fait pas proxy pour l’API Jellyfin. Cela entraîne deux conséquences à connaître avant de commencer le dépannage.
Premièrement, Jellyfin doit être accessible depuis le navigateur, et pas uniquement depuis le VPS qui héberge Halcyon. Un Jellyfin lié à 127.0.0.1:8096 convient pour un test local, mais les rayons resteront vides pour tous les autres utilisateurs.
Deuxièmement, l’appel est cross-origin, depuis l’adresse de Halcyon vers celle de Jellyfin. Par défaut, Jellyfin répond aux requêtes API avec Access-Control-Allow-Origin: *. Il fonctionne donc sans configuration supplémentaire. Si vous avez restreint ce paramètre ou placé un proxy d’authentification devant l’API Jellyfin, la console du navigateur affiche blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource et le magasin se charge avec des rayons vides.
Placez-le derrière un reverse proxy et ajoutez une authentification en amont
vite preview est un serveur de prévisualisation. Il ne gère pas la terminaison TLS (transport layer security) et ne fournit aucun contrôle d’accès. Il doit donc être placé derrière nginx ou Caddy dès qu’il est accessible publiquement.
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 nom de domaine placé devant le conteneur nécessite un réglage supplémentaire. Halcyon répond à localhost, aux adresses IP brutes et aux noms de la machine sur laquelle il s’exécute, afin de se protéger contre le DNS rebinding. Dans un conteneur, la machine sur laquelle il s’exécute est le conteneur lui-même. Son hostname n’est donc pas le vôtre. Une requête reçue avec halcyon.example.com est refusée, et la réponse indique le host refusé. Ajoutez ce nom.
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-videoLa valeur est une liste séparée par des virgules. Un point initial, comme .example.com, correspond aux sous-domaines. La valeur all désactive le contrôle. Utilisez all uniquement sur une machine inaccessible depuis l’extérieur.
Une fois le store servi via https://, l’adresse Jellyfin saisie lors de la connexion doit également être https://. Un navigateur bloque un appel API en http:// effectué depuis une page HTTPS, et la console affiche Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. La connexion échoue simplement, sans explication dans Halcyon. Servez les deux composants via TLS, ou utilisez HTTP en clair pour les deux au sein d’un réseau privé.
Vient ensuite l’authentification. Le store demande les identifiants Jellyfin. Un inconnu qui trouve l’URL arrive donc sur une page de connexion. Une fonctionnalité modifie ce comportement. L’activation de Remote Play, dans Settings puis Connection, transmet votre session Jellyfin au serveur. Les visiteurs de /remote.html obtiennent alors leur propre instance de votre véritable bibliothèque. C’est précisément le rôle de cette fonctionnalité. La confidentialité de l’URL devient donc la seule protection entre Internet et vos films. Si vous activez Remote Play, placez un système de single sign-on devant l’ensemble du site avec Authentik comme passerelle SSO auto-hébergée, ou supprimez le hostname public et accédez au store via un tunnel WireGuard géré avec wg-easy.
Deux détails sont à prendre en compte. Le reverse proxy ne transporte que le store. Le flux Remote Play utilise WebRTC sur UDP et ne passe pas par un proxy HTTP. Il nécessite donc son propre chemin sur 3478/udp et sur 49200 à 49260/udp lorsque le relais TURN intégré est utilisé. De plus, le docker run en clair ci-dessus ne conserve aucun volume. Le seed Remote Play ne survit donc pas à docker rm. Le fichier Compose monte un volume halcyon-data sur /data et définit REMOTE_PLAY_SEED sur /data/remote-play-seed.json pour cette raison précise.
Que faire lorsque la boutique fonctionne mal
Halcyon effectue le rendu à la demande. Une boutique inactive ne compose aucune image, et la perte du focus de la fenêtre arrête la boucle d’animation. C’est pourquoi un onglet laissé ouvert ne vide pas la batterie d’un ordinateur portable. Cela aide une machine qui est simplement à la limite de ses capacités. En revanche, cela ne change rien pour une machine incapable d’afficher la boutique.
Pour ces clients, un mode 2.5D est disponible. Il utilise uniquement du HTML et du CSS, sans WebGL, et convient à du matériel aussi peu puissant qu’un Raspberry Pi. Vous pouvez basculer entre les modes 3D et 2.5D depuis les paramètres ou le menu d’alimentation, sans recharger la page. Tester les deux modes sur le même appareil ne prend donc que quelques secondes. Restez réaliste quant au résultat : en August 2026, l’auteur a qualifié le mode plat de rudimentaire et a indiqué que le projet en était à peine à ses débuts. Considérez-le comme une solution de repli pour les clients peu puissants.
Lorsqu’un client est trop peu puissant pour la boutique 3D, l’échec est manifeste. L’onglet se recharge tout seul ou le navigateur signale la perte du contexte WebGL, généralement pendant que les étagères se remplissent encore. Basculez cet appareil en mode 2.5D au lieu de réduire votre bibliothèque.
Figez l’image et vérifiez-la avant de la télécharger
Prenez cette partie au sérieux. Les tags v0.1.0 à v0.3.1 sont tous sortis à quelques jours d’intervalle, et v0.2.1 n’existe que parce que le push de l’image pour v0.2.0 a échoué. Le rythme ne s’est pas ralenti depuis : v0.25.0 a été publié le 7 octobre 2026, trois jours après v0.23.0. Les rapports de bugs sont les bienvenus en amont, mais pas les patches. Le flux de versions reflète donc l’état de travail d’une seule personne.
Exécuter latest en ayant l’habitude de docker pull signifie que le contenu du store peut changer sans préavis, n’importe quel mardi. Épinglez l’image par son digest, la seule référence qui ne peut pas changer.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0Cette commande affiche le digest correspondant au tag. Utilisez-le à la place du tag.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67deLe 11 octobre 2026, il s’agissait du digest correspondant au tag 0.25.0. Consultez vous-même le digest actuel au lieu de copier celui-ci, et lisez les notes de version avant de mettre à jour l’image, car une version de maintenance peut modifier la structure du store en plus d’apporter des correctifs.
FAQ
Halcyon a-t-il besoin d’un GPU sur mon VPS ?
Pas pour une utilisation normale. La boutique est rendue par three.js dans le navigateur. C’est donc la machine cliente qui effectue le rendu. Le conteneur sert les fichiers générés et quelques routes serveur légères sur le port 1420. L’exception concerne les instances privées de Remote Play. Elles exécutent Chromium en mode headless sur le serveur et diffusent le résultat. Avec l’image publiée, ce chemin nécessite de mapper /dev/dri dans le conteneur. Sans GPU, le serveur refuse de démarrer une boutique privée.
Puis-je exposer Halcyon sur Internet ?
Uniquement derrière une authentification. La boutique demande les identifiants Jellyfin. Toutefois, l’activation de Remote Play transmet votre session Jellyfin au serveur. Toute personne qui charge /remote.html obtient donc une instance de votre véritable bibliothèque sans se connecter. Placez un reverse proxy avec authentification unique devant le service, ou gardez le nom d’hôte hors du DNS public et accédez à la boutique via un VPN.
Pourquoi les étagères sont-elles vides après ma connexion ?
Le navigateur appelle directement l’API Jellyfin. Jellyfin doit donc être accessible depuis le navigateur, et pas uniquement depuis le VPS. Ouvrez la console du navigateur. blocked by CORS policy signifie que Jellyfin n’accepte pas la requête provenant de l’adresse de Halcyon. Un message Mixed Content signifie que la page utilise HTTPS alors que l’adresse Jellyfin saisie utilise HTTP en clair.
Ai-je besoin de --network host ?
Uniquement pour Remote Play. WebRTC doit annoncer l’adresse réelle de la machine. Derrière le bridge Docker, le conteneur ne peut proposer qu’une adresse 172.x, inaccessible depuis un téléphone de votre réseau. Pour parcourir la boutique dans un navigateur, -p 1420:1420 fonctionne et expose beaucoup moins l’hôte.
Quel tag d’image dois-je utiliser ?
Épinglez un digest plutôt que latest. Consultez le digest correspondant à une version avec docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0, utilisez ce digest, puis mettez à jour uniquement après avoir lu les notes de version. Au 11 octobre 2026, l’image publiée inclut à la fois linux/amd64 et linux/arm64. Un hôte arm64 la récupère donc comme n’importe quelle autre image et n’a pas besoin de la compiler depuis le clone.