Последний месяц я активно пытаюсь внедрить в трансляцию Mikulski_Radio возможность удаленного подключения микрофона для общения с чатом. Несмотря на то, что я несколько раз уже заходил в тупик, все же есть существенное продвижение к искомому результату. И, похоже, что эта идея реализуется, правда, в течение неопределенного времени, пока в пререлизной версии Liquidsoap не пофиксят некоторые баги.
Напомню, что стрим Mikulski_Radio транслируется не с физического ПК с установленным OBS, а с удаленной виртуальной машины (VPS) на базе Linux. VPS не имеет аудио/видеокарты и весь контроль над системой осуществляется через командную строку (терминал). Мой OBS - это код, написанный на языке программирования Liquidsoap, придуманном для создания радио-подобных трансляций. Текущая стабильная версия 2.1.4, предрелизная - 2.2.x P.S. Я не программист и не линуксоид. Поверхностно поднабрался всякого, изучая руководства и чужие скрипты, решая собственные задачи.
В Liquidsoap предусмотрена такая возможность удаленного подключения аудио-источника с помощью оператора input.harbor
. Грубо говоря, он запускает собственный мини-сервер, к которому можно подключиться используя Icecast-совместимый протокол, указав имя потока, порт и пароль:
mic = input.harbor("live", port=8000, password="hackme")
Для подключения можно использовать любые (как правило, это ди-джейские) приложения, которые умеют отдавать поток в Icecast. Например, Mixxx.
В процессе поиска мне попался VST-плагин — ShoutVST, который для меня куда удобнее, ведь я могу контролировать звук микрофона и того, что попадет в input.harbor
, напрямую из DAW.
Плагин нужно ставить на мастер-шину DAW или в дорожку с микрофоном после всех обработок эффектами.
В Hostname
указывается адрес VPS.Username
в Icecast, по умолчанию, всегда source
.
Далее вбиваются пароль с именем потока (Mountpoint
), задаваемые в аргументах input.harbor
.
Битрейт, сэмплрейт и формат на собственный вкус, а все остальное можно пропустить, т.к. не понадобится в этом случае.

