HLS Packaging using FFmpeg – Easy Step-by-Step Tutorial

In this tutorial, we shall learn about HLS packaging using FFmeg. The great part about using FFmpeg is that you can ingest a video, resize it, transcode, package, and stream it without leaving the command line!

We shall first take a look at all the steps to create a HLS packaging for VOD and then take a look at packaging for HLS Live Streaming.

If you want to learn more about HLS playlists, check out our collection of HLS m3u8 files to see examples from different vendors with different use-cases. And, if you are new to HLS (HTTP Live Streaming), please read our basic tutorial to HLS Streaming and why you should use ABR streaming.

Without further ado, let’s get started.

Basic Steps to HLS Packaging using FFmpeg

Okay, let’s see what the fundamental steps to packaging a VOD file using HLS are, shall we?

  1. read an input video from disk.
  2. scale/resize the video to the multiple resolutions required.
  3. transcode each of the scaled videos to the required bitrates
  4. transcode the audio to the required bitrates.
  5. combine the video and audio, package each combination, and create the individual TS segments and the playlists.
  6. create a master playlist that points to each of the variants.

Now, let’s tackle this step by step, shall we?

Resize a Video to Multiple Resolutions using FFmpeg

Okay, Step 1 and 2 involve reading a video from disk and scaling it to multiple resolutions. This can be done in a single command as follows (but this is incomplete as I have not specified the output files – its only a partial step in the process 🙂 )

ffmpeg -i brooklynsfinest_clip_1080p.mp4 -filter_complex "[0:v]split=3[v1][v2][v3]; [v1]copy[v1out]; [v2]scale=w=1280:h=720[v2out];[v3]scale=w=640:h=360[v3out]"

[0:v] refers to the input file’s first video stream. In our case, there is only one video stream and this is split into 3 outputs [v1], [v2], [v3]. Each of these are taken as inputs to a scaling function in FFmpeg that accepts a height and width number.

Here, we are scaling the input video to 1080p, 720p, and 360p.

And, [v1out], [v2out], [v3out] are variables that contain the output of the scaling process. Note, here we are assuming that the scaling process is going to retain the aspect ratio. Else, you can force it and apply letterboxing if necessary. For more on this topic, please check our tutorial on resizing and scaling videos using FFmpeg.

Transcode a Video to Multiple Bitrates for HLS Packaging using FFmpeg

Next, we move on to Steps 3 & 4 – we have to transcode the video to multiple bitrates as is typically done for ABR Video Streaming.

Remember, that we have already scaled the video at the required resolutions and stored the output in [v1out], [v2out], and [v3out]. Let’s use those directly as inputs for the transcoding step.

-map "[v1out]" -c:v:0 libx264 -x264-params "nal-hrd=cbr:force-cfr=1" -b:v:0 5M -maxrate:v:0 5M -minrate:v:0 5M -bufsize:v:0 10M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
-map "[v2out]" -c:v:1 libx264 -x264-params "nal-hrd=cbr:force-cfr=1" -b:v:1 3M -maxrate:v:1 3M -minrate:v:1 3M -bufsize:v:1 3M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
-map "[v3out]" -c:v:2 libx264 -x264-params "nal-hrd=cbr:force-cfr=1" -b:v:2 1M -maxrate:v:2 1M -minrate:v:2 1M -bufsize:v:2 1M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
-map a:0 -c:a:0 aac -b:a:0 96k -ac 2 \
-map a:0 -c:a:1 aac -b:a:1 96k -ac 2 \
-map a:0 -c:a:2 aac -b:a:2 48k -ac 2 \

Can you see what’s been done here? We have taken the three variables [v1out], [v2out], and [v3out] as our inputs and transcoded each of the inputs using libx264‘s slow preset, and at the desired bitrates.

Important: I have only one audio track in my video, hence, I have used -map a:0 in all my commands.

Note: You can also choose your own encoding parameters and modify it to your liking and requirements. I’ve used some simple parameters to mimic a CBR encode in this example. There are probably a zillion ways to transcode your videos using FFmpeg. You can choose between a combination of presets, crf values, CBR settings, etc.

Importantly, we have set the -keyint_min value to 48 which should force a keyframe periodically as this is very important in ABR streaming.

Now, we move on to the next step which is to create an HLS m3u8 playlist file for each of the renditions/variants.

Creating HLS Playlists (m3u8) using FFmpeg

