AllInfo
Main: Info Blog Temp Mail


unix 2019-06-08 19-49-01

Интернет-радиостанция на Liquidsoap + IceCast


Довольно много на хабре сказано про интернет-радиовещание изнутри. Есть даже хорошо написанные теоретические основы интернет-радиовещания, с которыми советую ознакомиться. В данной статье я бы хотел рассказать об организации ещё одной любительской интернет-радиостанции, построенной на связке незаслуженно малоизвестного Liquidsoap 1.0.1 и вездесущего IceCast 2.3.2. Статья расчитана на тех, кто хотя бы приблизительно знает, что такое аудиопоток, IceCast, линуксовская консоль и таки что он вообще хочет получить. Однако она и написана начинающим пользователем, поэтому моё решение даже не зарекается на звание оптимального.

Требуемый итоговый функционал

Возможность назначения своего плейлиста для произвольного временного отрезка
Поддержка подхвата OGG, MP3, FLAC в качестве источника для аудиопотока
Динамичность конфигурирования
Простота редактирования контента для радиоэфира
Работа под Linux
Возможность начинающему Linux-пользователю установить и настроить всё это

После довольно долгих проб различных способов получения данного функционала, я остановился именно на Liquidsoap + IceCast. Последний был взят за соответствие требованиям и широкую распространённость (в принципе, аналоги я даже не искал), а Liquidsoap за воистину потрясающие возможности, доступные посредством его функционального языка скриптования. До него я рассматривал ices, ices + ardj, AirTime, что-то ещё, что даже и не упомнить, но все они так или иначе мне не подходили. В общем, я решил воспользоваться Liquidsoap. Из недочетов я заметил только выдачу пустого потока при неизвестных обстоятельствах (после перезапуска) — решается перезагрузкой. К сожалению, всей мощи его я не смогу ощутить — вся документация написана на английском, а с ним у меня не всё гладко — однако что-то я усвоил и постараюсь описать всё, чем могу оперировать.

Установка

Не долго думая я решил всё это чудо поднять на своём ноутбуке под openSUSE 12.2 x64, дабы было удобно изучать функционал Liquidsoap, а уже потом перенести на рабочую машину. В репозиториях присутствовал лишь IceCast, Liquidsoap же пришлось собирать. Пользователи Debian/Ubuntu, Windows, Mac OS X, FreeBSD и ArchLinux могут взять готовые пакеты на официальном сайте.

IceCast

Установку IceCast я выполнил из стандартных репозиториев:
# zypper in icecast

Подробно описывать конфигурацию IceCast я не буду, лишь приведу рабочий пример того, что крутится у меня:

/etc/icecast.xml
<icecast>
<limits>
<clients>100</clients>
<sources>2</sources>
<threadpool>5</threadpool>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>10</source-timeout>
<burst-on-connect>1</burst-on-connect>
<burst-size>65535</burst-size>
</limits>

<authentication>
<source-password>mypass</source-password>
<relay-password>mysecondpass</relay-password>
<admin-user>adminuser</admin-user>
<admin-password>mythirdpass</admin-password>
</authentication>
<hostname>localhost</hostname>
<listen-socket>
<port>8000</port>
</listen-socket>
<fileserve>1</fileserve>

#_____________________________________________________________
# Наш запасной источник. В статье о нём не упоминается, ибо и у меня пока что он не сделан так, чтобы людям не стыдно было показать. А вообще к нему цепляется ices с музыкой из папки secure.

<mount>
<mount-name>/secure</mount-name>
<hidden>1</hidden> # Делаем невидимым - пользователи не смогут им воспользоваться
<charset>UTF8</charset> # Кодировка
</mount>

# Описываем соответствующие mount'ы
<mount>
<fallback-mount>/secure</fallback-mount> # Какой источник подхватывать, если текущий упал
<fallback-override>1</fallback-override> # Позволяет перебрасывать текущих клиентов на запасной источник без потери связи
<fallback-when-full>1</fallback-when-full> # При достижении максимума слушателей, новому клиенту будет представлен запасной источник
<mount-name>/HabraRadio_192</mount-name> # Название mount'а.
<charset>UTF8</charset> # Кодировка
</mount>

<mount>
<fallback-mount>/secure</fallback-mount>
<fallback-override>1</fallback-override>
<fallback-when-full>1</fallback-when-full>
<mount-name>/HabraRadio_320</mount-name>
<charset>UTF8</charset>
</mount>

<mount>
<fallback-mount>/secure</fallback-mount>
<fallback-override>1</fallback-override>
<fallback-when-full>1</fallback-when-full>
<mount-name>/HabraRadio_vorbis_avg_128</mount-name>
<charset>UTF8</charset>
</mount>

#_____________________________________________________________

