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.