WittCode💻

Create a Live Stream with Nginx and FFmpeg using HLS RTMP and Docker Compose

By

Learn how to create a live stream with Nginx and FFmpeg using HLS, RTMP, and Docker Compose. We will also go over what HLS is, what RTMP is, and how they work.

Table of Contents 📖

Project Demonstration

Below is what we will be building. The video on the left is being live streamed from Nginx using FFmpeg. The window on the right contains logging information from Nginx and FFmpeg about the current live stream.

Image

RTMP and HLS

Real Time Messaging Protocol, or RTMP, is a protocol for streaming data over the internet. It is typically used to stream video and audio. For example, we can stream a large video file with RTMP by breaking it up into smaller packets and sending them to the client. The client then reassembles these packets into video. HLS, or HTTP Live Streaming, is an HTTP based version of streaming data. For example, we can use it to stream live video over HTTP. The browser then reconstructs the video with an HTML video element.

INFO: Note that HLS was created by Apple and works natively on the HTML5 video element on IOS and Android. However, currently, we need an external library to get it to run on Desktop (Chrome, Firefox, etc.).

Environment Variable Setup

To begin, lets set some environment variables to define the location of our Nginx HTTP and RTMP servers.

NGINX_HOST=nginx-c
RTMP_PORT=1935
HTTP_PORT=5555

FFMPEG_HOST=ffmpeg-c

RTMP works on port 1935 by default but we specify it anyway. We also include the locations of the Nginx and FFmpeg containers in the Docker network.

INFO: Note that we will run Nginx in one container and FFmpeg in another. It is best practice to separate any process into its own Docker container, or one service per container.

Configure Nginx RTMP Server

Now lets configure Nginx to use RTMP by using the rtmp context.

rtmp {
  server {
    listen ${RTMP_PORT};
    chunk_size 4096;
    allow publish all;

    application hls {
        live on;
        record off;

        hls on;
        hls_path /tmp/hls;
        hls_fragment 5;
    }
  }
}
  • listen - Port to listen out for connections.
  • chunk_size - Maximum chunk size for stream. Default is 4096. The bigger the value the lower the CPU overhead.
  • allow publish - Allow publishing from specified addresses. For development we will allow anyone to publish. In production, we should restrict it to just our FFmpeg.
  • application - Creates an RTMP application with the specified name. Here we call it hls.
  • live on - Activate live broadcasting.
  • record off - Stream can be recorded in an flv file. Here we won't record anything.
  • hls on - Turn on HLS.
  • hls_path - Directory to place the HLS fragments. The tmp directory in linux stands for temporary and is used to store data that is needed for a short period of time.
  • hls_fragment - The HLS fragment length in time. Defaults to 5 seconds.

Configure Nginx HTTP Server

Now we need to configure an Nginx HTTP server to serve up the HLS fragments.

http {
  server {
      listen ${HTTP_PORT};
      server_name ${NGINX_HOST};

      location / {
          root /usr/share/nginx/html;
          try_files $uri /index.html;
      }

      location /hls {
          types {
              application/vnd.apple.mpegurl m3u8;
              video/mp2t ts;
          }
          root /tmp;
      }
  }
}

Now Nginx will serve up the HLS fragments from /hls and an index.html file from /. We also specify the types inside the hls location. The types directive maps file name extensions to MIME types of responses. video/mp2t is a MPEG transport stream while m3u8 are playlist files used by video players to store the location of media files.

Configure Nginx with nginx.conf

Now lets create the main Nginx configuration file, nginx.conf.

load_module /etc/nginx/modules/ngx_rtmp_module.so;
  
user  root;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

include /etc/nginx/conf.d/rtmp.conf;
include /etc/nginx/conf.d/http.conf;

The most important parts of this file are importing the Nginx RTMP module (which we will install later on) and importing the RTMP and HTTP configuration files.

Creating the HTML Video Element

Now lets create the HTML file that the Nginx HTTP server will serve up to consume from the RTMP stream.

<!DOCTYPE html>
<html lang="en">
  <head>
      <meta charset="UTF-8">
      <title>Live Streaming</title>
      <link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
  </head>
  <body>
    <video id="player" class="video-js vjs-default-skin" height="360" width="640" controls autoplay muted>
        <source src="http://localhost:5555/hls/my-video.m3u8" type="application/x-mpegURL" />
    </video>
    <script src="https://vjs.zencdn.net/8.10.0/video.min.js"></ script>
    <script>
      videojs('#player');
    </ script>
  </body>
</html>

Here we are using Video.js to display the HLS content because, in most browsers, the HTML video element cannot display RTMP streams. Video.js is a web video player that supports modern streaming formats.

Building the FFmpeg Docker Image

Now lets build the FFmpeg Docker image. For this, we will use Ubuntu version 24.04 as the base image.

FROM ubuntu:24.04

Now lets install FFmpeg from APT.

RUN apt update
RUN apt install -y ffmpeg

INFO: -y is for non-interactive mode so we don't have to enter Yes/No when installing packages.

Now we just need to copy over a media file. We'll also set the working directory as media.

WORKDIR /media
COPY <YOUR_VIDEO_FILE> .

Finally we just need to run FFmpeg when the container starts up.

