INTRO
Briefly, we need a Video on Demand (VOD) streaming solution, with Adaptive Bitrate Streaming (ABR). There are two industry standards to be followed to achieve ABR. The former (and the most supported one) is called HTTP Live Streaming (HLS), developed by Apple. The latter one is Dynamic Adaptive Streaming over HTTP (DASH). They both work as following:
You upload your single video source (mp4 AVC h.254 is recommended coded for input source)
Back-end service splits your video source into several variations of video resolution (like 1080p, 720p, 360p video files)
Each resolution of the video then split into small chunks of video playlist, each video chunk being around 5-10 seconds long
Master manifest file is generated, containing all video formats, bandwidth needs and chunked file locations
Then browser reads master manifest file for specific video and decides which quality of the video needs to be fetched from the backend service and appended to video source of the html video player, based on your bandwidth (internet speed)
-Why consider both HLS and DASH if they do the same thing?
-The answer is browser support:
For the sake of simplicity and to get general concept easier, we will discuss only HLS.
Basic terminology
Encoding - compressing a RAW video file to specific codec
Transcoding - converting a compressed video into other codec
Manifests
Manifest is an entry point to video. It is requested by browsers to get all the files, codecs and bandwidth needs of the specific video.
In HLS manifest is also referred to as Master Playlist and looks like this:
For DASH manifest description, follow this link.
Transcoding
Let's say you have a backend service that already implemented file upload feature, and now you need to transcode your uploaded video. To transcode video input into HLS manifest, we will use tool called FFMPEG. Start by installing FFMPEG on your development server, following resource documents How to install FFMPEG on Ubuntu.
Let's say you have following file system structure inside of your backend service:
And you want to do some black magic to generate HLS files from your source. Assuming that you have a control of unix terminal I/O from inside of the back-end service logic you have written, you need to execute following commands in terminal shell in the directory where your source video is uploaded to (in our case, it is ./media folder). some_fun_video_name.mp4 is supposed to be generated by your backend service while uploading the video, in form of Unique ID or HASH whatever you prefer, and to be stored in the DB. For simplicity, we use some_fun_video_name.mp4 as input file to be transcoded. Ideally, ./media/some_fun_video_name/hls directory structure should not exist, it is auto generated based on video title each time we start transcoding. As soon as the uploading is finished, following command needs to be executed to create directories for transcoded output files:
mkdir -p ./media/some_fun_video_name/hls
Then, we need to start FFMPEG transcode process that generates HLS files and playlists for four different video sizes (360p, 480p, 720p, 1080p) by executing following commands:
ffmpeg -i some_fun_video_name.mp4 -profile:v baseline -level 3.0 -s 640x360 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls ./media/some_fun_video_name/hls/360_out.m3u8
ffmpeg -i some_fun_video_name.mp4 -profile:v baseline -level 3.0 -s 800x480 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls ./media/some_fun_video_name/hls/480_out.m3u8
ffmpeg -i some_fun_video_name.mp4 -profile:v baseline -level 3.0 -s 1280x720 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls ./media/some_fun_video_name/hls/720_out.m3u8
ffmpeg -i some_fun_video_name.mp4 -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls ./media/some_fun_video_name/hls/1080_out.m3u8
NOTE: all the some_fun_video_name strings in this example needs to be replaced by video name you have!
FFMPEG now generated 4 different video quality variants and gave us .m3u8 playlist files for each video quality. Now we need to generate a Master playlist file that serves all other child playlists. We are going to create a new file called some_fun_video_name.m3u8 in the same directory as other .m3u8 files, it will be used as a master playlist to access the media. We need to write following scripts to some_fun_video_name.m3u8 master playlist file:
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=375000,RESOLUTION=640x360
360_out.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=750000,RESOLUTION=854x480
480_out.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=1280x720
720_out.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3500000,RESOLUTION=1920x1080
1080_out.m3u8
You can either use programming language of your choice to write to file, or even easier, you can create and write to that file in terminal as following:
touch ./media/some_fun_video_name/hls/some_fun_video_name.m3u8
printf '#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=375000,RESOLUTION=640x360\n360_out.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=750000,RESOLUTION=854x480\n480_out.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=1280x720\n720_out.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=3500000,RESOLUTION=1920x1080\n1080_out.m3u8' > ./media/some_fun_video_name/hls/some_fun_video_name.m3u8
Then it should give you an output similar to this:
All of these generated files must be served by your backend service. For simplicity, I used following python command to serve files:
python -m SimpleHTTPServer 9999
9999 in this case is a port number to run python service on. It gave me access to generated HLS files on browser like this:
At this point, we are done with transcoding, and now we need to proceed with consuming all the HLS playlists generated.
Consuming HLS playlist
To consume HLS playlist, I used Shaka-Player by made Google, as it supports both HLS and DASH manifests. Following are the steps I took to accomplish basic HLS streaming frontend consumer.
Create a file called index.html in the same directory as your uploaded video, so that it can be accessed by visiting http://localhost:9999/index.html
Construct a basic HTML page with one video element in the body
Include needed scripts for Shaka player in the header:
<script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/3.0.1/shaka-player.compiled.js"></script>
<script src="https://shaka-player-demo.appspot.com/node_modules/mux.js/dist/mux.min.js" defer></script>
Paste following script at the very bottom of the body in script tag
var manifestUri = 'http://localhost:9999/media/some_fun_video_name/hls/some_fun_video_name.m3u8';
function initApp() {
// Install built-in polyfills to patch browser incompatibilities.
shaka.polyfill.installAll();
// Check to see if the browser supports the basic APIs Shaka needs.
if (shaka.Player.isBrowserSupported()) {
// Everything looks good!
initPlayer();
} else {
// This browser does not have the minimum set of APIs we need.
console.error('Browser not supported!');
}
}
function initPlayer() {
// Create a Player instance.
var video = document.getElementById('video');
var player = new shaka.Player(video);
// Attach player to the window to make it easy to access in the JS console.
window.player = player;
// Listen for error events.
player.addEventListener('error', onErrorEvent);
// Try to load a manifest.
// This is an asynchronous process.
player.load(manifestUri).then(function() {
// This runs if the asynchronous load is successful.
console.log('The video has now been loaded!');
}).catch(onError); // onError is executed if the asynchronous load fails.
}
function onErrorEvent(event) {
// Extract the shaka.util.Error object from the event.
onError(event.detail);
}
function onError(error) {
// Log the error.
console.error('Error code', error.code, 'object', error);
}
document.addEventListener('DOMContentLoaded', initApp);
Save the document and visit http://localhost:9999/index.html , and you should see the video being streamed. Try changing your networks throttling to see the ABR is working as intended
Top comments (10)
@nodir_dev
Super nice article! many thanks to you!
would I ask to explain these arguments to me plz?
Thank you again for the article.
My email: moyi.pary@gmail.com
Good job man
Thanks a lot 🙃
Great tutorial!
What if I want to add an SRT Subtitle?
Best regards,
Thanks, I haven’t tried it with subtitles yet, but that’s good point, will share my findings here once I try it with subtitles
Nicely explained!!
But just had a small doubt, in point 3 you had mentioned about dividing it into smaller 5-10 seconds chuck. Could not find anything about that in the document.
Thak you.
5-10 second chunks are my approximate values for each output video file, not necessarily have to be that long. Length of the chunks represent duration of video which is obtained upon every buffer request. I just made up those numbers based on average video lengths of output files just for general conception
Thank you, man! Great article.
Nice post, I'll try to implement something like this.