Музыка без остановки: как устроен непрерывный эфир

Содержание 4 раздела
панорама страницы /radio - сетка карточек каналов слева, блок «сейчас играет» с обложкой трека и живым индикатором справа; тёмный фон, неоново-розовый акцент, крупный таймкод прогресс-бара

160 килобит в секунду, 44 100 герц и один общий указатель на сервере - примерно из этого собран непрерывный эфир на /radio. «Музыка без остановки» в разговорном смысле значит «включил и не переключаешь», а в техническом - совсем другое: поток, где позиция трека живёт не у вас в браузере, а в базе вещателя. Разница между этими двумя пониманиями и объясняет, почему у одних сервисов эфир рвётся на ходу, а у других - нет.

MP3, на котором держится поток, - кодек с почти тридцатилетней историей. Формат MPEG-1 Audio Layer III разрабатывала рабочая группа Института Фраунгофера под руководством Карлхайнца Бранденбурга; алгоритм тестировали на песне Сюзанны Веги «Tom's Diner», из-за чего Вегу полушутя зовут «матерью MP3». 23 апреля 2017 года истекли последние патенты на формат, и лицензионные отчисления с производителей прекратились. При 128 кбит/с файл сжимается примерно в 11 раз относительно несжатого CD-Audio (1411,2 кбит/с); рабочий диапазон битрейта - 8-320 кбит/с.

Кодек, битрейт и два файла на трек

В каталоге у каждого трека лежат два аудиофайла. Один - полноразмерный, для скачивания, второй - пережатый под стриминг. Перекодированием занимается ffmpeg через библиотеку libmp3lame: исходник гонится в MP3 с частотой дискретизации 44 100 Гц и тегами ID3v2.3. Битрейт стрима берётся из конфигурации и ограничен жёстким коридором 96-224 кбит/с; по умолчанию это 160 кбит/с.

Почему именно LAME, а не любой другой кодировщик: по результатам публичных тестов прослушивания, которые в 2004 году проводил Роберто Аморим, LAME давал наиболее близкое к оригиналу звучание на средних битрейтах от 128 кбит/с и выше. 160 кбит/с попадает как раз в эту зону - слышимой деградации на обычной акустике нет, а файл остаётся достаточно лёгким, чтобы грузиться через мобильную сеть.

Деталь: перекодируется только источник не длиннее 1800 секунд. Треки длиннее получат стрим-копию, обрезанную по этому порогу, - редкость для танцевальной музыки, где 6-8 минут обычны, а 30 минут уже исключение.

экран ffmpeg в терминале - перекодировка трека в MP3 через libmp3lame, видны флаги -codec:a libmp3lame -b:a 160k -ar 44100, прогресс-бар конверсии, тёмный фон
экран ffmpeg в терминале - перекодировка трека в MP3 через libmp3lame, видны флаги -codec:a libmp3lame -b:a 160k -ar 44100, прогресс-бар конверсии, тёмный фон

Сам каталог каналов - не произвольный список, а генерируемая сетка. Декадные каналы жёстко привязаны к годам: decade:80s - это треки с 1980 по 1989, decade:90s - с 1990 по 1999, и так до decade:20s (2020-2029). Жанровые каналы строятся из таблицы жанров и дедуплицируются по каноническому слагу: варианты вроде «italo-disco» и «italo-disco-remix» схлопываются в один канал, чтобы не плодить визуальные дубли в сетке. На страницах /radio отдаётся разметка schema.org двух типов - RadioBroadcastService и MusicPlaylist: первая описывает сервис вещания, вторая - состав эфира как плейлист, без перечисления сотен треков вручную.

Музыка без остановки: как устроена ротация на сервере

Каналов на /radio несколько десятков, и они делятся на два типа по тому, кто держит позицию. Broadcast-каналы - все, свежее, топ, по декадам, по жанрам, по схожим трекам и mood-сценарии - синхронные: в таблице radio_broadcasts лежит один указатель на текущий трек для всех слушателей сразу. Personal-канал один - избранное, и там у каждого своя очередь.

Синхронность устроена так. Браузер раз в 20 секунд дёргает ручку /api/radio/sync и получает четыре числа: идентификатор трека, started_at_ms - когда трек стартовал на сервере, duration_ms - его длина, и server_time_ms - серверное время в момент ответа. Разница между серверным временем и стартом даёт позицию в треке; клиент прибавляет прошедшее локальное время с поправкой на разницу часов и делает seek в эту точку. В итоге два слушателя, зашедшие на один канал с разницей в минуту, слышат одну и ту же секунду одной и той же песни.

Чтобы не тянуть полный payload каждый раз, sync отдаёт HTTP-заголовок ETag. Пока трек не сменился, сервер отвечает 304 Not Modified без тела - это и есть 19 из 20 запросов. Точный перезапрос срабатывает за 500 миллисекунд до конца трека: сервер ловит rollover и выдаёт следующую запись. После конца остаётся grace-окно в 750 мс - на случай, если клиент чуть отстал.

  • Broadcast-каналы (синхронные): all, fresh, top, decade:70s-20s, genre:*, mood:v-mashinu, mood:dance, similar:*. Один указатель - все слышат одно и то же.
  • Personal-канал (только «Избранное»): у каждого слушателя своя очередь, без общей позиции. Требует логина.
  • Жанровые каналы появляются в сетке только если в жанре ≥ 3 опубликованных треков - иначе это не радио, а одна-две песни под ярлыком.
  • Тонкие декадные каналы (меньше 5 треков) остаются доступны по прямому URL, но уходят под noindex,follow - индексировать одну-две песни как «радио» нечестно.

Порог в 5 треков подобран не с потолка: при нём под noindex уходят decade:70s (две записи) и decade:20s (четыре), а decade:10s (пять) остаётся в индексе - пять треков это минимум, где слушатель успевает услышать весь эфир до повтора. Жанровый порог жёстче: ниже трёх опубликованных треков канал вообще не попадает в сетку и sitemap, хотя по прямой ссылке по-прежнему открывается.

Почему поток рвётся именно на мобильной сети

Между командой «загрузить трек» и фактическим звуком HTML5-audio съедает 1-3 секунды на буферизацию. На домашнем интернете это незаметно; на мобильной сети, где задержки прыгают, за эти секунды серверный указатель уезжает вперёд, и слушатель начинает играть раньше остальных. Чтобы новичок догнал общий эфир, клиент делает две быстрые корректировки дрейфа - на 1500 мс и на 4000 мс после старта - и подтягивает позицию к серверной. Цель - вписаться в общий поток с погрешностью 300-500 мс.

Дрейф контролируется постоянно: если локальная позиция ушла от серверной больше чем на 700 мс, клиент делает seek обратно. 700 мс - компромисс. Человеческое ухо без референса «рядом играющего трека» замечает рассинхрон от 150 мс, но seek'ать на каждый тик слышно как микропрыжки. Порог держит «все в унисон», не дёргая аудио на каждый чих сети.

Если сеть совсем пропала, sync-запрос висит до таймаута в 10 секунд и обрывается. Три network-ошибки подряд на personal-канале - и клиент останавливается; на broadcast плеер доигрывает закешированный кусок и ждёт возврата сети. Здесь кроется главное отличие от классического интернет-радио: у вас в буфере не накапливается поток «на потом», как у Icecast-станций, - есть текущий трек целиком и указатель, где в нём общий эфир.

смартфон в подстаканнике автомобиля - на экране блок «сейчас играет» с живым индикатором и прогресс-баром, рядом кабель зарядки, за окном размытый ночной шоссе; неоново-розовый акцент UI
смартфон в подстаканнике автомобиля - на экране блок «сейчас играет» с живым индикатором и прогресс-баром, рядом кабель зарядки, за окном размытый ночной шоссе; неоново-розовый акцент UI