Now that we have the commands to transcode a video into multiple bitrate variants, let’s start creating HLS VOD Playlists FFmpeg.

Some of the important settings that are needed for HLS packaging are –

  • hls_playlist_type=vod: By setting this value, FFmpeg creates a VOD playlist, inserts #EXT-X-PLAYLIST-TYPE:VOD into the m3u8 header and forces hls_list_size to 0.
  • hls_time seconds: We need to use this to set the target segment length in seconds.
    • The default value is 2 seconds and the segment will be cut on the next key frame after this time has passed.
    • That is why it is important for us to make sure that there is a Key frame at the end of every N seconds for each of the bitstream variants so that they align with each other.
  • hls_segment_type: this takes on two values – mpegts or fmp4 and creates either TS segments or fmp4 (CMAF) segments which is useful for creating a single set of streams for both HLS and DASH.
  • -hls_flags independent_segments: Add the #EXT-X-INDEPENDENT-SEGMENTS to playlists when all the segments of that playlist are guaranteed to start with a Key frame.
  • hls_segment_filename filename: this is used to name the segments that are created during the packaging process.

Here’s an example of creating a playlist for a single video file

-f hls \
-hls_time 2 \
-hls_playlist_type vod \
-hls_flags independent_segments \
-hls_segment_type mpegts \
-hls_segment_filename stream_%v/data%02d.ts \
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" stream_%v/stream.m3u8

If you see the last line, you’ll notice a function called var_stream_map. What does this do?

var_stream_map is an FFmpeg function that helps us combine the various video and audio transcodes to create the different HLS playlists.

Suppose you have two renditions that use the same video but different audio. In that case, you can select the different video and audio versions and join them together instead of creating multiple encodes for the sake of creating different playlists.

For example, -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" implies that the audio stream denoted by a:0 is used in all three video renditions.

FFmpeg takes these video-audio combinations and creates the individual variants’ .m3u8 files with the name stream_%v.m3u8 where %v is an iterator that takes its value from the stream number being packaged.

Create an HLS Master Playlist (m3u8) using FFmpeg

If you have understood how to create an HLS playlist using FFmpeg, then creating a Master Playlist using FFmpeg is very simple. In case you don’t know what a master playlist is, it is simply a file that lists the playlists of the individual variants that hav ebene packaged using HLS.

To create a master playlist using FFmpeg, add the keyword -master_pl_name to your FFmpeg command and provide the name that you want to assign to the master playlist. For example, if I want to refer to the master playlist as “master.m3u8”, then all I have to say is

-master_pl_name master.m3u8

That’s it. After FFmpeg is done with the command line, you’ll have an HLS master playlist with the names of the other playlists listed.

Final Script for HLS Packaging using FFmpeg – VOD

Note that I have only one audio track in my video, hence, I have used -map a:0 in all my commands.

ffmpeg -i brooklynsfinest_clip_1080p.mp4 \
-filter_complex \
"[0:v]split=3[v1][v2][v3]; \
[v1]copy[v1out]; [v2]scale=w=1280:h=720[v2out]; [v3]scale=w=640:h=360[v3out]" \
-map "[v1out]" -c:v:0 libx264 -x264-params "nal-hrd=cbr:force-cfr=1" -b:v:0 5M -maxrate:v:0 5M -minrate:v:0 5M -bufsize:v:0 10M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
-map "[v2out]" -c:v:1 libx264 -x264-params "nal-hrd=cbr:force-cfr=1" -b:v:1 3M -maxrate:v:1 3M -minrate:v:1 3M -bufsize:v:1 3M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
-map "[v3out]" -c:v:2 libx264 -x264-params "nal-hrd=cbr:force-cfr=1" -b:v:2 1M -maxrate:v:2 1M -minrate:v:2 1M -bufsize:v:2 1M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
-map a:0 -c:a:0 aac -b:a:0 96k -ac 2 \
-map a:0 -c:a:1 aac -b:a:1 96k -ac 2 \
-map a:0 -c:a:2 aac -b:a:2 48k -ac 2 \
-f hls \
-hls_time 2 \
-hls_playlist_type vod \
-hls_flags independent_segments \
-hls_segment_type mpegts \
-hls_segment_filename stream_%v/data%02d.ts \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" stream_%v.m3u8

Let’s look at the output of this script.