<paths>
<basedir>/usr/share/icecast</basedir>
<logdir>/var/log/icecast</logdir>
<webroot>/usr/share/icecast/web</webroot>
<adminroot>/usr/share/icecast/admin</adminroot>
<alias source="/" dest="/status.xsl"/>
</paths>

<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
</logging>

<security>
<chroot>0</chroot>
<changeowner>
<user>icecast</user>
<group>icecast</group>
</changeowner>
</security>
</icecast>


Liquidsoap

Качаем, подготавливаем:
$ git clone https://github.com/savonet/liquidsoap-full.git liquidsoap
$ cd liquidsoap
$ make init
$ cp PACKAGES.minimal PACKAGES

Для меня достаточно минимального набора, но если кому нужна поддержка ещё чего-либо, то следует отредактировать файл PACKAGES.
Для нормальной компиляции программы я устанавливал следующие пакеты:
# zypper in make autoconf automake ocaml libao-devel libmad-devel libmp3lame-devel flac-devel libgavl-devel ocaml-camomile-devel ocaml-camlimages-devel ocaml-camomile-data libtheora-devel ocaml-findlib-devel libsamplerate-devel libtag-devel libvorbis-devel gcc-c++ ocaml-pcre-devel libtiff-devel libjpeg62-devel libXpm-devel

Собираем:
$ ./bootstrap
$ ./configure --with-user=user --with-group=users
$ make
# make install

К слову, после выполнения "./configure ..." стоит проверить, весь ли функционал, нужный нам, будет доступен в собранной программе. Сделать это можно просто посмотрев на таблицу, выведенную при завершении "./configure ..."
Всё, проверить работоспособность программы можно выполнив «liquidsoap --version» — ошибок быть не должно.

Ориентируемся с расписанием эфира

Предположим, что нам необходимо получить приблизительно следующее:
02:00-06:00 — ночной плейлист
06:00-09:00 — утренний плейлист
09:00-19:00 — дневной плейлист
19:00-02:00 — вечерний плейлист
пн, ср, пт — 21:00-22:00 — одна программа
пн, ср, чт, пт — 18:00-19:00 — вторая программа

Расположение в файловой системе:
radio
├── collection | Аудиофайлы
│ ├── efir | Музыка и джинглы
│ │ ├── daytime | Дневной плейлист
│ │ │ ├── jingles | Джинглы, играющие днём
│ │ │ └── music | Музыка, играющая днём
│ │ ├── evening | Соответственно, вечер
│ │ │ ├── jingles
│ │ │ └── music
│ │ ├── morning | Утро
│ │ │ ├── jingles
│ │ │ └── music
│ │ └── night | И ночь
│ │ ├── jingles
│ │ └── music
│ ├── programs
│ │ ├── 1_prog | 1 программа
│ │ ├── 2_prog | 2 программа
│ ├── promo | Информационные вставки
│ └── security | Папка с запасной музыкой
├── technical | Конфиги, логи
└── документация | Описание всего этого

Конфигурация Liquidsoap

Сразу приведу готовую конфигурацию:

./radio/technical/start_liquidsoap
#!/usr/local/bin/liquidsoap

# создаём переменные быстрого исправления в одном месте по необходимости
# базовая информация о выводимом потоке
out = output.icecast(
# хост с icecast
host = "127.0.0.1",

# его порт
port = 8000,

# логин
user = "source",

# и пароль
password = "mypass",

# название
name = "Интернет-радио",

# жанр
genre = "Rock",

# ссылка на сайт
url = "http://habrahabr.ru",

# кодировка
encoding = "UTF-8"
)

# включаем telnet-сервер
set("server.telnet.bind_addr","127.0.0.1")
set("server.telnet",true)

# _____________________________________
# Описание файловой структуры нашего радиосервера.
# Переменные можно не использовать, а писать сразу полные пути к плейлистам, но при изменении названия одной из папок, придётся править довольно много строк в конфигурации. Как показала практика, такой подход удобнее.

# абсолютный путь к рабочей директории
wd = "/home/user/radio"

# путь к папке с аудиофайлами
pl = "#{wd}/collection"

# техническая папка
tech = "#{wd}/technical"

# логи
set("log.file.path","#{tech}/liquidsoap.log") # путь к файлу лога
set("log.level", 3) # уровень логирования

# папка с информационными вставками
promo_dir = "#{pl}/promo"

# папка с программами
progr_dir = "#{pl}/programs"

# папка с изменяющимся эфиром
ef = "#{pl}/efir"

# папки соответствующих эфиров
ni = "#{ef}/night"
mo = "#{ef}/morning"
da = "#{ef}/daytime"
ev = "#{ef}/evening"