Personal-канал «Избранное» устроен по-другому, и разница стоит того, чтобы её увидеть. У каждого слушателя своя очередь, и общий указатель тут бессмысленен. Браузер помнит последние 50 сыгранных треков в localStorage под ключом dm.radio.recent и передаёт их идентификаторы в параметре exclude ручки /api/radio/personal/next - сервер отдаёт следующий трек из ваших лайков, не повторяя то, что только что было. За 30 секунд до конца текущего трека клиент заранее подгружает следующий, чтобы переход был без паузы на загрузку. Лимит exclude - 100 идентификаторов, чтобы URL не разбухал.

Не Icecast и не кроссфейд

Классическое интернет-радио устроено иначе: станция генерирует непрерывный аудиопоток, сервер-репликатор (Shoutcast от Nullsoft или свободный Icecast) раздаёт его копии всем клиентам, и трафик сервера растёт пропорционально числу слушателей. Там слушатель подключается к «трубе» и ловит whatever playing прямо сейчас. У Disco Magic трубы нет - браузер тянет MP3-файл трека по HTTP, а синхронность обеспечивает API с указателем. Это дешевле по трафику и позволяет отдавать стрим-копию в 160 кбит/с, а полную версию - отдельным файлом для скачивания.

Кроссфейда здесь тоже нет, и это сделано осознанно. Кроссфейд - плавное перетекание хвоста одного трека в начало другого - норма для диджейского сета и часовых миксов, но для радио, где ты пришёл слушать песни целиком, он режет концовки. Вместо этого работает server-side rollover: за 500 мс до конца клиент переспрашивает сервер, получает следующий трек и грузит его. Шов есть, но он честный - вы слышите трек от первого такта до последнего, а не затухающий хвост под вступление следующего.

С mood-сценариями тот же механизм работает на отфильтрованном пуле. Канал mood:v-mashinu собирает треки с темпом 120-140 BPM из трёх танцевальных жанров - это не случайный «всё подряд», а отбор по темпу, под который удобно вести машину. Связка с темпом разобрана отдельно в гайде по BPM диско, а про то, как собрать такой поток руками в плейлист, - в разборе плейлистов итало-диско.

Чтобы при заходе на канал не было пустой карточки «сейчас играет» те 200-600 миллисекунд, пока летит первый sync, сервер встраивает начальный снимок эфира прямо в страницу: текущий трек, его смещение и длина отдаются в HTML, и плеер сразу рисует прогресс-бар, не дожидаясь ответа API. Дальше вступает heartbeat, и карточка обновляется уже через sync. Эта связка «серверный снимок + клиентская синхронизация» и есть то, что делает эфир непрерывным в восприятии - вы никогда не ловите момент «загрузки радио», только уже играющий трек.

Где у непрерывного эфира края

У потока есть границы, которые лучше знать заранее. Ретрансляция между вкладками: если вы открыли канал во второй вкладке, первая тихо останавливается - два потока не дёргают API одновременно. Регистрация нужна только «Избранному»; все broadcast-каналы слушаются анонимно. Rate-limit стоит на ручке personal-next: при флуде запросов с одного IP сервер отвечает 429 с заголовком Retry-After, и клиент уходит в экспоненциальный backoff. Это защита от злоупотреблений, а не платная стена.

Семантика «непрерывный эфир» здесь буквальная: трек сменяет трек по серверному указателю, без рекламы и без вмешательства слушателя. Как европейские радиостанции выстроили ретро-вещание в этом же жанре - отдельно в обзоре ретро-радиостанций Европы. А если хочется не общего эфира, а своего порядка - тот же каталог отдаётся плейлистом, и разница между двумя способами слушать сведена к одному: кто держит позицию - сервер или вы.

Ко всем статьям