Generating HLS/M3U8 Using FFmpeg
FFmpeg 是开源的音视频处理项目,以使用灵活、功能丰富著称,被各大互联网公司用来构建多媒体服务的基础。
最近的项目中需要提供音视频多码率转码支持,集中调研了 FFmpeg 对 Apple HTTP Live Streaming (HLS) 的支持,总结一下,遂成本文。
说明:本文中使用的命令行 ffmpeg
版本为:
~ ffmpeg ffmpeg version n4.1.3 Copyright (c) 2000-2019 the FFmpeg developers built with gcc 8.2.1 (GCC) 20181127 configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3 libavutil 56. 22.100 / 56. 22.100 libavcodec 58. 35.100 / 58. 35.100 libavformat 58. 20.100 / 58. 20.100 libavdevice 58. 5.100 / 58. 5.100 libavfilter 7. 40.101 / 7. 40.101 libswscale 5. 3.100 / 5. 3.100 libswresample 3. 3.100 / 3. 3.100 libpostproc 55. 3.100 / 55. 3.100
🔗hls
Muxer
FFmpeg 通过 hls 封装器格式可以支持 HTTP Live Streaming (HLS) 输出。其会将原视频分割为多个 MPEG-TS (.ts) 格式的片段文件,以及一个 M3U8 (.m3u8) 格式的播放列表文件。
官方提供的示例:
ffmpeg -i in.mkv -c:v h264 -flags +cgop -g 30 -hls_time 1 out.m3u8
会生成一个播放列表文件 out.m3u8
,以及多个视频分片文件 out0.ts
, out1.ts
, out2.ts
等。
另外,FFmpeg 也提供了 segment 封装器格式可以生成 HTTP Live Streaming (HLS) 格式的输出。
🔗单码率 M3U8
🔗起始
一个典型的生成 HLS 输出的命令行如下:
ffmpeg -hide_banner -loglevel warning \
-ss 10 -t 10 \
-i test.avi \
-c:v libx264 -crf 23 -preset veryfast \
-c:a aac -b:a 128k -ac 2 \
-f hls \
-hls_time 4 \
-hls_playlist_type vod \
-hls_segment_filename hls.test%d.ts \
-hls_list_size 0 \
hls.test.m3u8
-f hls
行指定了输出格式为hls
,ffmpeg 会使用 hls 来对输出视频做封装操作。-hls_time 4
行指定了输出视频片段的长度,期望输出的视频片段每一个的时长都是 4s。-hls_playlist_type vod
行指定了播放列表的类型;vod
即点播,event
即直播。-hls_segment_filename hls.test%d.ts
行指定了输出片段文件名格式,可以使用%d
或%5d
的格式化符-hls_list_size 0
行指定了输出视频片段的最大个数;如果设置为 0,说明不限制个数。
该命令会生成以下文件:
- hls.test0.ts
- hls.test1.ts
- hls.test2.ts
- hls.test3.ts
- hls.test.m3u8
生成的 hls.test.m3u8
文件如下:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:7 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:6.760000, hls.test0.ts #EXTINF:6.040000, hls.test1.ts #EXTINF:4.520000, hls.test2.ts #EXTINF:2.680000, hls.test3.ts #EXT-X-ENDLIST
注意到视频片段的长度并不是指定的 4s。这是因为 FFmpeg 在生成片段时,总是会在满足时长要求条件的第一个关键帧 (I-frame) 处截断,而不是严格按照时长来截断的。
🔗指定 GOP
如果能够使得视频在指定的时间间隔处存在关键帧,则输出的片段时长都能保持一致。可以通过 -g
参数来设置 GOP (相邻的两个关键帧之间的帧数)。
新的命令如下:
ffmpeg -hide_banner -loglevel warning \
-ss 10 -t 10 \
-i test.avi \
-c:v libx264 -crf 23 -preset veryfast \
-g 30 \
-c:a aac -b:a 128k -ac 2 \
-f hls \
-hls_time 4 \
-hls_playlist_type vod \
-hls_segment_filename hls.test%d.ts \
-hls_list_size 0 \
hls.test.m3u8
生成的文件如下:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:5 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:4.280000, hls.test0.ts #EXTINF:4.800000, hls.test1.ts #EXTINF:3.600000, hls.test2.ts #EXTINF:3.720000, hls.test3.ts #EXTINF:3.600000, hls.test4.ts #EXT-X-ENDLIST
片段的时长和目标时长接近了很多,但还是有一些差异。
🔗取消运动场景检测
FFmpeg 在编码时进行运动检测。当检测到运动场景变换时,其会自动插入一帧关键帧;该行为可以通过 -sc_threshold
禁用。
新命令如下:
ffmpeg -hide_banner -loglevel warning \
-ss 10 -t 10 \
-i test.avi \
-c:v libx264 -crf 23 -preset veryfast \
-g 30 \
-sc_threshold 0 \
-c:a aac -b:a 128k -ac 2 \
-f hls \
-hls_time 4 \
-hls_playlist_type vod \
-hls_segment_filename hls.test%d.ts \
-hls_list_size 0 \
hls.test.m3u8
新的播放列表如下:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:5 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:4.800000, hls.test0.ts #EXTINF:3.600000, hls.test1.ts #EXTINF:3.600000, hls.test2.ts #EXTINF:4.800000, hls.test3.ts #EXTINF:3.200000, hls.test4.ts #EXT-X-ENDLIST
🔗closed GOP
对于视频分片来说,通常还需要设置 closed GOP 模式,防止视频帧去引用 GOP 之外的帧。
FFmpeg 中通过 -flags +cgop
选项设置 closed GOP 模式。
ffmpeg -hide_banner -loglevel warning \
-ss 10 -t 10 \
-i test.avi \
-c:v libx264 -crf 23 -preset veryfast \
-g 30 \
-sc_threshold 0 \
-c:a aac -b:a 128k -ac 2 \
-f hls \
-hls_time 4 \
-hls_playlist_type vod \
-hls_segment_filename hls.test%d.ts \
-hls_list_size 0 \
hls.test.m3u8
🔗多码率 M3U8
🔗多码率
FFmpeg 可以直接支持对多码率 HLS 输出的支持。其在生成多个单码率播放列表之外,还会生成一个层级播放列表文件。
命令如下:
ffmpeg -hide_banner -loglevel warning \ -ss 10 -t 10 \ -i test.avi \ -g 30 \ -sc_threshold 0 \ -c:a aac -b:a 128k -ac 2 \ -f hls \ -hls_time 4 \ -hls_playlist_type event \ -hls_segment_filename hls.test%d.%v.ts \ -hls_list_size 0 \ -map v:0 -c:v:0 libx264 -b:v:0 2000k \ -map v:0 -c:v:1 libx264 -b:v:1 6000k \ -map a:0 \ -map a:0 \ -var_stream_map "v:0,a:0 v:1,a:1" \ -master_pl_name hls.test.m3u8 \ hls.test.m3u8
说明如下:
-var_stream_map "v:0,a:0 v:1,a:1"
行通过逗号和空格分隔的参数指定输出两个码率的播放列表,并指定第一个播放列表对应的视频从第一个视频流和第一个音频流生成,第二个播放列表对应的视频片段从第二个视频流和第二个音频流生成;使用本参数之后,-hls_segment_filename
选项需要添加一个%v
的占位符,表示第几个播放列表。-master_pl_name hls.test.master.m3u8
行指定输出的层级播放列表文件名
注意,由于需要输出多码率,所以在输出参数需要通过 -map
参数准备多个输出流。
另外,在本文使用的ffmpeg版本中,-hls_playlist_type
需要设置为 event
,否则会报 core dump
异常。
生成的层级播放列表文件内容如下:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:BANDWIDTH=2340800,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2" hls.test.0.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6740800,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2" hls.test.1.m3u8
对于多码率的选择,可以参考 Apple 的推荐码率。
🔗多分辨率
如果需要输出的多码率视频匹配多个分辨率,可以使用 -complex_filter
组装过滤器图来实现。
命令如下:
ffmpeg -hide_banner -loglevel warning \ -ss 10 -t 10 \ -i test.avi \ -filter_complex "[v:0]split=2[vtemp001][vout002];[vtemp001]scale=w=960:h=540[vout001]" \ -g 30 \ -sc_threshold 0 \ -c:a aac -b:a 128k -ac 2 \ -f hls \ -hls_time 4 \ -hls_playlist_type event \ -hls_segment_filename hls.test%d.%v.ts \ -hls_list_size 0 \ -map "[vout001]" -c:v:0 libx264 -b:v:0 2000k \ -map "[vout002]" -c:v:1 libx264 -b:v:1 6000k \ -map a:0 \ -map a:0 \ -var_stream_map "v:0,a:0 v:1,a:1" \ -master_pl_name hls.test.m3u8 \ hls.test.m3u8
注意,过滤器图需要生成多个输出,-map
行需要与对应的输出匹配。
生成的播放列表文件内容如下:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:BANDWIDTH=2340800,RESOLUTION=960x540,CODECS="avc1.64001f,mp4a.40.2" hls.test.0.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6740800,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2" hls.test.1.m3u8
🔗FFmpeg 3.x
遗憾的是,-var_stream_map
选项是在 FFmpeg 4.x 版本以后添加的功能,在 3.x 版本中不被支持。
解决办法 删掉 3.x 安装 4.x (_^_^_)
我采用的方法是多输出然后手动合并。
首先执行:
ffmpeg -hide_banner -loglevel warning \ -ss 10 -t 10 \ -i test.avi \ -filter_complex "[v:0]split=2[vtemp001][vout002];[vtemp001]scale=w=960:h=540[vout001]" \ -g 30 \ -sc_threshold 0 \ -c:a aac -b:a 128k -ac 2 \ -f hls \ -hls_time 4 \ -hls_playlist_type event \ -hls_segment_filename hls.test%d.0.ts \ -hls_list_size 0 \ -map "[vout001]" -c:v:0 libx264 -b:v:0 2000k \ -map a:0 \ hls.test.0.m3u8 \ -g 30 \ -sc_threshold 0 \ -c:a aac -b:a 128k -ac 2 \ -f hls \ -hls_time 4 \ -hls_playlist_type event \ -hls_segment_filename hls.test%d.1.ts \ -hls_list_size 0 \ -map "[vout002]" -c:v:1 libx264 -b:v:1 6000k \ -map a:0 \ hls.test.1.m3u8
然后,使用 ffprobe
遍历每一个码率的片段文件,获取最大码率和分辨率之后手动生成层级播放列表文件。
以上。
参考:
- Using FFmpeg as a HLS streaming server (Part 1) – HLS Basics
- Using FFmpeg as a HLS streaming server (Part 2) – Enhanced HLS Segmentation
- Using FFmpeg as a HLS streaming server (Part 3) – Multiple Bitrates
- Using FFmpeg as a HLS streaming server (Part 4) – Multiple Video Resolutions
- Using FFmpeg as a HLS streaming server (Part 5) – Folder Structure