Downloading an HLS Stream with ffmpeg

June 19, 2023

I recently encountered a video served with an HLS stream that I wanted to have a copy of, but the website did not support downloading it. Trying yt-dlp yielded no results (it was not one of the “common” websites), but I did learn that ffmpeg supports conversion of HLS streams directly to a file. I referenced a blog post on codementor.io, though I’m sure that the same information is present in many other locations online.

Identify the playlist file

Opening the developer console’s Network Activity tab, I reloaded the page and found a request to a Cloudfront domain that requested a *.m3u8 file. This is a “playlist” file, which can include alternate resolutions/audio sources/etc. In my case, there was only one resolution available, so it was quite simple:

1#EXTM3U
2#EXT-X-VERSION:3
3#EXT-X-STREAM-INF:BANDWIDTH=702658,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=640x480
4chunklist.m3u8

The ending line, chunklist.m3u8 is a secondary file that identifies the filenames of each chunk in the video sequence (in my case, 10s of video per chunk).

(Optional) Identify the stream you want to select

If you have multiple stream sources available (e.g. a 640x480 stream and a 1080p stream), you can identify the options by running ffprobe with just the playlist file as input. Example:

 1$ PLAYLIST_FILE="https://example.com/some/path/vods/playlist.m3u8"
 2$ ffprobe "$PLAYLIST_FILE"
 3ffprobe version N-107137-gfee765c207-20220619 Copyright (c) 2007-2022 the FFmpeg developers
 4  built with gcc 11.2.0 (crosstool-NG 1.24.0.533_681aaef)
 5  configuration: <SNIP>
 6[hls @ 000001c73ab727c0] Skip ('#EXT-X-VERSION:3')
 7[hls @ 000001c73ab727c0] Opening 'https://REDACTED/media_0.ts' for reading
 8[hls @ 000001c73ab727c0] Opening 'https://REDACTED/media_1.ts' for reading
 9Input #0, hls, from 'https://REDACTED/chunklist.m3u8':
10  Duration: 00:44:23.48, start: 0.000000, bitrate: 0 kb/s
11  Program 0
12    Metadata:
13      variant_bitrate : 0
14  Stream #0:0: Data: timed_id3 (ID3  / 0x20334449)
15    Metadata:
16      variant_bitrate : 0
17  Stream #0:1: Video: h264 (Constrained Baseline) ([27][0][0][0] / 0x001B), yuv420p, 640x480, 25 fps, 25 tbr, 90k tbn
18    Metadata:
19      variant_bitrate : 0
20  Stream #0:2: Audio: aac (LC) ([15][0][0][0] / 0x000F), 44100 Hz, stereo, fltp
21    Metadata:
22      variant_bitrate : 0

Here, I only have one set of video and audio to choose. However, if my stream had multiple such sources, I could pick which streams I wanted using ffmpeg’s map flag.

Download with ffmpeg

Now, you can download the stream directly:

1$ ffmpeg -i "$PLAYLIST_FILE" -c copy 'out.mp4'
2<BIG SNIP, enumerating every chunk downloaded>
3$ file out.mp4
4out.mp4: ISO Media, MP4 Base Media v1 [IS0 14496-12:2003]

Alternatively, if you want to download each chunk and recompose them for some reason, you can do so:

1$ ffmpeg -i "$PLAYLIST_FILE" -c copy -f segment -segment_list out.list 'out%03d.ts'
2$ for f in ./*.ts; do echo "file '$f'" >> segment_files.txt ; done
3$ ffmpeg -f concat -safe 0 -i segment_files.txt -c copy out.mp4