Transcode video source to HLS playlist format for Video on Demand (VOD) Streaming
Published: 2020-07-28 10:52:11

## 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)](https://dev-to-uploads.s3.amazonaws.com/i/ycfg02331vh6qp2q1hak.png)
<figcaption>HLS vs DASH support (source MDN)</figcaption>

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)](https://dev-to-uploads.s3.amazonaws.com/i/jvqk8sw8ti2qg9qlqzrd.png)
<figcaption>HLS master playlist file (.m3u8 file extension)</figcaption>

For DASH manifest description, follow this [link](https://docs.aws.amazon.com/mediatailor/latest/ug/dash-manifest-vod.html).

## 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](https://ffmpeg.org/). Start by installing FFMPEG on your development server, following resource documents [How to install FFMPEG on Ubuntu](https://linuxize.com/post/how-to-install-ffmpeg-on-ubuntu-18-04/).

Let's say you have following file system structure inside of your backend service:

![FS](https://dev-to-uploads.s3.amazonaws.com/i/r16c1u9yl6buskeowora.png)
<figcaption>FS</figcaption>

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!


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](https://dev-to-uploads.s3.amazonaws.com/i/kayiwg56iodx5aki3oyt.png)
<figcaption>folder ./media/some_fun_video_name/hls</figcaption>


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:

![FS](https://dev-to-uploads.s3.amazonaws.com/i/i1uz5jiq444tzy95b7n4.png)

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:

![Access to FS on browser](https://dev-to-uploads.s3.amazonaws.com/i/nlf3ur6ih90snqf8j1ey.png)
<figcaption>Access to FS on browser</figcaption>

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](https://github.com/google/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:

```html
<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

```JS
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

![Final Result](https://dev-to-uploads.s3.amazonaws.com/i/alimig1gjxmyaf8ngjvh.png)
<figcaption>Final Result</figcaption>