Live Streaming
RTMP ingest, live HLS playlists, stream keys, and sharing files between containers with Docker volumes
Live Streaming
Live Streaming
- We can upload and host videos using a streaming protocol
- A Video on Demand (VOD) service
- What about live streaming?
- The video doesn't exist yet when viewers start watching
- Segments are created while the streamer is streaming
- Most live streaming isn't truly live
- There will be several seconds of delay in the stream
- Acceptable loss to gain reliability and scale
RTMP
- Real-Time Messaging Protocol
- Runs over TCP. Default port 1935
- The streamer pushes a continuous stream of video and audio to the server
- Streaming software (OBS, ffmpeg, phones, consoles) speaks RTMP
- Browsers don't
- Viewers need HLS, served over HTTP
- HTTP also works with caches and CDNs, which is how big platforms reach millions of viewers
- RTMP is used for ingest: Getting the stream into your system
Stream Keys
- A stream key is a secret that lets someone stream as a user
- Anyone with the key can stream as that user
- Treat it like an auth token
- Every user gets their own key
- Generate it randomly with at least 80 bits of entropy
- Unlike auth tokens, you can't store only a hash
- You need to show the key to its owner
Don't Leak the Key
- The ingest server names each stream's directory after its stream key
/hls/f3a9c2e17b4d80a65e2f1c8d3b7a4e08/index.m3u8
- If you host that directory as-is, the key is in the URL of every playlist and segment
- Visible in the Network tab of every viewer
- Any viewer could stream as that user
- The key must never appear in any URL, playlist, or segment path requested by viewers
- Host each stream at a public path instead
- eg.
/live/{username}/index.m3u8 - Your server maps the public name to the stream key
- eg.
The Pipeline
- Typical setup (eg. Twitch/YouTube Live/etc.)
Streamer (OBS)
│ RTMP: rtmp://localhost:1935/live + stream key
▼
Ingest server
│ Converts RTMP stream to HLS: A playlist + segments
▼
Web server (Your app)
│ HTTP: Normal GET requests for the playlist and segments
▼
Viewers (hls.js)
- Viewers never talk to the ingest server
The Ingest Server
- Receives RTMP streams and turns them into HLS
- Writes a playlist and segments to disk for each stream
- Keeps updating them as long as the stream is live
- Asks your app whether each stream is allowed
- For the HW, you don't write this
- Use the
tiangolo/nginx-rtmpimage - Same idea as using the
postgresimage instead of writing a database
- Use the
- Your job is everything around it
- Stream keys, deciding who can stream, serving streams to viewers, the front end
The Ingest Server
nginx.conf
- The ingest server is configured with an
nginx.conffile - Copy it into the container at
/etc/nginx/nginx.conf
events {}
rtmp {
server {
listen 1935;
application live {
live on;
# Called when a stream starts and ends
on_publish http://app:8080/api/live/start;
on_publish_done http://app:8080/api/live/end;
# Write each stream as HLS to /hls/{stream_key}/index.m3u8
hls on;
hls_path /hls;
hls_nested on;
}
}
}
nginx.conf
- Accept RTMP streams on port 1935 for the application named
live - Streamers enter:
- Server:
rtmp://localhost:1935/live - Stream Key: Their key
- Server:
- The stream key becomes the name of the stream
nginx.conf
- When a stream starts, the ingest server sends a request to your app
appis the name of your app's service in your compose file- Service names are hostnames between containers
- Change the paths to the endpoints you choose
nginx.conf
- Write every live stream as HLS in
/hls- Inside the ingest container
hls_nested: Each stream gets its own directory named after its stream key/hls/{stream_key}/index.m3u8/hls/{stream_key}/0.ts,1.ts, ...
on_publish
- When a streamer connects, the ingest server sends a POST request to your app
- The body is url-encoded
- Same format as the HTML forms from HW2
- Several fields.
nameis the stream key the streamer entered
on_publish_donesends the same format when the stream ends- Remove the stream from your active streams
POST /api/live/start HTTP/1.1
Host: app:8080
Content-Type: application/x-www-form-urlencoded
app=live&...&call=publish&name=f3a9c2e17b4d80a65e2f1c9d3b7a4e08&type=live
- Respond
200 OKto allow the stream - Respond
403 Forbiddento reject it. The ingest server disconnects the streamer
Three Containers
- Your app now has three services
app: Your Go serverdb: Your databaseingest: nginx-rtmp. Write a Dockerfile in ./ingest
docker compose upmust start all three- nginx looks up the hostname for
on_publishandon_publish_donewhen it starts- If your app's container doesn't exist yet, the ingest server fails
- Use
depends_onto ensure your app starts first
services:
ingest:
build: ./ingest
# ...
db:
image: postgres
# ...
app:
build: .
# ...Live Playlists
- Live HLS playlists change while you watch
- New segments are added every few seconds
- Old segments are removed from the playlist and deleted
- No
#EXT-X-ENDLISTuntil the stream is over
- The player re-requests the playlist every few seconds to find new segments
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:41
#EXT-X-TARGETDURATION:5
#EXTINF:5.000,
41.ts
#EXTINF:5.000,
42.ts
#EXTINF:5.000,
43.ts
Live Playlists
- Caching: The playlist changes every few seconds
- Your caching headers might cause browsers to reuse an old playlist
- Don't send caching headers for live playlists
- To be extra sure, send
Cache-Control: no-store
- To be extra sure, send
- Segments never change once written
- Caching them is fine
- One rendition per stream
- Whatever quality the streamer sends. No ABR for this objective
Docker Volumes
Containers Have Their Own Files
- Each container has its own filesystem
- The ingest server writes streams to
/hlsin the ingest container- Your app can't see them
- Files written inside a container are lost when the container is recreated
docker compose up --build --force-recreate- Uploaded avatars and videos disappear
- We need storage that lives outside the container
Volumes
- A volume maps storage outside the container to a directory inside the container
- This mirrors all files in local directory
./streamsand the container directoryhls- You will be able to see all the hls files in your local directory
- Recommended to use volumes starting with avatar uploads
- You can see the files on your machine. Great for debugging
- Mount the same storage into two containers and they can share files
- Mounting
./streamsto a directory in your app container lets your app access the streams
- Mounting
services:
ingest:
volumes:
- ./streams:/hls
# ...Volume Gotchas
--renew-anon-volumesdeletes anonymous volumes when testing and grading- Bind mounts and named volumes persist
- A volume hides what the image had at that path
- Mount
./uploadsat/app/publicand your front end disappears - Mount only the directories that hold uploaded or streamed files
- Mount
- Directories must exist before you write to them
- Create them in your code at startup if they don't exist