基于播放器流程对FFmpeg源码脉络梳理

背景

很久以前对FFmpeg的源码脉络详细的梳理过,当时阅读的是雷神的FFmpeg 源码分析的博客文章,最近经常使用FFmpeg进行编解码、解封装等处理,但对FFmpeg内部的脉络只记得一点点它的结构设计及它的功能及API的使用方式,所以就准备了这篇博客。单纯分析FFmpeg的结构设计会非常枯燥,结合FFplay播放器的流程去梳理FFmpeg的脉络应该会起到事半功倍的效果。

本人才学疏浅,ffmpeg 工程非常复杂庞大,理解可能有误,还请热心指正。

播放器流程梳理

我这里把播放器的流程分为9个模块:文件的读取模块、文件格式解封装模块、视频解码模块、音频的解码模块、视频流格式转换模块、音频流重采样模块、音视频同步模块、视频渲染播放模块、音频播放模块。下面从播放器的角度简单介绍下这9个模块的功能。

播放器简单结构图:
在这里插入图片描述

文件的读取模块

播放器整体流程可以完全类比成工厂里面的流水线。文件的读取模块是流水线的第一个工序,它具备两大功能。

  1. 从本地/网络源源不断的读取数据
  2. 屏蔽数据的格式把数据流传送给下个模块

文件格式解封装模块

解封装模块的主要功能是:

  1. 解析媒体文件的格式信息
  2. 解析媒体流信息
  3. 媒体流信息打上时间戳

媒体文件的格式信息及媒体流信息的确定通常读取文件部分信息,确定媒体文件格式及媒体流格式。例如:FLV封装格式,开头的3个字符是“FLV”。也可以根据文件名后缀推测媒体文件的格式信息。媒体流信息的时间戳也要在这个模块计算,用于音视频之间的同步。

视频解码模块

解码模块主要功能就是把H264/H265格式的视频流解码成YUV格式。

音频的解码模块

解码模块主要功能就是把AAC格式的视频流解码成PCM格式。

视频流格式转换模块

视频解码模块输出的YUV格式可能不能直接在显示系统上面渲染,需要转化为显示系统支持的格式。

音频流重采样模块

重采样模块的主要功能与视频格式转换模块是相同的。由于音频渲染模块对音频的声道数、采样率、采样格式的限制,同样需要把音频解码之后的音频格式转化为音频渲染模块支持的格式。

音视频同步模块

设备、网络都没有任何限制的理想情况下,视频按照指定的帧率播放,音频按照采样率播放,它们默认就是同步的;但是这种情况在现实情况下是不存在的,特别是播放网络视频时,由于网络的不确定性必须在音频和视频之间添加规则让他们同步渲染。

常用的方式就是音频按照自己的采样率、声道数、采样格式正常的播放,同时保存当前音频帧的时间戳。视频流在渲染时通过对比当前帧的时间戳与音频的时间戳,决定是加快渲染还是延时渲染还是正常渲染。

视频渲染播放模块

FFplay 视频渲染模块采用的是SDL技术。是一套开放源代码的跨平台多媒体开发库,如果想了解跨平台渲染库的实现,可以学习SDL源码。

Android端视频渲染有多重方式,这里我介绍3种方式:

  1. OpenGL 渲染:通过 YUV -> RGB -> Texture的操作,然后把Texture通过OpenGL 操作渲染到屏幕上。
  2. NDK ANativeWindow技术:通过把Java层的Surface转化到NDK中的ANativeWindow,可以通过把YUV -> RGB, 再把RGB数据拷贝到ANativeWindow的Buffer中就可以实现渲染。
  3. MediaCodec: 通过向MediaCodec绑定渲染Surface,MediaCodec解码后自动完成渲染。

音频播放模块

FFplay 音频渲染模块采用的也是SDL技术。

Android端音频渲染方式:

  1. OpenSL
  2. AudioTrack
  3. AAudio

总结

播放器的流程大概就是上面的过程,下面的章节会把播放器的部分模块通过站在FFmpeg的视角再详细讲解下,下一章节的介绍才是整篇文章的重点。

从FFmpeg角度分析播放器流程

文件的读取模块

FFmpeg内部把文件的读取模块分为3层:

  1. AVIOContext、URLContext
  2. URLProtocol
  3. file(FileContext)、http(HttpContext)、udp(UdpContext)、tcp(TcpContext)

读取模块的最底层是file、http、udp、tcp的操作,底层模块的操作会被抽象成URLProtocol,每一个底层文件都对应着一个URLProtocol。URLContext对URLProtocol进行了一层封装,相关操作也只是简单的中转一下调用底层具体文件或协议的支撑函数。AVIOContext是对URLContext的功能的扩展,增加了缓冲机制。

当我们读写文件的时候,操作流程是:AVIOContext -> URLContext -> URLProtocol -> FileContext(file).

该模块主要通过ffmpeg libavformat目录中的file.c、avio.c、aviobuf.c等文件,实现文件的读写。

URLProtocol ff_file_protocol = {

    .name                = "file",

    .url_open            = file_open,

    .url_read            = file_read,

    .url_write           = file_write,

    .url_seek            = file_seek,

    .url_close           = file_close,

    .url_get_file_handle = file_get_handle,

    .url_check           = file_check,

    .url_delete          = file_delete,

    .url_move            = file_move,

    .priv_data_size      = sizeof(FileContext),

    .priv_data_class     = &file_class,

    .url_open_dir        = file_open_dir,

    .url_read_dir        = file_read_dir,

    .url_close_dir       = file_close_dir,

};



URLProtocol ff_udp_protocol = {

    .name                = "udp",

    .url_open            = udp_open,

    .url_read            = udp_read,

    .url_write           = udp_write,

    .url_close           = udp_close,

    .url_get_file_handle = udp_get_file_handle,

    .priv_data_size      = sizeof(UDPContext),

    .priv_data_class     = &udp_class,

    .flags               = URL_PROTOCOL_FLAG_NETWORK,

};



URLProtocol ff_tcp_protocol = {

    .name                = "tcp",

    .url_open            = tcp_open,

    .url_accept          = tcp_accept,

    .url_read            = tcp_read,

    .url_write           = tcp_write,

    .url_close           = tcp_close,

    .url_get_file_handle = tcp_get_file_handle,

    .url_shutdown        = tcp_shutdown,

    .priv_data_size      = sizeof(TCPContext),

    .flags               = URL_PROTOCOL_FLAG_NETWORK,

    .priv_data_class     = &tcp_class,

};

FileContext、HttpContext、TcpContext表示了这些具体文件协议结构和相关的参数。

typedef struct FileContext {

    const AVClass *class;

    int fd;

    int trunc;

    int blocksize;

#if HAVE_DIRENT_H

    DIR *dir;

#endif

} FileContext;



typedef struct TCPContext {

    const AVClass *class;

    int fd;

    int listen;

    int open_timeout;

    int rw_timeout;

    int listen_timeout;

} TCPContext;

文件格式解封装模块

FFmpeg内部把文件格式的解封装模块分为3层:

  1. AVInputFormat
  2. FLVContext、AVIContext、UDPContext等
  3. AVStream

FFmpeg中每一个文件格式都对应着一个AVInputFormat. 比如flv文件格式对应着AVInputFormat的name是ff_live_flv_demuxer;avi文件格式对应着AVInputFormat的name是ff_avi_demuxer。AVInputFormat 中封装了具体文件格式操作的函数指针。比如我们读一个flv文件时,真正操作的方法是分别是flv_read_packet。

