I once made a video livestreaming rival to Twitch for fun.. lmao nerd. Here’s how it worked!

The feed itself: Your easiest and free-est option is nginx-rtmp-module. It’s a plugin for the nginx webserver that accepts rtmp streams (the kind you send to Twitch, etc).

[Edit: It looks like winshining/nginx-http-flv-module is a newer version of this, might support more modern things ongoing. Have a look at it, the config below might not work with it].

Not Laravel yet, right? Well we’re going to need to auth your streams otherwise anyone could stream to anyone’s channel! Lets have a look at that config. Oh, yeah. You could use any web backend here, I just use Laravel myself. Back to the config, on_connect is what we’re after.

on_connect http://example.com/my_auth;

Here you get to talk to your application. No matter what you do behind the scenes here, return a 200 to allow the stream to connect or anything in the 4xx/5xx range to tell nginx to disconnect it. A 3xx response will redirect the stream internally (I think? It’s been a while).

You’ve probably figured out (or you will soon) that if you stream to a stream key.. how do you embed that? You’d have to embed the stream key on the page for anyone with a view source to see. Here’s something neat I came up with. The gist is that a stream comes in, you check the stream key in your app, you allow/deny, the stream is internally redirected to a channel name, the channel name is then referenced instead of the stream key.

This is some quite old draft code I just found in my backups. I don’t think it was the final version. Rewrite it properly!

routes/api.php:

Route::get("streamauth", function(Request $request) {
    $streamKey = Key::where('key', $request->input('name'))->first();

    $rtmpUrl = "rtmp://xx.yy.zz.aa/live";

    return redirect($rtmpUrl."/".$streamKey->channel->slug);
});

nginx.conf:

## This is the config file for the stream ingest server
# We accept the stream (from OBS, xsplit, etc) here
# and turn it into a HLS stream (live video in fragments)
# served through HTTP. Reason we use this is HLS is very
# easy to serve using a Content Delivery Network (CDN)
# closer to individual viewers

# Generic guff. Run as the "nobody" user so if this service gets hacked
# they can't do much else
user  nobody;

# Worker processes being set to auto means it'll spewn one worker per
# CPU we have available. It can be manually set but not really any
# point to do so right now
worker_processes  auto;

# Log files for debugging issues. Here we output logs to a file
# in a full on service we'd probably have a central log server.
error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

# Process ID is stored in this file, generally used to reference it
# later e.g. to kill the process when we want it to stop.
pid        logs/nginx.pid;

events {
    # How many connections we can handle at the same time.
    # The more the better but if the underlying system can't
    # handle it we'll start seeing issues.
    worker_connections  1024;
}

# RTMP - This is the realtime media protocol (i think) that OBS and the likes
# use to stream audio/video. It's a pretty robust protocol however it does
# require an open connection to keep streaming to it. This is why we convert
# to HLS rather than show RTMP to the viewer, as that doesn't require an active
# connection.

rtmp {
    server {
        # Listen on port 1935, the RTMP standard port
        listen 1935;

        # The bigger the chunk the less CPU we need. Comes at a cost of
        # more stream delay I believe. 
        chunk_size 4096;

        # Ping the RTMP connection every "ping" interval. Timeout the
        # connection if we don't get a reply in "ping_timeout". 
        ping 10s;
        ping_timeout 10s;

        # How many streams we want this server to handle at the same time
        # More CPU/RAM/etc (mostly cpu) resources are required for a higher number
        max_streams 100;

        # Generic name "app" this appears when the user inserts their stream settings
        # e.g. rtmp://ingest.thud.tv/app
        application app {

            # When someone attempts to stream to this server we check against this URL
            # On that side we'll also receive the stream key as configured in OBS.
            # Essentially there (but not technically correct) we'll receive the data as
            # http://thud.app.icnerd.com/api/streamauth?name=<stream key> which we can
            # then use to authenticate and identify the incoming stream. 

            # In this case when we request and it's a legitimate stream we get a redirect
            # from the main service. The redirect points us to the internal "live" app
            # below. In the event it's not a legit stream we receive a "401" HTTP code
            # and we drop the stream. 

            on_publish http://thud.app.icnerd.com/api/streamauth;

            # If it aint live it aint workin!
            live on;
        }

        # If we were to use this in OBS this would appear as rtmp://<this server IP>/live
        # but that wont actually work without already being authed first. This is essentially
        # internal to this server. In reality we're redirected to it quietly when our stream
        # key is legit. 

        application live {

            # Here's where it gets a bit confusing - we are authing again! This time however
            # we've already had our stream key authenticated. Now we've been asked to stream
            # to our channel name. If I was using the "ribbalicious" stream key I'd now be
            # essentially streaming to rtmp://<this server IP>/live?name=ribbalicious

            # Why bounce around all over the shop like this? If we didn't then we'd have to 
            # reference the stream key when playing the stream later on the site. That would
            # be absolute chaos when people realised. They'd be taking over the stream every
            # 5 seconds.

            on_publish http://thud.app.icnerd.com/api/streamauth;

            # If it aint live it aint workin!
            live on;

            # Convert the rtmp stream into an HLS stream.
            hls on;

            # Store the HLS fragments in this directory
            hls_path /tmp/hls;

            # Fragment into a new file every
            hls_fragment 1s;

            # Reference this length of the stream in the m3u8 file. 
            hls_playlist_length 10s;
        }

    }
}


# The serving side of things
http {

    # These are just dealios that make the server faster. Not really important but help
    sendfile off;
    tcp_nopush on;
    aio on;
    directio 512;

    server {
        # The base URL. If this were set to /shibble then we'd reference http://ingest.thud.tv/shibble
        # As it's / we don't need to specify a path before the stream name we're after. Keeps things tidy.

        location / {
            types {
                # Sets up what type of files we're serving and what file/mime type they are
                # Not required but it makes sure the CDN etc know what we're doing. 

                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }

            # Serve files from this directory. This is the same dir we set the HLS fragments to go into earlier.
            root /tmp/hls;

            # We set a no-cache header here so that people don't get served dead files. The m3u8 file in particular
            # changes very frequently. The chunks of files will re-use names whenever the stream is re-started for
            # the same channel. We don't want them caching else the stream will be chaos and might show chunks from
            # the previous stream

            add_header Cache-Control no-cache;

            # Browser security thing. We have to tell the browser (where relevant, not so much behind a CDN)
            # that it's OK to serve files from this server cross-origin - cross origin means from a different
            # domain than where the site's running - e.g. it's OK to embed video from ingest.thud.tv on www.thud.tv
            add_header Access-Control-Allow-Origin *;
        }
    }
}

After you’ve got that set up you’d use something like videojs to play your HLS stream across all shapes and sizes of browser. Maybe put it behind a CDN (I used BunnyCDN) in my for-fun go at it). I never figured out chat, was too focused on trying to make it use IRC. I think now I’d use websockets with beyondcode/laravel-websockets.

Fun fact the first thing that I successfully streamed from OBS with a stream key to the proper output channel on the website without leaking a stream key and without the stream dropping a single frame was this video, Learn to Fly - Foo Fighters Rockin’ 1000 Official Video.

I made a followup ish sort of post to this, How much would it cost to start a new live streaming site?, if you’re so bored you want to read more about this.


Maybe you should subscribe to my mail list so you can hear when I do another nerd-dump.