@ltlovezh
2020-09-11T12:26:34.000000Z
字数 18322
阅读 2359
AAC
AVC
HEVC
本文主要介绍音频AAC、视频H264和H265等编码格式的元数据,以及它们在MediaCodec中的处理逻辑和在封装容器中的存储逻辑。
AAC元数据一般包含Profile、SampleRate和Channel,这些信息在封装容器中有两种存储方式:
AudioSpecificConfig
一般存储在封装容器的独立模块,作为AAC全局元数据,例如:Flv的第一个音频Tag包含AudioSpecificConfig;Mp4的ESDS Box也包含AudioSpecificConfig,其格式如下所示:
// 如果5bit等于31的话,就再读取6bit+32就是Audio Object Type
5 bits: object type
if (object type == 31)
6 bits + 32: object type
// 4bit是采样率数组的索引,若等于15,则再读取24bit就直接是采样率
4 bits: frequency index
if (frequency index == 15)
24 bits: frequency
// 读取4bit是通道数数组的索引
4 bits: channel configuration
var bits: AOT Specific Config
audio object type是profile的索引,frequency index是采样率的索引,channel configuration是声道数的索引,具体取值可以参考:MPEG-4 Audio。
最简单的AudioSpecificConfig只要两个字节,例如:Profile: LC AAC,Sample Rate: 44100,Channel Count: 2,参照索引表,audio object type为2,frequency index为4,channel configuration为2,那么最终的AudioSpecificConfig就是0x1210,10进制就是18, 16。
ADTS
是固定字节数的AAC头部(protection_absent为1,占7字节;protection_absent为0,占9字节),里面同样包含了profile索引、采样率索引和声道数索引以及一个ADTS帧的长度,表示ADTS头和ES AAC的总长度。ADTS头部存储在每个AAC音频帧前面。mpegts中的AAC流就是这种格式,每个Bit的含义可以参考:ADTS
或者AAC ADTS格式分析。因为ADTS
的前12 Bit固定为1,所以可利用这一点快速判断AAC是否包含ADTS头部。
不管是AudioSpecificConfig,还是ADTS,都包含了audio object type、sample rate index(frequency index)和channel configuration。
从FLV和Mp4中demux出的AAC,直接保存在本地是无法播放的(ES AAC),因为缺少7字节的ADTS头部。需要在ES AAC帧前添加7字节的ADTS头部,才能正常解码播放。生成7字节的ADTS头部,可以参考libavformat/adtsenc.c
中的adts_write_frame_header
函数。
MediaCodec编码输出的也是ES AAC,若想保存成文件直接播放,也需要添加ADTS头部。
两种AAC元数据格式之间的转换,需要依赖AAC元数据作为媒介。整体转换行为:ADTS Buffer <-> AAC元数据 <-> AudioSpecificConfig。
libavformat/adtsenc.c
是ADTS AAC的AVOutputFormat实现,当mux成AAC时,负责添加额外的ADTS头部,这样才能保证生成的裸AAC文件可以解码播放。
adtsenc.c
的主要逻辑是解析AVStream->codecpar->extradata
表示的AudioSpecificConfig结构,获得AAC Profile、采样率和声道数,然后基于这些AAC元数据,为每个音频帧添加ADTS头部。
adts_decode_extradata
函数解析AudioSpecificConfig(extradata),得到AAC元数据;adts_write_frame_header
函数根据AAC元数据,生成ADTS Buffer。
adts AVOutputFormat: AudioSpecificConfig -> AAC元数据 -> ADTS
libavcodec/aac_adtstoasc_bsf.c
是一个bsf filter,可以根据AVPacket中的ADTS头部,生成AudioSpecificConfig,并且保存在AVBSFContext->par_out->extradata
中,同时把AVPacket中的ADTS头部删掉。使用者需要把AVBSFContext->par_out
作为新的AVCodecParameters,去初始化AVCodecContext(decode)或者更新AVStream->codecpar(mux)。
aac_adtstoasc_filter
函数首先通过avpriv_aac_parse_header
解析ADTS Buffer,得到AAC元数据,然后根据AAC元数据,生成AudioSpecificConfig,保存在AVBSFContext->par_out->extradata
,作为对外的输出。
aac_adtstoasc bsf filter: ADTS Buffer -> AAC元数据 -> AudioSpecificConfig
MediaCodec编码AAC时,会分别输出AudioSpecificConfig和ES AAC,所以存储为AAC文件时,需要添加ADTS头部,才能解码播放。
获取AudioSpecificConfig有两种方式,测试下来两种方式获取的数据是一致的:
MediaCodec编码的ES AAC,通过FFmpeg Mux时,需要先把AudioSpecificConfig
写入AVStream->codecpar->extradata
,然后才能调用avformat_write_header
写入容器头信息。当封装容器是Mp4时,AudioSpecificConfig会写入ESDS BOX,ES AAC直接写入Mdat Sample;当容器是TS时,libavformat/mpegtsenc.c
会根据AudioSpecificConfig,转换为ADTS AAC(adts AVOutputFormat),然后写入到TS容器。
MediaCodec解码AAC时,AudioSpecificConfig有三种输入方式:
csd-0
为key,把AudioSpecificConfig以csd-0
设置给MediaFormat,然后通过MediaFormat配置解码器:MediaCodec.configure(MediaFormat),后续只要输入ES AAC就可以了。BUFFER_FLAG_CODEC_CONFIG
Flag,发送给解码器,后续只要输入ES AAC就可以了。MP4(libavformat/movenc.c
)和FLV(libavformat/flvenc.c
)封装容器,有独立的模块存储AudioSpecificConfig,比如:Mp4的ESDS Box包含了AudioSpecificConfig,Flv第一个音频Tag也包含了AudioSpecificConfig。
Mp4和FLV分别存储AudioSpecificConfig和ES AAC。
TS容器(libavformat/mpegtsenc.c
)没有独立存储AudioSpecificConfig,而是在每个音频帧前添加了ADTS头部,这样可以保证每个TS文件可以独立播放。
TS直接存储ADTS AAC。
裸的AAC容器(libavformat/adtsenc.c
),也是在每个音频帧前包含ADTS,这样可以保证裸AAC文件可以直接解码。
当需要在不同封装容器之间转封装时,就涉及到AudioSpecificConfig与ADTS的互相转换了。例如:从Mp4容器拆分出aac时,就会通过libavformat/adtsenc.c
生成ADTS AAC;从TS转封装为Mp4时,需要通过aac_adtstoasc bsf filter
提取AudioSpecificConfig,并删除AVPacket的ADTS头部。
当通过FFMpeg把AAC封装进Mp4时,有两种输入方式:
AudioSpecificConfig
存储在AVStream->codecpar->extradata
,然后通过avformat_write_header
写入到封装容器中,例如:Mp4的ESDS BOX,FLv的第一个音频TAG,后续可以直接输入ES AAC。可参考libavformat/movenc.c
中的mov_write_esds_tag
函数:track->vos_data就是AVStream->codecpar->extradata
数据。此外,通过写入一个假的asc,可以验证extradata会被直接写入到ESDS BOX。AudioSpecificConfig
,也可以直接输入ADTS AAC,此时av_interleaved_write_frame
函数会借助libavformat/movenc.c
文件中AVOutputFormat.check_bitstream
函数设置的aac_adtstoasc bsf
从AVPacket提取AudioSpecificConfig
(可参考mov_check_bitstream
函数),然后删除ADTS头部,最后写入AudioSpecificConfig
和ES AAC(这种情况下,av_write_trailer
函数负责把AudioSpecificConfig写入ESDS BOX)。这两种方式下,Mp4最终都是分别存储AudioSpecificConfig和ES AAC。
除此之外,当AVStream->codecpar->extradata
为空,并且是ES AAC时,ES AAC会被直接写入Mdat Sample,此时ESDS BOX不包含AudioSpecificConfig数据。这种Mp4,在Mac上用QuickTime播放有画面无声音,但是ffplay可以正常播放出声音和画面,更进一步从这种Mp4分离出的AAC裸流也无法播放,缺少ADTS头部信息。
为什么ESDS BOX没有包含AudioSpecificConfig,并且是ES AAC时,ffplay仍然可以播放音频那?这种情况下,虽然ESDS Box没有包含AAC元数据,但是Mp4a Box包含了声道数、采样位数和采样率,FFMpeg兼容比较好,所以ES AAC可以播放,但是从这种Mp4单独分离出AAC裸流时,因为没有extradata(ESDS BOX没有包含AudioSpecificConfig),所以无法生成ADTS头部,即是ES AAC裸流,所以无法解码播放。
当通过FFMpeg把AAC封装进TS时,也有两种输入方式:
libavformat/mpegtsenc.c
文件中的AVOutputFormat.check_bitstream
函数只会针对H264添加h264_mp4toannexb bsf
,针对H265添加hevc_mp4toannexb bsf
,不会处理AAC编码格式。此时,ADTS AAC会直接写入TS容器。AudioSpecificConfig
存储在AVStream->codecpar->extradata
,然后输入ES AAC,此时libavformat/mpegtsenc.c
会借助adts AVOutputFormat(libavformat/adtsenc.c
)解析AudioSpecificConfig
,得到AAC元数据,然后生成ADTS AAC写入TS容器。当封装容器是TS时,不管通过哪种方式输入AAC,最终都是存储ADTS AAC格式。
当通过FFMpeg Demux Mp4时,若Mp4包含AudioSpecificConfig
,那么avformat_open_input
打开文件后,AVStream->codecpar->extradata
就是AudioSpecificConfig了。若Mp4不包含AudioSpecificConfig
,那么AVStream->codecpar->extradata
将一直为空。
当通过FFMpeg Demux TS时,AVStream->codecpar->extradata
将一直为空(因为没有单独存储的AudioSpecificConfig),如果需要获取AudioSpecificConfig
,则可以通过aac_adtstoasc bsf
操作包含ADTS头部的AVPacket获取。
H264的关键信息是SPS和PPS,H265的关键信息是VPS、SPS和PPS。因为H264和H265存在AVCC和Annexb两种格式,所有VPS、SPS和PPS也存在两种存储形式。
Mp4的avcC Box(stsd -> avc1 -> avcC)和FLV的第一个视频Tag都包含了AVCDecoderConfigurationRecord,其中存储了Nalu Length Size、SPS和PPS,如下所示:
aligned(8) class AVCDecoderConfigurationRecord {
unsigned int(8) configurationVersion = 1;
unsigned int(8) AVCProfileIndication;
unsigned int(8) profile_compatibility;
unsigned int(8) AVCLevelIndication;
bit(6) reserved = ‘111111’b;
// lengthSizeMinusOne + 1表示Nalu Length Size,即一个Nalu长度用几个字节表示,一般是4字节
unsigned int(2) lengthSizeMinusOne;
bit(3) reserved = ‘111’b;
// sps的个数
unsigned int(5) numOfSequenceParameterSets;
for (i=0; i< numOfSequenceParameterSets; i++) {
// 两个字节表示一个sps的长度
unsigned int(16) sequenceParameterSetLength ;
// sps的内容
bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;
}
// pps的个数
unsigned int(8) numOfPictureParameterSets;
for (i=0; i< numOfPictureParameterSets; i++) {
// 两个字节表示一个pps的长度
unsigned int(16) pictureParameterSetLength;
// pps的内容
bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;
}
}
Mp4的hvcC Box(stsd -> hvc1(hev1) -> avcC)和FLV的第一个视频Tag都包含了HEVCDecoderConfigurationRecord,其中存储了Nalu Length Size、VPS、SPS和PPS,如下所示:
class HEVCDecoderConfigurationRecord {
unsigned int(8) configurationVersion;
unsigned int(2) general_profile_space;
unsigned int(1) general_tier_flag;
unsigned int(5) general_profile_idc;
unsigned int(32) general_profile_compatibility_flags;
unsigned int(48) general_constraint_indicator_flags;
unsigned int(8) general_level_idc;
bit(4) reserved = ‘1111’b;
unsigned int(12) min_spatial_segmentation_idc;
bit(6) reserved = ‘111111’b;
unsigned int(2) parallelismType;
bit(6) reserved = ‘111111’b;
unsigned int(2) chromaFormat;
bit(5) reserved = ‘11111’b;
unsigned int(3) bitDepthLumaMinus8;
bit(5) reserved = ‘11111’b;
unsigned int(3) bitDepthChromaMinus8;
bit(16) avgFrameRate;
bit(2) constantFrameRate;
bit(3) numTemporalLayers;
bit(1) temporalIdNested;
// lengthSizeMinusOne + 1表示Nalu Length Size,即一个Nalu长度用几个字节表示,一般是4字节
unsigned int(2) lengthSizeMinusOne;
// 数组长度
unsigned int(8) numOfArrays;
for (j=0; j < numOfArrays; j++) {
bit(1) array_completeness;
unsigned int(1) reserved = 0;
// nalu的类型
unsigned int(6) NAL_unit_type;
// 上面👆nalu类型的
unsigned int(16) numNalus;
for (i=0; i< numNalus; i++) {
unsigned int(16) nalUnitLength;
bit(8*nalUnitLength) nalUnit;
}
}
除了H264的AVCDecoderConfigurationRecord格式和H265的HEVCDecoderConfigurationRecord格式,还可以用StartCode分割(VPS)、SPS和PPS,例如:(00 00 00 01 VPS) 00 00 00 01 SPS 00 00 00 01 PPS。
当通过MediaCodec编解码H264和H265时,输入输出的视频元数据和裸NALU都是通过StartCode分割的。
H264两种元数据格式之间的转换,需要依赖SPS和PPS作为媒介。整体转换行为:AVCDecoderConfigurationRecord <-> SPS和PPS <-> 00 00 00 01 SPS 00 00 00 01 PPS。
libavcodec/h264_mp4toannexb_bsf.c
文件的h264_extradata_to_annexb
函数实现了解析AVCDecoderConfigurationRecord格式,生成00 00 00 01 SPS 00 00 00 01 PPS的功能,并且会把AVPacket中AVCC格式的H264,转换为Annexb格式(Nalu Length Size替换为00 00 00 01,I帧前添加00 00 00 01 SPS 00 00 00 01 PPS)。
h264_mp4toannexb bsf: AVCDecoderConfigurationRecord -> 00 00 00 01 SPS 00 00 00 01 PPS
libavformat/avc.c
文件的ff_isom_write_avcc
函数负责把AVStream->par->extradata
中的SPS和PPS组织成AVCDecoderConfigurationRecord格式,并写入avcC Box。
ff_isom_write_avcc
的主要逻辑是判断AVStream->par->extradata
是什么形式的SPS和PPS,若AVStream->par->extradata
是以StartCode分割的SPS和PPS,则首先提取SPS和PPS,然后按照 AVCDecoderConfigurationRecord格式写入;否则,则可以直接写入。
ff_isom_write_avcc: 00 00 00 01 SPS 00 00 00 01 PPS -> AVCDecoderConfigurationRecord
H265两种元数据格式之间的转换,需要依赖VPS、SPS和PPS作为媒介。整体转换行为:HEVCDecoderConfigurationRecord <-> VPS、SPS和PPS <-> 00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS。
libavcodec/hevc_mp4toannexb_bsf.c
文件的hevc_extradata_to_annexb
函数实现了解析HEVCDecoderConfigurationRecord格式,生成00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS的功能,并且会把AVPacket中AVCC格式的H265,转换为Annexb格式(Nalu Length Size替换为00 00 00 01,I帧前添加00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS)。
hevc_mp4toannexb bsf: HEVCDecoderConfigurationRecord -> 00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
libavformat/hevc.c
文件的ff_isom_write_hvcc
函数负责把AVStream->par->extradata
中的VPS、SPS和PPS组织成HEVCDecoderConfigurationRecord格式,并写入hvcC Box。
ff_isom_write_hvcc
的主要逻辑是判断AVStream->par->extradata
是什么形式的VPS、SPS和PPS,若AVStream->par->extradata
是以StartCode分割的VPS、SPS和PPS,则首先提取VPS、SPS和PPS,然后按照HEVCDecoderConfigurationRecord格式写入;否则,则可以直接写入。
ff_isom_write_hvcc: 00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS -> HEVCDecoderConfigurationRecord
MediaCodec编码H264时,会单独输出SPS、PPS以及StartCode分割的NALU裸流。
有两种方式可以获取SPS和PPS,测试下来发现两种方式获取的数据是一致的:
csd-0
获取00 00 00 01 SPS
,通过csd-1
获取00 00 00 01 PPS
。MediaCodec.BUFFER_FLAG_CODEC_CONFIG
标志位,那么输出的ByteBuffer就是00 00 00 01 SPS 00 00 00 01 PPS
。这两种方式获取的SPS和PPS是一致的,并且都会携带00 00 00 01
分隔符。区别是第一种方式的SPS和PPS是分开存储的,第二种方式是一起存储的。第一种方式拼接在一起就等于第二种方式获取的数据。
因为SPS和PPS是单独输出的,所以MediaCodec
输出的I帧前面不会再次携带SPS和PPS信息。
有种说法是当上述两种方式都取不到SPS和PPS时,I帧前面就会携带SPS和PPS,这点还没遇到过,有待确认。
下面是MediaCodec编码H264时,输出的SPS、PPS和I帧前面几字节的数据:
// SPS,起始4字节是分隔符
sps length: 22, content: 0 , 0 , 0 , 1 , 103 , 100 , 0 , 32 , -84 , -76 , 5 , -96 , 89 , -46 , -112 , 80 , 96 , 96 , 109 , 10 , 19 , 80
// PPS,起始4字节是分隔符
pps length: 9, content: 0 , 0 , 0 , 1 , 104 , -18 , 6 , -30 , -64
// I 帧,起始4字节是分隔符
0, 0, 0, 1, 101, -72, 64, -9, -5, -12, ......
可见,MediaCodec编码输出的H264裸流是以StartCode分割的NALU。
MediaCodec编码H265时,会单独输出VPS、SPS和PPS以及StartCode分割的NALU裸流。
H265在H264的基础上新增了VPS信息
有两种方式可以获取VPS、SPS和PPS,测试下来发现两种方式获取的数据是一致的:
csd-0
获取00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
。MediaCodec.BUFFER_FLAG_CODEC_CONFIG
标志位,那么输出的ByteBuffer就是00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
。因为VPS、SPS和PPS是单独输出的,所以MediaCodec
输出的I帧前面不会再次携带SPS和PPS信息。
通过MediaCodec解码音频和视频时,输入的PTS单位必须是微秒,FFmpeg av_read_frame返回的AVPacket,时间戳是基于AVStream的time_base,所以必须将AVPacket的pts和dts从AVStream的time_base转换到
time_base = 1000000
,再送给MediaCodec;否则会出现异常情况,比如:多个AVPacket的pts间隔非常短,系统会认为视频帧率太高,解码器已经超负荷,导致硬解码器(OMX.qcom.video.decoder.avc)创建失败,转成系统内部的google软解码器(OMX.google.h264.decoder),从而导致解码速度大幅下降。
MediaCodec解码H264时,SPS和PPS有三种输入方式:
csd-0
为key,把00 00 00 01 SPS
设置给MediaFormat,以csd-1
为key,把00 00 00 01 PPS
设置给MediaFormat,然后通过MediaFormat配置解码器:MediaCodec.configure(MediaFormat),后续只要输入StartCode分割的NALU裸流就可以了。00 00 00 01 SPS 00 00 00 01 PPS
,结合BUFFER_FLAG_CODEC_CONFIG
Flag,把SPS和PPS送给解码器,后续只要输入StartCode分割的NALU裸流就可以了。 00 00 00 01 SPS 00 00 00 01 PPS
,直接送入解码器。不管哪种输入方式,SPS、PPS和H264裸流都是StartCode分割,不能是AVCC格式的Nalu Size。
MediaCodec解码H265时,VPS、SPS和PPS有三种输入方式:
csd-0
为key,把00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
设置给MediaFormat,然后通过MediaFormat配置解码器:MediaCodec.configure(MediaFormat),后续只要输入StartCode分割的NALU裸流就可以了。00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
,结合BUFFER_FLAG_CODEC_CONFIG
Flag,把VPS、SPS和PPS送给解码器,后续只要输入StartCode分割的NALU裸流就可以了。 00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
,直接送入解码器。不管哪种输入方式,VPS、SPS、PPS和H265裸流都是StartCode分割,不能是AVCC格式的Nalu Size。
MP4(libavformat/movenc.c
)和FLV(libavformat/flvenc.c
)封装容器,有独立的模块存储AVCDecoderConfigurationRecord和HEVCDecoderConfigurationRecord,比如:Mp4的avcC Box包含了AVCDecoderConfigurationRecord,hvcC Box包含了HEVCDecoderConfigurationRecord, Flv第一个视频Tag也包含了VCDecoderConfigurationRecord或者HEVCDecoderConfigurationRecord。
Mp4和Flv存储AVCC格式的H264,即单独存储AVCDecoderConfigurationRecord和NALU裸流,并且NALU裸流是NALU Size + NALU + NALU Size + NALU格式,不是StartCode分割。H265也是如此。
TS容器(libavformat/mpegtsenc.c
)没有单独存储AVCDecoderConfigurationRecord或者HEVCDecoderConfigurationRecord,而是在每个I帧前添加了(00 00 00 01 VPS) 00 00 00 01 SPS 00 00 00 01 PPS,这样可以保证每个TS文件可以独立播放。
TS存储Annexb格式的H264,所有的NALU都是StartCode分割,并且I帧前添加了StartCode分割的SPS和PPS,H265也是如此。
当需要在不同封装容器之间转封装时,就涉及到AVCDecoderConfigurationRecord与00 00 00 01 SPS 00 00 00 01 PPS之间的互相转换了,也就是H264 AVCC和Annexb格式之间的转换,H265也是如此。
从TS转封装为Mp4时,FFmpeg的avformat_find_stream_info
函数可以从StartCode分割的NALU(AVPacket),提取出00 00 00 01 SPS 00 00 00 01 PPS作为AVStream->codecpar->extradata
,并且Mp4 Muxer会重新把extradata组织成AVCDecoderConfigurationRecord格式写入avcC Box,最后Mp4 Muxer也会把StartCode分割的H264 NALU,重新组织成NALU Size + NALU + NALU Size + NALU格式写入Mdat Sample,这样就完成了H264从Annexb格式到AVCC格式的转换,H265也是如此。
从Mp4转封装为TS时,主要依赖h264_mp4toannexb bsf
从AVCDecoderConfigurationRecord中提取出00 00 00 01 SPS 00 00 00 01 PPS,并且把NALU Size + NALU + NALU Size + NALU格式的H264转换为StartCoder分割的NALU,然后在每个I帧前插入00 00 00 01 SPS 00 00 00 01 PPS,写入pes结构,这样就完成了H264从AVCC格式到Annexb格式的转换,H265也是如此。
当通过FFMpeg把H264封装进Mp4时,有两种输入方式:
AVStream->codecpar->extradata
写入AVCDecoderConfigurationRecord格式元数据,AVPacket写入NALU Size + NALU + NALU Size + NALU格式的H264裸流,这样AVCDecoderConfigurationRecord数据会直接写入avcC Box,AVPacket也会直接写入Mdat Sample。AVStream->codecpar->extradata
写入00 00 00 01 SPS 00 00 00 01 PPS格式元数据,AVPacket也是StartCode分割的NALU裸流,这种情况下,libavformat/avc.c
文件的ff_isom_write_avcc
函数会把extradata中StartCode分割的SPS和PPS组织成AVCDecoderConfigurationRecord格式写入avcC Box,同时ff_mov_write_packet
函数也会把StartCode分割的NALU裸流替换为NALU Size + NALU格式的H264裸流(若I帧前包含了SPS和PPS,那么也会予以保留,即:只把StartCode替换为NALU Size,而不删除任何NALU),写入Mdat Sample。这两种方式下,Mp4最终都是分别存储AVCDecoderConfigurationRecord元数据和NALU Size + NALU格式的H264裸流。
除此之外,当AVStream->codecpar->extradata
为空,AVPacket是Annexb格式的H264时,StartCode分割的NALU会被直接写入Mdat Sample,此时,Mp4容器下包含空的avcC Box。这种Mp4,在Mac用上QuickTime播放有声音无画面,但是ffplay可以正常播放出声音和画面,因为avformat_find_stream_info
函数可以从AVPacket(StartCode分割的NALU)中提取出00 00 00 01 SPS 00 00 00 01 PPS作为AVStream->codecpar->extradata
,这样解码器仍然可以正常解码。
H264的解码器(软解:libavcodec/h264dec.c,硬解:libavcodec/mediacodecdec.c),在处理AVCodecContext->extradata
时,会兼容startCode分隔的SPS、PPS和AVCDecoderConfigurationRecord两种形式(ff_h264_decode_extradata函数),从extradata中提取出SPS和PPS,作为解码参数。
H265的整体逻辑与H264类似,只不过HEVCDecoderConfigurationRecord数据通过
ff_isom_write_hvcc
函数存储在hvcC Box,最终Mp4也是分别存储HEVCDecoderConfigurationRecord元数据和NALU Size + NALU格式的H265裸流。
当通过FFmpeg把H264封装进TS时,有两种输入方式:
AVStream->codecpar->extradata
写入00 00 00 01 SPS 00 00 00 01 PPS或者什么都不写,AVPacket写入StartCode分割的NALU裸流(I帧前包含StartCode分割的SPS和PPS),这种情况下AVPacket中的Annexb H264裸流会直接写入pes结构。AVStream->codecpar->extradata
写入AVCDecoderConfigurationRecord格式元数据,AVPacket写入NALU Size + NALU格式的H264裸流,这种情况下,TS容器会先通过h264_mp4toannexb bsf
把H264从AVCC格式转换为Annexb格式,然后写入pes结构。libavcodec/h264_mp4toannexb_bsf.c
的职责就是解析AVCDecoderConfigurationRecord元数据,提取出SPS和PPS,然后把00 00 00 01 SPS 00 00 00 01 PPS插入在AVPacket中I帧前面,并且把NALU Length Size替换为StartCode,生成标准的Annexb H264,用于写入pes结构。
H265的整体逻辑与H264类似,只不过bsf换成了
libavcodec/hevc_mp4toannexb_bsf.c
文件中的hevc_mp4toannexb
。
当通过FFmpeg Demux Mp4时,若Mp4的avcC Box包含AVCDecoderConfigurationRecord,那么avformat_open_input
打开文件后, AVStream->codecpar->extradata
就是AVCDecoderConfigurationRecord了,此时读取的AVPacket是NALU Size + NALU格式的H264裸流;若Mp4的avcC Box为空(此时,Mp4包含Annexb格式的H264),那么avformat_open_input
打开文件后,AVStream->codecpar->extradata
也会为空,但是avformat_find_stream_info
函数会解析AVPacket提取出00 00 00 01 SPS 00 00 00 01 PPS作为AVStream->codecpar->extradata
。
H265逻辑与H264类似,只不过多了VPS,不再赘述。
关于avformat_find_stream_info函数,Demux TS时详细介绍。
当通过FFmpeg Demux TS时,avformat_open_input
函数打开输入文件后,AVStream->codecpar->extradata
是空的,因为TS没有单独存储的AVCDecoderConfigurationRecord或者HEVCDecoderConfigurationRecord数据,但是avformat_find_stream_info
函数会解析Annexb格式的AVPacket,提取出00 00 00 01 SPS 00 00 00 01 PPS作为AVStream->codecpar->extradata
。
H265逻辑与H264类似,只不过多了VPS,不再赘述。
avformat_find_stream_info
函数的职责就是从AVPacket中提取出各种编码格式的元数据,因为有的封装容器不单独存储元数据,而是和裸流一起存储,比如TS容器。该函数提取不同编码格式元数据的逻辑可以简单概括如下:
libavcodec/h264_parser.c
文件的h264_split
函数从Annexb H264的I帧前提取出00 00 00 01 SPS 00 00 00 01 PPS作为extradata;若视频是H265,则主要通过libavcodec/hevc_parser.c
文件的hevc_split
函数从Annexb H265的I帧前提取出00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS作为extradata。extract_extradata bsf
(libavcodec/extract_extradata_bsf.c)从AVPacket提取元数据。实际上,
h264_split
和hevc_split
函数会把I帧前的所有NALU作为extradata。
AVStream->codecpar->extradata
只能是AudioSpecificConfig,ADTS头部不需要存储在extradata。AVStream->codecpar->extradata
都可以兼容两种元数据格式。avformat_open_input
后,AVStream->codecpar->extradata
就可以获得元数据;否则,要通过avformat_find_stream_info
解析AVPacket获取extradata。FFMpeg3.3及以前版本,H264通过libavcodec/h264_parser.c
文件的h264_split
函数解析AVPacket获得H264元数据;H265通过libavcodec/hevc_parser.c
文件的hevc_split
函数解析AVPacket获得H265元数据;FFmpeg 4.x版本则统一使用过extract_extradata bsf
(libavcodec/extract_extradata_bsf.c)提取元数据。