- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

Установка

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

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» — ошибок быть не должно.

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

Предположим, что нам необходимо получить приблизительно следующее:

  1. 02:00-06:00 — ночной плейлист
  2. 06:00-09:00 — утренний плейлист
  3. 09:00-19:00 — дневной плейлист
  4. 19:00-02:00 — вечерний плейлист
  5. пн, ср, пт — 21:00-22:00 — одна программа
  6. пн, ср, чт, пт — 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 — одно удовольствие. Хорошей всем музыки.

Автор: nivs


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/18301

Ссылки в тексте:

[1] теоретические: http://habrahabr.ru/post/149681/

[2] основы: http://habrahabr.ru/post/153817/

[3] Liquidsoap 1.0.1: http://savonet.sourceforge.net/

[4] IceCast 2.3.2: http://www.icecast.org/

[5] официальном сайте: http://savonet.sourceforge.net/download.html