FLV(Flash Video) 是一种非常流行的多媒体封装格式。它可以以文件的方式存储,也可以承载在RTMP协议之上。作为网络传输的一种封装格式。像我们现在大部分的视频网站,或者是直播网站都是使用这种格式。所以对于从事多媒体行业的人员来说,FLV也是一个我们应该掌握的基本的封装格式了。
在介绍FLV文件结构之前,我们需要先理解两个概念,大端模式和小端模式。
大端模式: 数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端模式: 数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
假如我们现在有一个无符号整型数: 0x12345678; 然后我们把它放在一块起初地址是0x10000000内存上。如果是大端模式的话,它应该怎么存储呢。
0x10000000 0x12
0x10000001 0x34
0x10000002 0x56
0x10000003 0x78
如果是小端的话:
0x10000000 0x78
0x10000001 0x56
0x10000002 0x34
0x10000003 0x12
然后在现实生活的实践中。一般计算机的主机字节序都是默认小端的,然后网络上传输的字节序都是大端的。如果有网络编程经验都了解我们在编程过程中经常要做一个字节序转换的工作。就是出于主机序与网络字节序不一致。
好了,现在我们回归正题,继续讲我们的FLV封装格式吧。在FLV的官方文档 Adobe Flash Video File Format Specification 中这样描述了FLV的字节序问题。
FLV files store multibyte integers in big-endian byte order.
说明FLV 是一大端模式存储的。其实也好理解。Adobe 设计这个封装格式的目的是承载在RTMP之上作网络传输使用。所以直接让它以大端模式存储,数据封装好复合网络字节序可直接发送,接收端的解析的时候也不用再做字节序的转换工作。
那弄懂了这些问题之后我们就可以开始来讲FLV文件结构了。FLV文件结构,总体上来分可以分为两部分,FLV Header 和FLV file body.
Signature: 占三个字节,分别为字符 ‘F’, ‘L’, ‘V’。表明自己的身份。就是FLV文件。如果我们希望探测一个文件是不是FLV文件,检测头三个字节的内容就可以。
Version: 表明当前文件使用的FLV格式的版本,当前版本为Version 1, 所以这个字段就一直为固定值 0x01.
TypeFlags: TypeFlags字段的第0位和第2位用于指示文件包含视频内容和音频内容。
DataOffset: 记录FLV Header结构的大小,包含了DataOffset字段本身。我们计算一下就知道在Version 1当中这个字段的值为 9。
关于FLV file body。官方文档原话如下:
After the FLV header, the remainder of an FLV file consists of alternating back-pointers and tags.
在FLV Header的后面。FLV文件的剩余部分有交替出现的”反向指针”和“标记”组成。
结构如下:
PreviousTagSize: 这个就是上文提到的反向指针-“back-pointer”。指示之前一个TAG的大小。
FLVTAG: 这个就是FLV文件中最重要的部分了,真正记录了实际音视频信息以及内容的字段。
Fliter: 标志TAG数据在做实际处理时是不是需要一些额外的前处理工作,如解密。
TagType: 指示本TAG包含的数据的类型,只支持三种类型:8代表音频数据,9代表视频数据,18代表脚本数据。
DataSize: 指示FLVTAG 总Data 字段的数据长度。我们可以看到,除去Data字段的长度的不固定的,FLVTAG的其他字段是可以确定的。所以在FLV file body的结构中说到。 PreviousTagSize 字段的值等于 11 + DataSize的值。FLVTAG剩余字段的长度正好的11个字节。
Timestamp and TimestampExtended: 时间戳信息。以毫秒为单位,从注释中可以知道,这个字段的时间戳是一个相对值。相对于第一个TAG。同时也意味着第一个TAG的时间戳将总是为0。当我们在取这两字段的值的时候需要注意字节序。在拼装这两个字段时。Timestamp是低24位,大端模式。TimestampExtended是高8位。
Uint32_t timestamp = convertNetToHost(Timestamp | (TimestampExtended <<24));
StreamID: 总是为0.
AudioTagHeader: 如果是音频,这里放置音频的Tag 头。
VideoTagHeader: 如果是一个视频TAG,放置视频的Tag 头。
EncryptionHeader: 如果Filter==1,TAG内容被加密,则这个位置会放置一个EncryptionTagHeader。
FilterParams: 如果Filter==1,这里放置相关的参数信息。FilterParams。
Data: 根据TagType的不同放置不同格式的数据。
SoundFormat: 音频数据的编码格式。比如我们常用的AAC的话就是10。 0b1010。对于不同的编码格式选用设么值我们可以参考上图。
SoundRate: 音频数据采样率的索引。对于AAC来说,该自动总是3.
SoundSize: 每个采样点数据的大小, 这个字段只左右于未压缩格式。什么叫未压缩格式呢。PCM 就是未压缩格式。而AAC这类就是已经压缩过的格式了。这个字段就是1。
SoundType: 指示是单声道还是立体声。对于AAC的话总是1:Stereo
SoundData: 实际的音频数据。如果是AAC格式即 SoundFormat==10。 这个SoundData又必须遵从AACAUDIODATA的格式。
注:在AAC格式的时候,SoundRate固定指定为3 –44-kHz,SoundSize 固定指定为 1– 16Bits。然后SoundType 为1 – Stereo。这里并不是说AAC只能是44-kHz 采样。 采样深度16bits。 立体声格式。而是FLV 不关心这里的这三个字段的值。AAC的以上三个信息不应该从AUDIODATA的字段里面获得,而是应该从AAC编码数据中获得。
接下来我们就看一下当FLV文件存储AAC音频是SoundData字段的格式吧:
AACPacketType: 当我们是第一个AAC audio tag的时候,这个位会是 0。余下的TAG 这个位为1。我们通过上面的图可以知道,AACPacketType字段的值直接影响的Data字段的格式。
Data: 第一个AAC Audio TAG,即AACPacketType == 0。这个时候,Data字段就是一个AudioSpecificConfig结构体。当AACPacketType ==1时。Data字段就是AAC裸流。所谓裸流就是没有ADTS头的。AudioSpecificConfig 结构的具体信息呢我有需要到 ISO 14496-3 去找。
从AudioSpecificConfig结构的构成语法我们可以知道,根据audioObjectType的不同。又是不同的结构。可能是GASpecificConfig,CelpSpecificConfig,HvxcSpecificConfig等。那么我们看看AAC到底是什么audioObjectType
可以知道AAC 的ObjectTypeID可能是以下值:
1,2,3,4,6,17,19,20,23
我们看到无论是哪个TypeID对应的都是GASpecificConfig。
所以AAC的AudioSpecificConfig的完整版是
audioObjectType : 5 bits
samplingFrequencyIndex: 4 bits
channelConfiguration: 4 bits
GASpecificConfig
然后我们在 ISO/IEC 14496-3 subpart 4 中查看GASpecificConfig的语法定义:
我们以AAC-LC做为例子。我们知道AAC-LC的AudioObjectType的值应该是2.
我们最后得到的GASpecificConfig应该是
frameLengthFlag: 1 bit
dependsOnCoreCoder :1 bit
extensionFlag: 1 bit
所以综合起来看的话AAC-LC的AudioSpecificConfig如下:
AudioSpecificConfig.audioObjectType :2 (AAC LC) (5 bits)
AudioSpecificConfig.samplingFrequencyIndex :(4 bits)
AudioSpecificConfig.channelConfiguration :(4 bits)
AudioSpecificConfig.GASpecificConfig.frameLengthFlag :(1 bit)
AudioSpecificConfig.GASpecificConfig.dependsOnCoreCoder : (1 bit)
AudioSpecificConfig.GASpecificConfig.extensionFlag : (1 bit)
加起来一共两个字节。
好了,Audio Tag这部分已经清晰了。下面我们来看看Video Tag
FrameType: 指示当前TAG的帧类型
CodecID: 指定当前TAG的编解码格式,
我们最常用的编解码格式就是H.264(AVC)了。所以这里CodecID 恒为7。
那现在我们就来看看 if CodecID == 7 时的AVCVIDEOPACKET的结构是什么样的吧。
可以看出AVCVIDEOPACKET回在实际的Data前面再增加两个字段:
AVCPacketType: 如果值为0的话,Data部分将是这个FLV文件中AVC视频编码数据序列的描述信息。这些描述信息又将要遵循AVCDecoderConfigurationRecord的语法定义。值为1的话,就是普通的AVC视频帧数据了。如果为2的话,表示结束。此时Data字段为空。
CompositionTime: 如果Data部分时视频帧的话。这个字段记录偏移时间。
那下面我们就看看AVCDecoderConfigurationRecord的语法吧。
configurtaionVersion: 第一字段,占8位。其值始终为1。
AVCProfileIndication: 这个字段也占8位,填写AVC的profile. 我们一般接触比较多的profile. 有Baseline Profile(66), Main Profile(77), Extended Profile(88), High Profile(100)。这个值就是直接去AVC Sps的 profile_idc字段的值。
profile_compatibility: 这个字段就是Sps 中哪些Constraints_set 的值。所以直接去Sps第二个字节。
AVCLevelIndication: 这个字段就是Sps中的level_idc字段的值。
所以可以看出,就是直接Copy了Sps的前三个字节。
lengthSizeMinusOne:指示AVC视频中NALUnitLength字段的字节长度。 比如NALUintlength字段我们用4字节。那么这里就是4-1 = 3; 一般我们NALUingLenth字段就固定为4了,所以这个字段就是3: ‘11’b。
numOfSequenceParameterSets: 指示在这个AVCDecoderConfigurtaionRecord中包含的Sps的个数。一般就一个咯,所以取值为’00001’b。
sequenceParameterSetLength: 单个Sps的长度。
spsquenceParameterSetNALUint:Sps的实际内容。
numOfPictureParameterSets: pps的个数。一般就是1.
pictureParameterSetLength: pps的长度。注意字节序。大端
pictureParamterSetNALUint: pps的实际内容。
如果此时AVCPacketType==1.且在第一个TAG中 lengthSizeMinusOne被设置为3(4-1)。那么此时的Data字段将在实际的NALUint数据前面加上一个4字节的NALUintLength字段。注意为大端模式。然后才是实际的NALUint的数据。
可以知道SCRIPTDATA有两个字段组成,
Name, Value:都是SCRIPTDATAVALUE类型的数据。
Type: 一个字节,表明数据类型。
ScriptDataValue: 实际的脚本数据。根据Type字段的不同,类型不同。
根据Type类型,会出现的类型如下:
Property类型里面两个字段,
PropertyName是 SCRIPTDATASTRING类型的。
PropertyData 是 SCRIPTDATAVALUE类型的
走到这里PropertyData就形成了嵌套了。所以分析起来会复杂一些了。
这里有出现了一种新类型,SCRIPTDATAOBJECTEND
这个就是一个有限长度的SCRIPTDATAVALUE数组。头四个字节记录数组长度。
我们可以看到SCRIPTDATASTRING的StringLength占两个字节,也就是所它能表达的最长为65535个字节,如果超出这个字节的话,那么就把StringLength扩展到4字节。这样就扩展出了一种新类型了。SCRIPTDATALONGSTRING
然后在一般的FLV文件中,我们都可以看到有一个以onMetaData为Marker的段。这样的一个脚本tag用于记录FLV文件所包含的流的一个属性。
如果我们解析一个FLV文件,我们通过这个onMetaData的脚本数据就能获得关于这个文件中的音视频流的基本信息。例如,编解码格式,时长,文件大小,视频帧率,视频宽高等等。通过前面的讲解其实我们知道这些信息我们也可以分别从音视频的TAG中获得。而视频宽高的信息也可以从Sps中计算出来。Sps中保存了横竖方向的宏块个数。宏块个数 * 16就是像素分辨率了。