FFmpeg 从入门到出家

2018-01-04 18:20:19 +08:00
 KSClive

金山云多媒体 SDK 团队在移动直播、短视频等项目中遇到了许多 FFmpeg 问题,特设立《 FFmpeg 从入门到出家》系列文稿,希望博君一笑的同时,能让大家对 FFmpeg 有更深入的了解。

视频流媒体中程中视频数据的传输占据了绝大部分的带宽,如何提升编码效率,使用更少的带宽,提供更优质的画面质量,是音视频开发人员一直努力的重点。HEVC(High Efficiency Video Coding,也叫 H.265)编码格式的推出,给这一方向带来了突破点,但由于其算法复杂度较高,前期未曾得到普遍应用,而随着移动设备计算能力的提高和越来越多的设备开始支持 HEVC 的硬件编 /解码,直播平台也开始逐渐引入 HEVC 视频格式。

HEVC 属视频编码层面标准,如果在视频流媒体中进行应用,还需要相应的封装格式和流媒体协议的支持。鉴于直播的大部分推拉流协议是基于 RTMP 的,本文主要介绍如何在 RTMP 协议中增加对 HEVC 视频编码格式的支持,其他协议或私有协议,可参考本文自行添加。

  1. 背景介绍

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

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

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

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

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

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

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

相信广大的音视频开发者对于 FFmpeg 并不陌生,由于它在多媒体处理上提供的强大功能以及开源易于修改维护的特性,使得其被广泛应用于各音视频相关软件中。官方 FFmpeg 中,并没有对 RTMP/FLV 中进行 HEVC 的相关扩展,我们基于此作出了修改。本文后面介绍的就是如何在 FFmpeg 中,对 RTMP 进行 HEVC 扩展。如果您的开发工程中并没有用到 FFmpeg,可直接阅读第四章节,也能够很轻松的在您的代码中增加这部分内容。

2798 次点击
所在节点    推广
2 条回复
hyyou2010
2018-01-05 09:28:24 +08:00
很有价值。目前 FFmpeg 的入门指导资料不多,特别是中文的,能够从总体上梳理的资料很少。
KSClive
2018-01-05 16:17:32 +08:00
FFmpeg 简析

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

2.1 总体说明

FFmpeg 包含如下类库:

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

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

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

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

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 - 封装了协议对象及协议操作对象。

[if !supportLists]4. [endif]数据存放

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

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

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

图 2. FFmpeg 结构体关系图
2.3 代码结构

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

char *url = "http://192.168.1.105/test.flv";

AVPacket pkt;

int ret = 0;

//注册复用器、编码器等

av_register_all();

avformat_network_init();

//打开文件

AVFormatContext *fmtCtx = avformat_alloc_context();

ret = avformat_open_input(&fmtCtx, url, NULL, NULL);

ret = avformat_find_stream_info(fmtCtx, NULL);

//读取音视频数据

while(ret >= 0)

{

ret = av_read_frame(s, &pkt);

}

2.3.1 注册

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

static void register_all(void)

{

avcodec_register_all();



/* (de)muxers */

……

REGISTER_MUXDEMUX(FLV, flv);

……

}

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 函数,在这里面完成了查找流媒体协议和解复用器的工作。

static intinit_input(AVFormatContext *s, const char *filename,

AVDictionary **options)

{

int ret;

……

if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)

return ret;



if (s->iformat)

return 0;

return av_probe_input_buffer2(s->pb, &s->iformat, filename,

s, 0, s->format_probesize);

}

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

static conststructURLProtocol *url_find_protocol(const char *filename)

{

constURLProtocol **protocols;

……

protocols = ffurl_get_protocols(NULL, NULL);

if (!protocols)

return NULL;

for (i = 0; protocols[i]; i++) {

constURLProtocol *up = protocols[i];

if (!strcmp(proto_str, up->name)) {

av_freep(&protocols);

return up;

}

if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&

!strcmp(proto_nested, up->name)) {

av_freep(&protocols);

return up;

}

}

av_freep(&protocols);



return NULL;

}

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

[if !supportLists]1. [endif]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。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/420106

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX