zoukankan      html  css  js  c++  java
  • FFmpeg 框架详解

    [总结]FFMPEG 视音频编解码零基础学习方法

    架构图

    FFMPEG+SDL 的视频播放器

    最简单的基于 FFMPEG+SDL 的视频播放器 ver2 (采用 SDL2.0)

    FFmpeg 解码一个视频流程:

    FFmpeg解码一个视频流程

    FFmpeg解码一个视频流程

    SDL2.0 显示 YUV 的流程:

    SDL2.0显示YUV的流程

    SDL2.0显示YUV的流程

    FFMPEG 的视频编码器(YUV 编码为 H.264)

    最简单的基于 FFMPEG 的视频编码器(YUV 编码为 H.264)

    最简单的基于 FFmpeg 的视频编码器 - 更新版(YUV 编码为 HEVC(H.265))

    最简单的基于 FFmpeg 的编码器 - 纯净版(不包含 libavformat)

    FFmpeg 编码视频的流程图

    通过该流程,不仅可以编码 H.264/H.265 的码流,而且可以编码 MPEG4/MPEG2/VP9/VP8 等多种码流。实际上使用 FFmpeg 编码视频的方式都是一样的。图中蓝色背景的函数是实际输出数据的函数。浅绿色的函数是视频编码的函数。

    FFmpeg编码视频的流程图

    FFmpeg编码视频的流程图

    简单介绍一下流程中各个函数的意义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    av_register_all()  // 注册FFmpeg所有编解码器。
    avformat_alloc_output_context2() // 初始化输出码流的AVFormatContext。
    avio_open() // 打开输出文件。
    av_new_stream() // 创建输出码流的AVStream。
    avcodec_find_encoder() // 查找编码器。
    avcodec_open2() // 打开编码器。
    avformat_write_header() // 写文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。
    avcodec_encode_video2() // 编码一帧视频。即将AVFrame(存储YUV像素数据)编码为AVPacket(存储H.264等格式的码流数据)。
    av_write_frame() // 将编码后的视频码流写入文件。
    flush_encoder() // 输入的像素数据读取完成后调用此函数。用于输出编码器中剩余的AVPacket。
    av_write_trailer() // 写文件尾(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

    “纯净” 的基于 FFmpeg 的视频编码器

    以下记录一个更加 “纯净” 的基于 FFmpeg 的视频编码器。此前记录过一个基于 FFmpeg 的视频编码器:

    《最简单的基于 FFmpeg 的视频编码器 - 更新版(YUV 编码为 HEVC(H.265))》

    这个视频编码器调用了 FFmpeg 中的 libavformat 和 libavcodec 两个库完成了视频编码工作。但是这不是一个 “纯净” 的编码器。

    上述两个库中 libavformat 完成封装格式处理,而 libavcodec 完成编码工作。

    一个 “纯净” 的编码器,理论上说只需要使用 libavcodec 就足够了,并不需要使用 libavformat。一下记录的编码器就是这样的一个 “纯净” 的编码器,它仅仅通过调用 libavcodec 将 YUV 数据编码为 H.264/HEVC 等格式的压缩视频码流。

    仅使用 libavcodec(不使用 libavformat)编码视频的流程:

    仅使用libavcodec(不使用libavformat)编码视频的流程

    仅使用libavcodec(不使用libavformat)编码视频的流程

    流程图中关键函数的作用如下所列:

    1
    2
    3
    4
    5
    avcodec_register_all()  // 注册所有的编解码器。
    avcodec_find_encoder() // 查找编码器。
    avcodec_alloc_context3() // 为AVCodecContext分配内存。
    avcodec_open2() // 打开编码器。
    avcodec_encode_video2() // 编码一帧数据。

    两个存储数据的结构体如下所列:

    1
    2
    AVFrame  // 存储一帧未编码的像素数据。
    AVPacket // 存储一帧压缩编码数据。

    对比:

    简单记录一下这个只使用 libavcodec 的 “纯净版” 视频编码器和使用 libavcodec+libavformat 的视频编码器的不同。

    (1) 下列与 libavformat 相关的函数在 “纯净版” 视频编码器中都不存在。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    av_register_all注册所有的编解码器,复用/解复用器等等组件。其中调用了
    avcodec_register_all() // 注册所有编解码器相关的组件。
    avformat_alloc_context() // 创建AVFormatContext结构体。
    avformat_alloc_output_context2() // 初始化一个输出流。
    avio_open() // 打开输出文件。
    avformat_new_stream() // 创建AVStream结构体。avformat_new_stream()中会调用
    avcodec_alloc_context3() // 创建AVCodecContext结构体。
    avformat_write_header() // 写文件头。
    av_write_frame() // 写编码后的文件帧。
    av_write_trailer() // 写文件尾。

    (2) 新增了如下几个函数

    1
    2
    avcodec_register_all()  // 只注册编解码器有关的组件。
    avcodec_alloc_context3() // 创建AVCodecContext结构体。

    可以看出,相比于 “完整” 的编码器,这个纯净的编码器函数调用更加简单,功能相对少一些,相对来说更加的“轻量”。

    解码框架图

    FFmpeg解码

    FFmpeg解码

    编码框架图

    FFmpeg编码

    FFmpeg编码

    通用函数解析

    函数解析

    av_register_all()

    ffmpeg 注册复用器,编码器等的函数 av_register_all()。该函数在所有基于 ffmpeg 的应用程序中几乎都是第一个被调用的。只有调用了该函数,才能使用复用器,编码器等。

    函数调用关系图如下图所示。av_register_all() 调用了 avcodec_register_all()avcodec_register_all() 注册了和编解码器有关的组件:硬件加速器,解码器,编码器,Parser,Bitstream Filter。av_register_all() 除了调用 avcodec_register_all() 之外,还注册了复用器,解复用器,协议处理器。

    av_register_all

    av_register_all

    内存的分配和释放(av_malloc()、av_free() 等)

    内存操作的常见函数位于 libavutilmem.c 中。本文记录 FFmpeg 开发中最常使用的几个函数:av_malloc()av_realloc()av_mallocz()av_calloc()av_free()av_freep()

    av_malloc() 就是简单的封装了系统函数 malloc(),并做了一些错误检查工作。

    关于 size_t

    size _t 这个类型在 FFmpeg 中多次出现,简单解释一下其作用。size _t 是为了增强程序的可移植性而定义的。不同系统上,定义 size_t 可能不一样。它实际上就是 unsigned int。

    为什么要内存对齐?

    FFmpeg 内存分配方面多次涉及到 “内存对齐”(memory alignment)的概念。

    这方面内容在 IBM 的网站上有一篇文章,讲的挺通俗易懂的,在此简单转述一下。

    程序员通常认为内存就是一个字节数组,每次可以一个一个字节存取内存。例如在 C 语言中使用 char * 指代 “一块内存”,Java 中使用 byte[] 指代一块内存。如下所示。

    但那实际上计算机处理器却不是这样认为的。处理器相对比较 “懒惰”,它会以 2 字节,4 字节,8 字节,16 字节甚至 32 字节来存取内存。例如下图显示了以 4 字节为单位读写内存的处理器 “看待” 上述内存的方式。

    上述的存取单位的大小称之为内存存取粒度。

    下面看一个实例,分别从地址 0,和地址 1 读取 4 个字节到寄存器。

    从程序员的角度来看,读取方式如下图所示。

    而 2 字节存取粒度的处理器的读取方式如下图所示。

    可以看出 2 字节存取粒度的处理器从地址 0 读取 4 个字节一共读取 2 次;从地址 1 读取 4 个字节一共读取了 3 次。由于每次读取的开销是固定的,因此从地址 1 读取 4 字节的效率有所下降。

    4 字节存取粒度的处理器的读取方式如下图所示。

    可以看出 4 字节存取粒度的处理器从地址 0 读取 4 个字节一共读取 1 次;从地址 1 读取 4 个字节一共读取了 2 次。从地址 1 读取的开销比从地址 0 读取多了一倍。由此可见内存不对齐对 CPU 的性能是有影响的。

    1
    2
    3
    4
    5
    6
    av_malloc()  // 是FFmpeg中最常见的内存分配函数, av_malloc()就是简单的封装了系统函数malloc()
    av_realloc() // 用于对申请的内存的大小进行调整。
    av_mallocz() // 可以理解为av_malloc()+zeromemory
    av_calloc() // 则是简单封装了av_mallocz()
    av_free() // 用于释放申请的内存
    av_freep() // 简单封装了av_free()。并且在释放内存之后将目标指针设置为NULL

    常见结构体的初始化和销毁(AVFormatContext,AVFrame 等)

    FFMPEG 中最关键的结构体之间的关系

    常见的结构体如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 统领全局的基本结构体。主要用于处理封装格式(FLV/MKV/RMVB 等)
    AVFormatContext

    // 输入输出对应的结构体,用于输入输出(读写文件,RTMP 协议等)
    AVIOContext

    // 视音频流对应的结构体,用于视音频编解码
    AVStream,AVCodecContext

    // 存储非压缩的数据(视频对应 RGB/YUV 像素数据,音频对应 PCM 采样数据)
    AVFrame

    // 存储压缩数据(视频对应 H.264 等码流数据,音频对应 AAC/MP3 等码流数据)
    AVPacket

    他们之间的关系如下图所示:

    常见结构体之间的关系

    常见结构体之间的关系

    简单分析一下上述几个结构体的初始化和销毁函数。这些函数列表如下。

    结构体初始化销毁
    AVFormatContext avformat_alloc_context() avformat_free_context()
    AVIOContext avio_alloc_context()  
    AVStream avformat_new_stream()  
    AVCodecContext avcodec_alloc_context3()  
    AVFrame av_frame_alloc();
    av_image_fill_arrays()
    av_frame_free()
    AVPacket av_init_packet();
    av_new_packet()
    av_free_packet()

    avformat_alloc_context()

    avformat_alloc_context() 的定义位于 libavformatoptions.c

    avformat_alloc_context() 调用 av_malloc() 为 AVFormatContext 结构体分配了内存,而且同时也给 AVFormatContext 中的 internal 字段分配内存(这个字段是 FFmpeg 内部使用的,先不分析)。此外调用了一个 avformat_get_context_defaults() 函数。该函数用于设置 AVFormatContext 的字段的默认值。它的定义也位于 libavformatoptions.c,确切的说就位于 avformat_alloc_context()上面

    avformat_get_context_defaults() 首先调用 memset() 将 AVFormatContext 的所有字段置 0。而后调用了一个函数 av_opt_set_defaults() 。av_opt_set_defaults() 用于给字段设置默认值。

    avformat_alloc_context() 代码的函数调用关系如下图所示。

    avformat_alloc_context

    avformat_alloc_context

    avformat_free_context() 的声明位于 libavformatavformat.h

    avformat_free_context() 的定义位于 libavformatoptions.c

    avformat_free_context() 调用了各式各样的销毁函数:av_opt_free()av_freep()av_dict_free()。这些函数分别用于释放不同种类的变量,在这里不再详细讨论。

    在这里看一个释放 AVStream 的函数 ff_free_stream()。该函数的定义位于 libavformatoptions.c(其实就在 avformat_free_context() 上方), 与释放 AVFormatContext 类似,释放 AVStream 的时候,也是调用了 av_freep()av_dict_free() 这些函数释放有关的字段。如果使用了 parser 的话,会调用 av_parser_close() 关闭该 parser。

    avio_alloc_context()

    AVIOContext 的初始化函数是 avio_alloc_context(),销毁的时候使用 av_free() 释放掉其中的缓存即可。它的声明位于 libavformatavio.h 中

    avio_alloc_context() 定义位于 libavformataviobuf.c 中

    avio_alloc_context() 首先调用 av_mallocz() 为 AVIOContext 分配内存。而后调用了一个函数 ffio_init_context() 。该函数完成了真正的初始化工作

    avformat_new_stream()

    avformat_new_stream() 的声明位于 libavformatavformat.h 中

    AVStream 的初始化函数是 avformat_new_stream(),销毁函数使用销毁 AVFormatContext 的 avformat_free_context() 就可以了。

    avformat_new_stream() 的定义位于 libavformatutils.c 中

    avformat_new_stream() 首先调用 av_mallocz() 为 AVStream 分配内存。接着给新分配的 AVStream 的各个字段赋上默认值。然后调用了另一个函数 avcodec_alloc_context3() 初始化 AVStream 中的 AVCodecContext。

    avcodec_alloc_context3()

    avcodec_alloc_context3() 的声明位于 libavcodecavcodec.h 中

    avcodec_alloc_context3() 的定义位于 libavcodecoptions.c 中

    avcodec_alloc_context3() 首先调用 av_malloc() 为 AVCodecContext 分配存储空间,然后调用了一个函数 avcodec_get_context_defaults3() 用于设置该 AVCodecContext 的默认值

    avformat_new_stream() 函数的调用结构如下所示:

    avformat_new_stream

    avformat_new_stream

    av_frame_alloc()

    AVFrame 的初始化函数是 av_frame_alloc(),销毁函数是 av_frame_free()。在这里有一点需要注意,旧版的 FFmpeg 都是使用 avcodec_alloc_frame() 初始化 AVFrame 的,但是我在写这篇文章的时候,avcodec_alloc_frame() 已经被标记为 “过时的” 了,为了保证与时俱进,决定分析新的API——av_frame_alloc()

    av_frame_alloc() 的声明位于 libavutilframe.h

    av_frame_alloc() 的定义位于 libavutilframe.c

    av_frame_alloc() 首先调用 av_mallocz() 为 AVFrame 结构体分配内存。而后调用了一个函数get_frame_defaults() 用于设置一些默认参数

    从 av_frame_alloc() 的代码我们可以看出,该函数并没有为 AVFrame 的像素数据分配空间。因此 AVFrame 中的像素数据的空间需要自行分配空间,例如使用 avpicture_fill(), av_image_fill_arrays() 等函数。

    av_frame_alloc() 函数的调用结构如下所示:

    av_frame_alloc

    av_frame_alloc

    avpicture_fill()

    avpicture_fill() 的声明位于 libavcodecavcodec.h

    avpicture_fill() 的定义位于 libavcodecavpicture.c

    avpicture_fill() 仅仅是简单调用了一下 av_image_fill_arrays()。也就是说这两个函数实际上是等同的

    av_image_fill_arrays()

    av_image_fill_arrays() 的声明位于 libavutilimgutils.h 中

    av_image_fill_arrays() 的定义位于 libavutilimgutils.c 中

    av_image_fill_arrays() 函数中包含 3 个函数:av_image_check_size()av_image_fill_linesizes()av_image_fill_pointers()av_image_check_size() 用于检查输入的宽高参数是否合理,即不能太大或者为负数。av_image_fill_linesizes() 用于填充 dst_linesize。av_image_fill_pointers() 则用于填充 dst_data。它们的定义相对比较简单,不再详细分析。

    avpicture_fill() 函数调用关系如下图所示:

    avpicture_fill

    avpicture_fill

    av_init_packet()

    av_init_packet() 的声明位于 libavcodecavcodec.h

    av_init_packet() 的定义位于 libavcodecavpacket.c

    av_new_packet()

    av_new_packet() 的声明位于 libavcodecavcodec.h

    av_new_packet() 的定义位于 libavcodecavpacket.c

    av_new_packet() 调用了 av_init_packet(pkt)。此外还调用了一个函数 packet_alloc()

    packet_alloc() 中调用 av_buffer_realloc() 为 AVPacket 分配内存。然后调用 memset() 将分配的内存置 0。

    PS:发现 AVPacket 的结构随着 FFmpeg 的发展越发复杂了。原先 AVPacket 中的数据仅仅存在一个 uint8_t 类型的数组里,而现在已经使用一个专门的结构体 AVBufferRef 存储数据。

    av_new_packet() 代码的函数调用关系如下图所示:

    av_new_packet

    av_new_packet

    av_free_packet() 的声明位于 libavcodecavcodec.h

    av_free_packet() 的定义位于 libavcodecavpacket.c

    av_free_packet() 调用 av_buffer_unref() 释放 AVPacket 中的数据,而后还调用了av_packet_free_side_data() 释放了 side_data(存储封装格式可以提供的额外的数据)。

    avio_open2()

    该函数用于打开 FFmpeg 的输入输出文件。avio_open2() 的声明位于 libavformatavio.h 文件中

    1
    2
    int avio_open2(AVIOContext **s, const char *url, int flags,
    const AVIOInterruptCB *int_cb, AVDictionary **options);

    avio_open2() 函数参数的含义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    s:函数调用成功之后创建的AVIOContext结构体。
    url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
    flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
    AVIO_FLAG_READ:只读。
    AVIO_FLAG_WRITE:只写。
    AVIO_FLAG_READ_WRITE:读写。
    int_cb:目前还没有用过。
    options:目前还没有用过。

    函数调用结构图:

    avio_open2

    avio_open2

    av_find_decoder() 和 av_find_encoder()

    avcodec_find_encoder() 用于查找 FFmpeg 的编码器,

    avcodec_find_decoder() 用于查找 FFmpeg 的解码器。

    avcodec_find_encoder() 的声明位于 libavcodecavcodec.h

    1
    AVCodec *avcodec_find_encoder(enum AVCodecID id);

    函数的参数是一个编码器的 ID,返回查找到的编码器(没有找到就返回 NULL)。

    avcodec_find_decoder() 的声明也位于 libavcodecavcodec.h

    1
    AVCodec *avcodec_find_decoder(enum AVCodecID id);

    函数的参数是一个解码器的 ID,返回查找到的解码器(没有找到就返回 NULL)。

    avcodec_find_encoder() 和 avcodec_find_decoder() 的函数调用关系图如下所示:

    函数调用关系图

    函数调用关系图

    avcodec_find_encoder() 的源代码位于 libavcodecutils.c

    avcodec_find_encoder() 调用了一个 find_encdec(),注意它的第二个参数是 1。

    find_encdec() 的源代码位于 libavcodecutils.c

    find_encdec() 中有一个循环,该循环会遍历 AVCodec 结构的链表,逐一比较输入的 ID 和每一个编码器的 ID,直到找到 ID 取值相等的编码器。

    在这里有几点需要注意:

    (1)first_avcodec 是一个全局变量,存储 AVCodec 链表的第一个元素。

    (2)remap_deprecated_codec_id() 用于将一些过时的编码器 ID 映射到新的编码器 ID。

    (3)函数的第二个参数 encoder 用于确定查找编码器还是解码器。当该值为 1 的时候,用于查找编码器,此时会调用 av_codec_is_encoder() 判断 AVCodec 是否为编码器;当该值为 0 的时候,用于查找解码器,此时会调用 av_codec_is_decoder() 判断 AVCodec 是否为解码器。

    avcodec_find_decoder() 的源代码位于 libavcodecutils.c

    avcodec_find_decoder() 同样调用了 find_encdec(),只是第 2 个参数设置为 0。

    avcodec_open2()

    该函数用于初始化一个视音频编解码器的 AVCodecContext。

    avcodec_open2() 的声明位于 libavcodecavcodec.h

    1
    int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

    用中文简单转述一下 avcodec_open2() 各个参数的含义:

    1
    2
    3
    avctx:需要初始化的 AVCodecContext。
    codec:输入的 AVCodec
    options:一些选项。例如使用 libx264 编码的时候,“preset”,“tune”等都可以通过该参数设置。

    avcodec_open2() 函数调用关系非常简单,如下图所示:

    avcodec_open2

    avcodec_open2

    avcodec_open2() 的定义位于 libavcodecutils.c

    avcodec_open2() 的源代码量是非常长的,但是它的调用关系非常简单——它只调用了一个关键的函数,即 AVCodec 的 init(),后文将会对这个函数进行分析。

    我们可以简单梳理一下 avcodec_open2() 所做的工作,如下所列:

    (1)为各种结构体分配内存(通过各种 av_malloc() 实现)。

    (2)将输入的 AVDictionary 形式的选项设置到 AVCodecContext。

    (3)其他一些零零碎碎的检查,比如说检查编解码器是否处于 “实验” 阶段。

    (4)如果是编码器,检查输入参数是否符合编码器的要求

    (5)调用 AVCodec 的 init() 初始化具体的解码器。

    前几步比较简单,不再分析。在这里我们分析一下第 4 步和第 5 步。

    检查输入参数是否符合编码器要求

    在这里简单分析一下第 4 步,即 “检查输入参数是否符合编码器的要求”。这一步中检查了很多的参数,在这里我们随便选一个参数 pix_fmts(像素格式)看一下,如下所示。

    代码:

    可以看出,该代码首先进入了一个 for() 循环,将 AVCodecContext 中设定的 pix_fmt 与编码器 AVCodec 中的 pix_fmts 数组中的元素逐一比较。

    先简单介绍一下 AVCodec 中的 pix_fmts 数组。AVCodec 中的 pix_fmts 数组存储了该种编码器支持的像素格式,并且规定以 AV_PIX_FMT_NONE(AV_PIX_FMT_NONE 取值为 -1)为结尾。例如,libx264 的 pix_fmts 数组的定义位于 libavcodeclibx264.c,如下所示。

    代码:

    从 pix_fmts_8bit 的定义可以看出 libx264 主要支持的是以 YUV 为主的像素格式。

    现在回到 “检查输入 pix_fmt 是否符合编码器的要求” 的那段代码。如果 for() 循环从 AVCodec->pix_fmts 数组中找到了符合 AVCodecContext->pix_fmt 的像素格式,或者完成了 AVCodec->pix_fmts 数组的遍历,都会跳出循环。如果发现 AVCodec->pix_fmts 数组中索引为 i 的元素是 AV_PIX_FMT_NONE(即最后一个元素,取值为 -1)的时候,就认为没有找到合适的像素格式,并且最终提示错误信息。

    AVCodec->init()

    avcodec_open2() 中最关键的一步就是调用 AVCodec 的 init() 方法初始化具体的编码器。AVCodec 的 init() 是一个函数指针,指向具体编解码器中的初始化函数。这里我们以 libx264 为例,看一下它对应的 AVCodec 的定义。

    libx264 对应的 AVCodec 的定义位于 libavcodeclibx264.c

    代码:

    可以看出在 ff_libx264_encoder 中 init() 指向 X264_init() 。X264_init() 的定义同样位于libavcodeclibx264.c

    X264_init() 的代码以后研究 X264 的时候再进行细节的分析,在这里简单记录一下它做的两项工作:

    (1)设置 X264Context 的参数。X264Context 主要完成了 libx264 和 FFmpeg 对接的功能。可以看出代码主要在设置一个 params 结构体变量,该变量的类型即是 x264 中存储参数的结构体 x264_param_t。 (2)调用 libx264 的 API 进行编码器的初始化工作。例如调用 x264_param_default() 设置默认参数,调用 x264_param_apply_profile() 设置 profile,调用 x264_encoder_open() 打开编码器等等。

    最后附上 X264Context 的定义,位于 libavcodeclibx264.c

    avcodec_close()

    该函数用于关闭编码器。avcodec_close() 函数的声明位于 libavcodecavcodec.h

    1
    int avcodec_close(AVCodecContext *avctx);

    该函数只有一个参数,就是需要关闭的编码器的 AVCodecContext。

    函数的调用关系图如下所示:

    avcodec_close

    avcodec_close

    avcodec_close() 的定义位于 libavcodecutils.c

    从 avcodec_close() 的定义可以看出,该函数释放 AVCodecContext 中有关的变量,并且调用了 AVCodec 的 close() 关闭了解码器。

    解码

    图解 FFMPEG 打开媒体的函数 avformat_open_input

    FFMPEG 打开媒体的的过程开始于 avformat_open_input,因此该函数的重要性不可忽视。

    在该函数中,FFMPEG 完成了:

    • 输入输出结构体 AVIOContext 的初始化;

    • 输入数据的协议(例如 RTMP,或者 file)的识别(通过一套评分机制):

      • 判断文件名的后缀
      • 读取文件头的数据进行比对;
    • 使用获得最高分的文件协议对应的 URLProtocol,通过函数指针的方式,与 FFMPEG 连接(非专业用词);

    剩下的就是调用该 URLProtocol 的函数进行 open, read 等操作了

    以下是通过 eclipse+MinGW 调试 FFMPEG 源代码获得的函数调用关系图:

    可见最终都调用了 URLProtocol 结构体中的函数指针。

    URLProtocol 结构如下,是一大堆函数指针的集合(avio.h 文件)

    代码

    URLProtocol 功能就是完成各种输入协议的读写等操作

    但输入协议种类繁多,它是怎样做到 “大一统” 的呢?

    原来,每个具体的输入协议都有自己对应的 URLProtocol。

    比如 file 协议(FFMPEG 把文件也当做一种特殊的协议)(*file.c 文件)

    代码:

    或者 rtmp 协议(此处使用了 librtmp)(librtmp.c 文件)

    代码:

    可见它们把各自的函数指针都赋值给了 URLProtocol 结构体的函数指针

    因此 avformat_open_input 只需调用 url_open, url_read 这些函数就可以完成各种具体输入协议的 open, read 等操作了

    avformat_open_input()

    FFMPEG 源码分析:avformat_open_input()(媒体打开函数)

    avformat_open_input()

    个人感觉这个函数确实太重要了,可以算作 FFmpeg 的 “灵魂”

    函数用于打开多媒体数据并且获得一些相关的信息。它的声明位于 libavformatavformat.h

    1
    int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);

    参数说明:

    1
    2
    3
    4
    ps:函数调用成功之后处理过的 AVFormatContext 结构体。
    file:打开的视音频流的 URL。
    fmt:强制指定 AVFormatContext 中 AVInputFormat 的。这个参数一般情况下可以设置为 NULL,这样 FFmpeg 可以自动检测 AVInputFormat。
    dictionay:附加的一些选项,一般情况下可以设置为 NULL。

    函数执行成功的话,其返回值大于等于 0。

    函数调用结构图如下所示:

    avformat_open_input

    avformat_open_input

    avformat_open_input() 定义位于 libavformatutils.c 中

    avformat_open_input() 源代码比较长,一部分是一些容错代码,比如说如果发现传入的 AVFormatContext 指针没有初始化过,就调用 avformat_alloc_context() 初始化该结构体;还有一部分是针对一些格式做的特殊处理,比如 id3v2 信息的处理等等。有关上述两种信息不再详细分析,在这里只选择它关键的两个函数进行分析:

    • init_input():绝大部分初始化工作都是在这里做的。

    • s->iformat->read_header():读取多媒体数据文件头,根据视音频流创建相应的 AVStream。

    init_input()

    init_input() 作为一个内部函数,竟然包含了一行注释(一般内部函数都没有注释),足可以看出它的重要性。它的主要工作就是打开输入的视频数据并且探测视频的格式。该函数的定义位于 libavformatutils.c

    代码:

    这个函数在短短的几行代码中包含了好几个 return,因此逻辑还是有点复杂的,我们可以梳理一下:

    在函数的开头的 score 变量是一个判决 AVInputFormat 的分数的门限值,如果最后得到的 AVInputFormat 的分数低于该门限值,就认为没有找到合适的 AVInputFormat 。

    FFmpeg 内部判断封装格式的原理实际上是对每种 AVInputFormat 给出一个分数,满分是 100 分,越有可能正确的 AVInputFormat 给出的分数就越高。最后选择分数最高的 AVInputFormat 作为推测结果。score 的值是一个宏定义 AVPROBE_SCORE_RETRY,我们可以看一下它的定义:

    1
    #define AVPROBE_SCORE_RETRY (AVPROBE_SCORE_MAX/4)

    其中 AVPROBE_SCORE_MAX 是 score 的最大值,取值是 100:

    1
    #define AVPROBE_SCORE_MAX       100 ///< maximum score

    由此我们可以得出 score 取值是 25,即如果推测后得到的最佳 AVInputFormat 的分值低于 25,就认为没有找到合适的 AVInputFormat。

    整个函数的逻辑大体如下:

    (1)当使用了自定义的 AVIOContext 的时候(AVFormatContext 中的 AVIOContext 不为空,即 s->pb!=NULL),如果指定了 AVInputFormat 就直接返回,如果没有指定就调用 av_probe_input_buffer2() 推测 AVInputFormat。这一情况出现的不算很多,但是当我们从内存中读取数据的时候(需要初始化自定义的 AVIOContext),就会执行这一步骤。

    (2)在更一般的情况下,如果已经指定了 AVInputFormat,就直接返回;如果没有指定 AVInputFormat,就调用 av_probe_input_format(NULL,…) 根据文件路径判断文件格式。这里特意把 av_probe_input_format() 的第 1 个参数写成 “NULL”,是为了强调这个时候实际上并没有给函数提供输入数据,此时仅仅通过文件路径推测 AVInputFormat。

    (3)如果发现通过文件路径判断不出来文件格式,那么就需要打开文件探测文件格式了,这个时候会首先调用 avio_open2() 打开文件,然后调用 av_probe_input_buffer2() 推测 AVInputFormat。

    avformat_find_stream_info()

    该函数可以读取一部分视音频数据并且获得一些相关的信息。

    avformat_find_stream_info() 的声明位于 libavformatavformat.h

    1
    int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

    简单解释一下它的参数的含义:

    1
    2
    ic:输入的 AVFormatContext。
    options:额外的选项,目前没有深入研究过。

    函数正常执行后返回值大于等于 0。

    PS:由于该函数比较复杂,所以只看了一部分代码,以后有时间再进一步分析。

    函数的调用关系如下图所示:

    avformat_find_stream_info

    avformat_find_stream_info

    avformat_find_stream_info() 的定义位于 libavformatutils.c

    由于avformat_find_stream_info() 代码比较长,难以全部分析,在这里只能简单记录一下它的要点。该函数主要用于给每个媒体流(音频 / 视频)的 AVStream 结构体赋值。我们大致浏览一下这个函数的代码,会发现它其实已经实现了解码器的查找,解码器的打开,视音频帧的读取,视音频帧的解码等工作。换句话说,该函数实际上已经 “走通” 的解码的整个流程。下面看一下除了成员变量赋值之外,该函数的几个关键流程。

    • 查找解码器:find_decoder()

    • 打开解码器:avcodec_open2()

    • 读取完整的一帧压缩编码的数据:read_frame_internal()

      注:av_read_frame() 内部实际上就是调用的 read_frame_internal()

    • 解码一些压缩编码数据:try_decode_frame()

    av_read_frame()

    ffmpeg 中的 av_read_frame() 的作用是读取码流中的音频若干帧或者视频一帧。例如,解码视频的时候,每解码一个视频帧,需要先调用 av_read_frame() 获得一帧视频的压缩数据,然后才能对该数据进行解码(例如 H.264 中一帧压缩数据通常对应一个 NAL)。

    上代码之前,先参考了其他人对 av_read_frame() 的解释,在此做一个参考:

    通过 av_read_packet(),读取一个包,需要说明的是此函数必须是包含整数帧的,不存在半帧的情况,以 ts 流为例,是读取一个完整的 PES 包(一个完整 pes 包包含若干视频或音频 es 包),读取完毕后,通过 av_parser_parse2() 分析出视频一帧(或音频若干帧),返回,下次进入循环的时候,如果上次的数据没有完全取完,则 st = s->cur_st ; 不会是 NULL,即再此进入 av_parser_parse2() 流程,而不是下面的 av_read_packet() 流程,这样就保证了,如果读取一次包含了 N 帧视频数据(以视频为例),则调用 av_read_frame() N 次都不会去读数据,而是返回第一次读取的数据,直到全部解析完毕。

    av_read_frame() 的声明位于 libavformatavformat.h

    1
    int av_read_frame(AVFormatContext *s, AVPacket *pkt);

    av_read_frame() 使用方法在注释中写得很详细,用中文简单描述一下它的两个参数:

    1
    2
    s:输入的AVFormatContext
    pkt:输出的AVPacket

    如果返回 0 则说明读取正常。

    函数调用结构图如下所示:

    av_read_frame

    av_read_frame

    av_read_frame() 的定义位于 libavformatutils.c

    read_frame_internal() 代码比较长,这里只简单看一下它前面的部分。它前面部分有 2 步是十分关键的:

    (1)调用了 ff_read_packet() 从相应的 AVInputFormat 读取数据。

    (2)如果媒体频流需要使用 AVCodecParser,则调用 parse_packet() 解析相应的 AVPacket。

    ff_read_packet() 中最关键的地方就是调用了 AVInputFormat 的 read_packet() 方法。 AVInputFormat 的 read_packet() 是一个函数指针,指向当前的 AVInputFormat 的读取数据的函数。在这里我们以 FLV 封装格式对应的 AVInputFormat 为例,看看 read_packet() 的实现函数是什么样子的。

    FLV 封装格式对应的 AVInputFormat 的定义位于 libavformatflvdec.c

    代码:

    从 ff_flv_demuxer 的定义可以看出,read_packet() 对应的是 flv_read_packet() 函数。在看 flv_read_packet() 函数之前,我们先回顾一下 FLV 封装格式的结构,如下图所示。

    PS:原图是网上找的,感觉画的很清晰,比官方的 Video File Format Specification 更加通俗易懂。但是图中有一个错误,就是 TagHeader 中的 StreamID 字段的长度写错了(查看了一下官方标准,应该是 3 字节,现在已经改过来了)。

    FLV封装格式

    FLV封装格式

    从图中可以看出,FLV 文件体部分是由一个一个的 Tag 连接起来的(中间间隔着 Previous Tag Size)。每个 Tag 包含了 Tag Header 和 Tag Data 两个部分。

    Tag Data 根据 Tag 的 Type 不同而不同:可以分为音频 Tag Data,视频 Tag Data 以及 Script Tag Data。下面简述一下音频 Tag Data 和视频 Tag Data。

    Audio Tag Data

    Audio Tag 在官方标准中定义如下。

    Audio Tag

    Audio Tag

    Audio Tag 开始的第 1 个字节包含了音频数据的参数信息,从第 2 个字节开始为音频流数据。
    第 1 个字节的前 4 位的数值表示了音频数据格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    0 = Linear PCM, platform endian
    1 = ADPCM
    2 = MP3
    3 = Linear PCM, little endian
    4 = Nellymoser 16-kHz mono
    5 = Nellymoser 8-kHz mono
    6 = Nellymoser
    7 = G.711 A-law logarithmic PCM
    8 = G.711 mu-law logarithmic PCM
    9 = reserved
    10 = AAC
    14 = MP3 8-Khz
    15 = Device-specific sound

    第 1 个字节的第 5-6 位的数值表示采样率:0 = 5.5kHz,1 = 11KHz,2 = 22 kHz,3 = 44 kHz

    第 1 个字节的第 7 位表示采样精度:0 = 8bits,1 = 16bits

    第 1 个字节的第 8 位表示音频类型:0 = sndMono,1 = sndStereo

    其中,当音频编码为 AAC 的时候,第一个字节后面存储的是 AACAUDIODATA,格式如下所示。

    AACAUDIODATA格式

    AACAUDIODATA格式

    Video Tag Data

    Video Tag 在官方标准中的定义如下:

    Video Tag

    Video Tag

    Video Tag 也用开始的第 1 个字节包含视频数据的参数信息,从第 2 个字节为视频流数据。

    第 1 个字节的前 4 位的数值表示帧类型(FrameType):

    1
    2
    3
    4
    5
    1: keyframe (for AVC, a seekableframe)(关键帧)
    2: inter frame (for AVC, a nonseekableframe)
    3: disposable inter frame (H.263only)
    4: generated keyframe (reservedfor server use only)
    5: video info/command frame

    第 1 个字节的后 4 位的数值表示视频编码 ID(CodecID):

    1
    2
    3
    4
    5
    6
    7
    1: JPEG (currently unused)
    2: Sorenson H.263
    3: Screen video
    4: On2 VP6
    5: On2 VP6 with alpha channel
    6: Screen video version 2
    7: AVC

    其中,当音频编码为 AVC(H.264)的时候,第一个字节后面存储的是 AVCVIDEOPACKET,格式如下所示。

    AVCVIDEOPACKET格式

    AVCVIDEOPACKET格式

    了解了 FLV 的基本格式之后,就可以看一下 FLV 解析 Tag 的函数 flv_read_packet()了

    flv_read_packet() 的定义位于 libavformatflvdec.c

    flv_read_packet() 的代码比较长,但是逻辑比较简单。它的主要功能就是根据 FLV 文件格式的规范,逐层解析 Tag 以及 TagData,获取 Tag 以及 TagData 中的信息。比较关键的地方已经写上了注释,不再详细叙述。

    parse_packet() 给需要 AVCodecParser 的媒体流提供解析 AVPacket 的功能。

    从代码中可以看出,最终调用了相应 AVCodecParser 的 av_parser_parse2() 函数,解析出来 AVPacket。此后根据解析的信息还进行了一系列的赋值工作,不再详细叙述。

    avcodec_decode_video2()

    ffmpeg 中的 avcodec_decode_video2() 的作用是解码一帧视频数据。输入一个压缩编码的结构体 AVPacket,输出一个解码后的结构体 AVFrame。该函数的声明位于 libavcodecavcodec.h

    1
    2
    3
    int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
    int *got_picture_ptr,
    const AVPacket *avpkt);

    查看源代码之后发现,这个函数竟然十分的简单,源代码位于 libavcodecutils.c

    从代码中可以看出,avcodec_decode_video2() 主要做了以下几个方面的工作:

    (1)对输入的字段进行了一系列的检查工作:例如宽高是否正确,输入是否为视频等等。

    (2)通过 ret = avctx->codec->decode(avctx, picture, got_picture_ptr,&tmp) 这句代码,调用了相应 AVCodec 的 decode() 函数,完成了解码操作。

    (3)对得到的 AVFrame 的一些字段进行了赋值,例如宽高、像素格式等等。

    其中第二部是关键的一步,它调用了 AVCodec 的 decode() 方法完成了解码。AVCodec 的 decode() 方法是一个函数指针,指向了具体解码器的解码函数。在这里我们以 H.264 解码器为例,看一下解码的实现过程。H.264 解码器对应的 AVCodec 的定义位于 libavcodech264.c,如下所示。

    代码:

    从 ff_h264_decoder 的定义可以看出,decode() 指向了 h264_decode_frame() 函数。

    从 h264_decode_frame() 的定义可以看出,它调用了 decode_nal_units() 完成了具体的 H.264 解码工作。

    avformat_close_input()

    该函数用于关闭一个 AVFormatContext,一般情况下是和 avformat_open_input() 成对使用的。

    函数的调用关系如下图所示:

    avformat_close_input

    avformat_close_input

    avformat_close_input() 的源代码位于 libavformatutils.c

    从源代码中可以看出,avformat_close_input() 主要做了以下几步工作:

    (1)调用 AVInputFormat 的 read_close() 方法关闭输入流

    (2)调用 avformat_free_context() 释放 AVFormatContext

    (3)调用 avio_close() 关闭并且释放 AVIOContext

    编码

    avformat_alloc_output_context2()

    在基于 FFmpeg 的视音频编码器程序中,该函数通常是第一个调用的函数(除了组件注册函数 av_register_all())。

    avformat_alloc_output_context2() 函数可以初始化一个用于输出的 AVFormatContext 结构体。它的声明位于 libavformatavformat.h

    1
    2
    int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,
    const char *format_name, const char *filename);

    代码中的英文注释写的已经比较详细了,在这里拿中文简单叙述一下。

    1
    2
    3
    4
    5
    ctx:函数调用成功之后创建的AVFormatContext结构体。
    oformat:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。
    PS:使用该参数需要自己手动获取AVOutputFormat,相对于使用后两个参数来说要麻烦一些。
    format_name:指定输出格式的名称。根据格式名称,FFmpeg会推测输出格式。输出格式可以是“flv”,“mkv”等等。
    filename:指定输出文件的名称。根据文件名称,FFmpeg会推测输出格式。文件名称可以是“xx.flv”,“yy.mkv”等等。

    函数执行成功的话,其返回值大于等于 0。

    首先贴出来最终分析得出的函数调用结构图,如下所示:

    avformat_alloc_output_context2

    avformat_alloc_output_context2

    avformat_alloc_output_context2() 的函数定义位于 libavformatmux.c

    从代码中可以看出,avformat_alloc_output_context2() 的流程如要包含以下 2 步:

    1) 调用 avformat_alloc_context() 初始化一个默认的 AVFormatContext。

    2) 如果指定了输入的 AVOutputFormat,则直接将输入的 AVOutputFormat 赋值给 AVOutputFormat 的 oformat。如果没有指定输入的 AVOutputFormat,就需要根据文件格式名称或者文件名推测输出的 AVOutputFormat。无论是通过文件格式名称还是文件名推测输出格式,都会调用一个函数 av_guess_format()

    avformat_alloc_context() 首先调用 av_malloc() 为 AVFormatContext 分配一块内存。然后调用了一个函数 avformat_get_context_defaults() 用于给 AVFormatContext 设置默认值

    avformat_alloc_context() 首先调用 memset() 将 AVFormatContext 的内存置零;然后指定它的 AVClass(指定了 AVClass 之后,该结构体就支持和 AVOption 相关的功能);最后调用 av_opt_set_defaults() 给 AVFormatContext 的成员变量设置默认值(av_opt_set_defaults() 就是和 AVOption 有关的一个函数,专门用于给指定的结构体设定默认值,此处暂不分析)。

    av_guess_format() 中使用一个整型变量 score 记录每种输出格式的匹配程度。函数中包含了一个 while() 循环,该循环利用函数 av_oformat_next() 遍历 FFmpeg 中所有的 AVOutputFormat,并逐一计算每个输出格式的 score。具体的计算过程分成如下几步:

    1) 如果封装格式名称匹配,score 增加 100。匹配中使用了函数 av_match_name()

    2) 如果 mime 类型匹配,score 增加 10。匹配直接使用字符串比较函数 strcmp()

    3) 如果文件名称的后缀匹配,score 增加 5。匹配中使用了函数 av_match_ext()

    while() 循环结束后,得到得分最高的格式,就是最匹配的格式。

    下面看一下一个 AVOutputFormat 的实例,就可以理解 “封装格式名称”,“mine 类型”,“文件名称后缀” 这些概念了。下面是 flv 格式的视音频复用器(Muxer)对应的 AVOutputFormat 格式的变量 ff_flv_muxer

    代码:

    avformat_write_header()

    FFmpeg 的写文件用到的 3 个函数:

    • avformat_write_header()
    • av_write_frame()
    • av_write_trailer()

    其中 av_write_frame() 用于写视频数据,avformat_write_header() 用于写视频文件头,而 av_write_trailer() 用于写视频文件尾。

    本文首先分析avformat_write_header()

    PS:需要注意的是,尽管这 3 个函数功能是配套的,但是它们的前缀却不一样,写文件头 Header 的函数前缀是 “avformat_”,其他两个函数前缀是 “av_”(不太明白其中的原因)。

    avformat_write_header() 的声明位于 libavformatavformat.h

    1
    int avformat_write_header(AVFormatContext *s, AVDictionary **options);

    简单解释一下它的参数的含义:

    1
    2
    s:用于输出的AVFormatContext。
    options:额外的选项,目前没有深入研究过,一般为NULL。

    函数正常执行后返回值等于 0。

    avformat_write_header() 的调用关系如下图所示:

    avformat_write_header

    avformat_write_header

    avformat_write_header() 的定义位于 libavformatmux.c

    从源代码可以看出,avformat_write_header() 完成了以下工作:

    (1)调用 init_muxer() 初始化复用器

    (2)调用 AVOutputFormat 的 write_header()

    init_muxer() 代码很长,但是它所做的工作比较简单,可以概括成两个字:检查。函数的流程可以概括成以下几步:

    (1)将传入的 AVDictionary 形式的选项设置到 AVFormatContext

    (2)遍历 AVFormatContext 中的每个 AVStream,并作如下检查:

    • a) AVStream 的 time_base 是否正确设置。如果发现 AVStream 的 time_base 没有设置,则会调用 avpriv_set_pts_info() 进行设置。

    • b) 对于音频,检查采样率设置是否正确;对于视频,检查宽、高、宽高比。

    • c) 其他一些检查,不再详述。

    AVOutputFormat->write_header()

    avformat_write_header() 中最关键的地方就是调用了 AVOutputFormat 的 write_header()

    write_header() 是 AVOutputFormat 中的一个函数指针,指向写文件头的函数。不同的 AVOutputFormat 有不同的 write_header() 的实现方法。在这里我们举例子看一下 FLV 封装格式对应的 AVOutputFormat,它的定义位于 libavformatflvenc.c

    从 ff_flv_muxer 的定义中可以看出,write_header() 指向的函数为 flv_write_header()。我们继续看一下 flv_write_header() 函数。flv_write_header() 的定义同样位于 libavformatflvenc.c

    从源代码可以看出,flv_write_header() 完成了 FLV 文件头的写入工作。该函数的工作可以大体分为以下两部分:

    (1)给 FLVContext 设置参数

    (2)写文件头,以及相关的 Tag

    可以参考下图中 FLV 文件头的定义比对一下上面的代码。

    FLV Header.png

    FLV Header.png

    avcodec_encode_video()

    该函数用于编码一帧视频数据。avcodec_encode_video2() 函数的声明位于 libavcodecavcodec.h

    1
    2
    int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
    const AVFrame *frame, int *got_packet_ptr);

    该函数每个参数的含义在注释里面已经写的很清楚了,在这里用中文简述一下:

    1
    2
    3
    4
    avctx:编码器的AVCodecContext。
    avpkt:编码输出的AVPacket。
    frame:编码输入的AVFrame。
    got_packet_ptr:成功编码一个AVPacket的时候设置为1。

    函数返回 0 代表编码成功。

    函数的调用关系如下图所示:

    avcodec_encode_video

    avcodec_encode_video

    avcodec_encode_video2() 的定义位于 libavcodecutils.c

    从函数的定义可以看出,avcodec_encode_video2() 首先调用了 av_image_check_size() 检查设置的宽高参数是否合理,然后调用了 AVCodec 的 encode2() 调用具体的解码器。

    av_image_check_size() 主要是要求图像宽高必须为正数,而且取值不能太大。

    AVCodec 的 encode2() 是一个函数指针,指向特定编码器的编码函数

    从 ff_libx264_encoder 的定义可以看出,encode2() 函数指向的是 X264_frame() 函数。

    X264_frame() 函数的定义位于 libavcodeclibx264.c

    av_write_frame()

    av_write_frame() 用于输出一帧视音频数据,它的声明位于 libavformatavformat.h

    1
    int av_write_frame(AVFormatContext *s, AVPacket *pkt);

    简单解释一下它的参数的含义:

    1
    2
    s:用于输出的AVFormatContext。
    pkt:等待输出的AVPacket。

    函数正常执行后返回值等于 0。

    av_write_frame() 的调用关系如下图所示:

    av_write_frame

    av_write_frame

    av_write_frame() 的定义位于 libavformatmux.c

    从源代码可以看出,av_write_frame() 主要完成了以下几步工作:

    (1)调用 check_packet() 做一些简单的检测

    (2)调用 compute_pkt_fields2() 设置 AVPacket 的一些属性值

    (3)调用 write_packet() 写入数据

    check_packet() 的功能比较简单:首先检查一下输入的 AVPacket 是否为空,如果为空,则是直接返回;然后检查一下 AVPacket 的 stream_index(标记了该 AVPacket 所属的 AVStream)设置是否正常,如果为负数或者大于 AVStream 的个数,则返回错误信息;最后检查 AVPacket 所属的 AVStream 是否属于 attachment stream,这个地方没见过,目前还没有研究。

    compute_pkt_fields2() 函数的定义位于 libavformatmux.c

    compute_pkt_fields2() 主要有两方面的功能:

    • 一方面用于计算 AVPacket 的 duration, dts 等信息;
    • 另一方面用于检查 pts、dts 这些参数的合理性(例如 PTS 是否一定大于 DTS)。具体的代码还没有细看,以后有时间再进行分析。

    write_packet() 函数的定义位于 libavformatmux.c

    write_packet() 函数最关键的地方就是调用了 AVOutputFormat 中写入数据的方法。如果 AVPacket 中的 flag 标记中包含 AV_PKT_FLAG_UNCODED_FRAME,就会调用 AVOutputFormat 的 write_uncoded_frame() 函数;如果不包含那个标记,就会调用 write_packet() 函数。 write_packet() 实际上是一个函数指针,指向特定的 AVOutputFormat 中的实现函数。例如,我们看一下 FLV 对应的 AVOutputFormat,位于 libavformatflvenc.c

    从 ff_flv_muxer 的定义可以看出,write_packet() 指向的是 flv_write_packet() 函数。在看 flv_write_packet() 函数的定义之前,先回顾一下 FLV 封装格式的结构。

    av_write_trailer()

    av_write_trailer() 用于输出文件尾,它的声明位于 libavformatavformat.h

    1
    int av_write_trailer(AVFormatContext *s);

    它只需要指定一个参数,即用于输出的 AVFormatContext。

    函数正常执行后返回值等于 0。

    av_write_trailer() 的调用关系如下图所示:

    av_write_trailer

    av_write_trailer

    av_write_trailer() 的定义位于 libavformatmux.c

    从源代码可以看出 av_write_trailer() 主要完成了以下两步工作:

    (1)循环调用 interleave_packet() 以及 write_packet(),将还未输出的 AVPacket 输出出来。

    (2)调用 AVOutputFormat 的 write_trailer(),输出文件尾。

    其中第一步和 av_write_frame() 中的步骤大致是一样的(interleave_packet() 这一部分在并不包含在 av_write_frame() 中,而是包含在 av_interleaved_write_frame() 中,这一部分源代码还没有分析)

    AVOutputFormat 的 write_trailer() 是一个函数指针,指向特定的 AVOutputFormat 中的实现函数。我们以 FLV 对应的 AVOutputFormat 为例,看一下它的定义

    从 FLV 对应的 AVOutputFormat 结构体的定义我们可以看出,write_trailer() 指向了flv_write_trailer() 函数。

    flv_write_trailer() 函数的定义位于 libavformatflvenc.c

    从 flv_write_trailer() 的源代码可以看出该函数做了以下两步工作:

    (1)如果视频流是 H.264,则添加包含 EOS(End Of Stream) NALU 的 Tag。

    (2)更新 FLV 的时长信息,以及文件大小信息。

    其中,put_avc_eos_tag() 函数用于添加包含 EOS NALU 的 Tag(包含结尾的一个 PreviousTagSize)

    可以参考 FLV 封装格式理解上述函数。由于前面的文章中已经描述过 FLV 封装格式,在这里不再重复叙述,在这里仅在此记录一下 AVCVIDEOPACKET 的格式,如下所示。

    AVCVIDEOPACKET格式

    AVCVIDEOPACKET格式

    可以看出包含 EOS NALU 的 AVCVIDEOPACKET 的 AVCPacketType 为 2。在这种情况下, AVCVIDEOPACKET 的 CompositionTime 字段取 0,并且无需包含 Data 字段。

    日志输出系统

    日志输出系统

    av_log()

    本文分析一下 FFmpeg 的日志(Log)输出系统的源代码。日志输出部分的核心函数只有一个: av_log()。使用 av_log() 在控制台输出日志的效果如下图所示。

    av_log控制台日志输出

    av_log控制台日志输出

    FFmpeg 日志输出系统的函数调用结构图如图所示:

    FFmpeg 日志输出系统的函数调用结构图

    FFmpeg 日志输出系统的函数调用结构图

    av_log() 是 FFmpeg 中输出日志的函数。随便打开一个 FFmpeg 的源代码文件,就会发现其中遍布着 av_log() 函数。一般情况下 FFmpeg 类库的源代码中是不允许使用 printf() 这种的函数的,所有的输出一律使用 av_log()

    av_log() 的声明位于 libavutillog.h

    1
    void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);

    这个函数的声明有两个地方比较特殊:

    (1)函数最后一个参数是 “…”。

    在 C 语言中,在函数参数数量不确定的情况下使用 “…” 来代表参数。例如 printf() 的原型定义如下

    1
    int printf (const char*, ...);

    (2)它的声明后面有一个 av_printf_format(3, 4)。有关这个地方的左右还没有深入研究,网上资料中说它的作用是按照 printf() 的格式检查 av_log() 的格式。

    av_log() 每个字段的含义如下:

    • avcl:指定一个包含 AVClass 的结构体。
    • level:log 的级别
    • fmt:和 printf() 一样。

    由此可见,av_log() 和 printf() 的不同主要在于前面多了两个参数。其中第一个参数指定该 log 所属的结构体,例如 AVFormatContext、AVCodecContext 等等。第二个参数指定 log 的级别,源代码中定义了如下几个级别。

    1
    2
    3
    4
    5
    6
    7
    8
    #define AV_LOG_QUIET    -8
    #define AV_LOG_PANIC 0
    #define AV_LOG_FATAL 8
    #define AV_LOG_ERROR 16
    #define AV_LOG_WARNING 24
    #define AV_LOG_INFO 32
    #define AV_LOG_VERBOSE 40
    #define AV_LOG_DEBUG 48

    从定义中可以看出来,随着严重程度逐渐下降,一共包含如下级别:

    • AV_LOG_PANIC,
    • AV_LOG_FATAL,
    • AV_LOG_ERROR,
    • AV_LOG_WARNING,
    • AV_LOG_INFO,
    • AV_LOG_VERBOSE,
    • AV_LOG_DEBUG。

    每个级别定义的数值代表了严重程度,数值越小代表越严重。默认的级别是 AV_LOG_INFO。此外,还有一个级别不输出任何信息,即 AV_LOG_QUIET。

    当前系统存在着一个 “Log 级别”。所有严重程度高于该级别的 Log 信息都会输出出来。例如当前的 Log 级别是 AV_LOG_WARNING,则会输出 AV_LOG_PANIC,AV_LOG_FATAL,AV_LOG_ERROR,AV_LOG_WARNING 级别的信息,而不会输出 AV_LOG_INFO 级别的信息。可以通过 av_log_get_level() 获得当前 Log 的级别,通过另一个函数 av_log_set_level() 设置当前的 Log 级别。

    可以通过 av_log_set_level() 设置当前 Log 的级别。

    接头体成员管理系统

    AVClass

    FFmpeg 源代码简单分析:结构体成员管理系统 - AVClass

    TODO

    AVOption

    FFmpeg 源代码简单分析:结构体成员管理系统 - AVOption

    TODO

    libswscale

    sws_getContext()

    FFmpeg 源代码简单分析:libswscale 的 sws_getContext()

    TODO

    sws_scale()

    FFmpeg 源代码简单分析:libswscale 的 sws_scale()

    TODO

    libavdevice

    avdevice_register_all()

    FFmpeg 源代码简单分析:libavdevice 的 avdevice_register_all()

    gdigrab

    FFmpeg 源代码简单分析:libavdevice 的 gdigrab

    from: 2019/05/27/FFmpeg/FFmpeg 框架函数分析 /

  • 相关阅读:
    toPrimitive方法使用
    使用js导入Excel数据,转化为json,导出指定json,合并单元格为excel
    vue-router基本使用
    json另类使用
    z-index无效情况
    构造函数另类使用。
    在worker中使用offscreenCanvas
    使用git提交代码一条龙
    IntelliJ IDEA使用技巧一览表
    Android studio 常用快捷键
  • 原文地址:https://www.cnblogs.com/lidabo/p/15007020.html
Copyright © 2011-2022 走看看