AVInputFormat ff_live_flv_demuxer = {

    .name           = "live_flv",

    .long_name      = NULL_IF_CONFIG_SMALL("live RTMP FLV (Flash Video)"),

    .priv_data_size = sizeof(FLVContext),

    .read_probe     = live_flv_probe,

    .read_header    = flv_read_header,

    .read_packet    = flv_read_packet,

    .read_seek      = flv_read_seek,

    .read_close     = flv_read_close,

    .extensions     = "flv",

    .priv_class     = &live_flv_class,

    .flags          = AVFMT_TS_DISCONT

};



AVInputFormat ff_avi_demuxer = {

    .name           = "avi",

    .long_name      = NULL_IF_CONFIG_SMALL("AVI (Audio Video Interleaved)"),

    .priv_data_size = sizeof(AVIContext),

    .extensions     = "avi",

    .read_probe     = avi_probe,

    .read_header    = avi_read_header,

    .read_packet    = avi_read_packet,

    .read_close     = avi_read_close,

    .read_seek      = avi_read_seek,

    .priv_class = &demuxer_class,

};

FLVContext、AVIContext、UDPContext表示了这些具体媒体的解复用结构和相关的基础程序。

typedef struct FLVContext {

    const AVClass *class; ///< Class for private options.

    int trust_metadata;   ///< configure streams according onMetaData

    int wrong_dts;        ///< wrong dts due to negative cts

    uint8_t *new_extradata[FLV_STREAM_TYPE_NB];

    int new_extradata_size[FLV_STREAM_TYPE_NB];

    int last_sample_rate;

    int last_channels;

    struct {

        int64_t dts;

        int64_t pos;

    } validate_index[2];

    int validate_next;

    int validate_count;

    int searched_for_end;

} FLVContext;



typedef struct AVIContext {

    const AVClass *class;

    int64_t riff_end;

    int64_t movi_end;

    int64_t fsize;

    int64_t io_fsize;

    int64_t movi_list;

    int64_t last_pkt_pos;

    int index_loaded;

    int is_odml;

    int non_interleaved;

    int stream_index;

    DVDemuxContext *dv_demux;

    int odml_depth;

    int use_odml;

#define MAX_ODML_DEPTH 1000

    int64_t dts_max;

} AVIContext;



typedef struct TCPContext {

    const AVClass *class;

    int fd;

    int listen;

    int open_timeout;

    int rw_timeout;

    int listen_timeout;

} TCPContext;

AVInputFormat与具体的解复用结构对应的关系是通过AVFormatContext中的iformat和priv_data关联起来。

AVInputFormat和具体的音视频编码算法格式是通过AVFormatContext结构中的streams 字段关联媒体格式,解复用模块分离音视频裸数据,这些媒体压缩格式的信息保存在streams中。通过 streams 传递给下级音视频解码器。

该模块主要通过ffmpeg libavformat目录中的 flvdec.c、avidec.c、utils_format.c 等文件,实现文件的格式及媒体信息的解析。

解码模块

FFmpeg内部把解码模块分为3层:

  1. AVCodecContext
  2. AVCodec
  3. H264Context、HEVCContext等

FFmpeg中每一个编解码器都对应一个AVCodec。比如H264解码器对应的AVCodec的name是ff_h264_decoder;H265解码器对应的AVCodec的name是ff_hevc_decoder;AVCodec中封装了具体编解码器操作的函数指针。比如我们解码一个H264视频流时,通过AVCodec调用decode,但真正操作的方法h264_decode_frame方法。

