1. ffmpeg cpu解码
视频解码,是将压缩后的视频(压缩格式如H264)通过对应解码算法还原为YUV视频流的过程;在计算机看来,首先输入一段01串(压缩的视频),然后进行大量的 浮点运算 ,最后再输出更长的一段01串(还原的非压缩视频)。计算机内部可以进行浮点数计算的部件是CPU,目前市场上涌现了一批GPU和类GPU芯片,如Nvidia、 海思芯片 甚至Intel自家的核显。利用前者进行解码一般称为“ 软解码 ”,后者被称为“ 硬解码 ”,如果没有特殊指定,FFMPEG是用CPU进行解码的,即 软解 。本文将介绍的是 软解 ,也就是FFMPEG最通用的做法。
1.1 ffmpeg 软解API变化
FFPEAG官方参考技术手册: ffmpeg.org/developer.h…
1.2 ffmpeg解码套路
和很多工具一样,FFMPEG解码也是有套路的,以下是雷神的解码过程:
最新版的解码过程如下所示:
1.3 详解解码过程
1.3.1 连接和打开视频流
[int avformat_network_init(void)]:官方文档建议加上
avformat_network_init()
,虽然这个不是必须的。深入阅读该实现过程,说白了,该函数会初始化和启动底层的**
TLS库
,这也就解释了网上很多资料关于
如果要打开网络流的话,这个API是必须的**的说法了。
int avformat_open_input(AVFormatContext ** ps, const char* filename, AVInputFormat* fmt, AVDictionary ** options) avformat_open_input()官方说法是“打开并读取视频头信息”,该函数较为复杂,笔者还没有完全吃透他的每一行源码,大致了解其功能为AVFormatContext内存分配, 如果是视频文件,会探测其封装格式并将视频源装入内部buffer中;如果是网络流视频,则会创建socket等工作连接视频获取其内容,装入内部buffer中。最后读取视频头信息 。
上面步骤结束后,就可以调用API
av_dump_format()
打印文件的基本信息了,如文件时长、
比特率
、fps、编码格式等,信息大概如下:
Input #0, avi, from '${input_video_file_name}': Metadata: encoder : Lavf57.83.100 Duration: 00:10:00.00, start: 0.000000, bitrate: 4196 kb/s Stream #0:0: Video: h264 (High) (H264 / 0x34363248), yuvj420p(pc, bt709, progressive), 1920x1080, 4194 kb/s, 12 fps, 12 tbr, 12 tbn, 24 tbc
另外,函数
av_register_all()
在FFMPEG4.0及以上版本中被弃用了,见
av_register_all() has been deprecated in ffmpeg 4.0
。**
1.3.2 定位视频流数据
无论是离线的还是在线的视频文件,相对正确的称呼应该是“多媒体”文件。要知道,这些文件一般不止有一路 视频流数据 ,可能同时包括多路音频数据、视频数据甚至字幕数据等。因此我们在做解码之前,需要首先找到我们需要的视频流数据。
1.3.3 准备解码器codec
codec是FFMPEG的灵魂,顾名思义,解码必须由解码器完成。**准备解码器的步骤包括:寻找合适的解码器 -> 拷贝解码器(optiona)-> 打开解码器。
AVCodec* avcodec_find_decoder(enum AVCodecID id)
avcodec_find_decoder
是从codec库内返回与
id
匹配到的解码器。另外还有一个与其对应的寻找解码器的API-
AVCodec* avcodec_find_decoder_by_name(const char* name)
,这个函数是从codec库内返回名字为
name
的解码器,一般在硬解码时,会通过应解码器名字指定应解码器。(硬解码的流程会更复杂些,往往还需要打开相关硬件的底层库驱动等,本文不会涉及)。
AVCodecContext* avcodec_alloc_context3(const AVCodec* codec)
和
int avcodec_parameters_to_context(const AVCodec* codec, const AVCodecParameters* par)
avcodec_alloc_context3()
创建了
AVCodecContext
,而
avcodec_parameters_to_context()
才真正执行了内容拷贝。
1.3.4 解码
解码的核心是
重复进行取包、拆包解帧的工作
,这里说的包是FFMPEG非常重要的数据结构之一:
AVPacket
,帧是其中同样重要的数据结构:
AVFrame
。
AVPacket
该数据结构的介绍和分析网上资料很多,推荐阅读 FFMPEG 结构体:AVPacket解析 ,简言之,该结构保存了解码,或者说解压缩之前的多媒体数据,包括流数据本身和附加信息。AVPacket是由函数 int av_read_frame(AVFormatContext* s, AVPacket* pkt) 获取得到的,该函数的具体实现在新版本中做了改良,确保每次取出的一定是完整的帧数据。
AVFrame
该数据结构的介绍和分析网上资料也不少,推荐阅读 FFMPEG 结构体分析:AVFrame ,简言之,该结构保存了解码后,即解压缩后的帧本身的数据和附加信息。AVFrame在新版本中由函数[int avcodec_send_packet(AVCodecContext* avctx, AVPacket* pkt)]和int avcodec_receive_frame(AVCodecContext* avctx, AVFrame* frame)产生,前者真正地执行了解码操作,后者则是从缓存或者解码器内存中取出解压出来地帧数据。
老版本中用的是
avcodec_decode_video2()
,目前已经被弃用。 此外需要注意的是,一般而言,一次
avcodec_send_packet()
对应一次
avcodec_receive_frame()
,但是也会有一次对应多次的情况。这个关系参考一个
AVPacket
对应一个或多个
AVFrame
。
1.4 解码其他注意事项
1.4.1 帧转码
软解得到的帧格式是YUV格式的,具体格式可以存放在
AVFrame
的
format(类型为int)
成员中,打印出数值后,再到
AVPixelFormat
中查找具体是哪个格式。一般而言,大多是实际使用场景中,最常用的是RGB格式,因此接下来就以RGB举例说明如何做帧转码。注意,其他格式的做法也是一样的。核心是调用
int sws_scale(struct SwsContext* c, ...)
,该函数接受的参数有一大堆,具体参数和对应的含义建议查询官网,该函数主要做了尺寸缩放(scale)和转码(transcode)工作。第一个参数struct [SwsContext] c,需要调用struct SwsContext* sws_getContext(..., enum AVPixelFormat dstFormat, ...)创建,该函数也是一堆参数,请自行官网查询,其中参数enum
AVPixelFormat
dstFormat
,指定了目标格式,随后调用
sws_scale()
后得到的目标帧就是
dstFormat
格式的。因此,如果你的目标格式是RGB,只需要指定
dstFormat
为需要的RGB类型即可,FFMPEG中的RGB系列的有
AV_PIX_FMT_RGB24
、
AV_PIX_FMT_ARGB
等。
1.4.2 帧输出
除了考虑输出帧的格式,另一个实际的问题是:**解出来的帧放在哪儿,怎么放?放在哪儿的问题看个人需求,有些可能直接dump到磁盘,保存成本地视频文件或者一帧一帧的图片;在有些应用场景,解码可能只是系统最前端模块,此时可能需要存放到共享内存或者系统内存。随之而来的是怎么放的问题,前者如保存成视频,可以通过
fopen()
创建视频文件,接着再解码的循环内部调用
fwrite()
将帧数据保存到文件,最后用
fclose()
关闭即可;后者一定涉及到需要把
AVFrame
的帧数据转化成
uint8_t*
/
unsigned char*
的操作,可以调用API函数
int av_image_copy_to_buffer()
达到这个目的。
1.4.3 刷新缓冲区
在实际做解码工作时一定要注意
刷新缓冲区
!!!如果不这么做的话,最后解码出来的帧数目和实际视频帧数是对不齐的,会发现总是少了一些尾帧。原因就是FFMPEG内部有一个buffer,需要再把buffer的帧刷出来。其实做法也很简单,在解码的最后,将packet的
data
和
size
成员分别赋值为
nullptr
和
0
,这个时候缓冲区所有的帧数据都会被放进一个packet中,因此最后再进行一次解码就可以拿出所有的帧数据了。
1.4.4 帧释放
FFMPEG非常重要的一点,有些申请的变量一定要在结束前显示释放。具体哪些API的调用需要显示释放,在官方文档上都有详细的说明。这里补充本样例代码的变量释放部分:了解了以上几点,整个解码流程是真正搭建起来了。最后提
AVDictionary
,一个名称为
可选项
,但是实际上非常有用的结构。
AVDictionary options
参数,尽管这个参数可以被置为
nullptr,但实际上这个参数的用处还是挺大的,比如设置FFMPEG缓存区大小、探测码流格式的时间、最大延时、超时时间、以及支持的协议的白名单等。
2. ffpeag 硬解
视频硬解码和软解码有什么区别?本质上没什么区别,都是用芯片执行编解码计算。
软硬的称呼容易引起歧义,实质上:用CPU通用 计算单元 (无论是Intel还是AMD)就是软解;用专用芯片模组(GPU、QSV等)就是硬解。
因此区别也就出来了:底层接口不同、 指令集 不同、硬件驱动不同。由此引申出来的问题也就显而易见了:
至于实际生活生产中, 到底选择硬解码还是软解码?
要视不同情况而定。比如:
2.1 支持的硬解格式
首先来看下FFMPEG原生支持哪些硬解码类型,在
AVHWDeviceType
(libavutil/hwcontext.h)中列举出所有原生支持的硬解码类型:
enum AVHWDeviceType {
AV_HWDEVICE_TYPE_NONE,
AV_HWDEVICE_TYPE_VDPAU,
AV_HWDEVICE_TYPE_CUDA,
AV_HWDEVICE_TYPE_VAAPI,
AV_HWDEVICE_TYPE_DXVA2,
AV_HWDEVICE_TYPE_QSV,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_DRM,
AV_HWDEVICE_TYPE_OPENCL,
AV_HWDEVICE_TYPE_MEDIACODEC,
上面的AV_HWDEVICE_TYPE_CUDA
就是笔者目前正在做的CUDA是NVIDIA的硬件加速库,AV_HWDEVICE_TYPE_QSV
则是以前做的QSV是Intel提供的一套集显上的硬件加速方案。
那么,究竟要怎么知道系统当前的FFMPEG究竟支持哪些硬件库?
可以通过命令行查看:ffmpeg -hwaccel
。在hardware acceleration methods:
下面可以看到当前FFMPEG集成的硬解码库。
然后,如果发现自己需要的硬件库不在当前FFMPEG中怎么办?
答案是:很可能需要自己重新编译源码。
2.2 硬解API
硬解步骤和软解步骤类似,笔者绘制了一幅FFMPEG硬件解码流程图:图中橙色部分是硬解码中有而软解码没有的部分。
2.2.1 寻找硬解codec
AVCodec* avcodec_find_decoder_by_name(const char *name)
通过名字来寻找对应的AVCodec
。每一个解码器的名字一定是全局唯一的,在AVCodec
头文件中有相应的描述:
Name of the codec implementation. The name is globally unique among encoders and among decoders (but an encoder and a decoder can share the same name). This is the primary way to find a codec from the user perspective.
其实在FFMPEG内部每一个解码器codec都是一个结构体,维护了该解码器自己的信息、具体执行的函数等信息。比如Intel的QSV解码器(在libavcodec/qsvdec_h2645.c)是:
AVCodec ff_h264_qsv_decoder = {
.name = "h264_qsv",
.long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video acceleration)"),
.priv_data_size = sizeof(QSVH2645Context),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_H264,
.init = qsv_decode_init,
.decode = qsv_decode_frame,
.flush = qsv_decode_flush,
.close = qsv_decode_close,
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_DR1 | AV_CODEC_CAP_AVOID_PROBING | AV_CODEC_CAP_HYBRID,
.priv_class = &class,
.pix_fmts = (const enum AVPixelFormat[]){ AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
AV_PIX_FMT_QSV,
AV_PIX_FMT_NONE },
.hw_configs = ff_qsv_hw_configs,
.bsfs = "h264_mp4toannexb",
.wrapper_name = "qsv",
可以看到这个codec支持的codec id
是AV_CODEC_ID_H264
,支持的目标像素格式有{ AV_PIX_FMT_NV12,AV_PIX_FMT_P010,AV_PIX_FMT_QSV,AV_PIX_FMT_NONE }
。 是的,硬件解码器不同于通用解码器,只能支持有限的目标像素格式。 再来看看CUDA解码器(在libavcodec/cuviddec.c),同样的,他也只能支持有限的目标像素格式:
AVCodec ff_##x##_cuvid_decoder = { \
.name = #x "_cuvid", \
.long_name = NULL_IF_CONFIG_SMALL("Nvidia CUVID " #X " decoder"), \
.type = AVMEDIA_TYPE_VIDEO, \
.id = AV_CODEC_ID_##X, \
.priv_data_size = sizeof(CuvidContext), \
.priv_class = &x##_cuvid_class, \
.init = cuvid_decode_init, \
.close = cuvid_decode_end, \
.decode = cuvid_decode_frame, \
.receive_frame = cuvid_output_frame, \
.flush = cuvid_flush, \
.bsfs = bsf_name, \
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AVOID_PROBING | AV_CODEC_CAP_HARDWARE, \
.pix_fmts = (const enum AVPixelFormat[]){ AV_PIX_FMT_CUDA, \
AV_PIX_FMT_NV12, \
AV_PIX_FMT_P010, \
AV_PIX_FMT_P016, \
AV_PIX_FMT_NONE }, \
.hw_configs = cuvid_hw_configs, \
.wrapper_name = "cuvid", \
2.2.2 寻找硬解目标像素
硬解码codec支持的目标像素是有限的、且各自不一定相同。因此找到了硬解码codec之后,就得准备设置它的目标像素(pixel format)。
enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name)
这个函数是通过名称去寻找对应的AVHWDeviceType
,这是一个枚举类型的变量(定义在libavutil/hwcontext.h头文件中):
enum AVHWDeviceType {
AV_HWDEVICE_TYPE_NONE,
AV_HWDEVICE_TYPE_VDPAU,
AV_HWDEVICE_TYPE_CUDA,
AV_HWDEVICE_TYPE_VAAPI,
AV_HWDEVICE_TYPE_DXVA2,
AV_HWDEVICE_TYPE_QSV,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_DRM,
AV_HWDEVICE_TYPE_OPENCL,
AV_HWDEVICE_TYPE_MEDIACODEC,
AV_HWDEVICE_TYPE_VULKAN,
这个类型和名称的关系表就简单多了,在FFMPEG代码中是用hw_type_names关系表来维护的(定义在libavutil/hwcontext.c文件中):
static const char *const hw_type_names[] = {
[AV_HWDEVICE_TYPE_CUDA] = "cuda",
[AV_HWDEVICE_TYPE_DRM] = "drm",
[AV_HWDEVICE_TYPE_DXVA2] = "dxva2",
[AV_HWDEVICE_TYPE_D3D11VA] = "d3d11va",
[AV_HWDEVICE_TYPE_OPENCL] = "opencl",
[AV_HWDEVICE_TYPE_QSV] = "qsv",
[AV_HWDEVICE_TYPE_VAAPI] = "vaapi",
[AV_HWDEVICE_TYPE_VDPAU] = "vdpau",
[AV_HWDEVICE_TYPE_VIDEOTOOLBOX] = "videotoolbox",
[AV_HWDEVICE_TYPE_MEDIACODEC] = "mediacodec",
[AV_HWDEVICE_TYPE_VULKAN] = "vulkan",
const AVCodecHWConfig * avcodec_get_hw_config (const AVCodec *codec, int index)
紧接着,调用这个函数去获取到该解码器codec的硬件属性,比如可以支持的目标像素格式等。而这个信息就存储在AVCodecHWConfig
中:
typedef struct AVCodecHWConfig {
* A hardware pixel format which the codec can use. !!!硬解码codec支持的像素格式!!!
enum AVPixelFormat pix_fmt;
* Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible
* setup methods which can be used with this configuration.
int methods;
* The device type associated with the configuration.
* Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and
* AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused.
enum AVHWDeviceType device_type;
} AVCodecHWConfig;
enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt); 这是一个回调函数,它的作用就是告诉解码器codec自己的目标像素格式是什么。在上一步骤获取到了硬解码器codec可以支持的目标格式之后,就通过这个回调函数告知给codec,具体做法:
enum AVPixelFormat get_hw_format(struct AVCodecContext *s, const enum AVPixelFormat *fmt) {
for (const enum AVPixelFormat *p = fmt; *p != -1; p++) {
if (*p == hw_pix_fmt) return *p;
return AV_PIX_FMT_NONE;
我们也可以通过阅读AVCodec
结构内对于这个回调函数的定义,可以知道:
fmt是这个解码器codec支持的像素格式,且按照质量优劣进行排序;
如果没有特别的需要,这个步骤是可以省略的。内部默认会使用“native”的格式。
* callback to negotiate the pixelFormat
* @param fmt is the list of formats which are supported by the codec, it is terminated by -1 as 0 is a valid format, the formats are ordered by quality. The first is always the native one.
* @note The callback may be called again immediately if initialization for the selected (hardware-accelerated) pixel format failed.
* @warning Behavior is undefined if the callback returns a value not in the fmt list of formats.
* @return the chosen format
* - encoding: unused
* - decoding: Set by user, if not set the native format will be chosen.
2.2.3 准备和打开硬解码
int av_hwdevice_ctx_create(AVBufferRef **pdevice_ref, enum AVHWDeviceType type, const char *device, AVDictionary *opts, int flags)
这个函数的作用是,创建硬件设备相关的上下文信息AVHWDeviceContext
,包括分配内存资源、对硬件设备进行初始化。 准备好硬件设备上下文AVHWDeviceContext
后,需要把这个信息绑定到AVCodecContext
,就可以像软解一样的流程执行解码操作了。
2.2.4 取回数据
按照一般软解的流程,在调用avcodec_receive_frame()
之后,得到的数据其实还在硬件模组/芯片上,也就是说,如果是用CUDA解码,数据是在显存上(或者说是在显卡encoder/decoder的buffer上)的。对于很多应用而言,解码之后往往还要进行后续操作,比如保存成一幅幅图片之类的,那么就需要把数据取回。
int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags)
这个函数是负责在cpu内存和硬件内存(原文是hw surface)之间做数据交换的。也就是说,它不但可以把数据从硬件surface上搬回系统内存,反向操作也支持;甚至可以直接在硬件内存之间做数据交互。
\