固阳音箱协会

FFmpeg代码从入门到精通——基础篇

金山云LIVE2019-02-17 14:49:19

引言

金山云多媒体SDK团队在移动直播、短视频等开发项目中基于FFmpeg积累了许多实践经验,我们希望通过分享《FFmpeg从入门到精通》系列文稿,帮助各位加深对FFmpeg的理解,少踩些坑。


视频流媒体中视频数据的传输占据了绝大部分的带宽,如何提升编码效率,使用更少的带宽,提供更优质的画面质量,是音视频开发人员努力的重点。

HEVC(High Efficiency Video Coding,也叫H.265)编码格式的推出,给这一方向带来了突破点,但由于其算法复杂度较高,前期未曾得到普遍应用,而随着移动设备计算能力的提高和越来越多的设备开始支持HEVC的硬件编/解码,直播平台也开始逐渐引入HEVC视频格式。


HEVC属视频编码层面标准,如果在视频流媒体中进行应用,还需要相应的封装格式和流媒体协议的支持。鉴于直播的大部分推拉流协议是基于RTMP的,本文主要介绍如何在RTMP协议中增加对HEVC视频编码格式的支持,其他协议或私有协议,可参考本文自行添加。此外,除推流端和播放端要做出修改,服务端也要同步进行相应修改,才能够保证HEVC在直播中的正常使用。


01

背景介绍

典型的直播框架通常包括三大部分,如下图所示:

1.  推流端:负责音视频数据的采集、处理、编码及封     装后将数据推送至源站;

2.  服务端:涵盖源站和CDN,接收来自推流端的音视     频数据,然后将数据分发至各播放端;

3.  播放端:从CDN拉取直播数据,解复用、解码后渲     染音视频数据;


图1. 直播框架图


引入HEVC编码,涉及到的变动部分如上图中红色字体所标注:

1.  编码模块:需要支持HEVC格式的编解码,该部分不     属于本文的介绍范畴,我们有在其它文章中介绍如     何在iOS11上进行HEVC的硬编硬解,感兴趣的朋     友可自行查阅;

2.  封装/传输模块:RTMP、HTTP-FLV流媒体协议需     要增加对HEVC视频编码格式的支持,该部分是本     文介绍的重点。


相信广大的音视频开发者对于FFmpeg并不陌生,由于它在多媒体处理上提供的强大功能以及开源易于修改维护的特性,使得其被广泛应用于各音视频相关软件中。由于Adobe暂停了对RTMP/FLV标准的更新,所以至今标准中也没有支持HEVC视频编码格式。为避免各终端和服务器间的兼容性问题,FFmpeg也没有在RTMP/FLV的协议实现中进行HEVC的相关扩展。经过CDN联盟讨论,我们制定了相关的协议扩展规范,并在FFmpeg中完成了相关代码实现。


本文后面介绍的就是如何在FFmpeg中,对RTMP进行HEVC扩展。如果您的开发工程中并没有用到FFmpeg,可直接阅读第四章节,也能够很轻松的在您的代码中增加这部分内容。


02

FFmpeg 简 析

FFmpeg从无到有,发展至今,功能日益强大,代码也越来越多,很多初学者都被其众多的源文件、庞大的结构体和复杂的算法打消了继续学习的念头。本章节将从总体对FFmpeg进行简单的解析,教您如何阅读FFmpeg源码。


2.1  总体说明

FFmpeg包含如下类库:

•  libavformat - 用于各种音视频封装格式的生成和解    析,包括获取解码所需信息、读取音视频数据等功    能。各种流媒体协议代码(如rtmpproto.c等)以及音    视频格式的(解)复用代码(如flvdec.c、flvenc.c等)都    位于该目录下。

•  libavcodec - 音视频各种格式的编解码。各种        格式的编解码代码(如aacenc.c、aacdec.c等)都位    于该目录下。

•  libavutil - 包含一些公共的工具函数的使用库,包括    算数运算,字符操作等。

•  ibswscale - 提供原始视频的比例缩放、色彩映射    转换、图像颜色空间或格式转换的功能。

•  libswresample - 提供音频重采样,采样格式转换和    混合等功能。

•  libavfilter - 各种音视频滤波器。

•  libpostproc - 用于后期效果处理,如图像的去块效    应等。

•  libavdevice - 用于硬件的音视频采集、加速和显      示。