AVCodec ff_h264_decoder = {

    .name                  = "h264",

    .long_name             = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),

    .type                  = AVMEDIA_TYPE_VIDEO,

    .id                    = AV_CODEC_ID_H264,

    .priv_data_size        = sizeof(H264Context),

    .init                  = ff_h264_decode_init,

    .close                 = h264_decode_end,

    .decode                = h264_decode_frame,

    .capabilities          = /*AV_CODEC_CAP_DRAW_HORIZ_BAND |*/ AV_CODEC_CAP_DR1 |

                             AV_CODEC_CAP_DELAY | AV_CODEC_CAP_SLICE_THREADS |

                             AV_CODEC_CAP_FRAME_THREADS,

    .flush                 = flush_dpb,

    .init_thread_copy      = ONLY_IF_THREADS_ENABLED(decode_init_thread_copy),

    .update_thread_context = ONLY_IF_THREADS_ENABLED(ff_h264_update_thread_context),

    .profiles              = NULL_IF_CONFIG_SMALL(profiles),

    .priv_class            = &h264_class,

};



AVCodec ff_hevc_decoder = {

    .name                  = "hevc",

    .long_name             = NULL_IF_CONFIG_SMALL("HEVC (High Efficiency Video Coding)"),

    .type                  = AVMEDIA_TYPE_VIDEO,

    .id                    = AV_CODEC_ID_HEVC,

    .priv_data_size        = sizeof(HEVCContext),

    .priv_class            = &hevc_decoder_class,

    .init                  = hevc_decode_init,

    .close                 = hevc_decode_free,

    .decode                = hevc_decode_frame,

    .flush                 = hevc_decode_flush,

    .update_thread_context = hevc_update_thread_context,

    .init_thread_copy      = hevc_init_thread_copy,

    .capabilities          = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_DELAY |

                             AV_CODEC_CAP_SLICE_THREADS | AV_CODEC_CAP_FRAME_THREADS,

    .profiles              = NULL_IF_CONFIG_SMALL(profiles),

};

H264Context、HEVCContext 表示这些具体的编解码器内部使用的参数结构和相关的基础程序。由于H264、H265编解码器需要的参数太多,这里不再进行拷贝复制。

AVCodecContext与具体的编码器对应的关系是通过AVCodecContext中的AVCodec *codec和priv_data关联起来。priv_data 表示AVCodec对应的Context。比如 H264Context、HEVCContext等。

该模块主要通过ffmpeg libavcodec目录中的 h264.h、h264.c、hevc.h、hevc.c等文件,实现视频流的解码。

视频流格式转换模块

FFmpeg内部提供的视频流格式转换模块效率太差,很多项目中都是使用libyuv库进行视频格式的转换,这里不再讲解,对libyuv感兴趣的可以自己了解。

音频流重采样模块

音频流的重采样可以使用SwrContext进行操作,这里不再进行讲解。就是API的使用,没有太多结构的设计。

音视频渲染模块

音视频的渲染基本上都是使用平台相关的API操作或者跨平台方案实现,与FFmpeg引擎已经没有任何关联,具体的操作方式上面已经进行大概的介绍,详细的使用方式可以自己了解。

这里附上一张雷神绘制的FFmpeg解码时具体的FFMpeg操作流程图,当作缅怀。虽然现在FFmpeg的API发生的很多变化,但里面具体的结构是不变的。学习完雷神的FFmpeg流程分析,再看新的FFmpeg源码会上手非常快。
在这里插入图片描述
为了加深对FFmpeg源码的理解,这里列了3个思考题。分析完这3个思考题,你就会彻底搞明白FFmpeg从编译到运行时的代码结构,面对FFmpeg的编译及源码修改脑子里也会非常清晰如何做。

思考

1.FFmpeg编译裁剪的时候需要指定需要的各种协议(Protocol)、封装格式(Muxer)、解封装格式(DeMuxer)、编码格式(Codec),不需要的会被裁减掉。FFmpeg运行的时候这些制定的协议、格式是如何被加载、又是如何被找到?

2.FFmpeg 是如何把AvFormatContext、AVIOContext、URLContext、URLProtocol、AVInputFormat、AVCodecContext、AVCodec 串联起来的?

当我们通过FFmpeg操作一个文件时,FFmpeg 中各模块及Context之间的调用顺序是什么?


版权声明:本文为zhang_jun_ling原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。