从事音视频技术开发对FFmpeg都不会感到陌生,通过它可以完成音视频采集、编解码、转码、后处理以及流媒体服务等诸多的功能,可以说涵盖了音视频开发中绝大多数的领域。金山云多媒体SDK团队在移动直播、短视频等项目中遇到了许多问题,本文为《FFmpeg从入门到出家》系列的第一篇上半部分,由LiveVideoStack审校整理,希望能让大家对FFmpeg有更深入了解。
文 / 施雪梅
视频流媒体中程中视频数据的传输占据了绝大部分的带宽,如何提升编码效率,使用更少的带宽,提供更优质的画面质量,是音视频开发人员一直努力的重点。HEVC(High Efficiency Video Coding,也叫H.265)编码格式的推出,给这一方向带来了突破点,但由于其算法复杂度较高,前期未曾得到普遍应用,而随着移动设备计算能力的提高和越来越多的设备开始支持HEVC的硬件编/解码,直播平台也开始逐渐引入HEVC视频格式。
HEVC属视频编码层面标准,如果在视频流媒体中进行应用,还需要相应的封装格式和流媒体协议的支持。鉴于直播的大部分推拉流协议是基于RTMP的,本文主要介绍如何在RTMP协议中增加对HEVC视频编码格式的支持,其他协议或私有协议,可参考本文自行添加。此外,除推流端和播放端要做出修改,用到的RTMP Server部分也要同步进行相应修改,才能够保证HEVC在直播中的正常使用。
1.背景介绍
典型的直播框架通常包括三大部分,如下图所示:
图1. 直播框架图
引入HEVC编码,涉及到的变动部分如上图中红色字体所标注:
相信广大的音视频开发者对于FFmpeg并不陌生,由于它在多媒体处理上提供的强大功能以及开源易于修改维护的特性,使得其被广泛应用于各音视频相关软件中。但官方FFmpeg中没有对RTMP FLV中进行HEVC的相关扩展,这是因为FLV与RTMP是Adobe发行的标准,而Adobe暂停了对FLV与RTMP标准的更新,HEVC的相关扩展属于私有标准,所以为了减少国际上不必要的不兼容性麻烦,官方FFmpeg并不会对FLV与RTMP中扩展HEVC进行支持。经过CDN联盟讨论,我们制定了相关的协议扩展规范,并在FFmpeg中完成了相关代码实现。
本文后面介绍的就是如何在FFmpeg中,对RTMP进行HEVC扩展。如果您的开发工程中并没有用到FFmpeg,可直接阅读第四章节,也能够很轻松的在您的代码中增加这部分内容。
2.FFmpeg简析
FFmpeg从无到有,发展至今,功能日益强大,代码也越来越多,很多初学者都被其众多的源文件、庞大的结构体和复杂的算法打消了继续学习的念头。本章节将从总体对FFmpeg进行简单的解析,教您如何阅读FFmpeg源码。
2.1总体说明
FFmpeg包含如下类库:
2.2常用结构
FFmpeg里面最常用的数据结构,按功能可大致分为以下几类(以下代码行数,以branch: origin/release/3.4为准):
1. 封装格式
2. 编解码
3. 网络协议
4. 数据存放
上述结构体的关系图如下所示(箭头表示派生出):
图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);
}
1.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。
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。
3.FLV文件结构解析
FLV(FLASH VIDEO),是一种常用的文件封装格式,目前国内外大部分视频分享网站都是采用的这种格式。其标准定义为《Adobe Flash Video File Format Specification》。RTMP协议也是基于FLV视频格式的。
FLV的文件格式在该规范中已阐述清楚,本章节不再重复描述,而是结合下面的示例具体阐述如何分析FLV文件。
图3. FLV文件结构示例1
图4. FLV文件结构示例2
FLV文件的分析工具有很多,这里给大家推荐FLV Parser这个小软件,通过它可以很容易的看到文件的组成结构。
3.1文件结构
从整个文件上看,FLV是由Header和File Body组成,如下图所示:
图5. FLV文件总体结构
以图3. FLV文件结构示例1为例分析整体结构:
1.位置0x00000000 - 0x00000008, 共9个字节,为FLV Header,其中:
2.位置0x00000009 - ,为FLV File Body:
3.2Tag定义
FLV File Body是由一系列的PreviousTagSize + Tag组成,其中PreviousTagSize的长度为4个字节,用来表示前一个Tag的长度;Tag里面的数据可能是video、audio或者scripts,其定义参见E.4.1 FLV Tag,结构如下:
图6. FLV Tag 结构
以图3. FLV文件结构示例1为例分析Tag结构:
3.3Audio Tags
如果TAG包中的TagType等于8,表示该Tag中包含的数据类型为Audio。StreamID之后的数据就是AudioTagHeader,其定义详见E.4.2.1 AUDIODATA。结构如下:
图7. FLV Audio Tag结构
需要说明的是,通常情况下AudioTagHeader之后跟着的就是AUDIODATA数据了,但有个特例,如果音频编码格式为AAC,AudioTagHeader中会多出1个字节的数据AACPacketType,这个字段来表示AACAUDIODATA的类型:
以图3. FLV文件结构示例为例分析AudioTag结构:
1.位置0x00000219 : 0xAF, 二进制表示为1010 1111:
2.位置0x0000021A : 0x00,十进制为0,并且Audio的编码格式为AAC,说明AACAUDIODATA中存放的是AAC sequence header;
3.位置0x0000021B - 0x0000021C : AUDIODATA数据,即AAC sequence header。
3.3.1 AudioSpecificConfig
AAC sequence header中存放的是AudioSpecificConfig,该结构包含了更加详细的音频信息,《ISO-14496-3 Audio》中的1.6.2.1 章节对此作了详细定义。
通常情况下,AAC sequence header这种Tag在FLV文件中只出现1次,并且是第一个Audio Tag,它存放了解码AAC音频所需要的详细信息。
为什么AudioTagHeader中定义了音频的相关参数,我们还需要传递AudioSpecificConfig呢?
因为当SoundFormat为AAC时,SoundType须设置为1(立体声),SoundRate须设置为3(44KHZ),但这并不意味着FLV文件中AAC编码的音频必须是44KHZ的立体声。播放器在播放AAC音频时,应忽略AudioTagHeader中的参数,并根据AudioSpecificConfig来配置正确的解码参数。
3.4 Video Tag
如果TAG包中的TagType等于9,表示该Tag中包含的数据类型为Video。StreamID之后的数据就是VideoTagHeader,其定义详见E.4.3.1 VIDEODATA,结构如下:
图8. FLV Video Tag结构
VideoTagHeader之后跟着的就是VIDEODATA数据了,但是和AAC音频一样,它也存在一个特例,就是当视频编码格式为H.264的时候,VideoTagHeader会多出4个字节的信息,AVCPacketType和CompositionTime。
以图4. FLV文件结构示例2为例分析VideoTagHeader结构:
1.位置0x0000022C : 0x17, 二进制表示为0001 0111:
2.位置0x0000022D : 0x00,十进制为0,并且Video的编码格式为AVC,说明VideoTagBody中存放的是AVC sequence header;
3.位置0x0000022E - 0x00000230 : 转十进制为0,表示相对时间戳为0;
4.位置0x00000231 - 0x0000021C : VIDEODATA数据,即AVC sequence header。
3.4.1 AVCDecoderConfigurationRecord
AVC sequence header中存放的是AVCDecoderConfigurationRecord,《ISO-14496-15 AVC file format》对此作了详细定义。它存放的是AVC的编码参数,解码时需设置给解码器后方可正确解码。
通常情况下,AVC sequence header这种Tag在FLV文件中只出现1次,并且是第一个Video Tag。
有关AVCDecoderConfigurationRecord结构的代码解析,可以参考中的ff_isom_write_avcc方法。
3.4.2 CompositionTime(相对时间戳)
相对时间戳的概念需要和PTS、DTS一起理解:
如果视频里各帧的编码是按输入顺序依次进行的,则解码和显示时间相同,应该是一致的。但在编码后的视频类型中,如果存在B帧,输入顺序和编码顺序并不一致,所以才需要PTS和DTS这两种时间戳。视频帧的解码一定是发生在显示前,所以视频帧的PTS,一定是大于等于DTS的,因此CTS=PTS-DTS。
FLV Video Tag中的TimeStamp,不是PTS,而是DTS,视频帧的PTS需要我们通过DTS + CTS计算得到。
为什么Audio Tag不需要CompositionTime呢?
因为Audio的编码顺序和输入顺序一致,即PTS=DTS,所以它没有CompositionTime的概念。
3.5 Script Data Tags
如果TAG包中的TagType等于18,表示该Tag中包含的数据类型为SCRIPT。
SCRIPTDATA 结构十分复杂,定义了很多格式类型,每个类型对应一种结构,详细可参考E.4.4 Data Tags
onMetaData是SCRIPTDATA中一个非常重要的信息,其结构定义可参考E.5 onMetaData。它通常是FLV文件中的第一个Tag,用来表示当前文件的一些基本信息: 比如视音频的编码类型id、视频的宽和高、文件大小、视频长度、创建日期等。
LiveVideoStack 2018年春季招聘
LiveVideoStack是专注在音视频、多媒体开发的技术社区,通过传播最新技术探索与应用实践,帮助技术人员成长,解决企业应用场景中的技术难题。如果你有意为音视频、多媒体开发领域发展做出贡献,欢迎成为LiveVideoStack的一员。我们正在招募商务助理,高级编辑,策划编辑,课程经理。
本文分享自 LiveVideoStack 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!