Video - ABR and Background Jobs
Adaptive bit-rate streaming with HLS, running slow jobs in the background, and playing video on the front end
Video Uploads
Uploading Video
- We now support file uploads
- We allow users to upload images
- How do we support video uploads?
- The same exact way as images!
- Files are just bytes
- HW3 upload form: A multipart request with parts named "title", "description", and "video"
- Parse the parts, save the video to disk, store its path in your database
- Host it with
Content-Type: video/mp4and support Range requests
The Problem
- You host a video on your web app
- You want high quality so you host a large 1080p mp4
- The entire file is 100's of MB
- Range requests let users start watching without downloading the entire file
- But every user gets the same quality
- Someone visits your site using eduroam on a bad day..
- The video buffers, stutters, or doesn't play at all
- We need a solution that:
- Allows users with slow connections to enjoy your content
- Delivers high quality to users with high-speed Internet
Adaptive Bit-Rate Streaming
Segments
- Split the video into many short files called segments
- Typically ~2-10 seconds of playback each
- The browser downloads one segment at a time
- Only a few seconds need to be downloaded before playback starts
- If the user skips around in the video, only request the segment they skipped to
- If the user leaves the page without finishing the video, the rest is never downloaded
- Each segment is a small, regular file
- No Range requests needed
Adaptive Bit-Rate
- Instead of hosting a single video at a single resolution
- Host multiple versions of the same video at different resolutions
- Each resolution requires a different bit-rate to stream
- Create segments for every version
- With the video split into ~2-10 second segments
- The browser can switch to a different version at any segment
- It chooses the highest bit-rate that fits its current bandwidth
Adaptive Bit-Rate
- The browser adapts the requested bit-rate based on current conditions
- Limited interruption for the user, though quality can change over time
| Segment | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| Bandwidth | Fast | Fast | Drops | Slow | Slow | Recovers | Fast |
| Quality | 1080p | 1080p | 480p | potato | potato | 480p | 1080p |
HLS vs MPEG-DASH
- Two major protocols for segmented, adaptive bit-rate video
- Both use an index file listing the segments. The browser requests segments as it needs them
- HTTP Live Streaming (HLS) - Developed by Apple
- Index files are
.m3u8playlists. Segments are commonly.tsfiles - Wide-spread support. Spec freely available in RFC8216
- Index files are
- Dynamic Adaptive Streaming over HTTP (MPEG-DASH) - Developed by MPEG
- Index file is an XML
.mpdfile - Not supported on Apple devices
- Spec published as ISO/IEC 23009-1 - Available for $245 (!)
- Index file is an XML
- We'll use HLS
HLS Files
- A master playlist lists every version (rendition) of the video
- Each rendition has its own media playlist listing its segments
- Each segment is a few seconds of video and audio
- Your server hosts all of these files
- The browser only needs the URL of the master playlist
public/videos/abc123/
├── master.m3u8
├── 0/
│ ├── index.m3u8
│ ├── index0.ts
│ ├── index1.ts
│ └── index2.ts
└── 1/
├── index.m3u8
├── index0.ts
├── index1.ts
└── index2.ts
Master Playlist
master.m3u8lists each rendition- Its bandwidth, resolution, and codecs
- The path to its media playlist, relative to this file
- The browser chooses a rendition based on its bandwidth
Media Playlist
1/index.m3u8lists the segments of the 360p rendition#EXTINFis the duration of the next segment in seconds#EXT-X-ENDLISTmeans there are no more segments
- Segment paths are relative to the playlist
Watching an HLS Video
- The browser requests each file as it needs it
GET /public/videos/abc123/master.m3u8 Pick a rendition
GET /public/videos/abc123/1/index.m3u8 Start with 360p
GET /public/videos/abc123/1/index0.ts
GET /public/videos/abc123/1/index1.ts Bandwidth looks good
GET /public/videos/abc123/0/index.m3u8 Switch to 720p
GET /public/videos/abc123/0/index2.ts
- These are all normal GET requests for files
- Your server doesn't need to know anything about HLS to host them
- Just the right MIME types
Converting Video
Media Processing
- You do not always want to store user uploaded media as-is
- Users can upload anything. Never trust your users!
- Even your most trustworthy users will upload large media files
- Process the media when it's uploaded
- Convert/Compress the video
- Store and serve the converted files
- All media stored in formats you choose
- For ABR: Convert each upload into HLS at several bit-rates
ffmpeg
- ffmpeg is the answer for video manipulation
- A command line program
- Your Go code runs it with a system call
- Same as typing the command into the command line
exec.Command("ffmpeg", "-i", "input.avi", "-f", "mp4", "output.mp4").Run()- ffmpeg must be installed in your container
- Add this near the top of your Dockerfile so it's cached and you only wait for the install once
RUN apt-get update && apt-get install -y ffmpegThe Provided Conversion
- You are not expected to research ffmpeg for the HW
- This function converts an mp4 into HLS with 2 renditions: 720p and 360p
- Let's see what each argument does
func MP4ToHLS(inputPath string, outputDir string) error {
return exec.Command("ffmpeg", "-i", inputPath,
"-map", "0:v:0", "-map", "0:a:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=-2:720", "-b:v:0", "2500k",
"-filter:v:1", "scale=-2:360", "-b:v:1", "800k",
"-f", "hls", "-hls_playlist_type", "vod",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1",
outputDir+"/%v/index.m3u8",
).Run()
}The Provided Conversion
-iis the input file-mapselects streams from the input for the output0:v:0is the first video stream of the first (only) input.0:a:0is its first audio stream- Mapped twice: Video and audio for each rendition
- If the input has no audio,
0:a:0matches nothing and ffmpeg fails
func MP4ToHLS(inputPath string, outputDir string) error {
return exec.Command("ffmpeg", "-i", inputPath,
"-map", "0:v:0", "-map", "0:a:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=-2:720", "-b:v:0", "2500k",
"-filter:v:1", "scale=-2:360", "-b:v:1", "800k",
"-f", "hls", "-hls_playlist_type", "vod",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1",
outputDir+"/%v/index.m3u8",
).Run()
}The Provided Conversion
- Output video stream 0: Scaled to 720 pixels tall at 2500 kb/s
- Output video stream 1: Scaled to 360 pixels tall at 800 kb/s
-2for the width: Compute the width that preserves the aspect ratio- Rounded to an even number (Required by the encoder)
func MP4ToHLS(inputPath string, outputDir string) error {
return exec.Command("ffmpeg", "-i", inputPath,
"-map", "0:v:0", "-map", "0:a:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=-2:720", "-b:v:0", "2500k",
"-filter:v:1", "scale=-2:360", "-b:v:1", "800k",
"-f", "hls", "-hls_playlist_type", "vod",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1",
outputDir+"/%v/index.m3u8",
).Run()
}The Provided Conversion
-f hls: Output HLS-hls_playlist_type vod: Video on Demand- Keep every segment in the playlist and end it with
#EXT-X-ENDLIST - (As opposed to a live stream)
- Keep every segment in the playlist and end it with
-master_pl_name: Also write a master playlist namedmaster.m3u8
func MP4ToHLS(inputPath string, outputDir string) error {
return exec.Command("ffmpeg", "-i", inputPath,
"-map", "0:v:0", "-map", "0:a:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=-2:720", "-b:v:0", "2500k",
"-filter:v:1", "scale=-2:360", "-b:v:1", "800k",
"-f", "hls", "-hls_playlist_type", "vod",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1",
outputDir+"/%v/index.m3u8",
).Run()
}The Provided Conversion
-var_stream_map: Group the streams into renditions- Rendition 0: video 0 with audio 0. Rendition 1: video 1 with audio 1
%vin the output path is replaced by the rendition numberoutputDir/0/index.m3u8andoutputDir/1/index.m3u8
Runwaits until ffmpeg finishes and returns an error if it failed
func MP4ToHLS(inputPath string, outputDir string) error {
return exec.Command("ffmpeg", "-i", inputPath,
"-map", "0:v:0", "-map", "0:a:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=-2:720", "-b:v:0", "2500k",
"-filter:v:1", "scale=-2:360", "-b:v:1", "800k",
"-f", "hls", "-hls_playlist_type", "vod",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1",
outputDir+"/%v/index.m3u8",
).Run()
}Hosting HLS Files
- Two new file types to host
| Extension | MIME type |
|---|---|
.m3u8 |
application/vnd.apple.mpegurl |
.ts |
video/mp2t |
- The files are in nested directories
- eg.
/public/videos/abc123/0/index0.ts
- eg.
- Store the path to
master.m3u8in your database as the video'shls_path
Background Jobs
Conversions Are Slow
- Converting a video to multiple bit-rates can take a long time
- Seconds for a short clip. Up to minutes for a long video
- Uses a lot of CPU
- If your server converts the video while handling the upload request:
- The user waits on the upload page with no response
- The browser may give up and time out
- Nothing else can happen on that keep-alive connection until the conversion finishes
- Users will think your app is broken
- Respond before the conversion finishes!
Respond First, Process Later
- Receive and buffer the upload
- Save the mp4 to disk and add the video to your database
- Respond to the user
- Start the conversion in the background concurrently
- Handle other requests from this user on the same connection
- When the conversion finishes, update the video in your database
- The video can be watched as an mp4 while it's being converted
- Users see the HLS version once it's ready
Video on the Front End
The Video Element
- Can't use
<img>for video - Use the
<video>element! - Attributes:
controls: Displays the control buttons for the userautoplay: Plays on page load [if allowed by the browser]muted: Mute the audio (Required in Chromium for autoplay)
<video width="400" controls autoplay muted>
<source src="space.mp4" type="video/mp4">
Your browser does not support video playback
</video>The Video Element
- Specify the file in a
<source>element- Requested just like the
srcof an<img>
- Requested just like the
- Can list multiple sources
- The browser uses the first one it supports
- The text is displayed by very old browsers that don't support video
- Can also set
srcon the video element- The provided front end does this in JavaScript
<video width="400" controls autoplay muted>
<source src="space.mp4" type="video/mp4">
Your browser does not support video playback
</video>videoPlayer.src = video.video_path;Playing HLS
- Most browsers cannot play HLS using only the
<video>element- Safari can. Many others can't
- You cannot rely on the browser to support it
- Use a JavaScript video player
- It downloads the playlists and segments, and feeds them to the
<video>element
- It downloads the playlists and segments, and feeds them to the
- The provided front end uses hls.js
- Included in
layout.html
- Included in
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>The Provided Front End
- The video page requests the video from your API
- If the video has an
hls_path, play it with hls.js - Otherwise, play the mp4 directly
- With Range requests if your server and browser supports them