Downloading an HLS Stream with ffmpeg
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