Most of the more technical posts on here serve two purposes; to share with a specific group of people, and to remind myself when I need to do it again in a few years time. This one is no exception!

The background behind this is a community group looking to stream video to a small group of people but not wanting to use YouTube or similar for various reasons and the budget didn't extent to professional streaming services like Wowza or Vimeo's streaming offering.

The aim was to make this as 'idiot-proof' as possible. From the organisations point of view they send a stream to an RTSP and it "just works" from OBS, or various other streaming apps. They don't need to worry about setting up the stream or streaming keys or anything like that which might change per broadcast. The URL for viewing it never changes.

Nginx: A veritable swiss-army knife daemon.

I have used nginx for many years, and really have to stop thinking of it as a "web server" - these days it can do so much more, and one of those many talents is a very capable RTSP server, which is what we'll take advantage of here.

It also performs exceptionally well, with a few tweaks to the default config. Indeed, if you don't do transcoding (more on this later) then I found I could reliably serve 60 concurrent viewers with Digital Ocean's $10/mo virtual machine, a fraction of the price of dedicated soltuions.

This is relatively straightforward but I will, however, assume you already know how to build a virtual machine and basic Linux system tasks.

Configuration

There are essentially two "halves" to this, the media processing part, and then simply serving up the stream.

Firstly, we configure nginx to handle the RTSP by putting this at the top of your nginx.conf

worker_processes 1;
worker_rlimit_nofile 100000;
error_log off;

events {
   worker_connections 4000;
   use epoll;
   multi_accept on;
}

