Server de Streaming 
=====================
Toata lumea stie sa faca streaming pe Twitch sau Youtube insa aici vom descrie situatia in care vrei sa faci streaming pe un server privat.
Este util in situatia in care vreti sa transmiteti un meci de fotbal de exemplu de pe Voyo si pe youtube este interzis. De asemeni solutia este foarte securizata, streamul se va putea viziona pe serverul
unde aveti pluginul pt stream de catre prieteni care se vor inregistra, voi explica mecanismul mai jos.
Pe calculatorul cu windows veti folosi arhicunoscutul OBS prin intermediul caruia veti face stream catre un custom server.
In Settings/Stream veti alege Service Custom unde veti pune adresa serverului catre care faceti stream:
rtmps://stream.example.com:4443/live
Dedesubt trebuie sa declarati un stremkey prin intermediul caruia puteti avea control, acel streamkey se va declara si in playerul in care se va face vizionarea.
Mai intai de toate aveti nevoie de un server cu linux unde aveti nevoie de 2 domenii:
-stream.example.com - A/CNAME Record obisnuit cu IP public server
-player.example.com - A/CNAME Record cu cloudflare proxied (portocaliu)
Ambele domenii vor pointa in DNS catre acelasi IP public al VPS-ului insa unul pe direct iar celalalt proxied.
Acest server cu linux este recomandat sa fie un VPS in cloud care are nevoie de un bandwidth foarte bun, recomandat macar 1 gbps up/down daca se vor conecta mai multi prieteni.
Instalati prima data docker:
$ apt update && apt upgrade -y
$ apt install docker.io -y
Daca pe masina aveti ip de retea atunci se forwardeaza:
TCP 443
TCP 4443
Eu am pus totul cu root, recomandat este sa se foloseasca totusi un user separat.
Se da allow din firewall pe masina la aceste porturi (iptables, ufw, depinde ce ai).
Exemplu basic iptables pt custom ports exista
aici
Se fac folderele rtmp, php, haproxy, mysql in /root unde vor fi fisierele aferente fiecarui container.
In acest folder vom porni urmatoarele containere:
-nginx cu pluginul RTMP pt stream
-php
-mysql pt baza de date
-haproxy pt a permite sa faceti broadcast securizat de la OBS-ul vostru catre acest server.
Le vom lua pe fiecare in parte, nu e chiar simplu setupul:
Mentionez faptul ca ambele domenii vor avea nevoie de certificate nginx, nu mai acopar aici aceast aspect, fisierele generate trebuie folosite in haproxy si in nginx.
Pt nginx trebuie 2 certificate pt stream.example.com si pt player.example.com sau unul singur wildcard pt *.example.com (asta daca aveti acest domeniu).
HAProxy
Se creeaza fisierul starthaproxy.sh cu care se porneste containerul:
docker run -itd --name haproxy --memory="80m" --restart=always \
--user root --privileged --network=host --dns 1.1.1.1 \
-v /root/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg \
-v /root/haproxy/errors:/etc/haproxy/errors \
-v /opt:/etc/ssl \
haproxy:3.2
Containerul are nevoie de niste fisiere cu erori posibile, luati
acest zip si dezarhivati-l in /root/rtmp.
De asemeni este nevoie de fisierul de configurare haproxy.cfg
De asemeni exista si un fisier pem, acel fisier este certificat bundle stream.example.com care se obtine din cele doua fisiere certificat generate cu LetEncrypt
Se pun efectiv unul sub altul fisierele fullchain si privkey intr-un fisier nou: sigmelu.pem
8404 asculta pe toate interfetele deci vedeti cum il ascundeti din firewall sau eventual un vhost nginx inantea lui.
Nginx
Containerul cu nginx trebuie compilat cu docker build folosind Dockerfile (ultima versiune 2026):
Eu in acest container pe langa RTMP am instalat mai multe module nginx printre care si webdav si image-filter-watermark, se pot scoate f usor insa nu incurca, webdav cel putin este foarte foartte util.
Creati imaginea cu urmatoarea comanda:
docker build -t nginxrtmp:1 .
Sa presupunem ca imaginea creata folosing acest Dockerfile este nginxrtmp:1, creati startnginx.sh:
docker run -itd --name rtmp \
-v /etc/localtime:/etc/localtime:ro \
-v /root/rtmp/nginx:/etc/nginx \
-v /root/rtmp/html:/var/www/html \
-v /opt:/etc/ssl \
-v /root/rtmp/logs:/var/log/nginx \ #deci daca am omis symlinks in Dockerfile
--network=host \
nginxrtmp:1
Pt ca acest container sa porneasca si sa functioneze corespunzator veti avea nevoie si de o structura de subfoldere in /root/rtmp/nginx: conf.d, parole.
De asemeni /root/rtmp/html/unde practic este home directory-ul nginx-ului.
Iata fisierele de configrare ale serverului si ale vhostului cu playerul:
-nginx.conf (se afla in /root/rtmp/nginx):
-cloudflare-realip.inc
Este foarte important de stiut ca setupul presupune Sa aveti control asupra zonei DNS care trebuie sa fie obligatoriu la cloudflare unde recordul catre webplayer.example.com trebuie sa fie proxied(portocaliu)!!!!
Nu functioneaza pe direct!!!!
-player.conf (se afla in /root/rtmp/nginx/conf.d):
-iplist.txt (se afla in /root/rtmp/html):
Acest fisier asigura securiiitatea streamului, clientii care se conecteaza sa vizioneze streamul trebuie sa aiba IP-ul declarat in acest fisier.
Contentul fisierului se genereaza cu ajutorul unui mecanism systemd dupa cum urmeaza:
Se creeaza in /etc/systemd/system doua fisiere:
1. iplist.path:
[Unit]
Description=Watch /root/rtmp/html/iplist.txt for changes
[Path]
PathModified=/root/rtmp/html/iplist.txt
[Install]
WantedBy=multi-user.target
2. iplist.service:
[Unit]
Description=Restart-iplist
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/docker exec -t rtmptest /usr/sbin/nginx -s reload
[Install]
RequiredBy=iplist.path
$ systemctl restart iplist.{path,service}
$ systemctl status iplist.{path,service}
Dupa ce un user se va loga, scriptul php va updata /root/rtmp/html/iplist.txt si serviciul asta iplist(target si service) va vedea ca s-a modifica si ca atare va da reload la nginx.
Valabil si cand un user se va deloga sau daca va inchide browserul si la un moment dat ii va da timeout, de asemeni IP-ul sau se va sterge automat.
Fisierul iplist.txt trebuie sa aiba chmod 777.
Se va crea si un subfolder si anume control unde este un panel de administrare de unde se confirma useri, se sterg etc.
Sursa completa a playerului va fi atasata la sfarsitul tutorialului.
PHP
Containerul cu php trebuie de asemeni compilat cu docker build folosind Dockerfile:
Creati imaginea cu urmatoarea comanda: (aveti
aici imaginea facuta, gata de importat cu docker load -i)
docker build -t php-fpm-websocket:8.3 .
Porniti containerul cu urmatorul fisier startphp.sh:
docker run -itd --name php \
-v /root/rtmp/html:/var/www/html \
-v /root/rtmp/php.ini:/usr/local/etc/php/php.ini \
-v /root/rtmp/fpm:/usr/local/etc/php-fpm.d \
-v /etc/localtime:/etc/localtime:ro \
-v /root/rtmp/sessions:/var/lib/php-sessions \
--privileged --network=host \
php-fpm-websocket:8.3
# Wait for the container to start
sleep 5
# Start the WebSocket server inside the running container
docker exec -d php sh -c '/usr/local/bin/php /var/www/html/live/chat_server.php >> /tmp/chat_server.log 2>&1'
docker exec -d php sh -c '/usr/local/bin/php /var/www/html/live/stream_server.php >> /tmp/stream_server.log 2>&1'
Se vede clar ca se foloseste /root/rtmp/fpm pt a suprascrie ce este in container in /usr/local/etc/php-fpm.d. De asemeni este si volum pt sesiuni.
Scopul pt care am facut treaba asta este ca am vrut ca php-fpm sa porneasca pe portul 9001 si nu 9000, treaba asta se specifica in www.conf pe care trebuie sa il creati in folderul fpm:
S-ar fi putut face mai elegant direct din Dockerfile insa nu am testat, ca atare aveti nevoie de fisierele astea din subfolder fpm:
www.conf:
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9001
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
zz-docker.conf:
[global]
daemonize = no
[www]
listen = 127.0.0.1:9001
listen.allowed_clients = 127.0.0.1
docker.conf:
[global]
error_log = /proc/self/fd/2
; https://github.com/docker-library/php/pull/725#issuecomment-443540114
log_limit = 8192
[www]
; if we send this to /proc/self/fd/1, it never appears
access.log = /proc/self/fd/2
clear_env = no
; Ensure worker stdout and stderr are sent to the main error log.
catch_workers_output = yes
decorate_workers_output = no
E de asemeni nevoie si acesti parametri in php.ini:
expose_php = Off
display_errors = Off
session.use_strict_mode = 1
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Strict
Pt serverul de chat va trebui initializata componenta cboden/ratchet:
$ docker exec -it php sh
/var/www/html # cd live
/var/www/html/live # composer require cboden/ratchet
Comanda asta creeaza subfolderul vendor cu tot ce trebuie si de asemeni in folderul principal apar composer.json si composer.lock.
Are si securizare, acum nu se mai pot trimite mesaje fara sa fii logat.
Protectii aplicate: codul HTML sau JavaScript trimis prin chat este afisat ca text si nu se executa, text foarte lung trunchiat, caractere invizibile, trimis in exces si conexiune fara token.
Noutate!!!! Chatul merge acum si in full screen.
Mentenanta componenta chat:
### Intrare si verificare versiuni:
docker exec -it php sh
cd /var/www/html/live
php -v
composer show cboden/ratchet
### Verificare actualizari disponibile (nu modifica nimic):
composer outdated --direct
composer outdated
### Simulare, apoi audit inainte de update:
composer update --dry-run
composer audit
### Update real, apoi audit dupa:
composer update
composer audit
Mysql
Exista
aici un tutorial cu un manager de parole care foloseste exact acelasi container mysql 5.7, care se porneste asa:
#!/bin/bash
docker run -d -it \
--name mysql57 \
--restart always \
--network marianet --ip 172.19.10.10 \
-e MYSQL_ROOT_PASSWORD='PAROLA_ROOT' \
-e MYSQL_DATABASE=rtmp \
-v /root/mysql57/mysql:/var/lib/mysql \
-v /etc/localtime:/etc/localtime:ro \
mysql:5.7
Este obligatoriu network marianet deci se creeaza in prealabil asa:
docker network create --driver bridge --subnet 172.19.10.0/24 --gateway 172.19.10.1 marianet
Se creeaza baza de date rtmp singur prin parametrul -e daca nu exista si se pun doua tabele:
Se poate si importa un fisier sql exportat in prealabil:
docker exec mysql57 sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction rtmp' > /root/mysql57/export.sql
docker exec -i mysql57 sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" rtmp' < /root/mysql57/export.sql
Pt aplicatia web player este nevoie de user admin:
docker exec -it php bash
php -r 'echo password_hash("parolastrong", PASSWORD_DEFAULT);'
Apoi inapoi in containerul mysql
INSERT INTO users (id, username, password, status) VALUES (4, 'admin', '$2y$10$zXkZdnLH0f1hBhigUvf2YeXboshdmeh9u4JGMkd6rDucnW3GIxfUC', 'active');
SELECT id, username, status, LENGTH(password) AS len FROM users;
In momentul in care baza de date este pornita si nginx si php sunt configurate corect si de asemeni pornite corect, prin intermediul HAProxy puteti porni streamul cu OBS catre stream.example.com cu URL:
rtmps://stream.example.com:4443/live.
Sus cand am discutat de firewall nu am specificat nimic de portul 1935 care este inchis, broadcast cu OBS se face secure, cu certificat.
Streamul din OBS se face cu RTMP H264 insa serverul nginx il preia si il transpune in HLS. In configul de nginx in sectiunea rtmp se poate observa hls. Playerul web se conecteaza la HLS, streamul de fapt este HLS.
Exista si DASH, sunt mai multe variante mai moderne care permit sa faci stream cu codecuri mai noi si mai eficiente de ex av1 insa se reduce f mult si probabilitatea ca cel care se uita sa nu aiba cu ce sa decodeze.
Aceasta este o solutie folosita de ani de zile si chiar daca este putin invechita inca se descurca excelent.
Asadar, aveti
aici arhiva care contine folderul live, deci trebuie sa fie dupa dezarhivare /root/rtmp/html/live. Acesta este playerul care in browser raspunde la https://player.example.com.
Toate fisierele trebuie sa aiba chmod 644 mai putin included.php care are 640.
Va logati cu admin si aveti si un panel de administrare. Linkul este protejat cu htpasswd, pt asta generati .htpasswd in /root/rtmp/nginx/parole/control
htpasswd -c /root/rtmp/nginx/parole/control/.htpasswd admin
Dupa ce puneti fisierul, va functiona si panelul de administrare.
De aici puteti activa/dezactiva un user, puteti sterge sesiunile unui user sau il puteti sterge de tot.
Vedeti de asemeni cine este online, este recomandat sa tineti deschis acest panel pt heartbeat care controleaza timerul sesiunilor. Un client conectat face refresh automat la 37s si oricum aveti lista si statusul clientilor. Un client este delogat dupa last seen > 303s cand este marcat offline cand i se sterge sesiunea si ip-ul din iplist!
Acest admin pannel face singur refresh automat la 1 minut si aveti si timer afisat in pagina. Nu se poate accesa decat cu cont admin.
Revenind la player, o sa vedeti in consola tab Network URL-ul care are linkul catre serverul nginx:
https://stream.example.com/hlsT/359.1791568775.43c85af80d35f2c5cd51c73a0b6462bbaeae6b4b85a16faf08a702702cc5fdc5/956483acb98x.m3u8.
Eh, aici lucrurile se complica putin, aveti nevoie de niste key:
openssl rand -hex 16 # CHEIE_NOUA (32 caractere hex)
openssl rand -hex 6 # NUME_PUBLIC (12 caractere hex)
openssl rand -hex 32 # STREAM_SECRET (64 caractere hex):
In timp ce streamul functioneaza, URL-ul la care se conecteaza playerul contine un token care se roteste la 1 minut, dupa care se genereaza altul. Metoda este f complicata, php apeleaza nginx direct pt treaba asta.
Generati aceste key si le puneti in fisierele aferente:
*
CHEIE_NOUA - Numele streamkey-ului pe care il setati in OBS, care este o keye secreta, numai de unde faceti broadcast cu OBS o folositi. De asemeni trebuie si in /root/rtmp/nginx/nginx.conf (blocul /rtmp-auth)
*
NUME_PUBLIC - In consola browserului veti vedea fisierele m3u8 care servesc streamul si URL contine https://stream.example.com/hlsT/token/
NUME_PUBLIC.m3u8. De asemeni trebuie pusa in /root/rtmp/nginx/nginx.conf (blocul /rtmp-auth) si /root/rtmp/html/live/dashboard.php
*
STREAM_SECRET - cheia HMAC, sta numai in fisierul /root/rtmp/html/live/included.php.
Am explicat ca sa stiti ce e cu ele.
Mai trebuie de asemeni sa creati un subfolder /root/rtmp/html/hlskeys unde se creeaza fisiere .key prin intermediul carora se cripteaza m3u8, trebuie chmod 755 ca sa poata scrie in el.
In acest subfolder trebuie si un .htaccess care contine un rand cu:
Require all denied.
In timp ce OBS face broadcast se poate da cat pe fisier sa vedeti ce contine si unde se poate vedea encriptarea:
docker exec -it rtmptest cat /tmp/hls/956483acb98x.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:260
#EXT-X-TARGETDURATION:2
#EXT-X-KEY:METHOD=AES-128,URI="https://webplayer.example.com/key.php?file=956483acb98x-255.key",IV=0x000000000000000000000000000000FF
#EXTINF:2.000,
956483acb98x-260.ts
#EXTINF:2.000,
956483acb98x-261.ts
#EXTINF:2.000,
956483acb98x-262.t
De asemeni aceste key se rotesc la 1 minut si nu exista in acelasi timp pe server decat maxim 2, cea actuala si cea expirata. Sau chiar una, pt ca cea expirata se sterge imediat:
ls -lah /root/rtmp/html/hlskeys
total 12K
drwxr-xr-x 2 nfsnobody root 51 Oct 10 11:40 .
drwxr-xr-x 19 nfsnobody root 4.0K Oct 10 10:46 ..
-rw-r--r-- 1 nfsnobody nfsnobody 16 Oct 10 11:41 956483acb98x-450.key
-rw-r--r-- 1 root root 19 Oct 10 10:47 .htaccess
Playerul este simplist si se conecteaza la nginx, care nginx preia secure streamul RTMP(s) din obs, il transforma in hls si il serveste mai departe catre player cu acel token rotit. Nu este real time, like I said, utilitatea acestei solutii este sa fie folosit de ex pt o transmisie sportiva. Delay-ul este in jur de 20 s. Calitatea este in functie de ce setari folositi la OBS: Am explicat mai sus ca in OBS la stream faceti catre server custom, iar la Output eu de ex folosesc NVIDIA NVENC H.264, Output 1920x1080, Rate Control VBR, Bitrate 4000 kbps, Maximum Bitrate 9000 kbps, Preset P5: Slow, Tuning Ultra Low Latency, Multipass Mode Two Passes(Quarter Resolution), Profile High. Recomand sa faceti stream cu 60 fps, nu cu 30, conteaza f mult.
Nu conteaza ce bandwidth aveti acasa unde ruleaza OBS pt ca trimiteti streamul o data catre nginx, banda conteaza la server (upload). Daca se conecteaza sa zicem 10 insi, serverul face upload x10 si la un bitrate mediu de 7000kbps va fi x10.
Exista si o caseta de chat, veti putea trimite mesaje real time tuturor celor care sunt logati, nu are chat privat.
Dpdv security, daca setati corect serviciul systemd iplist, si keyle de care am explicat, numai cine este logat poate viziona streamul. URL-ul se vede cat se poate de clar in consola browserului si se poate testa cu un script dintr-o locatie de unde te conectezi la webplayer deci ai sesiune activa cu IP declarat. Sa zicem ca va fi testtoken.sh:
#!/bin/bash
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36'
read -r -p "Pune URL-ul complet (.m3u8): " URL
URL=$(echo "$URL" | tr -d "'\" \r")
start=$(date +%s)
while true; do
code=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' "$URL" \
-H 'referer: https://webplayer.example.com/' \
-H "user-agent: $UA")
el=$(( $(date +%s) - start ))
printf '%s %3ss %s\n' "$(date +%T)" "$el" "$code"
[ "$code" != "200" ] && { echo "Token invalid dupa ~${el}s"; break; }
sleep 10
done
Cum se foloseste: In browserul unde sunteti logat la Webplayer, F12 in consola tab Network, porniti stream, puneti filtru pe m3u8 si dati clic drepta pe cel mai recent fisier m3u8->Copy->Copy URL
Executati scriptul ./testtoken.sh si paste la respectivul URL. O sa vedeti ca expira in MAXIM 3 minute. Deci dupa 3 min devine inutil.
Asadar Streamul nu se poate monta in niciun player, are o protectie in acest sens, este privat, repet, daca serverul este pe un VPS se poate folosi lejer. Dati acces din panelul de administrare numai pretenilor, orice user nu se poate loga decat o data, se permite numai o singura sesiune, deci exclus ca va da parola altcuiva sa se conecteze la stream. Playerul are signup insa contul se creeaza in pending, numai voi cu admin il puteti confirma.
Streamul se poate captura cu OBS si se poate salva DAR peste streamul video am pus un watermakr cu username|IP|data care se vede discret si o data la 8s isi schimba pozitia pe ecran. Cine inregistreaza streamul o sa salveze cu tot cu watermark, trebuie sa te pricepi un pic sa ii faci bypass sa nu se mai afiseze.
Aceasta este o solutie privata, utila dupa cum spuneam pt o transmisiune sportiva care nu se poate viziona gratis nicaieri. Luati abonament pe o luna, dati drumul in browser si stream-uiti mai departe catre serverul privat. O mica paranteza, am observat ca la multe platforme de ex si la Voyo, nu puteti captura streamul in OBS, se vede ecran negru. Solutia este sa dezactivati decodarea hw in browser, se poate face si cu chrome si cu firefox si de abia dupa aia puteti captura imagine plus sunet. Limitati-va la un numar mic de oameni care se vor uita, prieteni, familie, max 10-15 insi (fiecare cu contul lui)
Repet, fiecare isi face cont si trebuie individual sa le confirmati la fiecare, am incercat sa creez un environment cat mai secure.
Enjoy!