如果您之前没有阅读FFmpeg代码的经验,建议优先阅读libavformat、libavcodec以及libavutil下面的代码,它们提供了音视频开发的最基本功能,应用范围也是最广的。


2.2  常用结构

FFmpeg里面最常用的数据结构,按功能可大致分为以下几类(以下代码行数,以branch: origin/release/3.4为准):


1.  封装格式

•  AVFormatContext - 描述了媒体文件的构成及基本    信息,是统领全局的基本结构体,贯穿程序始终,很    多函数都要用它作为参数;

•  AVInputFormat - 解复用器对象,每种作为输入的    封装格式(例如FLV、MP4、TS等)对应一个该结构    体,如libavformat/flvdec.c的ff_flv_demuxer;

•  AVOutputFormat - 复用器对象,每种作为输出的    封装格式(例如FLV, MP4、TS等)对应一个该结构    体,如libavformat/flvenc.c的ff_flv_muxer;

•  AVStream - 用于描述一个视频/音频流的相关数据    信息。


2.  编解码

•  AVCodecContext - 描述编解码器上下文的数据结    构,包含了众多编解码器需要的参数信息;

•  AVCodec - 编解码器对象,每种编解码格式(例如    H.264、AAC等)对应一个该结构     体,如                libavcodec/aacdec.c的ff_aac_decoder。每个        AVCodecContext中含有一个AVCodec;

•  AVCodecParameters - 编解码参数,每个            AVStream中都含有一个                              AVCodecParameters,用来存放当前流的编解码参    数。


3.  网络协议

•  AVIOContext - 管理输入输出数据的结构体;

•  URLProtocol - 描述了音视频数据传输所使用的协    议,每种传输协议(例如HTTP、RTMP)等,都会对    应一个URLProtocol结构,如libavformat/http.c中的    ff_http_protocol;

•  URLContext - 封装了协议对象及协议操作对象。


4.  数据存放

•  AVPacket - 存放编码后、解码前的压缩数据,即ES数据;

•  AVFrame - 存放编码前、解码后的原始数据,如      YUV格式的视频数据或PCM格式的音频数据等

上述结构体的关系图如下所示(箭头表示派生出):


图2. FFmpeg结构体关系图



2.3  代码结构

下面这段代码完成了读取媒体文件中音视频数据的基本功能,本节以此为例,分析FFmpeg内部代码的调用逻辑。


2.3.1  注册

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

REGISTER_MUXDEMUX实际上调用的是av_register_input_format和av_register_output_format,通过这两个方法,将(解)复用器分别添加到了全局变量first_iformat与first_oformat链表的最后位置。

编/解码其注册过程相同,此处不再赘述。


2.3.2  文件打开

FFmpeg读取媒体数据的过程始于avformat_open_input,该方法中完成了媒体文件的打开和格式探测的功能。但FFmpeg是如何找到正确的流媒体协议和解复用器呢?可以看到avformat_open_input方法中调用了init_input函数,在这里面完成了查找流媒体协议和解复用器的工作。

1. s->io_open实际上调用的就是io_open_default,它最终调用到url_find_protocol方法。

ffurl_get_protocols可以得到当前编译的FFmpeg支持的所有流媒体协议,通过url的scheme和protocol->name相比较,得到正确的protocol。例如本例中URLProtocol最终指向了libavformat/http.c中的ff_http_protocol。


2. av_probe_input_buffer2最终调用到av_probe_input_format3,该方法遍历所有的解复用器,即first_iformat链表中的所有节点,调用它们的read_probe()函数计算匹配得分,函数最终返回计算找到的最匹配的解复用器。本例中AVInputFormat最终指向了libavformat/flvdec.c中的ff_flv_demuxer。


2.3.3  数据读取

av_read_frame作用是读取媒体数据中的每个音视频帧,该方法中最关键的地方就是调用了AVInputFormat的read_packet()方法。AVInputFormat的read_packet()是一个函数指针,指向当前的AVInputFormat的读取数据的函数。在本例中,AVInputFormat为ff_flv_demuxer,也就是说read_packet最终指向了flv_read_packet。


本期我们重点讲解了FFmpeg的基础知识,在下一期,我们会继续推出FFmpeg从入门到精通中级篇——FLV文件结构解析,满满的干货,敬请期待!

Copyright © 固阳音箱协会@2017