Остается только в скрипте прописать наложение input.harbor
поверх плейлиста с помощью оператора add
:
videos = mksafe(playlist("~/mp4"))
mic = mksafe(input.harbor("live", port=8000, password="hackme"))
stream = add([mic, videos])
Но тут ждал первый облом. При использовании видеофайлов в качестве плейлиста, после подключения к input.harbor
возникает бесконечная ошибка с буфером и звук с микрофона не поступает даже с глитчами на стрим. Попытки увеличить буфер, вплоть до абсурдных значений не привели к ожидаемому результату, как и использование оператора buffer.adaptive
, а также прочих способов обойти эту проблему.
Убедившись, что все прекрасно работает (даже с наименьшим возможным буфером), когда аудио-плейлист смешивается с отдельным видео (например, зацикленной анимацией):
music = mksafe(playlist("~/mp3))
background = mksafe(single("~/background.mp4))
mic=mksafe(input.harbor("mount", port=, password="", buffer=1.5, max=3.0))
music = add([mic, music])
stream = mux_video(video=background, music)
Я начал искать способ разделения видео-источника на два отдельных — видео и аудио, чтобы примешать к аудио input.harbor
, а затем собрать все снова вместе.
Но бесчисленные попытки приводили либо к нерабочим билдам с конфликтами синхронизации (один и тот же источник не может принадлежат ко двум разным внутренним процессам), либо к тому, что стрим запускался с видео, а в качестве аудио принимался только input.harbor
.
Любопытно то, что если вместо input.harbor
использовать input.rtmp
(подключаться из дома через OBS и брать из этого стрима только аудио), то все работало как надо, за тем исключением, что трансляция намертво зависала, после того как останавливался подключаемый стрим в OBS.
Возникла идея, чтобы продублировать источник и у одной копии взять аудио, а у второй видео. Но плейлист здесь бы не подошел: даже если сделать не рандомный порядок воспроизведения, а заведомо прописанный список, то первый же реквест бы все поломал.
Поэтому я решил попробовать запустить дополнительный экземпляр Liquidsoap, в который бы два раза завел стрим через input.rtmp
:
stream_video = mksafe(input.rtmp(id="video",listen=false, "rtmp://IP/stream/live"))
stream_audio = mksafe(input.rtmp(id="audio",listen=false, "rtmp://IP/stream/live"))
mic=mksafe(input.harbor("mount", port=, password="", buffer=1.5, max=3.0))
host = add(normalize=false, [mic, stream_audio])
radio = mux_audio(audio=drop_video(host), drop_audio(stream_video))
И это работает! Правда, лишь некоторое время спустя я заметил, что в этом случае происходит весьма ощутимая рассинхронизация видео со звуком.
Все эти манипуляции я проводил на последней стабильной версии Liquidsoap — 2.1.4. Эта версия также последняя и в линейке 2.1.x, а за ней должна последовать 2.2.0.
В 2.2.x ввели понятие «мультитрека», что собой подразумевает возможность выдергивать из аудио-видео источника отдельные дорожки (например, если в видеофайле хранится несколько дорожек аудио с разными озвучками), проводить над ними манипуляции и собирать воедино.
Когда я зашел в тупик с двумя rtmp-источниками, то обратился к сообществу Liquidsoap на github, где мне и сказали, что следует пробовать предрелизную версию 2.2.x и что в ветку 2.1.x никаких больше правок вноситься не будет, так как весь упор сейчас идет на отлов багов и доработку новой версии. Там же мне подсказали, как правильно написать код для мультитрека:
videos = playlist("path/to/mp4")
let {audio = video_audio, ...tracks} = source.tracks(videos)
mic = input.harbor("mount", port=, password="")
let {audio = mic_audio} = source.tracks(mic)
audio = track.audio.add([mic_audio, video_audio])
stream = source(tracks.{
audio = audio
})
Освоив установку роллинг-релизов с github через opam (+pin add), а также создание свитчей Ocaml для постройки отдельного окружения, чтобы, в случае чего, мгновенно переключиться обратно к стабильной версии без каких-либо проблем.. Я запустил скрипт — и все получилось!
Единственное, стоит отметить, что добавление изображений и текста производится для первоначального видео-источника, и только потом он делится на мультитрек (вообще, разрабы добавили оператор track.video.add_image
, который позволяет добавлять и к отделенной видеодорожке, но пока еще нет track.video.add_text
, так что это лишено смысла на сегодня).
videos = playlist("f:/VODs/NEW_PLAYLIST")
videos = video.add_image(x=0, y=0, width=1280, height=720, file="~/overlay.png", videos)
videos = video.add_text(color=0xFFFFFF, speed=0, x=0, y=0, size=30, "Example Text", videos)
let {audio = video_audio, ...tracks} = source.tracks(videos)
mic = mksafe(input.harbor("live", port=8000, password="hackme", buffer=1., max=2.0))
let {audio = mic_audio} = source.tracks(mic)
audio = track.audio.add(normalize=false, [mic_audio, videos_audio])
stream = source(tracks.{
audio = audio
})
Послесловие с багами
Однако, радость от успеха продлилась недолго: первым выскочил баг с наложением изображений и текста поверх видео (операторы video.add_image
и video.add_text
). Итоговый стрим очень сильно начинал лагать, при этом оверлеи не применялись.
Я подумал, что это не проблема, так как можно вернуться к идее с дополнительным экземпляром Liquidsoap: основной стрим со всей графикой через input.rtmp
запустить во второй и в нем уже применить мультитрек.
А чтобы избежать конфликтов версий (основной стрим в этом случае запускается на версии 2.1.4, а вспомогательный на 2.2.x), то я еще освоил создание Daemon для Liquidsoap — системную службу, которая запускает и управляет приложениями в фоновом процессе. Впрочем, это оказалось несложно, ведь есть готовый скрипт для их создания на github — liquidsoap-daemon.
Надо было лишь убедиться, что скрипты могут запускаться независимо с разных свитчей/окружений Ocaml.
Алгоритм такой: переключаемся на свитч с установленным 2.1.4. (opam switch
), создаем daemon и он закрепляется за этим окружением. Затем можно переключиться на свитч с 2.2.x и при запуске скрипта 2.1.4. через daemon (sudo system ctl start script214-liquidsoap
) он запустится с корректной версии.
Впрочем, и здесь обнаружился баг. В 2.2.x input.rtmp
хоть и распознается декодером, но к выходному стриму невозможно подключиться. И проблема только в видео — если выводить аудио, то все в порядке. Соответственно, в 2.1.4. такого бага нет.
Я передал всю эту информацию разработчикам и спустя некоторое время они внесли правки для операторов video.add
.
Но тут вылезли новые проблемы: во-первых, дополнительные слои текста/изображений стали очень сильно нагружать ЦПУ, а во-вторых, метаданные трека захватывались некорректно (с дополнительными системными символами), что приводило вплоть до вылета скрипта, если выводить такие данные на экран с помощью библиотеки gd
.
И мне снова пришлось обратиться к разработчикам напрямую.
Баг с метаданными поправили очень быстро.
Поэтому, я решил, что можно уменьшить количество отображаемого текста на экране, чтобы снизить нагрузку на ЦПУ и можно запускаться. И лишь спустя часов 10 стабильной работы я обнаружил, что происходит утечка ОЗУ. Оперативная память забивается по 30-40мб в час…
В общем, подводя итоги, можно сказать, что идея оказалась вполне себе работоспособной.
Надо лишь дождаться фикса найденных мною багов. Но сколько это займет времени неизвестно.