ENTRYPOINT ffmpeg -re -i <YOUR_VIDEO_FILE> -c:v libx264 -c:a aac -f flv rtmp://nginx-c/hls/my-video
  • -re - Tells FFmpeg to read the same number of frames per second as the framerate of the input video. In other words, read the input at its native frame rate.
  • -i - The media input element.
  • -c:v - Specifying the video codec. The recommended encoder for RTMP is h.264.
  • -c:a - Specifying the audio codec. Here we specify it as aac which stands for advanced audio coding. It is the successor of the MP3 format and recommended for RTMP streaming.
  • -f - Forces the input/output file format. Here we set it to flv, or Flash Video, a widely recognized format for RTMP.
  • Send the output to the RTMP server on Nginx. It needs to match our application name which we set as hls. The name provided at the end will be the name of the output file fragments in the tmp directory.

Building the Nginx Docker Image

To use RTMP with Nginx, we need to install the RTMP module. The developers of the official Nginx image made it very easy to install third party modules. We can install modules through pkg-oss, a tool that allows us to create, install, and upgrade third party modules for Nginx. This is all handled for us inside a Dockerfile at the following URL.

https://github.com/nginxinc/docker-nginx/blob/master/modules/Dockerfile

INFO: Note that Docker BuildKit is required to use the following Dockerfile. Docker BuildKit is enabled by default in Docker version 23 but needs to be enabled in previous versions by setting the environment variable DOCKER_BUILDKIT to 1.

We can download the contents of this Dockerfile using cURL.

curl -o Dockerfile https://raw.githubusercontent.com/nginxinc/docker-nginx/master/modules/Dockerfile

The -o option tells cURL to write to a file instead of stdout. We specify the modules we want to install using build arguments. We will do this with Docker Compose.

Creating the Nginx Service

Lets start building the Nginx service with Docker Compose.

version: '3.9'
services:

  nginx:
    image: nginx-i
    container_name: ${NGINX_HOST}
    build: 
      context: nginx
      dockerfile: Dockerfile
      args:
        ENABLED_MODULES: rtmp
    env_file: .env
    ports:
      - ${HTTP_PORT}:${HTTP_PORT}
      - ${RTMP_PORT}:${RTMP_PORT}
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/rtmp.conf:/etc/nginx/templates/rtmp.conf.template
      - ./nginx/http.conf:/etc/nginx/templates/http.conf.template
      - ./nginx/index.html:/usr/share/nginx/html/index.html
  • Create the Nginx service, call the image nginx-i, and name the container after the NGINX_HOST environment variable.
  • Build the image from the Nginx Dockerfile we downloaded from Github. Specify a build argument to add the RTMP module.
  • Add the environment variables to the image.
  • Map the HTTP and RTMP ports from the host machine to the container.
  • Add the volumes. We need to copy over both the RTMP and HTTP configuration files, the main nginx.conf configuration file, and the index.html file.

INFO: Build arguments are passed to Docker at build time, allowing us to customize the build.

Creating the FFmpeg Service

Now Lets start building the FFmpeg service with Docker Compose.

ffmpeg:
  image: ffmpeg-i
  container_name: ${FFMPEG_HOST}
  build:
    context: ./ffmpeg
    dockerfile: Dockerfile
  env_file: .env
  depends_on:
    - nginx
  • Create the FFmpeg service, call the image ffmpeg-i, and name the container after the FFMPEG_HOST environment variable.
  • Build the image using the Dockerfile we created.
  • Add the environment variables to the image.
  • Make it so the FFmpeg service isn't started until after the Nginx service has started. This is so we don't stream to the RTMP server when it isn't ready.

Running the Program

To run the program, all we need to do is run the command docker compose up at the top level of the directory.

docker compose --env-file .env up

We also supply our environment variable file. This will load the environment variables into our docker-compose.yaml file. Output should be similar to the following.

ffmpeg-c  | Output #0, flv, to 'rtmp://nginx-c/hls/my-video':
ffmpeg-c  | frame=    0 fps=0.0 q=0.0 size=       0kB time=00:00:00.04 bitrate=  92.4kbits/s speed=0.785x    
ffmpeg-c  | frame=    0 fps=0.0 q=0.0 size=       0kB time=00:00:00.78 bitrate=   5.0kbits/s speed=1.42x    
ffmpeg-c  | frame=   15 fps= 14 q=31.0 size=     187kB time=00:00:01.17 bitrate=1305.2kbits/s speed=1.09x    
ffmpeg-c  | frame=   67 fps= 42 q=31.0 size=     345kB time=00:00:02.04 bitrate=1378.7kbits/s speed=1.29x    
ffmpeg-c  | frame=   96 fps= 46 q=31.0 size=     394kB time=00:00:02.53 bitrate=1271.4kbits/s speed=1.22x    
ffmpeg-c  | frame=  127 fps= 49 q=31.0 size=     446kB time=00:00:03.05 bitrate=1198.3kbits/s speed=1.18x    
ffmpeg-c  | frame=  157 fps= 51 q=31.0 size=     498kB time=00:00:03.54 bitrate=1151.4kbits/s speed=1.15x    
ffmpeg-c  | frame=  187 fps= 52 q=31.0 size=     551kB time=00:00:04.05 bitrate=1112.7kbits/s speed=1.13x    
ffmpeg-c  | frame=  218 fps= 53 q=31.0 size=     600kB time=00:00:04.56 bitrate=1075.7kbits/s speed=1.11x

Now all we need to do is visit localhost:5555 and enjoy the live stream.

Image