[关闭]
@ltlovezh 2020-09-11T12:26:34.000000Z 字数 18322 阅读 2322

音视频元数据那些事

AAC AVC HEVC


本文主要介绍音频AAC、视频H264和H265等编码格式的元数据,以及它们在MediaCodec中的处理逻辑和在封装容器中的存储逻辑。

音频AAC

AAC元数据一般包含Profile、SampleRate和Channel,这些信息在封装容器中有两种存储方式:

AudioSpecificConfig

AudioSpecificConfig一般存储在封装容器的独立模块,作为AAC全局元数据,例如:Flv的第一个音频Tag包含AudioSpecificConfig;Mp4的ESDS Box也包含AudioSpecificConfig,其格式如下所示:

  1. // 如果5bit等于31的话,就再读取6bit+32就是Audio Object Type
  2. 5 bits: object type
  3. if (object type == 31)
  4. 6 bits + 32: object type
  5. // 4bit是采样率数组的索引,若等于15,则再读取24bit就直接是采样率
  6. 4 bits: frequency index
  7. if (frequency index == 15)
  8. 24 bits: frequency
  9. // 读取4bit是通道数数组的索引
  10. 4 bits: channel configuration
  11. 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

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头部
ADTS AAC

不管是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头部。

AudioSpecificConfig与ADTS互相转换

两种AAC元数据格式之间的转换,需要依赖AAC元数据作为媒介。整体转换行为:ADTS Buffer <-> AAC元数据 <-> AudioSpecificConfig。

根据AudioSpecificConfig生成ADTS

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

根据ADTS生成AudioSpecificConfig

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

AAC与MediaCodec

MediaCodec编码AAC

MediaCodec编码AAC时,会分别输出AudioSpecificConfig和ES AAC,所以存储为AAC文件时,需要添加ADTS头部,才能解码播放。

获取AudioSpecificConfig有两种方式,测试下来两种方式获取的数据是一致的:

  1. dequeueOutputBuffer拿到的outputBufferIndex等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED时,先拿到新的MediaFormat,然后通过MediaFormat.getByteBuffer("csd-0")获取AudioSpecificConfig。
  2. dequeueOutputBuffer拿到的outputBufferIndex大于等于0时,若MediaCodec.BufferInfo.flags包含MediaCodec.BUFFER_FLAG_CODEC_CONFIG标志位,那么输出的ByteBuffer就是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

MediaCodec解码AAC时,AudioSpecificConfig有三种输入方式:

  1. 首先以csd-0为key,把AudioSpecificConfig以csd-0设置给MediaFormat,然后通过MediaFormat配置解码器:MediaCodec.configure(MediaFormat),后续只要输入ES AAC就可以了。
  2. 在MediaCodec.start()之后,输入任何ES AAC裸流之前,首先通过ByteBuffer存储AudioSpecificConfig,结合BUFFER_FLAG_CODEC_CONFIGFlag,发送给解码器,后续只要输入ES AAC就可以了。
  3. 在每个ES AAC帧前添加ADTS头部,生成ADTS AAC,直接送入解码器。

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头部。

Mux时的AAC元数据

Mp4

当通过FFMpeg把AAC封装进Mp4时,有两种输入方式:

  1. 首先需要把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。
  2. 若没有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裸流,所以无法解码播放。

TS

当通过FFMpeg把AAC封装进TS时,也有两种输入方式:

  1. 直接输入ADTS AAC,libavformat/mpegtsenc.c文件中的AVOutputFormat.check_bitstream函数只会针对H264添加h264_mp4toannexb bsf,针对H265添加hevc_mp4toannexb bsf,不会处理AAC编码格式。此时,ADTS AAC会直接写入TS容器。
  2. 首先把AudioSpecificConfig存储在AVStream->codecpar->extradata,然后输入ES AAC,此时libavformat/mpegtsenc.c会借助adts AVOutputFormat(libavformat/adtsenc.c)解析AudioSpecificConfig,得到AAC元数据,然后生成ADTS AAC写入TS容器。

当封装容器是TS时,不管通过哪种方式输入AAC,最终都是存储ADTS AAC格式。

Demux时的AAC元数据

Mp4

当通过FFMpeg Demux Mp4时,若Mp4包含AudioSpecificConfig,那么avformat_open_input打开文件后,AVStream->codecpar->extradata就是AudioSpecificConfig了。若Mp4不包含AudioSpecificConfig,那么AVStream->codecpar->extradata将一直为空。

TS

当通过FFMpeg Demux TS时,AVStream->codecpar->extradata将一直为空(因为没有单独存储的AudioSpecificConfig),如果需要获取AudioSpecificConfig,则可以通过aac_adtstoasc bsf操作包含ADTS头部的AVPacket获取。

视频H264和H265

H264的关键信息是SPS和PPS,H265的关键信息是VPS、SPS和PPS。因为H264和H265存在AVCC和Annexb两种格式,所有VPS、SPS和PPS也存在两种存储形式。

H264 AVCDecoderConfigurationRecord

Mp4的avcC Box(stsd -> avc1 -> avcC)和FLV的第一个视频Tag都包含了AVCDecoderConfigurationRecord,其中存储了Nalu Length Size、SPS和PPS,如下所示:

  1. aligned(8) class AVCDecoderConfigurationRecord {
  2. unsigned int(8) configurationVersion = 1;
  3. unsigned int(8) AVCProfileIndication;
  4. unsigned int(8) profile_compatibility;
  5. unsigned int(8) AVCLevelIndication;
  6. bit(6) reserved = 111111b;
  7. // lengthSizeMinusOne + 1表示Nalu Length Size,即一个Nalu长度用几个字节表示,一般是4字节
  8. unsigned int(2) lengthSizeMinusOne;
  9. bit(3) reserved = 111b;
  10. // sps的个数
  11. unsigned int(5) numOfSequenceParameterSets;
  12. for (i=0; i< numOfSequenceParameterSets; i++) {
  13. // 两个字节表示一个sps的长度
  14. unsigned int(16) sequenceParameterSetLength ;
  15. // sps的内容
  16. bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;
  17. }
  18. // pps的个数
  19. unsigned int(8) numOfPictureParameterSets;
  20. for (i=0; i< numOfPictureParameterSets; i++) {
  21. // 两个字节表示一个pps的长度
  22. unsigned int(16) pictureParameterSetLength;
  23. // pps的内容
  24. bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;
  25. }
  26. }

H265 HEVCDecoderConfigurationRecord

Mp4的hvcC Box(stsd -> hvc1(hev1) -> avcC)和FLV的第一个视频Tag都包含了HEVCDecoderConfigurationRecord,其中存储了Nalu Length Size、VPS、SPS和PPS,如下所示:

  1. class HEVCDecoderConfigurationRecord {
  2. unsigned int(8) configurationVersion;
  3. unsigned int(2) general_profile_space;
  4. unsigned int(1) general_tier_flag;
  5. unsigned int(5) general_profile_idc;
  6. unsigned int(32) general_profile_compatibility_flags;
  7. unsigned int(48) general_constraint_indicator_flags;
  8. unsigned int(8) general_level_idc;
  9. bit(4) reserved = 1111b;
  10. unsigned int(12) min_spatial_segmentation_idc;
  11. bit(6) reserved = 111111b;
  12. unsigned int(2) parallelismType;
  13. bit(6) reserved = 111111b;
  14. unsigned int(2) chromaFormat;
  15. bit(5) reserved = 11111b;
  16. unsigned int(3) bitDepthLumaMinus8;
  17. bit(5) reserved = 11111b;
  18. unsigned int(3) bitDepthChromaMinus8;
  19. bit(16) avgFrameRate;
  20. bit(2) constantFrameRate;
  21. bit(3) numTemporalLayers;
  22. bit(1) temporalIdNested;
  23. // lengthSizeMinusOne + 1表示Nalu Length Size,即一个Nalu长度用几个字节表示,一般是4字节
  24. unsigned int(2) lengthSizeMinusOne;
  25. // 数组长度
  26. unsigned int(8) numOfArrays;
  27. for (j=0; j < numOfArrays; j++) {
  28. bit(1) array_completeness;
  29. unsigned int(1) reserved = 0;
  30. // nalu的类型
  31. unsigned int(6) NAL_unit_type;
  32. // 上面👆nalu类型的
  33. unsigned int(16) numNalus;
  34. for (i=0; i< numNalus; i++) {
  35. unsigned int(16) nalUnitLength;
  36. bit(8*nalUnitLength) nalUnit;
  37. }
  38. }

StartCode分割的VPS、SPS和PPS

除了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分割的。

AVCDecoderConfigurationRecord与SPS和PPS互相转换

H264两种元数据格式之间的转换,需要依赖SPS和PPS作为媒介。整体转换行为:AVCDecoderConfigurationRecord <-> SPS和PPS <-> 00 00 00 01 SPS 00 00 00 01 PPS。

根据AVCDecoderConfigurationRecord得到SPS和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

根据SPS和PPS生成AVCDecoderConfigurationRecord

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

HEVCDecoderConfigurationRecord与VPS、SPS和PPS互相转换

H265两种元数据格式之间的转换,需要依赖VPS、SPS和PPS作为媒介。整体转换行为:HEVCDecoderConfigurationRecord <-> VPS、SPS和PPS <-> 00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS。

根据HEVCDecoderConfigurationRecord得到VPS、SPS和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

根据VPS、SPS和PPS生成HEVCDecoderConfigurationRecord

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

H264和H265与MediaCodec

MediaCodec编码H264

MediaCodec编码H264时,会单独输出SPS、PPS以及StartCode分割的NALU裸流。
有两种方式可以获取SPS和PPS,测试下来发现两种方式获取的数据是一致的:

  1. dequeueOutputBuffer拿到的outputBufferIndex等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED时,先拿到新的MediaFormat。然后通过csd-0获取00 00 00 01 SPS,通过csd-1获取00 00 00 01 PPS
  2. dequeueOutputBuffer拿到的outputBufferIndex大于等于0时,若MediaCodec.BufferInfo.flags包含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帧前面几字节的数据:

  1. // SPS,起始4字节是分隔符
  2. 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
  3. // PPS,起始4字节是分隔符
  4. pps length: 9, content: 0 , 0 , 0 , 1 , 104 , -18 , 6 , -30 , -64
  5. // I 帧,起始4字节是分隔符
  6. 0, 0, 0, 1, 101, -72, 64, -9, -5, -12, ......

可见,MediaCodec编码输出的H264裸流是以StartCode分割的NALU。

MediaCodec编码H265

MediaCodec编码H265时,会单独输出VPS、SPS和PPS以及StartCode分割的NALU裸流。

H265在H264的基础上新增了VPS信息

有两种方式可以获取VPS、SPS和PPS,测试下来发现两种方式获取的数据是一致的:

  1. dequeueOutputBuffer拿到的outputBufferIndex等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED时,先拿到新的MediaFormat,然后通过csd-0获取00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS
  2. dequeueOutputBuffer拿到的outputBufferIndex大于等于0时,若MediaCodec.BufferInfo.flags包含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

MediaCodec解码H264时,SPS和PPS有三种输入方式:

  1. 首先以csd-0为key,把00 00 00 01 SPS设置给MediaFormat,以csd-1为key,把00 00 00 01 PPS设置给MediaFormat,然后通过MediaFormat配置解码器:MediaCodec.configure(MediaFormat),后续只要输入StartCode分割的NALU裸流就可以了。
  2. 在MediaCodec.start()之后,输入任何NALU裸流之前,首先通过ByteBuffer存储00 00 00 01 SPS 00 00 00 01 PPS,结合BUFFER_FLAG_CODEC_CONFIGFlag,把SPS和PPS送给解码器,后续只要输入StartCode分割的NALU裸流就可以了。
  3. 在每个I帧前面拼接00 00 00 01 SPS 00 00 00 01 PPS,直接送入解码器。

不管哪种输入方式,SPS、PPS和H264裸流都是StartCode分割,不能是AVCC格式的Nalu Size。

MediaCodec解码H265

MediaCodec解码H265时,VPS、SPS和PPS有三种输入方式:

  1. 首先以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裸流就可以了。
  2. 在MediaCodec.start()之后,输入任何NALU裸流之前,首先通过ByteBuffer存储00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01 PPS,结合BUFFER_FLAG_CODEC_CONFIGFlag,把VPS、SPS和PPS送给解码器,后续只要输入StartCode分割的NALU裸流就可以了。
  3. 在每个I帧前面拼接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也是如此。

Mux时视频元数据

Mp4

当通过FFMpeg把H264封装进Mp4时,有两种输入方式:

  1. AVStream->codecpar->extradata写入AVCDecoderConfigurationRecord格式元数据,AVPacket写入NALU Size + NALU + NALU Size + NALU格式的H264裸流,这样AVCDecoderConfigurationRecord数据会直接写入avcC Box,AVPacket也会直接写入Mdat Sample。
  2. 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裸流。

TS

当通过FFmpeg把H264封装进TS时,有两种输入方式:

  1. AVStream->codecpar->extradata写入00 00 00 01 SPS 00 00 00 01 PPS或者什么都不写,AVPacket写入StartCode分割的NALU裸流(I帧前包含StartCode分割的SPS和PPS),这种情况下AVPacket中的Annexb H264裸流会直接写入pes结构。
  2. 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

Demux时视频元数据

Mp4

当通过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时详细介绍。

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容器。该函数提取不同编码格式元数据的逻辑可以简单概括如下:

实际上,h264_splithevc_split函数会把I帧前的所有NALU作为extradata。

总结

  1. 针对音频AAC,AVStream->codecpar->extradata只能是AudioSpecificConfig,ADTS头部不需要存储在extradata。
  2. 不管H264还是H265,FFMpeg Mux和Decode时,AVStream->codecpar->extradata都可以兼容两种元数据格式。
  3. AAC元数据是指Profile、SampleRate和Channel等信息,它们在不同封装容器中,可能以AudioSpecificConfig和ADTS两种形式存在。
  4. H264元数据是指SPS和PPS,它们在不同封装容器中,可能以AVCDecoderConfigurationRecord和00 00 00 01 SPS 00 00 00 01两种形式存在。
  5. H265元数据是指VPS、SPS和PPS,它们在不同封装容器中,可能以HEVCDecoderConfigurationRecord和00 00 00 01 VPS 00 00 00 01 SPS 00 00 00 01两种形式存在。。
  6. 当音视频元数据在封装容器中独立存储时,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)提取元数据。
  7. FFmpeg很强大,存在很多兼容逻辑,通过FFMpeg生成的音视频,ffplay可以播放,但是其他播放器不一定可以兼容播放。

参考文档

  1. Audio Specific Config
  2. RTMP推送AAC ADTS音频流
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注