Skip to content

Instantly share code, notes, and snippets.

@ionelmc
Last active August 26, 2022 07:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ionelmc/83b7a5315a587bd8cad9ab8af181061b to your computer and use it in GitHub Desktop.
Save ionelmc/83b7a5315a587bd8cad9ab8af181061b to your computer and use it in GitHub Desktop.
multistream-obs
YOUTUBE_KEY=
FACEBOOK_KEY=
TWITCH_KEY=
TWITCH_HOST=live-fra05.twitch.tv
TROVO_KEY=
*.sh text eol=lf
version: '3.7'
services:
nginx:
build:
context: .
dockerfile: nginx.Dockerfile
env_file:
- .env
depends_on:
- stunnel
ports:
- '80:80'
- '1935:1935'
stunnel:
build:
context: .
dockerfile: stunnel.Dockerfile
ports:
- '19351:19351'
- '19352:19352'
#!/bin/bash
set -xeuo pipefail
envsubst '$YOUTUBE_KEY $FACEBOOK_KEY $TWITCH_HOST $TWITCH_KEY $TROVO_KEY' < /etc/stub/nginx.conf > /etc/nginx/nginx.conf
cat /etc/nginx/nginx.conf
exec "$@"
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright (C) Roman Arutyunyan
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<head>
<title>RTMP statistics</title>
</head>
<body>
<xsl:apply-templates select="rtmp"/>
<hr/>
Generated by <a href='https://github.com/arut/nginx-rtmp-module'>
nginx-rtmp-module</a>&#160;<xsl:value-of select="/rtmp/nginx_rtmp_version"/>,
<a href="http://nginx.org">nginx</a>&#160;<xsl:value-of select="/rtmp/nginx_version"/>,
pid<xsl:value-of select="/rtmp/pid"/>,
built<xsl:value-of select="/rtmp/built"/>&#160;<xsl:value-of select="/rtmp/compiler"/>
</body>
<xsl:for-each select="rtmp/server/application">
<xsl:if test="name != 'live'">
<link rel="stylesheet" href="/{name}.css"/>
</xsl:if>
</xsl:for-each>
<meta http-equiv="refresh" content="5"/>
</html>
</xsl:template>
<xsl:template match="rtmp">
<table cellspacing="1" cellpadding="5">
<tr bgcolor="#999999">
<th>RTMP</th>
<th>#clients</th>
<th colspan="4">Video</th>
<th colspan="4">Audio</th>
<th>In bytes</th>
<th>Out bytes</th>
<th>In bits/s</th>
<th>Out bits/s</th>
<th>State</th>
<th>Time</th>
</tr>
<tr>
<td colspan="2">Accepted:
<xsl:value-of select="naccepted"/>
</td>
<th bgcolor="#999999">codec</th>
<th bgcolor="#999999">bits/s</th>
<th bgcolor="#999999">size</th>
<th bgcolor="#999999">fps</th>
<th bgcolor="#999999">codec</th>
<th bgcolor="#999999">bits/s</th>
<th bgcolor="#999999">freq</th>
<th bgcolor="#999999">chan</th>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bytes_in"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bytes_out"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bw_in"/>
<xsl:with-param name="bits" select="1"/>
<xsl:with-param name="persec" select="1"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bw_out"/>
<xsl:with-param name="bits" select="1"/>
<xsl:with-param name="persec" select="1"/>
</xsl:call-template>
</td>
<td/>
<td>
<xsl:call-template name="showtime">
<xsl:with-param name="time" select="/rtmp/uptime * 1000"/>
</xsl:call-template>
</td>
</tr>
<xsl:apply-templates select="server"/>
</table>
</xsl:template>
<xsl:template match="server">
<xsl:apply-templates select="application"/>
</xsl:template>
<xsl:template match="application">
<tr bgcolor="#999999">
<td class="{name}">
<b>
<xsl:value-of select="name"/>
</b>
<xsl:if test="name != 'live'">
<form action="/{name}/enable" method="post" class="enable">
<button type="submit">Enable</button>
</form>
<form action="/{name}/disable" method="post" class="disable">
<button type="submit">Disable</button>
</form>
</xsl:if>
</td>
</tr>
<xsl:apply-templates select="live"/>
<xsl:apply-templates select="play"/>
</xsl:template>
<xsl:template match="live">
<tr bgcolor="#aaaaaa">
<td>
<i>live streams</i>
</td>
<td align="middle">
<xsl:value-of select="nclients"/>
</td>
</tr>
<xsl:apply-templates select="stream"/>
</xsl:template>
<xsl:template match="play">
<tr bgcolor="#aaaaaa">
<td>
<i>vod streams</i>
</td>
<td align="middle">
<xsl:value-of select="nclients"/>
</td>
</tr>
<xsl:apply-templates select="stream"/>
</xsl:template>
<xsl:template match="stream">
<tr valign="top">
<xsl:attribute name="bgcolor">
<xsl:choose>
<xsl:when test="active">#cccccc</xsl:when>
<xsl:otherwise>#dddddd</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<td>
<xsl:choose>
<xsl:when test="string-length(name) = 0">[EMPTY]</xsl:when>
<xsl:otherwise>
<a href="#">
<xsl:attribute name="onclick">
var d=document.getElementById('<xsl:value-of select="../../name"/>-<xsl:value-of select="name"/>');
d.style.display=d.style.display=='none'?'':'none';
return false
</xsl:attribute>
<xsl:value-of select="name"/>
</a>
</xsl:otherwise>
</xsl:choose>
</td>
<td align="middle">
<xsl:value-of select="nclients"/>
</td>
<td>
<xsl:value-of select="meta/video/codec"/>&#160;<xsl:value-of select="meta/video/profile"/>&#160;<xsl:value-of
select="meta/video/level"/>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bw_video"/>
<xsl:with-param name="bits" select="1"/>
<xsl:with-param name="persec" select="1"/>
</xsl:call-template>
</td>
<td>
<xsl:apply-templates select="meta/video/width"/>
</td>
<td>
<xsl:value-of select="meta/video/frame_rate"/>
</td>
<td>
<xsl:value-of select="meta/audio/codec"/>&#160;<xsl:value-of select="meta/audio/profile"/>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bw_audio"/>
<xsl:with-param name="bits" select="1"/>
<xsl:with-param name="persec" select="1"/>
</xsl:call-template>
</td>
<td>
<xsl:apply-templates select="meta/audio/sample_rate"/>
</td>
<td>
<xsl:value-of select="meta/audio/channels"/>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bytes_in"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bytes_out"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bw_in"/>
<xsl:with-param name="bits" select="1"/>
<xsl:with-param name="persec" select="1"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="showsize">
<xsl:with-param name="size" select="bw_out"/>
<xsl:with-param name="bits" select="1"/>
<xsl:with-param name="persec" select="1"/>
</xsl:call-template>
</td>
<td>
<xsl:call-template name="streamstate"/>
</td>
<td>
<xsl:call-template name="showtime">
<xsl:with-param name="time" select="time"/>
</xsl:call-template>
</td>
</tr>
<tr style="display:none">
<xsl:attribute name="id">
<xsl:value-of select="../../name"/>-
<xsl:value-of select="name"/>
</xsl:attribute>
<td colspan="16" ngcolor="#eeeeee">
<table cellspacing="1" cellpadding="5">
<tr>
<th>Id</th>
<th>State</th>
<th>Address</th>
<th>Flash version</th>
<th>Page URL</th>
<th>SWF URL</th>
<th>Dropped</th>
<th>Timestamp</th>
<th>A-V</th>
<th>Time</th>
</tr>
<xsl:apply-templates select="client"/>
</table>
</td>
</tr>
</xsl:template>
<xsl:template name="showtime">
<xsl:param name="time"/>
<xsl:if test="$time &gt; 0">
<xsl:variable name="sec">
<xsl:value-of select="floor($time div 1000)"/>
</xsl:variable>
<xsl:if test="$sec &gt;= 86400">
<xsl:value-of select="floor($sec div 86400)"/>d
</xsl:if>
<xsl:if test="$sec &gt;= 3600">
<xsl:value-of select="(floor($sec div 3600)) mod 24"/>h
</xsl:if>
<xsl:if test="$sec &gt;= 60">
<xsl:value-of select="(floor($sec div 60)) mod 60"/>m
</xsl:if>
<xsl:value-of select="$sec mod 60"/>s
</xsl:if>
</xsl:template>
<xsl:template name="showsize">
<xsl:param name="size"/>
<xsl:param name="bits" select="0"/>
<xsl:param name="persec" select="0"/>
<xsl:variable name="sizen">
<xsl:value-of select="floor($size div 1024)"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="$sizen &gt;= 1073741824">
<xsl:value-of select="format-number($sizen div 1073741824,'#.###')"/>
T
</xsl:when>
<xsl:when test="$sizen &gt;= 1048576">
<xsl:value-of select="format-number($sizen div 1048576,'#.###')"/>
G
</xsl:when>
<xsl:when test="$sizen &gt;= 1024">
<xsl:value-of select="format-number($sizen div 1024,'#.##')"/>
M
</xsl:when>
<xsl:when test="$sizen &gt;= 0">
<xsl:value-of select="$sizen"/>
K
</xsl:when>
</xsl:choose>
<xsl:if test="string-length($size) &gt; 0">
<xsl:choose>
<xsl:when test="$bits = 1">b</xsl:when>
<xsl:otherwise>B</xsl:otherwise>
</xsl:choose>
<xsl:if test="$persec = 1">/s</xsl:if>
</xsl:if>
</xsl:template>
<xsl:template name="streamstate">
<xsl:choose>
<xsl:when test="active">active</xsl:when>
<xsl:otherwise>idle</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="clientstate">
<xsl:choose>
<xsl:when test="publishing">publishing</xsl:when>
<xsl:otherwise>playing</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="client">
<tr>
<xsl:attribute name="bgcolor">
<xsl:choose>
<xsl:when test="publishing">#cccccc</xsl:when>
<xsl:otherwise>#eeeeee</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<td>
<xsl:value-of select="id"/>
</td>
<td>
<xsl:call-template name="clientstate"/>
</td>
<td>
<a target="_blank">
<xsl:attribute name="href">
http://apps.db.ripe.net/search/query.html&#63;searchtext=
<xsl:value-of select="address"/>
</xsl:attribute>
<xsl:attribute name="title">whois</xsl:attribute>
<xsl:value-of select="address"/>
</a>
</td>
<td>
<xsl:value-of select="flashver"/>
</td>
<td>
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="pageurl"/>
</xsl:attribute>
<xsl:value-of select="pageurl"/>
</a>
</td>
<td>
<xsl:value-of select="swfurl"/>
</td>
<td>
<xsl:value-of select="dropped"/>
</td>
<td>
<xsl:value-of select="timestamp"/>
</td>
<td>
<xsl:value-of select="avsync"/>
</td>
<td>
<xsl:call-template name="showtime">
<xsl:with-param name="time" select="time"/>
</xsl:call-template>
</td>
</tr>
</xsl:template>
<xsl:template match="publishing">
publishing
</xsl:template>
<xsl:template match="active">
active
</xsl:template>
<xsl:template match="width">
<xsl:value-of select="."/>x
<xsl:value-of select="../height"/>
</xsl:template>
</xsl:stylesheet>
worker_processes 1;
include /etc/nginx/modules-enabled/*.conf;
events {
use epoll;
}
error_log /dev/stderr info;
http {
sendfile on;
server_tokens off;
tcp_nopush on;
tcp_nodelay on;
open_file_cache max=1000;
client_max_body_size 10m;
client_body_buffer_size 64k;
large_client_header_buffers 8 32k;
include mime.types;
log_format http '$time_iso8601 "$request" $status @$remote_addr ${request_time}s "$http_referer" "$http_user_agent"';
access_log /dev/stdout http;
gzip on;
gzip_proxied any;
gzip_vary on;
gzip_types
text/plain
text/css
text/js
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
lua_shared_dict streams 1m;
server {
listen 80 default_server reuseport;
server_name _;
server_name_in_redirect on;
location / {
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache';
if_modified_since off;
expires off;
etag off;
}
location = / {
access_log off;
rtmp_stat all;
rtmp_stat_stylesheet stylesheet.xsl;
}
location = /stylesheet.xsl {
access_log off;
}
location = /favicon.ico {
access_log off;
return 200;
}
location /control {
# https://github.com/arut/nginx-rtmp-module/wiki/Control-module
rtmp_control all;
}
location ~ /(\w+)/enable$ {
content_by_lua_block {
ngx.shared.streams:set(ngx.var[1], true)
ngx.redirect("/")
}
}
location ~ /(\w+)/disable$ {
default_type 'text/html';
content_by_lua_block {
ngx.shared.streams:set(ngx.var[1], false)
ngx.say(
"<script>request = new XMLHttpRequest();" ..
"request.open('GET', '/control/drop/publisher?app=".. ngx.var[1] .. "', true);" ..
"request.onload = function() { window.location.href = '/'; }; " ..
"request.send();</script>"
)
}
}
location ~ /(\w+).css$ {
access_log off;
content_by_lua_block {
if ngx.shared.streams:get(ngx.var[1]) then
ngx.say("." .. ngx.var[1] .. "{ background: green }")
ngx.say("." .. ngx.var[1] .. " .enable { display: none }")
else
ngx.say("." .. ngx.var[1] .. "{ background: red }")
ngx.say("." .. ngx.var[1] .. " .disable { display: none }")
end
}
}
location ~ /test/(\w+)$ {
content_by_lua_block {
if ngx.shared.streams:get(ngx.var[1]) then
ngx.exit(ngx.HTTP_OK)
else
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
}
}
}
}
rtmp_auto_push on;
rtmp {
log_format rtmp 'RTMP [$time_local] $command "$app" "$name" "$args" <$bytes_received >$bytes_sent @$remote_addr "$pageurl" "$flashver" ($session_readable_time)';
access_log /dev/stdout rtmp;
server {
listen 1935;
chunk_size 4096;
notify_method get;
application live {
live on;
record off;
push rtmp://127.0.0.1:1935/facebook;
push rtmp://127.0.0.1:1935/youtube;
push rtmp://127.0.0.1:1935/twitch;
push rtmp://127.0.0.1:1935/trovo;
}
application facebook {
live on;
record off;
on_publish http://127.0.0.1/test/facebook;
push rtmp://stunnel:19351/rtmp/$FACEBOOK_KEY;
}
application youtube {
live on;
record off;
on_publish http://127.0.0.1/test/youtube;
push rtmp://stunnel:19352/live2/$YOUTUBE_KEY;
}
application twitch {
live on;
record off;
on_publish http://127.0.0.1/test/twitch;
push rtmp://$TWITCH_HOST/app/$TWITCH_KEY;
}
application trovo {
live on;
record off;
on_publish http://127.0.0.1/test/trovo;
push rtmp://livepush.trovo.live/live/$TROVO_KEY;
}
}
}
# syntax=docker/dockerfile:1.2.1
FROM ubuntu:20.04
#################
RUN test -e /etc/apt/apt.conf.d/docker-clean # sanity check
ARG TZ=Europe/Bucharest
ENV TZ=$TZ
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl software-properties-common gpg-agent \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
strace gdb lsof locate net-tools htop iputils-ping dnsutils \
nano vim tree less telnet socat
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gettext-base nginx libnginx-mod-http-lua libnginx-mod-rtmp
COPY nginx-stylesheet.xsl /usr/share/nginx/html/stylesheet.xsl
COPY nginx.conf /etc/stub/
COPY nginx-entrypoint.sh /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "nginx", "-g", "daemon off;" ]
foreground = yes
setuid = www-data
setgid = www-data
pid =
debug = notice
syslog = no
failover = rr
[facebook]
client = yes
accept = 0.0.0.0:19351
connect = live-api-s.facebook.com:443
CApath = /etc/ssl/certs/
verifyChain = yes
[youtube]
client = yes
accept = 0.0.0.0:19352
connect = a.rtmp.youtube.com:443
connect = b.rtmp.youtube.com:443
CApath = /etc/ssl/certs/
verifyChain = yes
# syntax=docker/dockerfile:1.2.1
FROM ubuntu:20.04
#################
RUN test -e /etc/apt/apt.conf.d/docker-clean # sanity check
ARG TZ=Europe/Bucharest
ENV TZ=$TZ
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl software-properties-common gpg-agent \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
strace gdb lsof locate net-tools htop iputils-ping dnsutils \
nano vim tree less telnet socat
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates stunnel
COPY stunnel.conf /etc/stunnel/
CMD [ "stunnel" ]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment