Halcyon: відеомагазин 90-х для Jellyfin
Перетворіть бібліотеку Jellyfin на відеомагазин 1990-х у браузері: команда Docker, reverse proxy, синхронізація перегляду та чесні обмеження.
Що Halcyon робить із вашою бібліотекою Jellyfin
Halcyon Video перетворює вашу бібліотеку Jellyfin на інтерактивний відеомагазин у стилі 1990-х, яким можна пересуватися в браузері. Кожен ваш фільм стає коробкою на полиці. Ви проходите між рядами під люмінесцентними світильниками, дістаєте коробку, перевертаєте її, щоб прочитати характеристики на звороті, а потім несете її до стійки, щоб розпочати відтворення. Дані про початок, перебіг і завершення відтворення передаються назад до Jellyfin, тому позиції продовження та історія перегляду залишаються правильними.
Halcyon отримує дані з наявного сервера Jellyfin через Jellyfin API і не зберігає власної бібліотеки. У цьому посібнику передбачається, що Jellyfin уже запущений і коректно сканує медіафайли. Якщо це не так, спочатку налаштуйте Jellyfin як медіасервер на VPS, а потім поверніться, коли ваша бібліотека матиме правильний вигляд у звичайному вебклієнті. Такі застосунки встановлюють тому, що бібліотека вже є, а не тому, що вам потрібен ще один сервіс у списку self-hosting сервісів.
Проєкт поширюється за ліцензією GPL-3.0 і написаний однією людиною. У README прямо зазначено, що pull requests не приймаються. Розробка йде швидко, а другого мейнтейнера, який міг би виявити регресію, немає. Тому зафіксуйте версію image, перш ніж показувати відеомагазин іншим користувачам. В останньому розділі описано, як це зробити.
Де відбувається рендеринг?
У браузері. Halcyon — це застосунок на Vite і TypeScript, створений на основі three.js — JavaScript-бібліотеки, яка виводить 3D-графіку через WebGL (web graphics library, інтерфейс браузера до GPU). Геометрію магазину та зображення коробок компонує пристрій, підключений до екрана.
Контейнер виконує мінімум роботи. Він запускає npm run serve, тобто vite preview --port 1420 --strictPort --host, і обслуговує зібрані файли та кілька невеликих маршрутів middleware. Halcyon не виконує транскодування і не запускає рушій на сервері.
Отже, питання щодо GPU стосується клієнта. Невеликий VPS без проблем обслуговує такий застосунок, оскільки це означає віддавання статичних файлів через HTTP. Саме ноутбук, планшет або телевізор, на якому працює браузер, визначає, чи буде магазин працювати плавно, чи відображатиметься із затримками.
Одна функція порушує це правило. Remote Play запускає на сервері безголові екземпляри Chromium і передає відрендерений магазин на телефон або set top box через WebRTC (web real time communication). У цьому режимі рендеринг відбувається на сервері. За замовчуванням кількість екземплярів обмежена двома, а змінити її можна за допомогою REMOTE_PLAY_MAX_INSTANCES. Якщо пристрій /dev/dri не проброшено, ці екземпляри виконують рендеринг на CPU, тому VPS із двома ядрами відчуває кожного додаткового глядача.
Що магазин читає з вашої бібліотеки
Структура магазину походить із власної структури Jellyfin. Halcyon формує розділи на основі ваших бібліотек і жанрів та групує сиквели з BoxSets. Характеристики на звороті кожної обкладинки беруться з метаданих MediaStreams, які Jellyfin уже зберігає. Тому все, чого бракує в Jellyfin, буде відсутнє і на полиці.
Отже, магазин точно відображає ваші метадані. Бібліотека, наповнена arr-стеком у Docker Compose, де вже задані обкладинки та жанри, виглядає тут значно краще, ніж папка з окремими файлами та загальними назвами. Фотобібліотеки так само залежать від системи, яка їх індексувала. Це варто врахувати, коли ви порівнюєте PhotoPrism та Immich для фотографій, що зберігаються на тому самому сервері.
Спробуйте демонстрацію відеотеки, перш ніж щось установлювати
Проєкт публікує повністю працездатний магазин, підключений до синтетичної бібліотеки, у розміщеній демонстрації. Додавання ?demo=1 до будь-якої URL-адреси Halcyon дає такий самий результат у вашому розгортанні.
Використовуйте її для перевірки апаратного забезпечення. Демонстраційна бібліотека містить близько 2,000 назв і потребує приблизно 2 GB пам’яті браузера, тобто створює більше навантаження, ніж більшість персональних бібліотек. Якщо демонстрація працює ривками на пристрої, з якого ви плануєте переглядати відео, ваша власна бібліотека також працюватиме ривками. У такому разі слід увімкнути режим 2.5D, описаний нижче, а не переходити на більший VPS.
Запуск через Docker
Це команда, яку документує upstream.
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У журналі має бути вказано, що preview-сервер слухає порт 1420, а curl має відповідати на HTTP/1.1 200 OK. Якщо контейнер завершує роботу протягом кількох секунд, майже завжди проблема в порту. --strictPort означає, що сервер не переходить на 1421, коли 1420 вже зайнятий, а натомість завершує роботу.
--network host потрібен для Remote Play, а не для store. WebRTC має повідомити пристрою, який запитує потік, фактичну адресу машини. За стандартної Docker bridge-мережі контейнер бачить лише власну адресу 172.x. Жоден телефон у вашій мережі не може підключитися до цієї адреси, тому потік не встановлюється. Якщо store потрібен лише в браузері, опублікуйте порт.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoЦе кращий варіант за замовчуванням на VPS, оскільки host networking підключає контейнер до кожного інтерфейсу машини, зокрема до публічного. У розділі Запуск Docker на VPS описано решту компромісів цього підходу. --restart unless-stopped забезпечує повторний запуск store після перезавантаження, так само як Compose-сервіси, що запускаються під час завантаження.
Клонування репозиторію та запуск docker compose up -d натомість збирає image локально. За замовчуванням закріплений Compose-файл збирає image із вихідного коду та містить закоментований рядок із готовим image:. Розкоментуйте цей рядок, якщо в Compose потрібен опублікований image.
Є одне важливе обмеження станом на August 2026: опублікований image доступний лише для linux/amd64. Збірка arm64 у multi architecture push завершилася помилкою під час емуляції та очікує на native arm runners. На arm64 VPS завантаження завершується помилкою no matching manifest for linux/arm64/v8 in the manifest list entries. У такому разі потрібно зібрати image з клонованого репозиторію.
Налаштуйте підключення до свого сервера Jellyfin
Відкрийте http://<host>:1420 і увійдіть, указавши адресу сервера Jellyfin, ім’я користувача та пароль. Файл .env.local.example у репозиторії призначений лише для локальної розробки. Vite передає змінні з префіксом VITE_ у клієнтський код, тому пароль Jellyfin, записаний у цьому файлі, буде скомпільовано в JavaScript bundle, який завантажує кожен відвідувач. На сервері, доступному іншим користувачам, виконуйте вхід через інтерфейс.
Браузер безпосередньо звертається до Jellyfin. Контейнер Halcyon не проксирує Jellyfin API. Перед початком діагностики врахуйте два наслідки.
По-перше, Jellyfin має бути доступний із браузера, а не лише з VPS, на якому працює Halcyon. Jellyfin, прив’язаний до 127.0.0.1:8096, підходить для локального тестування, але для всіх інших користувачів полиці залишаться порожніми.
По-друге, це cross-origin запит: від адреси Halcyon до адреси Jellyfin. За замовчуванням Jellyfin відповідає на API-запити із заголовком Access-Control-Allow-Origin: *, тому додаткова конфігурація не потрібна. Якщо ви звузили це налаштування або розмістили authentication proxy перед Jellyfin API, консоль браузера повідомить про blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource, а store завантажиться з порожніми полицями.
Розмістіть його за reverse proxy із попередньою автентифікацією
vite preview — це preview server. Він не завершує TLS (transport layer security) і не має власного контролю доступу, тому для публічного доступу його потрібно розміщувати за 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;
}
}Для доменного імені перед контейнером потрібне ще одне налаштування. Halcyon відповідає на запити до localhost, необроблених IP-адрес і імен машини, на якій він працює, щоб захиститися від DNS rebinding. У контейнері машиною, на якій працює застосунок, є сам контейнер, тому його hostname відрізняється від вашого. Запит із адресою halcyon.example.com відхиляється, а у відповіді вказується hostname, який було відхилено. Додайте це ім’я.
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 лише на машині, до якої немає доступу ззовні.
Після того як store буде доступний через https://, адреса Jellyfin, яку ви вводите під час входу, також має бути https://. Браузер блокує звичайний виклик API через http:// зі сторінки HTTPS, а в консолі з’являється Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. Вхід просто не виконується, без пояснення в Halcyon. Використовуйте TLS для обох компонентів або залиште обидва доступними через звичайний HTTP у приватній мережі.
Далі налаштуйте автентифікацію. Store запитує облікові дані Jellyfin, тому незнайомий користувач, який знайшов URL, побачить екран входу. Одна функція змінює цю поведінку. Увімкнення Remote Play у Settings, а потім Connection, передає серверу вашу сесію Jellyfin. Тому відвідувачі /remote.html отримують власний екземпляр вашої реальної бібліотеки. Саме для цього призначена функція. Це означає, що між інтернетом і вашими фільмами залишається лише секретність URL. Якщо ви вмикаєте Remote Play, додайте single sign on перед усім сайтом за допомогою Authentik як self-hosted SSO gateway або приберіть публічне hostname та підключайтеся до store через WireGuard tunnel під керуванням wg-easy.
Із цим пов’язані ще 2 деталі. Reverse proxy передає лише store. Потік Remote Play працює через WebRTC over UDP і не проходить через HTTP proxy, тому для нього потрібні окремі правила доступу до 3478/udp і до 49200–49260/udp, якщо використовується вбудований TURN relay. Крім того, звичайний docker run вище не зберігає жодного volume, тому seed Remote Play не переживає docker rm. Саме тому Compose file монтує halcyon-data volume у /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.0–v0.3.1 з’явилися протягом кількох днів один за одним, а v0.2.1 існує лише тому, що передавання образу для v0.2.0 завершилося помилкою. Повідомлення про помилки вітаються в upstream, але патчі не приймаються, тому потік релізів відображає робочий стан однієї людини.
Запуск latest із використанням docker pull означає, що вміст сховища може змінитися будь-якого звичайного вівторка. Зафіксуйте образ за digest — це єдине посилання, яке не може змінитися.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1Ця команда виводить digest, що відповідає тегу. Використовуйте його замість тегу.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:747dcc821a3d2fa318b50e76024783c1835609047e84f502e23d021bc1898b2010 August 2026 цей digest мав значення 0.3.1. Самостійно перевірте поточне значення, а не копіюйте його. Також прочитайте примітки до релізу перед оновленням, оскільки patch release у цьому випадку може містити зміни структури сховища, а не лише виправлення.
FAQ
Чи потрібен Halcyon GPU на моєму VPS?
Для звичайного використання — ні. Store відображається за допомогою three.js у браузері, тому рендеринг виконує клієнтська машина, а container лише обслуговує статичні файли на порту 1420. Винятком є Remote Play: він запускає headless Chromium на сервері та передає результат потоково. У цьому режимі рендеринг виконується на CPU, якщо не підключити /dev/dri до container для апаратного прискорення.
Чи можна розмістити Halcyon у публічному інтернеті?
Лише за authentication. Store запитує облікові дані Jellyfin, але після ввімкнення Remote Play ваша сесія Jellyfin передається серверу. Тому кожен, хто відкриє /remote.html, отримає екземпляр вашої реальної бібліотеки без входу. Розмістіть перед ним reverse proxy з single sign-on або не публікуйте hostname у public DNS і підключайтеся до store через VPN.
Чому полиці порожні після входу?
Браузер безпосередньо звертається до API Jellyfin. Тому Jellyfin має бути доступним із браузера, а не лише з VPS. Відкрийте консоль браузера. blocked by CORS policy означає, що Jellyfin не приймає запит з адреси Halcyon. Повідомлення Mixed Content означає, що сторінка працює через HTTPS, а вказана вами адреса Jellyfin використовує звичайний HTTP.
Чи потрібен --network host?
Лише для Remote Play. WebRTC має оголошувати реальну адресу машини. За Docker bridge container може запропонувати лише адресу 172.x, до якої жоден телефон у вашій мережі не зможе підключитися. Для перегляду store у браузері достатньо -p 1420:1420, який відкриває значно меншу частину host.
Який image tag слід використовувати?
Зафіксуйте digest, а не latest. Прочитайте digest версії за допомогою docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1, використайте цей digest і оновлюйте його лише після ознайомлення з release notes. Станом на August 2026 опублікований image доступний лише як linux/amd64, тому host з arm64 має зібрати його з clone за допомогою docker compose up -d.