devzone.org.ua/post/proksuvannia-m3u8-zasobamy-icecast.md
2025-11-01 14:28:20 +02:00

125 lines
No EOL
8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Проксування потоку m3u8 засобами ffmpeg в Icecast
Я не маю значного досвіду з адміністрування стрімінгових сервісів, раніше на базі Icecast робив тільки локальну [ротацію музичної колекції](https://devzone.org.ua/post/veb-radio-v-linux-vstanovlennia-servera-icecast-ta-bazove-nalashtuvannia-rotatsiyi-z-ezstream), але зацікавив проєкт довкола-айтішних стрімів [eQtv](https://tv.equalitie.org/uk/live/). Цей матеріал - невеличка нотатка про налаштування проксі на прикладі сервісу [eQtv українською мовою для локальних мереж](https://devzone.org.ua/topic/retransliatsiia-eqtv-ukrayinskoiu-movoiu-dlia-lokalnykh-merez-audio-32-kbs).
Витягнути потік виявилось задачею не тривіальною, я її постійно відкладав але згодом таки знайшов в дебагах (`ctrl+shift+i`) наступні доріжки:
* `https://eqtv.live:8083/eqtvua/eqtvua480/chunks_dvr.m3u8` - схоже, що відповідає за відео
* `https://eqtv.live:8083/eqtvua/eqtvua_hd_ukr/chunks_dvr.m3u8` - відповідає за аудіо-ряд (їх 3)
## ffmpeg
Вже звичний мені ezstream не вміє проксувати потоки з URL, тому віднайшов спосіб з ffmpeg:
``` bash
ffmpeg -i "https://eqtv.live:8083/eqtvua/eqtvua_hd_ukr/chunks_dvr.m3u8?nimblesessionid=xxx" -c:a copy icecast://user:password@127.0.0.1:8000/eQtv.aac
```
У прикладі вище - потік передається в Icecast "як є" у форматі AAC, але такий формат мені не зовсім підходить, бо я хочу окрім оверлейних мереж Yggdrasil і Mycelium ще й стрімити в I2P з його "вузьким" каналом. Хоч конвертація вимагає додаткових ресурсів CPU, все ж вирішив її застосувати, звівши до поширеного формату MP3 з бітрейтом 32 kb/s:
``` bash
ffmpeg -i "https://eqtv.live:8083/eqtvua/eqtvua_hd_ukr/chunks_dvr.m3u8?nimblesessionid=xxx" -b:a 32k icecast://user:password@127.0.0.1:8000/eQtv.mp3
```
Як видно на прикладах вище, я додав до URL джерела аргумент `?nimblesessionid=xxx` - він потрібен для того, щоб ffmpeg не плодив нові сесії в процесі читання. Тут я ще не знаю, як довго протримається поточна сесія, але якщо будуть проблеми - напишу скрипт, що витягає актуальний її номер та доповню цей матеріал.
UPD. очікувано, сесія прожила менше доби, тому створив такий скрипт, будемо пускати його в `ExecStart` сервісу systemd, замість сирої команди `ffmpeg`:
``` /home/eqtv/stream.sh
#!/bin/bash
# завантажуємо актуальний файл m3u8, що містить активний номер сесії
# та витягаємо перше знайдене значення в змінну SESSION_ID
CHUNKS_DVR="/home/eqtv/chunks_dvr.m3u8"
wget -O $CHUNKS_DVR https://eqtv.live:8083/eqtvua/eqtvua_hd_ukr/chunks_dvr.m3u8
SESSION_ID=$(grep -oP '(?<=sessionid=)\d+' $CHUNKS_DVR -m 1)
if [ -z "$SESSION_ID" ]; then
echo "SESSION_ID is empty. Exiting script."
exit 1
fi
# запускаємо стрім з актуальним значенням SESSION_ID
ffmpeg -i "https://eqtv.live:8083/eqtvua/eqtvua_hd_ukr/chunks_dvr.m3u8?nimblesessionid=$SESSION_ID" -b:a 32k icecast://user:password@127.0.0.1:8000/eQtv.mp3
```
* цей скрипт передбачає роботу з systemd: під час помилки процесу, буде виконано `Restart=on-failure`, таким чином ключ сесії буде актуалізовано
## Icecast
Декілька слів про налаштування сервера Icecast. Раніше, для локальних колекцій, мета-інформація про стрім в мене обслуговувалась сервером ezstream. Тут його немає, тому я додав такий набір до конфігурації точки монтування Icecast:
``` /etc/icecast2/icecast.xml
<mount type="normal">
<mount-name>/eQtv.mp3</mount-name>
<username>user</username>
<password>password</password>
<stream-name>eQtv українською мовою (аудіо, 32 kb/s)</stream-name>
<stream-description>eQtv — це проект eQualitie, неприбуткової організації, що розробляє технології для підвищення цифрової стійкості, особливо для спільнот, яким загрожують цензура, стеження, зміна клімату та мережева ізоляція.</stream-description>
<stream-url>https://tv.equalitie.org/uk/live</stream-url>
</mount>
```
* відповідно, `user:password` мають відповідати тим, що вказані в команді `ffmpeg`
## Systemd
Команду `ffmpeg` я виконую від системного сервісу, створивши відповідного користувача:
``` /etc/systemd/system/eqtv-mp3.service
#/etc/systemd/system/eqtv-mp3.service
[Unit]
# якщо Icecast локальний, можна додати icecast2.target поряд з network-online
# After=network-online.target icecast2.target
After=network-online.target
[Service]
Type=simple
User=eqtv
Group=eqtv
# Затримка потрібна у моєму випадку через залежність від Icecast
# ExecStartPre=/bin/sleep 5s
# ExecStart=/usr/bin/ffmpeg -i ...
# ExecStart=/bin/bash /path/to/script.sh
ExecStart=/path/to/script.sh
# Журнали я вимкнув, але можна продебажити наступним чином
StandardOutput=null
# file:///home/eqtv/eqtv-mp3-debug.log
StandardError=null
# file:///home/eqtv/eqtv-mp3-error.log
# Бажано увімкнути, якщо замість команди ffmpeg в ExecStart
# використовується скрипт оновлення номеру сесії
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
* в `ExecStart` треба вказати відповідний набір атрибутів `ffmpeg` або шлях до скрипта (вище)
## Nginx
Як видно з прикладу, мій сервер Icecast крутиться на інтерфейсі `127.0.0.1` для локальних потреб (щоб не ганяти трафік через оверлей). Окрім локалхосту, в мене відбувається ретрансляція на різні мережі IPv6, тому для зручності я проксую клієнтський трафік через Nginx:
``` /etc/nginx/sites-available/default
#/etc/nginx/sites-available/default
server {
listen [202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8000;
listen [505:6847:c778:61a1:5c6d:e802:d291:8191]:8000;
listen xx.xx.xx.xx:8000;
access_log /var/log/nginx/icecast.access.log;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
}
```
* на прикладі `listen` - проксі на Yggdrasil та Mycelium і одна на локальну мережу IPv4
* `proxy_pass` - може бути відмінний локальний порт від вказаного у прикладах