rtmp {
   server {
      listen 1935;
      chunk_size 4000;
      notify_method get;
      
      # receive stream
      application livestream {
         live on;
         hls on;
         hls_path /mnt/hls;
         hls_fragment 6s;
         hls_playlist_length 60;
      }
 }

Incredibly, that's all nginx needs to listen to port 1935 for a stream, and automatically generate hls playlist files and segments!

Now, the HTTP configuration. Everyone seems to have their own preferred way to split up configs but the default would be to include the http {} section below in nginx.conf and the server {} block in /etc/nginx/sites-enabled/default.

Please use SSL for your server, i'd recommend using Let's Encrypt for simplicity, but won't go in to how to do that here.

There's a few global options here which will improve HTTP performance for this application, these are mostly determined by trial and error rather than any sort of research – they've worked well for me.

http {
   sendfile on;
   tcp_nopush on;
   tcp_nodelay on;
   keepalive_timeout 65;
   types_hash_max_size 2048;
   server_tokens off;
   open_file_cache max=1000 inactive=20s;
   open_file_cache_valid 30s;
   open_file_cache_min_uses 5;
   open_file_cache_errors off;
   client_body_timeout 10;
   send_timeout 2;
   reset_timedout_connection on;

   include /etc/nginx/mime.types;
   default_type application/octet-stream;
   
   ## disable logging (improves performance hugely!)
   access_log off;
   error_log off;

   gzip on;

   include /etc/nginx/sites-enabled/*;
}

The server config itself is relatively simple (this is probably in etc/nginx/sites-enabled/default);

server {
   ## your usual listen statements / ssl etc should be here!
   
   root /var/www/html;
   index index.html;
   
   location / {
      try_files $uri =404;
   }
   
   location /hls {
      alias /mnt/hls
      add_header 'Cache-Control' no-cache;
      add_header 'Access-Control-Allow-Origin' '*' always;
      add_header 'Access-Control-Expose-Headers' 'Content-Length';
      types {
         application/vnd.apple.mpegurl m3u8;
         video/mp2t ts;
      }
   }
}

The only unusual part is the /hls location, here we point to /mnt/hls (I explain that in a moment) and add a few headers to ensure the browser doesn't cache the HLS files and enable CORS.

The types {} block ensures that the correct Content-Type is sent for the files the HLS player will rely on.

/mnt/hls

You'll see we've set the HLS path above to /mnt/hls, which doesn't yet exist. There are huge performance benefits using a ramdisk for this purpose, which can be created simply;

Firstly, create a directory for the mountpoint mkdir /mnt/hls

Then add the following line to /etc/fstab and run mount /mnt/hls

tmpfs   /mnt/hls tmpfs   nodev,nosuid,noexec,nodiratime,size=512M   0 0

You don't need to worry about this filling up in normal use, nginx will keep on top of the HLS segment files and playlists etc.

After you've created this you can restart nginx;
/etc/init.d/nginx restart

Player

Now you have a stream you need something to play it with, there's a few browsers that support simple `<video>` tags but the simplest method I've found is video.js - you can customise this page with standard CSS etc, but all you really need is to replace your index.html file with a simple page like this;

<!DOCTYPE html>
<html>
<head>
  <link href="https://vjs.zencdn.net/7.11.2/video-js.css" rel="stylesheet" />
</head>
<body>
  <video-js id="video-player" width="700" class="video-js vjs-default-skin" preload="auto" autoplay controls playsinline muted>
    <source src="/hls/broadcast.m3u8" type="application/x-mpegURL">
  </video-js>

  <script src="https://vjs.zencdn.net/7.11.2/video.min.js"></script>
  <script>
   var player = videojs('video-player', {
     nativeAudioTracks: false,
     nativeVideoTracks: false,
     liveui: true,
     vhs: { overrideNative: true }
   });
   player.play();
  </script>
</body>
</html>

This will present a very basic player, but it'll work. There's a lot more information on the video.js site to customise the player interface.

Go Live!

That's it. Fire up OBS or your favourite streaming app and configure an RTSP server as rtsp://<your server ip>:1935/livestream/ and the stream key as broadcast

You'll note the name broadcast in the player HTML file where we reference broadcast.m3u8 - you can use any word here to create different streams that are entirely independant of each other, just change broadcast.m3u8 to <your stream key>.m3u8

Security Considerations

Note that this is NOT in any way shape or form 'secure' as described above. Anyone can view the stream, anyone can stream to it.

The simplest way to control access is via iptables, just block port 1935 to anyone you don't want to be able to stream. However a slightly nicer soluton would be to password-protect the endpoint;

This can be done trivially in nginx itself;

In nginx.conf directly below the live on; line add the following; on_publish http://localhost/stream_auth

Then, in the server {} config block in etc/nginx/sites-enabled/default add the following location;

location /stream_auth {
   if ($arg_pwd = 'Your-Secret-Password-Here') {
      return 200;
   }
   return 401;
}

Now, the streamer will require to use the password provided above (the username is ignored)

Performance and Scalability

On the $10/mo Digital Ocean VM this can comfortably serve around 100+* concurrent viewers without dropping frames. This is a very unscientific test, but given how easy it is to configure (backup the config files somewhere!) I spun up a higher performance one for just a few hours on the day it was needed.

I originally thought this would serve 60 or so concurrent viewers, then revised to 100, however I did a really simple test with access to some beefier hardware to simulate the load of many more viewers.

With 300 concurrent instances of youtube-dl consuming the live stream it worked absolutely fine. When I joined as the 301st viewer using an iOS device it was still working fine with no latency at all.

The peak according to netdata appeared to be around 186 requests per second to nginx, and the 5min load average peaked around 1.13.

This very unscientific test suggest this simple $10/mo Digital Ocean Virtual Machine could comfortably serve 300 viewers of a live stream.

Improvements

The biggest improvement is providing Adaptive HLS streams, allowing the content to adapt to the quality of connection yoru viewers are using (or even the size of the window or their device so as not to waste high bandwidth HD streams on those watching in smaller windows)

This does require a lot more processing power, however, but I've detailed how to adapt this simple configuration to do adaptive HLS streaming within nginx here.

For commercial / professional use you also want to consider things like backup streams, archival, lower latency etc but these are all beyond the scope of this simple guide and may be better suited to using off-the-shelf streaming servers.