=================================================================
音视频入门基础:AAC专题系列文章:
音视频入门基础:AAC专题(1)——AAC官方文档下载
音视频入门基础:AAC专题(2)——使用FFmpeg命令生成AAC裸流文件
音视频入门基础:AAC专题(3)——AAC的ADTS格式简介
音视频入门基础:AAC专题(4)——ADTS格式的AAC裸流实例分析
音视频入门基础:AAC专题(5)——FFmpeg源码中,判断某文件是否为AAC裸流文件的实现
音视频入门基础:AAC专题(6)——FFmpeg源码中解码ADTS格式的AAC的Header的实现
音视频入门基础:AAC专题(7)——FFmpeg源码中计算AAC裸流每个packet的size值的实现
音视频入门基础:AAC专题(8)——FFmpeg源码中计算AAC裸流AVStream的time_base的实现
音视频入门基础:AAC专题(9)——FFmpeg源码中计算AAC裸流每个packet的duration和duration_time的实现
音视频入门基础:AAC专题(10)——FFmpeg源码中计算AAC裸流每个packet的pts、dts、pts_time、dts_time的实现
=================================================================
一、引言
通过FFprobe命令:
ffprobe -of json -show_packets XXX.aac
可以显示AAC裸流每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的size:
这个“size”实际是AVPacket结构体中的成员变量size,为AVPacket的成员变量data指向的缓冲区的大小,也就是AAC裸流中某个packet的大小(单位为字节)。该值通过fftools/ffprobe.c中的show_packet函数打印出来:
static void show_packet(WriterContext *w, InputFile *ifile, AVPacket *pkt, int packet_idx)
{
//...
print_val("size", pkt->size, unit_byte_str);
//...
}
本文讲述这个“size”值是怎样被计算出来的。如果想直接看结论,可以跳到本文的最后,直接看“总结”。对于AAC裸流,size值是通过adts_aac_read_packet函数计算出来的。
二、adts_aac_read_packet函数的定义
adts_aac_read_packet函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavformat/aacdec.c中:
static int adts_aac_read_packet(AVFormatContext *s, AVPacket *pkt)
{
int ret, fsize;
retry:
ret = av_get_packet(s->pb, pkt, ADTS_HEADER_SIZE);
if (ret < 0)
return ret;
if (ret < ADTS_HEADER_SIZE) {
return AVERROR(EIO);
}
if ((AV_RB16(pkt->data) >> 4) != 0xfff) {
// Parse all the ID3 headers between frames
int append = ID3v2_HEADER_SIZE - ADTS_HEADER_SIZE;
av_assert2(append > 0);
ret = av_append_packet(s->pb, pkt, append);
if (ret != append) {
return AVERROR(EIO);
}
if (!ff_id3v2_match(pkt->data, ID3v2_DEFAULT_MAGIC)) {
av_packet_unref(pkt);
ret = adts_aac_resync(s);
} else
ret = handle_id3(s, pkt);
if (ret < 0)
return ret;
goto retry;
}
fsize = (AV_RB32(pkt->data + 3) >> 13) & 0x1FFF;
if (fsize < ADTS_HEADER_SIZE) {
return AVERROR_INVALIDDATA;
}
ret = av_append_packet(s->pb, pkt, fsize - pkt->size);
return ret;
}
该函数的作用是:获取一个ADTS音频帧的数据,赋值给形参pkt指向的packet。
形参s:既是输入型参数也是输出型参数,指向一个AVFormatContext对象。
形参pkt:输出型参数,指向一个AVPacket对象。执行adts_aac_read_packet函数后,pkt->data指向的的缓冲区会得到整个ADTS音频帧的数据,pkt->size会增至该ADTS音频帧的大小。
三、adts_aac_read_packet函数的内部实现分析
宏定义ADTS_HEADER_SIZE的值为7,表示不包含CRC校验的ADTS Header的长度:
#define ADTS_HEADER_SIZE 7
adts_aac_read_packet函数内部,首先通过av_get_packet函数从AVIOContext输入缓冲区或文件描述符中总共读取ADTS_HEADER_SIZE(7)个字节数据,也就是读取该音频帧的Header,追加到原来pkt->data指向的的缓冲区的尾部。关于av_get_packet函数的用法可以参考 《FFmpeg源码:append_packet_chunked、av_get_packet、av_append_packet函数分析》。如果实际读取到的大小小于7个字节,返回AVERROR(EIO)表示IO错误:
ret = av_get_packet(s->pb, pkt, ADTS_HEADER_SIZE);
if (ret < 0)
return ret;
if (ret < ADTS_HEADER_SIZE) {
return AVERROR(EIO);
}
由《音视频入门基础:AAC专题(3)——AAC的ADTS格式简介》可以知道,ADTS音频帧的adts_fixed_header中的syncword属性占12位,每个位都必须被设置为1。通过下面的if语句判断syncword属性的值是否正确,如果不正确,执行大括号内解析id3v2 header的流程。ID3v2是一系列元数据,里面存储了一些跟歌曲相关的信息(比如:演唱者、歌曲名、备注等)。关于AV_RB16宏定义的用法可以参考:《FFmpeg源码:AV_RB32、AV_RB16、AV_RB8宏定义分析》:
if ((AV_RB16(pkt->data) >> 4) != 0xfff) {
// Parse all the ID3 headers between frames
//...
}
获取adts_variable_header中的aac_frame_length属性,即该ADTS音频帧的总长度(包含ADTS Header、错误校验和AAC原始数据块,单位为字节),赋值给变量fsize。由《音视频入门基础:AAC专题(3)——AAC的ADTS格式简介》可以知道,ADTS Header至少占7个字节(当存在CRC校验时,ADTS Header占9字节;不存在CRC校验时,ADTS Header占7字节),所以如果从上面得到的该ADTS音频帧的总长度小于7,表示ADTS Header格式不正确,返回AVERROR_INVALIDDATA:
fsize = (AV_RB32(pkt->data + 3) >> 13) & 0x1FFF;
if (fsize < ADTS_HEADER_SIZE) {
return AVERROR_INVALIDDATA;
}
通过av_append_packet函数,增加形参pkt指向的packet的大小至aac_frame_length个字节,让pkt->data指向的的缓冲区得到整个ADTS音频帧的数据。关于av_append_packet函数的用法可以参考:《FFmpeg源码:append_packet_chunked、av_get_packet、av_append_packet函数分析》:
ret = av_append_packet(s->pb, pkt, fsize - pkt->size);
四、总结
1.AAC裸流每个packet的size值(该packet的大小,单位为字节)实际上是通过ADTS音频帧(adts音频压缩数据包)的adts_fixed_header中的aac_frame_length属性获取的。
2.如果仔细地观察,会发现AAC裸流每个packet的size值都不一样,这是因为ADTS音频帧存贮的是压缩后的音频数据。而WAV音频文件一般存贮的是PCM,也就是无压缩的原始音频数据,所以在存贮的是PCM数据的情况下,WAV音频文件每个packet的size值都是一样的。各位同学可以把本文跟《音视频入门基础:WAV专题(7)——FFmpeg源码中计算WAV音频文件每个packet的size值的实现》进行对比,以加深对音频帧size值的理解。
3.FFmpeg源码内部得到AAC裸流每个packet的size值是通过adts_aac_read_packet函数,但是解码ADTS Header获取里面信息(音频采样频率、声道数、采样位数等)是通过ff_adts_header_parse函数,关于ff_adts_header_parse函数可以参考:《音视频入门基础:AAC专题(6)——FFmpeg源码中解码ADTS格式的AAC的Header的实现》。可以看到这两个函数有部分功能是重复了,重复获取了ADTS音频帧的大小。FFmpeg对AAC裸流解封装时,会重复解码ADTS Header导致性能损失。也就是说:FFmpeg为了架构的通用性、扩展性、兼容性和代码的可读性造成了代码的冗余和重复,所以为了极致的性能,比如更快的解封装速度,很多开发者会选择不依赖任何第三方库自己写解析代码,或者改FFmpeg的源码实现优化。各位同学可以尝试改FFmpeg源码,使得对AAC裸流进行解复用时,只解码一次ADTS Header,从而提高解复用速度。