ffmpeg的解码过程

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解码也是有套路的,以下是雷神的解码过程:

最新版的解码过程如下所示:

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

1.3 详解解码过程

1.3.1 连接和打开视频流

  • 连接和打开视频流**必然是后续进行解码的关键,该步骤对应的API调用为:

[int avformat_network_init(void)]:官方文档建议加上avformat_network_init(),虽然这个不是必须的。深入阅读该实现过程,说白了,该函数会初始化和启动底层的TLS库,这也就解释了网上很多资料关于如果要打开网络流的话,这个API是必须的的说法了。

扫描二维码关注公众号,回复: 14729269 查看本文章
  • 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.4.1 帧转码

软解得到的帧格式是YUV格式的,具体格式可以存放在AVFrameformat(类型为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_RGB24AV_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的datasize成员分别赋值为nullptr0,这个时候缓冲区所有的帧数据都会被放进一个packet中,因此最后再进行一次解码就可以拿出所有的帧数据了。

1.4.4 帧释放

FFMPEG非常重要的一点,有些申请的变量一定要在结束前显示释放。具体哪些API的调用需要显示释放,在官方文档上都有详细的说明。这里补充本样例代码的变量释放部分:了解了以上几点,整个解码流程是真正搭建起来了。最后提AVDictionary,一个名称为可选项,但是实际上非常有用的结构。

AVDictionary options参数,尽管这个参数可以被置为nullptr,但实际上这个参数的用处还是挺大的,比如设置FFMPEG缓存区大小、探测码流格式的时间、最大延时、超时时间、以及支持的协议的白名单等。

2. ffpeag 硬解

视频硬解码和软解码有什么区别?本质上没什么区别,都是用芯片执行编解码计算。

软硬的称呼容易引起歧义,实质上:用CPU通用计算单元(无论是Intel还是AMD)就是软解;用专用芯片模组(GPU、QSV等)就是硬解。

因此区别也就出来了:底层接口不同、指令集不同、硬件驱动不同。由此引申出来的问题也就显而易见了:

  1. 首先,因为CPU是通用计算单元,所以接口通用,移植性好;而专用芯片模组之间无法移植互用;

  2. 其次,因为CPU接口通用,因此编解码内部很多细节方便开发人员修改;而专用芯片模组,接口和驱动都是不同厂商提供的,很多是非开源,因此比较难控制内部细节。

  3. 最后,目前用CPU做编解码的效果,在实际测试下来会比专用芯片模组的效果好些。不过这个问题可以通过优化算法和芯片解决,这就是厂商的事儿了,我们控制不了。

至于实际生活生产中,到底选择硬解码还是软解码?

要视不同情况而定。比如:

  1. CPU富余、需要精准控制解码流程、有解码算法的优化、通用性要求高,直接使用软解(也就是CPU解码);

  2. 有其他编解码芯片/模组、CPU不够用,就不得不需要转向硬解码(也就是专用芯片解码)。

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 idAV_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结构内对于这个回调函数的定义,可以知道:

    1. fmt是这个解码器codec支持的像素格式,且按照质量优劣进行排序;

    2. 如果没有特别的需要,这个步骤是可以省略的。内部默认会使用“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)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

猜你喜欢

转载自blog.csdn.net/yinshipin007/article/details/130034281