FFmpeg 与 VideoToolBox(1):准备工作

一、概述

早在2015年8月初,FFmpeg 就已经开始逐步支持 macOS 和 iOS 平台的 VideoToolBox,2017年7月低,FFmpeg 在 example 中新增了一个 hw_decode.c 用于演示对文件的硬解码。以前使用过其软编码(基于 x264 )和软解码,也单独使用 VideoToolBox 进行过硬编码和硬解码。但是距离上次使用 FFmpeg 后其 API 发生了一些改变,VideoToolBox 也支持 HEVC 了,所以重新研究一次。

二、软件准备工作

1、编译 FFmpeg for iOS

对 FFmpeg for iOS 进行交叉编译不是本文重点,在此略去。
对 FFmpeg for iOS 进行编译时,默认是开启了 VideoToolBox 的。如果心里没底,可以先验证一次。

1
2
3
4
5
6
7
8
9
const char *type_name = "videotoolbox";
enum AVHWDeviceType type = av_hwdevice_find_type_by_name(type_name);
if (type == AV_HWDEVICE_TYPE_NONE) {
fprintf(stderr, "Device type %s is not supported.\n", type_name);
fprintf(stderr, "Available device types:");
while((type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE)
fprintf(stderr, " %s", av_hwdevice_get_type_name(type));
fprintf(stderr, "\n");
}

2、安装 FFmpeg for macOS

FFmpeg for macOS 在本文主要用于准备相关视频文件。比如原始 mp4 文件过大需要对其裁剪、音视频分离等。
除了使用官方提供的安装包或自行编译外,也可使用 brew 进行安装,先看有哪些可用选项:

1
2
$ brew options ffmpeg | grep -vE '\s' | tr '\n' ' '
--with-chromaprint --with-fdk-aac --with-fontconfig --with-freetype --with-frei0r --with-game-music-emu --with-libass --with-libbluray --with-libbs2b --with-libcaca --with-libgsm --with-libmodplug --with-librsvg --with-libsoxr --with-libssh --with-libvidstab --with-libvorbis --with-libvpx --with-opencore-amr --with-openh264 --with-openjpeg --with-openssl --with-opus --with-rtmpdump --with-rubberband --with-sdl2 --with-snappy --with-speex --with-srt --with-tesseract --with-theora --with-tools --with-two-lame --with-wavpack --with-webp --with-x265 --with-xz --with-zeromq --with-zimg --without-gpl --without-lame --without-qtkit --without-securetransport --without-x264 --without-xvid —HEAD

如果使用尽可能完整的选项进行安装:

1
brew install ffmpeg $(brew options ffmpeg | grep -vE '\s' | grep -- '--with-' | tr '\n' ' ')

安装完成后,如果想回过头看看 brew 使用了哪些选项:

1
brew info ffmpeg

如果想看看编译时 configure 了哪些选项,直接执行 ffmpeg 命令即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ffmpeg
ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
built with Apple LLVM version 10.0.0 (clang-1000.11.45.2)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.0.2 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags='-I/Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home/include/darwin' --host-ldflags= --enable-gpl --enable-chromaprint --enable-ffplay --enable-frei0r --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libfdk-aac --enable-libfontconfig --enable-libfreetype --enable-libgme --enable-libgsm --enable-libmodplug --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopus --enable-librsvg --enable-librtmp --enable-librubberband --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtesseract --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libzimg --enable-libzmq --enable-opencl --enable-videotoolbox --enable-openssl --enable-libsrt --enable-lzma --enable-libopenjpeg --disable-decoder=jpeg2000 --extra-cflags=-I/usr/local/Cellar/openjpeg/2.3.0/include/openjpeg-2.3 --enable-nonfree
libavutil 56. 14.100 / 56. 14.100
libavcodec 58. 18.100 / 58. 18.100
libavformat 58. 12.100 / 58. 12.100
libavdevice 58. 3.100 / 58. 3.100
libavfilter 7. 16.100 / 7. 16.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
libpostproc 55. 1.100 / 55. 1.100
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...

Use -h to get full help or, even better, run 'man ffmpeg'

如果要看格式更友好的版本,可在执行 ffmpeg 时加上 -buildconf 参数:

1
ffmpeg -buildconf

三、视频文件准备工作

1、准备 mp4 文件

随意找一个 WWDC 的视频文件。使用 Mediainfo 查看其信息:

原始 mp4 文件信息

视频文件的音视频的编码分别是 AAC (LC) 和 AVC (Main@L3.1) 。
当然,也可以直接使用 ffprobe 命令查看信息:

1
ffprobe -show_format 503_hd_introducing_heif_and_hevc.mp4

2、裁剪 mp4 文件

因为在 iOS 上测试,视频文件作为资源文件,希望小一点,从第10秒开始截取20秒,其余不变:

1
ffmpeg -i 503_hd_introducing_heif_and_hevc.mp4 -ss 10 -t 20 -codec copy temp.mp4

截取后视频信息

3、裁剪不带 B 帧的 mp4 文件

为了简化,使用 x264 以 ultrafast 模式重编码以确保没有 B 帧。暂时不纠结 AVC profilelevel 的问题:

1
ffmpeg -v debug -i 503_hd_introducing_heif_and_hevc.mp4 -vcodec libx264 -preset ultrafast -ss 10 -t 20 temp.mp4

ultrafast 模式实际上其中一个参数指定了 B 帧数为 0(其他的如 superfast 不能保证不产生 B 帧),所以可以直接以 x264-params 选项指定::

1
ffmpeg -v debug -i 503_hd_introducing_heif_and_hevc.mp4 -vcodec libx264 -x264-params bframes=0 -ss 10 -t 20 temp.mp4

通过 x264opts 也可以设置:

1
ffmpeg -v debug -i 503_hd_introducing_heif_and_hevc.mp4 -vcodec libx264 -x264opts "bframes=0" -ss 10 -t 20 temp.mp4

我们知道, profile baseline 不支持 B 帧的,直接设置 profile 也是可以的(可换作 h264_videotoolbox 编码器):

1
ffmpeg -v debug -i 503_hd_introducing_heif_and_hevc.mp4 -vcodec h264_videotoolbox -profile:v baseline -ss 10 -t 20 temp.mp4

备注:使用 FFmpeg 的 -bf 选项貌似参数无效,抽空继续尝试。

截取后视频信息(使用 x264 重编码)

如何判断的确没有 B 帧了呢?可以使用 ffprobe 命令查看,如果视频流的 has_b_frames 值为 0 则表示没有 B 帧:

1
ffprobe -show_streams temp.mp4

也可以采用最直接的、读取数据的方式进行验证。或者使用 FFmpeg 解码,然后通过 AVFramepict_type 字段判断,有个 av_get_picture_type_char 可以直接返回帧类型的字符表示:

1
2
3
4
5
AVFrame *frame = ...
char picture_type = av_get_picture_type_char(frame->pict_type);
if (picture_type == 'B') {
// Do something
}

3、提取 H.264 数据

然后再来一份 H.264 数据(-bsf:v h264_mp4toannexb 可省略,使用该参数与否结果都是 Annex B 格式):

1
ffmpeg -i temp.mp4 -c:v copy -bsf:v h264_mp4toannexb -an temp.h264

H.264 文件信息

使用 VLC 播放试试:

使用 VLC 播放 H.264 文件

注意:在使用 ffmpeg -v debug -i 503_hd_introducing_heif_and_hevc.mp4 -ss 10 -t 40 -codec copy temp.mp4 进行裁剪时,第一个 Slice 值为 00000001 09…. ,是单元分割符(0x09 & 0001 1111 = 9),使用 ffplay 播放 H.264 文件时提示 sps 和 pps 找不到的错误,但能继续播放;在使用 ffmpeg -v debug -i 503_hd_introducing_heif_and_hevc.mp4 -vcodec libx264 -preset ultrafast -ss 10 -t 20 temp.mp4 进行裁剪时,第一个 Slice 值为 00000001 06….,是 SEI (0x06 & 0001 1111 = 6),使用 ffplay 播放 H.264 文件正常。可使用16进制编辑器删除非必要数据再尝试播放。另外,想通过第5个字节来判断IPB帧也是不行的。

为了在之后的测试中,能够使用标准 I/O 函数而不是 av_read_frame 函数逐帧读取,比如模拟从 FFmpeg 外部获取数据,可以在准备数据时用 av_read_frame 函数读取帧后写入文件时,先写入帧长度再写入帧数据。

1
2
3
4
5
6
7
8
9
10
11
12
FILE *frame_file = fopen("temp.data", "w");
while (!av_read_frame(format_ctx, packet))
{
if (video_stream_index == packet->stream_index)
{
fwrite(&packet->size, 4, 1, frame_file);
fwrite(packet->data, packet->size, 1, frame_file);
// Do other things
}
av_packet_unref(packet);
}
fclose(frame_file);

备注:因为没有使用 h264_mp4toannexb Bitstream Filters, 所以帧数据的每个 Slice 前面是 Slice 的长度而不是 Start code ( 00000001 )。这并不影响FFmpeg 解码。

参考资料

https://trac.ffmpeg.org/wiki/HWAccelIntro
http://ffmpeg.org/ffmpeg-all.html
https://stackoverflow.com/questions/19456745/libavcodec-libx264-do-not-produce-b-frames
https://stackoverflow.com/questions/15855535/setting-b-frames-in-a-video-with-ffmpeg
https://superuser.com/questions/996949/how-to-set-the-number-of-b-frames-in-ffmpeg-for-h-265-encoding
https://blog.csdn.net/dangxw_/article/details/50974880
https://blog.csdn.net/NB_vol_1/article/details/78363559
https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/decode_video.c
https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/hw_decode.c
https://en.wikipedia.org/wiki/YUV
https://github.com/mypopydev/