7 minutes
Streaming Service. Featuring Laravel and Nginx
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!
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);
});
## 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.