Skip to content

Режими програвання ​

Сервер працює в одному з двох режимів, і клієнт мусить знати, в якому саме. Дізнатися можна з GET /api/public/config → streamMode. Змінюється лише через .env і потребує перезапуску сервера.

Sync-режим (STREAM_MODE=false) ​

Аудіо качає сам клієнт, сервер лише каже, що і з якої секунди грати.

сервер ──sync (кожні 2 с)──> клієнт ──GET /api/audio/stream──> файл

Сервер надсилає sync з полями track, seek, serverTimeMs. Клієнт порівнює свою позицію з seek і підтягується, якщо розбіжність перевищує поріг (референсний клієнт бере 0,35 с).

Чергою керує лише сервер

Кінець треку визначає сам сервер — за тривалістю з метаданих і власним годинником відтворення. Клієнт не бере в цьому участі: немає події, якою можна повідомити «трек скінчився» або підказати його тривалість.

Практично це означає, що вашому клієнту не треба нічого робити на ended елемента <audio> — просто дочекайтеся наступного sync з новим track. Просування відбувається протягом двох секунд після реального кінця треку.

Переваги: якість не залежить від каналу сервера, перемотка миттєва. Ціна: синхронність приблизна, кожен клієнт вантажить файли окремо.

Stream-режим (STREAM_MODE=true) ​

Сервер декодує треки через FFmpeg, за потреби підмішує мікрофони живих ведучих, кодує спільний MP3 і віддає його одним потоком.

FFmpeg ──мікшер──> спільний MP3 ──GET /api/stream──> усі клієнти

Клієнт просто програє HTTP-потік. Позицію для інтерфейсу отримує подіями stream_get_seek і stream_ping — вони потрібні лише для показу таймера, бо саме звучання вже синхронне за побудовою.

Ендпоінт потоку захищений art-токеном.

Переваги: справжня синхронність, можливість живого ефіру. Ціна: постійне навантаження на CPU і канал сервера, потрібен FFmpeg.

Що підтримати клієнту ​

Мінімально — один режим, і чесно повідомляти, якщо сервер працює в іншому. Повноцінний клієнт перевіряє streamMode на старті й обирає гілку.

SyncStream
Джерело звуку/api/audio/stream?track=…/api/stream
Потрібен audioTokenтакні, лише art-токен
Позиціяз події syncstream_get_seek / stream_ping
Живі ведучінедоступнідоступні

Живі ведучі ​

Працюють тільки в stream-режимі і лише коли RADIO_HOSTS_MODE=true — інакше мікрофон ніде підмішати.

Коли ведучий ставить чергу на паузу, поле title у sync стає "Just chatting", artist порожніє, а тишу заповнює фонова музика. Клієнт має показати щось замість метаданих треку, інакше інтерфейс виглядатиме зламаним.

Синхронізовані тексти (караоке) ​

Текст тягнеться окремо від аудіо — GET /api/lyrics?title=…&artist=… із заголовком X-Art-Token, тіло відповіді зашифроване тим самим токеном (XOR). Формат для синхронізованого тексту: { synced: true, lines: [{ time, text }], offset }; offset — ручне калібрування в секундах, яке задає адмін у редакторі пісень, і його треба додати до кожного time перед порівнянням із поточною позицією.

У sync-режимі підсвітка активного рядка тривіальна: поточна позиція — це seek із sync, без жодних поправок. Клієнт качає файл сам, тож те, що звучить із <audio>, і є той самий момент, який сервер вважає поточним.

У stream-режимі "позиція сервера" ≠ "те, що чутно"

stream_get_seek віддає позицію FFmpeg-декодера на сервері — момент, коли семпл щойно закодовано, а не коли він долетів до слухача й пролунав. Між цими двома моментами лежить буфер <audio>-елемента (скільки вже довантажено, але ще не відтворено) і мережева затримка. На практиці це секунди, і величина непостійна — залежить від мережі й того, як саме браузер буферизує потік.

Референсний клієнт вимірює цю різницю сам, а не ігнорує: hearLag = (скільки аудіо вже в буфері <audio>, але ще не прозвучало) + (половина RTT, зі stream_ping), згладжене експоненційно (щоб рядок не смикався туди-сюди при короткочасних мережевих стрибках). Позиція для караоке — це позиція сервера мінус цей hearLag, а не сама по собі. Без цієї поправки рядки підсвічуватимуться на кілька секунд раніше, ніж пісня дійсно буде звучати.

Джингли зупиняють годинник, а не лише музику. Поки грає джингл, сервер не просуває позицію поточного треку — але клієнтський годинник, який рахує позицію екстраполяцією від останнього відомого seek (щоб не смикати рядки щосекунди зайвим запитом), про паузу сам по собі не знає. Клієнт має явно ловити stream_jingle_start / stream_jingle_end і на цей час зупиняти екстраполяцію — інакше рядки тексту «поїдуть» вперед за реальним годинником, поки насправді звучить джингл, а не пісня.

Фрази — не те саме, що джингли. Джингл підмінює собою весь потік між піснями: годинник зупиняється, track не змінюється, а слухач чує тишу в музиці й повний джингл замість неї. Фраза, навпаки, домішується поверх музики за кілька секунд до кінця пісні — так само, як голос ведучого, тільки без приглушення гучності самої пісні. Для клієнта це непомітно: жодна подія не летить, track/seek/текст пісні не змінюються, бо з погляду протоколу й далі грає та сама пісня, просто з голосом поверх неї в останні секунди.

Just chatting не ховає текст сам собою

Поки чат-пауза триває, track у sync не змінюється — це той самий трек, просто призупинений, тому найпростіша перевірка «трек змінився, тож оновимо текст» тут не спрацює: title/artist порожніють, а track лишається тим самим ідентифікатором. Якщо ваш клієнт вирішує, показувати текст чи ні, лише за наявністю поточного треку — під час паузи на екрані лишиться текст попередньої пісні, а годинник (якщо його ніхто не зупинив) дотягне підсвітку до останнього рядка й застигне там до кінця паузи. Ловіть саме stream_chat_mode_start / stream_chat_mode_end і ховайте текст цим прапорцем окремо, а не через відсутність треку.