Предполагается, что вы сумели создать свой простейший 24/7 стрим, следуя инструкциям из этого поста https://mikulski.rocks/ru/lofi-strim-24-7-guide/ (данный пост ссылается на шаблон скрипта оттуда). Немного освоились и привыкли к Linux-терминалу (я все показываю на примере Ubuntu), готовы углубиться в дебри Liquidsoap и сделать свое радио более интересным и функциональным. У меня есть для вас несколько примеров, которые могут пригодиться.
ДИСКЛЕЙМЕР: Я не программист и не линуксоид, а лишь энтузиаст-копипастер, который делится тем, в чем смог разобраться. Поэтому, не исключено, что знающим специалистам некоторые моменты или формулировки могут показаться неправильными или нелепыми. Большая часть приводимой информации взята из The Liquidsoap Book, по сути, учебника от самих разработчиков с массой толковых и рабочих примеров. Но который зачастую игнорируют даже опытные пользователи Liquidsoap, пользующиеся исключительно "сухой" документацией (в левом верхнем углу есть выпадающее меню с выбором версии). Всем тем, кто хочет вытянуть из своего скрипта побольше - настоятельно рекомендую к ознакомлению и то и другое! От версии к версии Liquidsoap, случается, что меняется синтаксис отдельных элементов. Основной перечень таких изменений можно найти здесь: https://www.liquidsoap.info/doc-2.2.0/migrating.html Также будет полезно завести виртуальную машину на своем ПК, чтобы сперва обкатывать все эксперименты и примерки на ней, прежде чем выгружать на VPS.
Добавление логотипа / изображения
Завершив гайд https://mikulski.rocks/ru/lofi-strim-24-7-guide/, вы получите на выходе нечто подобное:
Поверх видео-источника (в данном случае GIF-анимация) можно наложить любое изображение с помощью оператора video.add_image
.
Например, логотип канала в правый верхний угол:
background = single("/home/user/radio/background.gif")
background = video.add_image(x=1200, y=20, width=58, height=58, file="/home/user/radio/logo.png", background)
Думаю, что здесь все предельно ясно:x,y
= это координаты расположения на экранеwidth, height
= размер изображенияfile
= путь к изображению
Тень для текста (костыль)
К сожалению, в Liquidsoap нет инструмента, который бы позволил добавить тень для текста, чтобы улучшить его читаемость. Но ничто не запрещает продублировать те же самые данные, чуть сместив их по координатам.
Как и со всеми накладываемыми поверх изображениями, надо учитывать последовательность слоев. То есть, сначала пойдет код “с тенью”, а ниже – основной текст:
#тень текста / text shadow
background = video.add_text(color=0x000000, font="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", speed=0, x=52, y=52, size=26,
get_track_name_text,
background)
#отрисовка текста / drawing text
background = video.add_text(color=0xFFFFFF, font="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", speed=0, x=50, y=50, size=26,
get_track_name_text,
background)
Полоска прогресса воспроизведения
Идею я почерпнул из этой дискуссии https://github.com/savonet/liquidsoap/discussions/3149, где код показан только в общих чертах. Уже совместным мозговым штурмом с get_ked и SpaceMelodyLab мы смогли сделать рабочий вариант, при этом еще и упростив его до одной строчки (пример для разрешения 1280 x 720):
background = video.rectangle(color=0xFFFFFF, x=0, y=700, height=10, width={int(1280.0*source.elapsed(audio) / source.duration(audio))}, background)
#в версии Liquidsoap 2.2.0 для отрисовки прямоугольников изменился синтаксис: вместо video.rectangle -> video.add_rectangle
Стоит отметить, что в некоторых случаях (возможно из-за большого количества текста или изображений на экране), прямоугольник может отрисовываться с глитчами по его нижней границе.
Это можно исправить с помощью костыля: сделать прямоугольник “потолще” и нижнюю границу скрыть под основанием сцены: например, y=715, height=15
(полоска уйдет на 5 пикселей под экран и 10 пикселей останутся видимыми).
Показ следующего трека в очереди
При использовании плейлиста есть возможность “заглядывать” какой трек будет воспроизводиться следующим. Для этого нужно добавить в скрипт функцию log_next,
сохраняющую метаданные в файл (напр. под названием next_song
) и добавить аргумент check_next
к плейлист-источнику, чтобы запускать эту функцию.
def log_next(r)
m = request.metadata(r)
file.write(data="Upcoming Next : #{metadata.artist(m)} - #{metadata.title(m)}", "/home/user/radio/next_song")
true
end
audio = playlist(reload_mode="watch", "/home/user/radio/music", check_next=log_next)
С помощью оператора file.getter
создается новый источник, регулярно считывающий содержимое файла next_song
. Затем, через уже знакомый video.add_text
эта информация выводится на экран.
next_song = (file.getter("/home/user/radio/next_song"))
background = video.add_text(color=0xFFFFFF, font="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", speed=0, x=50, y=85, size=20,
next_song,
background)
Реквесты
В Liquidsoap есть несколько способов создать реквест-систему, чтобы ставить в очередь заявки от слушателей. Я приведу только один, в котором смог разобраться. Метод очень прост, но эффективен:
queue = request.queue()
audio = fallback(track_sensitive=true, [queue, audio])
def on_request()
fname = string.trim(file.contents("/home/user/radio/request"))
log.important("Playing #{fname}.")
queue.push.uri(fname)
end
file.watch("/home/mikulski/radio/request", on_request)
Создается источник с реквестами – queue
. Далее, когда появляется реквест, оператор fallback
переключает основной плейлист (audio
) на queue
. Когда очередь заказов становится пустой, то воспроизведение возвращается обратно к источнику audio
.
Иными словами, fallback
ставит на воспроизведение первый активный источник из перечня (в приоритете слева-направо [queue, audio]
) и когда тот перестает быть активным, переходит к следующему.
Аргумент track_sensitive
, если = true
, то Liquidsoap дожидается, когда закончится текущий трек и только после этого переключает источники; если = false
, то обрывает текущее воспроизведение и переключение происходит сразу же.
Функция on_request
считывает содержимое файла request
(его нужно создать до запуска скрипта), и когда в нем появляется в виде текста полный и корректный путь к треку (например, “/home/user/radio/music/Artist - Title.mp3
“), то этот трек проталкивается в очередь.
В то же время мы указываем, что нужно следить за изменениями в файле request
и когда они происходят, то запускается функция on_request
.
Это очень удобно в контексте чат-ботов: пользователь пишет в чат команду !sr Artist - Title
. Из нее забирается аргумент (Artist - Title
) и сохраняется в файл request, например, в виде
'/home/user/radio/music/' + argument + '.mp3'
Где argument
– это текст, следующий за командой !sr
, то есть Artist - Title
.
Здесь же и главный минус этого способа – запрос должен полностью совпадать с названием трека. Любая опечатка и лишний символ приведут к тому, что Liquidsoap не найдет файл и запрос не обработается.
Собрать такого чат-бота, в целом, несложно: в сети довольно много инструкций и шаблонов для актуальных платформ и мессенджеров.
Джинглы
Добавить свои фирменные перебивки (“Вы слушаете радио…”, “Спасибо, что остаетесь с нами…” и т.д.), также можно различными способами, но я остановлюсь на наиболее простом:
jingles = playlist(reload_mode="watch", "/home/user/radio/jingles")
audio = rotate(weights=[1, 5], [jingles, audio])
Создается плейлист-источник Jingles
с папкой в которой хранятся все аудио-файлы с джинглами (или один). Затем, через оператор rotate
и аргумент weights
указывается, что будет воспроизводиться 1 джингл после 5 треков основного плейлиста.
Естественно, эти значения можно менять на свое усмотрение.
Прямые включения | input.harbor
Если у вас есть желание выходить в прямой эфир, например, со своим dj-сетом или с подкастом, то такое тоже возможно, благодаря оператору input.harbor
.input.harbor
запускает изнутри Liquidsoap Icecast-совместимый сервер, к которому можно подключаться удаленно через программы, которые умеют транслировать на Icecast-сервера.
Например, Mixxx или VST-плагин ShoutVST.
Достаточно указать имя потока (mount
), порт подключения и пароль.
live = input.harbor("live", port=8000, password="hackme")
Чтобы переключиться с основного плейлиста на input.harbor
, когда тот становится активным можно воспользоваться все тем же оператором fallback
live = input.harbor("live", port=8000, password="hackme")
audio = fallback(track_sensitive=false, [live, audio])
#можно сделать так, но в документации рекомендуется создать новый источник для такой комбинации:
#audio_live = fallback(track_sensitive=false, [live, audio])
#следовательно, нужно будет изменить источник аудио и в mux_video-секции:
#radio = mux_video(video=background, audio_live)
Для input.harbor
существует ряд аргументов, наиболее интересными из которых является размер буфера. По умолчанию buffer
= 12. (секунд), а max
(максимальный размер буфера) = 20. (секунд). Значения являются типом float
, поэтому их следует писать с точкой.
Минимальные значения, которые мне удавалось установить для корректной работы 1. и 2.
live = input.harbor("live", port=8000, password="hackme", buffer=1., max=2.)
А вот как извлекать метаданные из input.harbor
я, к сожалению, подсказать не смогу.
Если же вы хотите выводить сигнал со своего микрофона поверх основного плейлиста, то можно воспользоваться оператором add
и через плагин ShoutVST подключаться к стриму прямо из своей DAW.
mic = input.harbor("live", port=8000, password="hackme", buffer=1., max=2.)
audio_mic = add([mic, audio])
#здесь же необходимо создать новый источник, иначе поломается отображение метаданных.
#следовательно, нужно будет изменить источник аудио и в mux_video-секции:
#radio = mux_video(video=background, audio_mic)
По умолчанию, оператор add
будет пытаться сравнять громкость микрофона и плейлиста, что не всегда удобно. Эту функцию можно отключить, чтобы громкость микрофона оставалась “как есть”
audio_mic = add(normalize=false,[mic, audio])
Или с помощью аргумента weights
, сделать микрофон, например, в два раза громче плейлиста
audio_mic = add(weights=[2., 1.], [mic, audio])
Если вы используете Firewall, то не забудьте открыть нужный порт и прописать IP, с которого можно будет подключаться к нему. Также, разрешенный IP можно указать в скрипте Liquidsoap (желательно в самом начале)
settings.harbor.bind_addrs.set(["0.0.0.0"])
#В версии 2.2.0 -> settings.harbor.bind_addrs := ["0.0.0.0"]
Интерактивные значения | Управление громкостью источника в реальном времени
Еще одна любопытная способность Liquidsoap – это интерактивное управление отдельными элементами через встроенный веб-интерфейс. Например, регулировка громкости источника (что будет удобно при подключении микрофона).
Сперва, нужно активировать этот веб-интерфейс добавив в шапку скрипта строчку
interactive.harbor(port=8010, uri="/control")
Так как порт 8000
занят подключением микрофона к input.harbor
, то надо указать любой свободный (напр. 8010
– не забудьте настроить Firewall!). В uri
можно указать любой другой адрес вместо control
на ваше усмотрение.
Веб-интерфейс будет доступен по адресу: http://IP_вашего_VPS:8010/control
Вот, что вы увидите, открыв эту страницу в браузере после запуска скрипта.
Пока что тут ничего нет, так как никаких значений в скрипте пока еще нет.
Самое время их добавить.
Чтобы управлять громкостью источника нужно вписать следующее
volume = interactive.float("volume", 1.)
audio = amplify(volume, audio)
Где 1. – это громкость источника по умолчанию (условно, 100%).
Теперь значения можно регулировать стрелочками, либо вбивать вручную.
Громкость будет меняться в реальном времени!
Но куда удобнее добавить “ограничители”: например, если нет необходимости поднимать громкость выше 100% и делать тише 50%. При этом, поле со значениями преобразуется в ползунок
volume = interactive.float("volume", min=0.5, max=1., 1.)
audio = amplify(volume, audio)
Видео-плейлист
Что ж, до текущего момента мы все рассматривали в контексте “у нас есть аудио-плейлист и зацикленная одиночная анимация в качестве фона”. Я думаю, что вы уже догадались, что вместо оператора single
для фонового изображения, можно так же использовать и playlist
.
Но можно ли воспроизводить плейлист с видео, в которых есть звуковая дорожка? Да, конечно. Единственное, что это более ресурсозатратно: для 720p желательно иметь VPS с двумя ядрами CPU.
Плюс, немного исправить скрипт. Убрать строчку со смешиванием аудио и видео источников (mux_video
), так как она больше не нужна. А все изображения и текст пристегнуть к источнику с видео
videos = playlist(reload_mode="watch", "/home/user/radio/videos_mp4")
videos = mksafe(videos)
#эта строчка больше не нужна -> radio = mux_video(video=background, audio)
#вместо background = video.add_text(..., get_track_name_text, background),
#background = video.add_image(..., background) и background = video.rectangle(..., background)
#будет:
videos = video.add_text(..., get_track_name_text, videos)
videos = video.add_image(..., videos)
videos = video.rectangle(..., videos)
Мой плейлист состоит только из mp4-файлов в разрешении 720p и с битрейтом в районе 3500kbps, но, я так понимаю, что подойдут любые форматы, которые поддерживает ffmpeg
.
Но, если вы захотите подключить микрофон через input.harbor
и оператором add
, то, к сожалению, вы получите в логе бесконечный бег строчки Buffer Overrun и никакого звука с микрофона на стриме.
Тем не менее, в версии Liquidsoap 2.2.0, благодаря новой функции multitrack
можно обойти и этот момент.
Liquidsoap-daemon
В первой части гайда я показал как запускать скрипт фоновым процессом через
nohup liquidsoap <название_скрипта>.liq &
И останавливать командой
killall liquidsoap
Что неудобно в некоторых случаях и не совсем правильно.
Куда лучше создать фоновую системную службу и управлять ею через команды systemctl
– в точности, как управляется Nginx-сервер из этого гайда -> Свой Рестрим-Сервер.
Для этих целей разработчики Liquidsoap приготовили репозиторий, который автоматически произведет все нужные настройки в системе, чтобы добавить такую службу для любого liquidsoap-скрипта.
Первым делом, нужно скачать этот репозиторий с github:
git clone https://github.com/savonet/liquidsoap-daemon.git
И перейти в скачанную папку:
cd liquidsoap-daemon
Теперь остается только исполнить bash
-скрипт (пользователем с sudo
-правами), где аргументом указывается имя liquidsoap-скрипта, предварительно положенного в директорию “~/liquidsoap-daemon/script/
” (именно так рекомендуется разработчиками) :
bash daemonize-liquidsoap.sh <script-name>
или
bash daemonize-liquidsoap.sh <script-name>.liq
или прописав полный путь к нему:
например,
bash daemonize-liquidsoap.sh /home/user/radio/<script-name>.liq
Теперь в системе появится Systemd
-служба с именем – <script-name>-liquidsoap
.
Запуск:
sudo systemctl start <script-name>-liquidsoap
Перезапуск(например, после внесения изменений в скрипт):
sudo systemctl restart <script-name>-liquidsoap
Остановить:
sudo systemctl stop <script-name>-liquidsoap
Посмотреть состояние службы:
sudo systemctl status <script-name>-liquidsoap
Насколько я понял, в настройки службы уже входит автоматический запуск при перезагрузке VPS и рестарт в случае если скрипт забагует и остановится.
Лог (если внутри liquidsoap-скрипта не прописано сохранение лога в другую директорию) будет храниться в папке ~/liquidsoap-daemon/log/
Итог
После всех дополнительных манипуляций скрипт теперь будет выглядеть примерно вот так:
settings.harbor.bind_addrs.set(["0.0.0.0"])
interactive.harbor(port=8010, uri="/control")
#функции для метаданных / metadata functions
song_author = ref('')
def apply_song(m) =
song_author := m["artist"]
end
song_title = ref('')
def apply_song2(m) =
song_title := m["title"]
end
def get_track_name_text()
"$(artist) - $(title)" % [
("artist", !song_author),
("title", !song_title)
]
end
def log_next(r)
m = request.metadata(r)
file.write(data="Upcoming Next : #{metadata.artist(m)} - #{metadata.title(m)}", "/home/user/radio/next_song")
true
end
# источники аудио / audio sources
audio = playlist(reload_mode="watch", "/home/user/radio/music", check_next=log_next)
audio = mksafe(audio)
queue = request.queue()
audio = fallback(track_sensitive=true, [queue, audio])
#live = input.harbor("live", port=8000, password="hackme")
#audio = fallback(track_sensitive=false, [live, audio])
mic = input.harbor("live", port=8000, password="hackme", buffer=1., max=2.)
audio_mic = add(normalize=false,[mic, audio])
#управление громкостью / volume control
volume = interactive.float("volume", min=0.5, max=1., 1.)
audio = amplify(volume, audio)
# реквесты / requests
def on_request()
fname = string.trim(file.contents("/home/user/radio/request"))
log.important("Playing #{fname}.")
queue.push.uri(fname)
end
file.watch("/home/user/radio/request", on_request)
#источник видео / video source
background = single("/home/user/radio/background.gif")
background = video.add_image(x=1200, y=20, width=58, height=58, file="/home/user/radio/logo.png", background)
#полоска прогресса / progress bar
background = video.rectangle(color=0xfcb900, x=0, y=700, height=10, width={int(1280.0*source.elapsed(audio) / source.duration(audio))}, background)
#вызов метаданных / calling metadata
audio.on_track(apply_song)
audio.on_track(apply_song2)
#чтение next_song / next-song reading
next_song = (file.getter("/home/user/radio/next-song"))
#тени текста / text shadows
background = video.add_text(color=0x000000, speed=0, x=52, y=52, size=26,
get_track_name_text,
background)
background = video.add_text(color=0x000000, speed=0, x=52, y=87, size=20,
next_song,
background)
#отрисовка текста / drawtext
background = video.add_text(color=0xFCB900, speed=0, x=50, y=50, size=26,
get_track_name_text,
background)
background = video.add_text(color=0xFCB900, speed=0, x=50, y=85, size=20,
next_song,
background)
# смешивание / mixing sources
radio = mux_video(video=background, audio_mic)
#rtmp+codec
url = "rtmp://localhost/live"
enc = %ffmpeg(format="flv",
%video(codec="libx264", width=1280, height=720, pixel_format="yuv420p",
b="750k", maxrate="750k", minrate="750k", bufsize="1500k", profile="Main", preset="veryfast", framerate=30, g=60),
%audio(codec="aac", samplerate=44100, b="128k"))
#вывод / output
output.url(fallible=true, url=url, enc, radio)