赞
踩
视频解码,是将压缩后的视频(压缩格式如H264)通过对应解码算法还原为YUV视频流的过程;在计算机看来,首先输入一段01串(压缩的视频),然后进行大量的浮点运算,最后再输出更长的一段01串(还原的非压缩视频)。计算机内部可以进行浮点数计算的部件是CPU,目前市场上涌现了一批GPU和类GPU芯片,如Nvidia、海思芯片甚至Intel自家的核显。利用前者进行解码一般称为“软解码”,后者被称为“硬解码”,如果没有特殊指定,FFMPEG是用CPU进行解码的,即软解。本文将介绍的是软解,也就是FFMPEG最通用的做法。
FFPEAG官方参考技术手册:ffmpeg.org/developer.h…
和很多工具一样,FFMPEG解码也是有套路的,以下是雷神的解码过程:
最新版的解码过程如下所示:
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
1.3.1 连接和打开视频流
连接和打开视频流**必然是后续进行解码的关键,该步骤对应的API调用为:
[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中。最后读取视频头信息。
上面步骤结束后,就可以调用APIav_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 定位视频流数据
无论是离线的还是在线的视频文件,相对正确的称呼应该是“多媒体”文件。要知道,这些文件一般不止有一路视频流数据,可能同时包括多路音频数据、视频数据甚至字幕数据等。因此我们在做解码之前,需要首先找到我们需要的视频流数据。
int avformat_find_stream_info(AVFormatContext** ic, AVDictionary ** options) avformat_find_stream_info()进一步解析该视频文件信息,主要是指AVFormatContext结构体的AVStream。 从雷神的FFmpeg源代码简单分析:avformat_find_stream_info()文章可以了解到,该函数内部已经做了一套完整的解码流程,获取了多媒体流的信息。 请注意,一个视频文件中可能会同时包括视频文件和音频文件等多个媒体流,这也就解释了为什么后续还要遍历AVFormatContext的streams成员(类型是AVStream)做对应的解码。
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()
才真正执行了内容拷贝。
打开解码器 - [avcodec_open2(AVCodecContext* avctx, const AVCodec* codec, AVDictionary ** options) ,该函数主要服务于解码器,包括为其分配相关变量内存、检查解码器状态等。
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 帧转码
软解得到的帧格式是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缓存区大小、探测码流格式的时间、最大延时、超时时间、以及支持的协议的白名单等。
视频硬解码和软解码有什么区别?本质上没什么区别,都是用芯片执行编解码计算。
软硬的称呼容易引起歧义,实质上:用CPU通用计算单元(无论是Intel还是AMD)就是软解;用专用芯片模组(GPU、QSV等)就是硬解。
因此区别也就出来了:底层接口不同、指令集不同、硬件驱动不同。由此引申出来的问题也就显而易见了:
首先,因为CPU是通用计算单元,所以接口通用,移植性好;而专用芯片模组之间无法移植互用;
其次,因为CPU接口通用,因此编解码内部很多细节方便开发人员修改;而专用芯片模组,接口和驱动都是不同厂商提供的,很多是非开源,因此比较难控制内部细节。
最后,目前用CPU做编解码的效果,在实际测试下来会比专用芯片模组的效果好些。不过这个问题可以通过优化算法和芯片解决,这就是厂商的事儿了,我们控制不了。
至于实际生活生产中,到底选择硬解码还是软解码?
要视不同情况而定。比如:
CPU富余、需要精准控制解码流程、有解码算法的优化、通用性要求高,直接使用软解(也就是CPU解码);
有其他编解码芯片/模组、CPU不够用,就不得不需要转向硬解码(也就是专用芯片解码)。
首先来看下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中怎么办?
答案是:很可能需要自己重新编译源码。
硬解步骤和软解步骤类似,笔者绘制了一幅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上搬回系统内存,反向操作也支持;甚至可以直接在硬件内存之间做数据交互。
原文链接:ffmpeg的解码过程 - 掘金
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。