It first produces a master playlist, three folders containing the individual segments, and the playlists for the variations.

HLS packaging using FFmpeg

Here is what the master.m3u8 file looks like –

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-STREAM-INF:BANDWIDTH=5605600,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2"
stream_0.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=3405600,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
stream_1.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=1205600,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2"
stream_2.m3u8

You can see that the master playlist references the individual playlists for the 1080p, 720p, and 360p HLS variants.

Now, let’s see what the 1080p HLS variant looks like. It clearly says that it is a VOD playlist, that the segments are independent, and the length of each segment is 2 seconds (as per our settings).

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:2.002000,
data00.ts
#EXTINF:2.002000,
data01.ts
#EXTINF:2.002011,
data02.ts
#EXTINF:2.002000,
data03.ts
#EXTINF:2.002000,
data04.ts
#EXTINF:2.002000,
data05.ts
#EXTINF:2.002000,
data06.ts
#EXTINF:2.002000,
data07.ts
#EXTINF:2.002011,
data08.ts
#EXTINF:2.002000,
data09.ts
#EXTINF:0.041711,
data10.ts
#EXT-X-ENDLIST

Live HLS Packaging using FFmpeg

If you want to create a live HLS playlist using FFmpeg, then the process is not very different from the VOD steps we just covered. Here are some of the changes you need to make.

  1. remove -hls_playlist_type vod
  2. add -hls_list_size and set it to a number that denotes the number of segments you want in the playlists of the individual variations.

For example, if we set -hls_list_size to 2, then the playlist will contain only two segments and FFmpeg will re-write this playlist by adding the new segments and shifting out the old ones.

Here is an example –

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:2.002000,
data01.ts
#EXTINF:2.002011,
data02.ts

And after a couple of seconds, segment data01.ts gets dropped and is replaced by segment data03.ts.

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:2.002011,
data02.ts
#EXTINF:2.002000,
data03.ts

Other useful HLS Packaging options in FFmpeg

Finally, let’s take a quick look at some of the interesting options that FFmpeg provides for HLS Packaging for VOD and Live Streaming.

  1. hls_base_url baseurl: This can be used to append the value denoted by baseurl to every entry in the playlist.
  2. hls_fmp4_init_filename filename : Set filename to the fragment files header file, default filename is init.mp4. This is used when you set the segment type to fmp4 instead of mpegts.
  3. hls_fmp4_init_resend: Resend init file after m3u8 file refresh every time, default is 0.
  4. iframes_only : Add the #EXT-X-I-FRAMES-ONLY to playlists that has video segments and can play only I-frames in the #EXT-X-BYTERANGE mode.

Conclusion

By now, I hope you have a good understanding of how FFmpeg can be used to transcode and package a video using the HLS streaming protocol. For a complete list of options for HLS packaging using FFmpeg, please take a look at the FFmpeg documentation. In a future article, we shall cover HLS Packaging using FFmpeg with Encryption.

Until then, take care and keep visiting OTTVerse.com.

krishna rao vijayanagar
Krishna Rao Vijayanagar
Founder at OTTVerse

Krishna Rao Vijayanagar, Ph.D., is the Editor-in-Chief of OTTVerse, a news portal covering tech and business news in the OTT industry.

With extensive experience in video encoding, streaming, analytics, monetization, end-to-end streaming, and more, Krishna has held multiple leadership roles in R&D, Engineering, and Product at companies such as Harmonic Inc., MediaMelon, and Airtel Digital. Krishna has published numerous articles and research papers and speaks at industry events to share his insights and perspectives on the fundamentals and the future of OTT streaming.

Pallycon April NAB 2024

