Introduction
I always wondered how the backend of internet radio stations worked. I recently came across the nginx-rtmp-module and decided to try it out. This module allows you to ingest a live stream using the RTMP protocol and serve it to multiple clients using HLS. HLS is a protocol that allows you to serve a live stream to multiple clients using HTTP.
What is RTMP?
RTMP stands for Real Time Messaging Protocol. It is a protocol that allows you to stream audio and video over the internet. It is used by many streaming services such as Twitch, YouTube and Facebook. RTMP is a TCP based protocol and uses port 1935 by default. It is used for ingestion and is not suitable for playback because it is not supported by most browsers.
Set up project files
For this project we require a few files in the root directory. These are:
- Dockerfile
- Nginx config file
- Makefile
Dockerfile
Pretty self explanatory, this is the file to create the Docker image. We manually install Nginx and the RTMP module. Then set up permissions and overwrite the default config file with the one shown below. Then for the CMD we start Nginx in the foreground.
FROM buildpack-deps:bullseye
# Versions of Nginx and nginx-rtmp-module to use
ENV NGINX_VERSION nginx-1.17.3
# Install dependencies
RUN apt-get update && \
apt-get install -y build-essential libpcre3 libpcre3-dev libssl-dev zlib1g zlib1g-dev certbot python3-certbot-nginx
# Download and decompress Nginx
RUN wget -O ${NGINX_VERSION}.tar.gz https://nginx.org/download/${NGINX_VERSION}.tar.gz && \
tar -zxf ${NGINX_VERSION}.tar.gz
# Download and decompress RTMP module
RUN git clone https://github.com/sergey-dryabzhinsky/nginx-rtmp-module.git
# Build and install Nginx
# The default puts everything under /usr/local/nginx, so it's needed to change
# it explicitly. Not just for order but to have it in the PATH
RUN cd ${NGINX_VERSION} && \
./configure \
--with-http_ssl_module \
--add-module=../nginx-rtmp-module && \
make && \
make install
# Create /nginx/hls folders to hold .m3u8 snippets
RUN mkdir /nginx && \
mkdir /nginx/hls
RUN chown -R www-data:www-data /nginx
# Overwrite default config file
COPY nginx.conf /usr/local/nginx/conf/nginx.conf
EXPOSE 1935 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]
Nginx config file
We will overwrite the default config file with this one. We listen on port 1935 for RTMP streams and port 80 for HLS streams. We also set up CORS to allow the HLS stream to be played in the browser.
worker_processes auto;
events {
worker_connections 1024;
}
# RTMP configuration
rtmp {
server {
listen 1935; # Listen on standard RTMP port
chunk_size 4000;
application show {
live on;
# Turn on HLS
hls on;
hls_path /nginx/hls/;
hls_fragment 3;
hls_playlist_length 60;
# disable consuming the stream from nginx as rtmp
deny play all;
}
}
}
http {
sendfile off;
tcp_nopush on;
# aio on;
directio 512;
default_type application/octet-stream;
server {
listen 80;
location / {
# Disable cache
add_header 'Cache-Control' 'no-cache';
# CORS setup
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length';
# allow CORS preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types {
application/dash+xml mpd;
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root /nginx/;
}
}
}
Makefile
This is not strictly required but makes things easier when you are running lots of commands. I have included commands to build the image locally, run it locally, build it for linux and push it to Dockerhub.
The build-linux
command is required as I am working on a Mac which uses the ARM architecture, and the regular docker build
command builds an image that runs on this architecture. We will be deploying the container on a Linux machine in a cloud provider which uses the AMD64 architecture, so we need the extra --platform linux/amd6
to build a compatible image.
There is no private information stored on the image so it is safe to push to Dockerhub. If you are using a private repository you can use the docker login
command to log in to your account before pushing.
build-local:
docker build -t rtmp .
run-local:
docker run -d -p 1935:1935 -p 80:80 --name nginx-rtmp rtmp
build-linux:
docker build --platform linux/amd64 -t rtmp .
push-dockerhub:
docker tag rtmp <your-dockerhub-username>/rtmp && \
docker push <your-dockerhub-username>/rtmp
Testing locally
For this step you will need to install OBS. This is a free and open source software for video recording and live streaming. It is available for Windows, Mac and Linux.
Run the container locally using the run-local
command. Then open OBS and go to Settings > Stream. Set the service to Custom and the server to rtmp://localhost/show
. The stream key can be anything you want, I used show
. Click Apply and then OK.
Then click on Start Streaming and you should see the stream appear in the browser at http://localhost/hls/show.m3u8
.
Deploy to Digital Ocean
This step can be automated (queue follow-up blog post) but I will outline the manual setup here to make it easier to understand. If you don't already have a Digital Ocean account you can sign up here.
Create a droplet
Log in to your Digital Ocean account and click on Create > Droplets. Select the Docker image (comes pre-installed with docker and related tools) - the cheapest plan will be fine. I chose the Frankfurt region but you can choose whichever is closest to you.
Configure the droplet
Once the droplet is created, ssh into it and run the follow commands to pull the image from Dockerhub and run it.
docker pull <your-dockerhub-username>/rtmp
docker run -d -p 1935:1935 -p 80:80 --name nginx-rtmp <your-dockerhub-username>/rtmp
Once this is up and running, you can test it by streaming from OBS as before. You will need to use the IP address of the droplet instead of localhost
in the OBS settings.
Set up HTTPS
Update the Dockerfile
:
FROM debian:bullseye
LABEL maintainer="Charles Harris <charles.harris.de@gmail.com>"
# Versions of Nginx and nginx-rtmp-module to use
ENV NGINX_VERSION nginx-1.17.3
# Install dependencies
RUN apt-get update && \
apt-get install -y build-essential libpcre3 libpcre3-dev libssl-dev zlib1g zlib1g-dev wget git
# Download and decompress Nginx
RUN wget -O ${NGINX_VERSION}.tar.gz https://nginx.org/download/${NGINX_VERSION}.tar.gz && \
tar -zxf ${NGINX_VERSION}.tar.gz
# Download and decompress RTMP module
RUN git clone https://github.com/sergey-dryabzhinsky/nginx-rtmp-module.git
# Build and install Nginx
RUN cd ${NGINX_VERSION} && \
./configure \
--with-http_ssl_module \
--add-module=../nginx-rtmp-module && \
make && \
make install
# Create /nginx/hls folders to hold .m3u8 snippets
RUN mkdir /nginx && \
mkdir /nginx/hls
RUN chown -R www-data:www-data /nginx
# Overwrite default config file
COPY nginx.conf /usr/local/nginx/conf/nginx.conf
EXPOSE 1935 80 443
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]
and the nginx.conf
file:
worker_processes auto;
events {
worker_connections 1024;
}
# RTMP configuration
rtmp {
server {
listen 1935; # Listen on standard RTMP port
chunk_size 4000;
application show {
live on;
# Turn on HLS
hls on;
hls_path /nginx/hls/;
hls_fragment 3;
hls_playlist_length 60;
# disable consuming the stream from nginx as rtmp
deny play all;
}
}
}
http {
sendfile off;
tcp_nopush on;
# aio on;
directio 512;
default_type application/octet-stream;
server {
listen 443 ssl;
server_name livestream.nkfunky.com;
ssl_certificate /etc/letsencrypt/live/livestream.nkfunky.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/livestream.nkfunky.com/privkey.pem;
location / {
# Disable cache
add_header 'Cache-Control' 'no-cache';
# CORS setup
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length';
# allow CORS preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types {
application/dash+xml mpd;
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root /nginx/;
}
}
}
then rebuild the image and push it to Dockerhub. Then ssh into the droplet and run the following commands to pull the new image and run it.
# Set up commands
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce
docker pull cookershades/rtmp
# Certbot setup certificate
sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" certbot/certbot certonly --non-interactive --agree-tos --email "charles.harris.de@gmail.com" --standalone -d livestream.nkfunky.com
# Start server
docker run -p 1935:1935 -p 80:80 -p 443:443 -v "/etc/letsencrypt:/etc/letsencrypt" cookershades/rtmp
LetsEncrypt SSL certificates have a validity of 90 days, so you will need to run the following command to request a certificate renewal within this time period:
# To renew the certbot certificate
sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" certbot/certbot renew
You can easily run this in a cron job on Vercel.