# папки с музыкой
mus_ni_dir = "#{ni}/music"
mus_mo_dir = "#{mo}/music"
mus_da_dir = "#{da}/music"
mus_ev_dir = "#{ev}/music"

# папки с джинглами
jin_ni_dir = "#{ni}/jingles"
jin_mo_dir = "#{mo}/jingles"
jin_da_dir = "#{da}/jingles"
jin_ev_dir = "#{ev}/jingles"

# плейлисты с программами. Обратите внимание - до этого указывались пути к папкам, а здесь - к простым текстовым файлам.
1_prog_pl = "#{progr_dir}/1_prog.pl"
2_prog_pl = "#{progr_dir}/2_prog.pl"


# _____________________________________
# Создаём объекты типа "source", в нашем случае это аудиоисточники.
# Здесь атрибут "reload" позволяет раз в 360 секунд перечитывать плейлист по пути, указанному далее.
# По умолчанию, музыка проигрывается рандомно, атрибут <code>mode = "normal"</code> указывает на проигрывание по порядку.

# загружаем плейлисты, джинглы, вставки, программы
mus_ni = playlist (reload = 360, "#{mus_ni_dir}")
mus_mo = playlist (reload = 360, "#{mus_mo_dir}")
mus_da = playlist (reload = 360, "#{mus_da_dir}")
mus_ev = playlist (reload = 360, "#{mus_ev_dir}")

jin_ni = playlist (reload = 360, "#{jin_ni_dir}")
jin_mo = playlist (reload = 360, "#{jin_mo_dir}")
jin_da = playlist (reload = 360, "#{jin_da_dir}")
jin_ev = playlist (reload = 360, "#{jin_ev_dir}")

promo = playlist (reload = 360, "#{promo_dir}")
1_prog = playlist (reload = 360, "#{1_prog_pl}", mode = "normal")
2_prog = playlist (reload = 360, "#{2_prog_pl}", mode = "normal")

# _____________________________________
# строим 4 потока, сразу всё перемешивая
# смешиваем вставки
ins_ni = rotate (weights = [2, 1], [jin_ni, promo])
ins_mo = rotate (weights = [2, 1], [jin_mo, promo])
ins_da = rotate (weights = [2, 1], [jin_da, promo])
ins_ev = rotate (weights = [2, 1], [jin_ev, promo])

# смешиваем вставки и потоки
ni = rotate (weights = [3, 1], [mus_ni, ins_ni])
mo = rotate (weights = [3, 1], [mus_mo, ins_mo])
da = rotate (weights = [3, 1], [mus_da, ins_da])
ev = rotate (weights = [3, 1], [mus_ev, ins_ev])

#_______________________________________________________________________
# конфигурируем расписание эфира

radio = switch (track_sensitive = true,
[
({ (1w21h - 1w22h) or (3w21h - 3w22h) or (5w21h - 5w22h)}, 1_prog),
({ (1w18h - 1w19h) or (3w18h - 3w19h) or (4w18h - 4w19h) or (5w18h - 5w19h)}, 2_prog),
({ 2h - 6h }, ni),
({ 6h - 9h }, mo),
({ 9h - 19h }, da),
({ 19h - 2h }, ev)
])

#_______________________________________________________________________

# добавляем crossfade
radio = crossfade(start_next=1., fade_out=1., fade_in=1., radio)

# и, наконец, запускаем вещалки с разным качеством

out(
%vorbis.abr(samplerate = 44100, channels = 2, bitrate = 128, max_bitrate = 192, min_bitrate = 96),
description = "Average vorbis 96-128-192 Kbps",
mount = "HabraRadio_vorbis_avg_128",
mksafe(radio)
)

out(
%mp3(bitrate = 320, id3v2 = true),
description = "MP3 320 Kbps",
mount = "HabraRadio_320",
mksafe(radio)
)

out(
%mp3(bitrate = 192, id3v2 = true),
description = "MP3 192 Kbps",
mount = "HabraRadio_192",
mksafe(radio)
)


Несколько комментариев:
1) Эти строки:
wd = "/home/user/radio"
pl = "#{wd}/collection"
ef = "#{pl}/efir"
ni = "#{ef}/night"
mus_ni_dir = "#{ni}/music"
mus_ni = playlist (reload = 360, "#{mus_ni_dir}")

вполне успешно можно заменить на
mus_ni = playlist (reload = 360, "/home/user/radio/collection/efir/night/music")

и ничего вам за это не будет — Liquidsoap просто вместо #{wd} подставляет значение переменной wd.

2) В месте, где мы внедряли вставки, были строки:
ins_ni = rotate (weights = [2, 1], [jin_ni, promo])
ni = rotate (weights = [3, 1], [mus_ni, ins_ni])

rotate() — позволяет регулировать очередью
weights = [2, 1], [jin_ni, promo] — указывает брать 2 трека из jin_ni, затем 1 из promo, после повторно 2 трека из jin_ni и так далее.
weights = [3, 1], [mus_ni, ins_ni] — указывает брать 3 трека из mus_ni, затем 1 трек из того уже перемешанного плейлиста, что получился строкой ранее (ins_ni).

3) При конфигурировании расписания эфира мы использовали следующие строки:
radio = switch (track_sensitive = true,
[
({ (1w21h - 1w22h) or (3w21h - 3w22h) or (5w21h - 5w22h)}, 1_prog),
({ (1w18h - 1w19h) or (3w18h - 3w19h) or (4w18h - 4w19h) or (5w18h - 5w19h)}, 2_prog),
({ 2h - 6h }, ni),
({ 6h - 9h }, mo),
({ 9h - 19h }, da),
({ 19h - 2h }, ev)
])


switch() — переключает аудиоисточники в заданное время.
track_sensitive = true — позволяет не прерывать текущий трек, даже если время активного плейлиста истекло. Т.е. если ночной трек начался в 05:59, то, пока он не закончится, в силу не вступит утренний плейлист.
({ (1w21h - 1w22h) or (3w21h - 3w22h) or (5w21h - 5w22h)}, 1_prog), — по понедельникам, средам и пятницам с 21 до 22 часов играть источник 1_prog.
Насколько я понял, те строкив списке switch(), что выше расположены, имеют высший приоритет.

Формирование плейлиста радиопрограммы

Принцип прост: в папке prog_1 лежат файлы радиопередачи вида «01_ProgName», где записаны голоса радиоведущих. Плейлист будет вида:

01_ProgName
Music152
02_ProgName
Music241
03_ProgName
Music937
...

Думаю, на bash было бы правильней писать генератор такого плейлиста, но я недавно дорвался до Python, посему на скорую руку написал на нём:

./radio/technical/generatorProg1.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import os
import random

finalPaylist = '/home/user/radio/collection/programs/1_prog.pl'
music = '/home/user/radio/collection/efir/evening/music/'
show = '/home/user/radio/collection/programs/1_prog/'
myShow = sorted(os.listdir(show))
myMusic = os.listdir(music)
listOfTracks = []

def getRandomTrack(list):
i = 0
buf = random.choice(myMusic)
while (buf in list) & (not i == 100):
i += 1
buf = random.choice(myMusic)
return buf

for i in range(60):
if not i%2:
try:
listOfTracks.append(show + myShow[i/2])
except :
listOfTracks.append(music + getRandomTrack(listOfTracks))
else:
listOfTracks.append(music + getRandomTrack(listOfTracks))

myFile = open(finalPaylist, 'w')
for i in range(len(listOfTracks)):
myFile.write(listOfTracks[i]+'\n')

В итоге получается плейлист, который покрывает как минимум час эфира даже без файлов радиопередачи. Главное не забыть подхватить этот плейлист в нерандомном режиме. Ну и для второй программы так же плейлист стоит генерировать.

Финальные штрихи

Заносим в crontab генерирование плейлистов:

crontab -e
0 19 * * * /home/user/radio/technical/generatorProg1.py
0 16 * * * /home/user/radio/technical/generatorProg2.py

Добавляем в автозапуск KDE пару строк:

/home/user/.kde4/Autostart/start_liquidsoap.sh
#!/bin/sh
cp /home/user/radio/technical/liquidsoap.log /home/user/radio/technical/liquidsoap_backup.log
cat /dev/null > /home/user/radio/technical/liquidsoap.log
liquidsoap /home/user/radio/technical/start_liquidsoap

Автозапустить IceCast тоже не помешало бы
/etc/init.d/icecast start
chkconfig --add icecast

И, что важно, на последок стоит настроить NTP — здесь довольно много зависит от верно установленного времени. Его я настроил из-под YaST.

В заключение

Сейчас это радио успешно работает, файлы добавляются ответственным человеком удалённо из-под Windows, что стало возможным с помощью связки openVPN+Samba, программы играют, логи пишутся. На будущее задумано ещё пара фич, среди которых: проигрывание различных вставок в начале каждого часа, реализация своей рандомизации плейлистов, удалённые конференц-звонки в прямой эфир, причёсывание всего и вся, настройка пущей отказоустойчивости, а так же поиск подводных камней. Вообще разбираться с Liquidsoap — одно удовольствие. Хорошей всем музыки.

UPD: Дополнение от milar.

UPD 2: Чтобы в эфире не было тишины после crossfade, необходимо перед crossfade добавить строку:
radio = mksafe(radio)

Большое спасибо kvaps за данное решение.

13.58.191.60 / 2024-12-30_17-10-24 UTC.