34 thoughts on “HLS Packaging using FFmpeg – Easy Step-by-Step Tutorial”

  1. SaiRama Mahesh Vemuri

    Hi Krishna Rao,

    This is so useful. Is there any way we can add cue markers in the child manifest files while packaging? Shaka packager adds cue markers but splits audio and video manifests. Any open source tool which supports this functionality?

    1. Checkout x9k3. It segments mpegts and will transfer SCTE-35 cues from the video into HLS tags.
      It doesnt scale video though.

  2. hi
    i run this command in terminal, but has error.

    error:
    Stream map ‘a:0’ matches no streams.
    To ignore this, add a trailing ‘?’ to the map.

  3. Thanks for the snippet. Worked very well.

    Except minor thing, I had to adjust paths in the master file.

  4. Hi, thanks for posting this, it’s very helpful for getting set up. I found two mistakes in the sample code which I had to correct to get a fully working example.

    The first problem is it creates an unplayable video because the stream .m3u8 files are put into the same directory as master.m3u8, but their .ts files are in stream_x directories. I fixed this by changing -var_stream_map argument:

    -var_stream_map “v:0,a:0 v:1,a:1 v:2,a:2” stream_%v/stream.m3u8

    The other problem is the bitrate settings are not being applied to the correct video streams, they’re all being applied to video stream 0 and it ends up using the lowest bitrate for everything. I fixed this by using the correct index in the b:v, maxrate, minrate and bufsize params e.g. -b:v:0 for the second stream becomes -b:v:1. That part of the command after being fixed looks like:

    -map [v1out] -c:v:0 libx264 -x264-params “nal-hrd=cbr:force-cfr=1” -b:v:0 5M -maxrate:v:0 5M -minrate:v:0 5M -bufsize:v:0 10M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
    -map [v2out] -c:v:1 libx264 -x264-params “nal-hrd=cbr:force-cfr=1” -b:v:1 3M -maxrate:v:1 3M -minrate:v:1 3M -bufsize:v:1 3M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
    -map [v3out] -c:v:2 libx264 -x264-params “nal-hrd=cbr:force-cfr=1” -b:v:2 1M -maxrate:v:2 1M -minrate:v:2 1M -bufsize:v:2 1M -preset slow -g 48

  5. Hi! Thanks for your tutorial, but I cannot get it to work. This is the error message i get for your very first command on this page (the split thing)
    Filter scale has an unconnected output

      1. Hello! Thanks for this tutorial!
        I’ve tried in many ways, and read every comment, but I still get this error:

        “Filter scale has an unconnected output”

        …when I try the first command you wrote, and I don’t really know what to do.

        This is what I’m doing:

        ffmpeg -i video1920_1080.mp4 -filter_complex “[0:v]split=3[v1][v2][v3]; [v1]copy[v1out]; [v2]scale=w=1280:h=720[v2out]; [v3]scale=w=640:h=360[v3out]”

        1. The guide breaks down the full command into smaller parts to explain each step. The stuff you’re copying from step 1 is an incomplete/invalid command. The full thing you want to run, combining all the bits from the other steps, is under “Final Script for HLS Packaging.”

    1. Had the same type of problem and I found my mistake. I got:

      [AVFilterGraph @ 0x55e144d014c0] No such filter: ”
      Error initializing complex filters.
      Invalid argument

      Make sure there are no trailing semicolons in the quoted parts. It seems to expect more input if they are present at the end.

      For example (taking from the example), the -filter_complex arguments are like this:
      -filter_complex \
      “[0:v]split=3[v1][v2][v3]; \
      [v1]copy[v1out]; [v2]scale=w=1280:h=720[v2out]; [v3]scale=w=640:h=360[v3out]”

      For your needs, if you’re customizing the command like this, for example:
      -filter_complex \
      “[0:v]split=2[v1][v2]; \
      [v1]copy[v1out]; [v2]scale=w=1280:h=720[v2out];” \

      “[v3]” was taken out and, at the end, the “;” is left in place. It seems reasonable to leave the semicolon there as they usually do trail statements. Well, not in this instance lol. Just in case it might help anyone save about 30 minutes or so, check for and remove trailing semicolons in quotes.

      1. I was able to fix the “no matches found: [v1out]” error by wrapping the -map parameters in quotes:

        ffmpeg -i input.mp4 \
        -filter_complex \
        “[0:v]split=3[v1][v2][v3]; \
        [v1]copy[v1out]; [v2]scale=w=960:h=540[v2out]; [v3]scale=w=480:h=270[v3out]” \
        -map “[v1out]” -c:v:0 libx264 -x264-params “nal-hrd=cbr:force-cfr=1” -b:v:0 5M -maxrate:v:0 5M -minrate:v:0 5M -bufsize:v:0 10M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
        -map “[v2out]” -c:v:1 libx264 -x264-params “nal-hrd=cbr:force-cfr=1” -b:v:1 3M -maxrate:v:1 3M -minrate:v:1 3M -bufsize:v:1 3M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
        -map “[v3out]” -c:v:2 libx264 -x264-params “nal-hrd=cbr:force-cfr=1” -b:v:2 1M -maxrate:v:2 1M -minrate:v:2 1M -bufsize:v:2 1M -preset slow -g 48 -sc_threshold 0 -keyint_min 48 \
        -map a:0 -c:a:0 aac -b:a:0 96k -ac 2 \
        -map a:0 -c:a:1 aac -b:a:1 96k -ac 2 \
        -map a:0 -c:a:2 aac -b:a:2 48k -ac 2 \
        -f hls \
        -hls_time 2 \
        -hls_playlist_type vod \
        -hls_flags independent_segments \
        -hls_segment_type mpegts \
        -hls_segment_filename stream_%v/data%02d.ts \
        -master_pl_name master.m3u8 \
        -var_stream_map “v:0,a:0 v:1,a:1 v:2,a:2” stream_%v.m3u8

  6. How can I generate adaptive bitrate for audio file?

    I have tried the above removing the flags for video but it’s not helping me. I tried generating variants separately and creating a master playlist manually but that too is not working.

  7. Thanks for this guide.

    I followed the article and tried to generate HSL packing for audio. It was successful as well because I was able to generate 3 different m3u8 bit-ranged variants for the raw mp3 and I have to manually create the master playlist.

    However the generated media is not playing in online HLS players (although it’s playing in safari). Another issue was when I seek the media to any unbuffered location on the player progress bar, the media stops playing and it stays stuck at one point

  8. I am using this command to create HLS ABR for audio files but the media get stuck when user seek to an unbuffered location on the media player progress bar.

    ‘ffmpeg -i https://storage.googleapis.com/content/audio/content-audio-1622806208264.mp3 -hls_playlist_type vod -hls_time 4 -hls_flags single_file -sc_threshold 0 -b:a 160k ./content-audio-1622806208264/content-audio-1622806208264_high.m3u8 -hls_playlist_type vod -hls_time 4 -hls_flags single_file -sc_threshold 0 -b:a 90k ./content-audio-1622806208264/content-audio-1622806208264_med.m3u8 -hls_playlist_type vod -hls_time 4 -hls_flags single_file -sc_threshold 0 -b:a 24k ./content-audio-1622806208264/content-audio-1622806208264_low.m3u8′

    I am generating the master playlist manually

  9. Is it possible to use an external live stream with this configuration?
    I tried but it runs continuously and if FFmpeg is stopped, after stopping the playlist files will created…

  10. Hello,
    I would like to express my gratitude and respect for this article. Great job, explained to the details.

    Thank you for this article!

    1. Thanks, Matthew – happy to hear that you found it useful. If you can share it with your colleagues, that would be very helpful for us! Looking forward to seeing you again!

  11. I have an input MPEG TS file ‘unit_test.ts’. This file has following content (shown by ffprobe):
    ———————————————
    Input #0, mpegts, from ‘unit_test.ts’:
    Duration: 00:00:57.23, start: 73674.049844, bitrate: 2401 kb/s
    Program 1
    Metadata:
    service_name : Service01
    service_provider: FFmpeg
    Stream #0:0[0x31]: Video: h264 (Main) ([27][0][0][0] / 0x001B), yuv420p(progressive), 852×480 [SAR 640:639 DAR 16:9], Closed Captions, 59.94 fps, 59.94 tbr, 90k tbn, 119.88 tbc
    Stream #0:1[0x34](eng): Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 448 kb/s
    Stream #0:2[0x35](spa): Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, stereo, fltp, 192 kb/s
    ———————————————-
    I want to output this into HLS master playlist and child playlists (one child playlist for one audio stream)

    1. How should I do this? what is the ffmpeg command for this?
    2. How can it be played? Can it be played in browser using video.js? If yes, how can the user select the playlist with the audiostream they want (eng or spa)? Is there any existing example web app for this?

    Please help
    Thanks
    -Mahesh

  12. How do I add base url to every entry in master playlist… I tried the hls_base_url option and segment_list_entry_prefix but it only adds it the value to the playlist of the different variants

  13. Pingback: HLS Packaging using FFmpeg – Easy Step-by-Step Tutorial – Collected Links

  14. I’ve tried to run this script and I get an output file must be specified error.

    If anyone has used this successfully can you post your config?

Leave a Comment

Your email address will not be published. Required fields are marked *

Enjoying this article? Subscribe to OTTVerse and receive exclusive news and information from the OTT Industry.