DEV Community

Cover image for Transcode video source to HLS playlist format for Video on Demand (VOD) Streaming
Nodirbek Sharipov
Nodirbek Sharipov

Posted on

Transcode video source to HLS playlist format for Video on Demand (VOD) Streaming

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:

  1. You upload your single video source (mp4 AVC h.254 is recommended coded for input source)

  2. Back-end service splits your video source into several variations of video resolution (like 1080p, 720p, 360p video files)

  3. Each resolution of the video then split into small chunks of video playlist, each video chunk being around 5-10 seconds long

  4. Master manifest file is generated, containing all video formats, bandwidth needs and chunked file locations

  5. 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:

HLS vs DASH support (source MDN)

HLS vs DASH support (source MDN)

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:

HLS master playlist file (.m3u8 file extension)

HLS master playlist file (.m3u8 file extension)

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:

FS

FS

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

NOTE: all the some_fun_video_name strings in this example needs to be replaced by video name you have!

After a while (transcoding takes some time based on PC it is running), output folder contents should look something like this:
folder ./media/some_fun_video_name/hls

folder ./media/some_fun_video_name/hls

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then it should give you an output similar to this:

FS

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
Enter fullscreen mode Exit fullscreen mode

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:

Access to FS on browser

Access to FS on browser

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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

Final Result

Final Result

Top comments (10)

Collapse
 
zamoosh profile image
Mohammad Rahimi

@nodir_dev
Super nice article! many thanks to you!
would I ask to explain these arguments to me plz?

ffmpeg 
-i input.mp4
-profile:v baseline        👈
-level 3.0        👈
-s 640x360
-start_number 0 
-hls_time 10 
-hls_list_size 0 
-f hls        👈
360_out.m3u8
Enter fullscreen mode Exit fullscreen mode

Thank you again for the article.
My email: moyi.pary@gmail.com

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
ilyosdev profile image
Ilyos Olimov

Good job man

Collapse
 
nodir_dev profile image
Nodirbek Sharipov

Thanks a lot 🙃

Collapse
 
rafypichardo profile image
rafypichardo

Great tutorial!

What if I want to add an SRT Subtitle?

Best regards,

Collapse
 
nodir_dev profile image
Nodirbek Sharipov

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

Collapse
 
abhijithdj007 profile image
abhijithdj007

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.

Collapse
 
nodir_dev profile image
Nodirbek Sharipov

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

Collapse
 
n1ezer profile image
Yerassyl Zhanymkanov

Thank you, man! Great article.

Collapse
 
64j0 profile image
Vinícius Gajo

Nice post, I'll try to implement something like this.