AVI文件在opencore框架下的解析
参考相关文档及opencore中pv_avifile_parse等实现,分析opencore下AVI文件解析的实现过程。
1. AVI容器介绍
AVI是微软1992年推出用于对抗苹果Quicktime的技术,尽管国际学术界公认AVI已经属于被淘汰的技术,但是由于windows的通用性,和简单易懂的开发API,还在被广泛使用。
AVI的文件结构分为头部、主体和索引三部分。主体中图像数据和声音数据是交互存放的。从尾部的索引可以索引跳到自己想放的位置。AVI本身只是提供了这么一个框架,内部的图像数据和声音顺据格式可以是任意的编码形式。因为索引放在了文件尾部,所以在播internet流媒体时已属力不从心。很简单的例子,从网络上下载的片子,如果没有下载完成,是很难正常播放出来。
另外一个问题是AVI对高码率VBR音频文件支持不好。VBR全称是Variable BitRate,就是动态比特率,可以根据当前的需要定义不同的比特率,避免了浪费,并且提高了利用率。随之问题也就来了,因为容器里的图像和声音是分开的,所以播放时需要一个图像和声音的同步过程,如果CBR音轨的话因为码率是定值,同步不成为问题,可是VBR音轨是不断的在变换,而AVI没有时间戳去让VBR音轨和图像同步,这样就会产生图像声音不同步的问题。
兼容的视频编码:MPEG-2、MPEG-4、MPEG-4 H.264、VC-1(支持不太好)、VC-1;兼容的音频编码:Linear PCM、DTS-HD DTS、Dolby Digital、AC3、Dolby Digital Plus、Dolby TrueHD、DTS Digital Surround。
2 . AVI文件组织方式
AVI(Audio Video Interleaved的缩写)是一种RIFF(Resource Interchange File Format的缩写)文件格式,多用于音视频捕捉、编辑、回放等应用程序中。通常情况下,一个AVI文件可以包含多个不同类型的媒体流(典型的情况下有一个音频流和一个视频流,现在有非标准插件可加入最多两个音频轨道),不过含有单一音频流或单一视频流的AVI文件也是合法的。AVI可以算是Windows操作系统上最基本的、也是最常用的一种媒体文件格式。
RIFF格式是一种树状的结构,其基本组成单元为LIST和CHUNK,分别如树的节点和叶子。RIFF格式也类似windows文件系统的组织形式,windows文件系统有目录和文件,分别对应RIFF中的LIST和CHUNK。Windows文件系统中的目录可以包含子目录和文件,而文件是保存数据的基本单元,RIFF也使用了这样的结构。在RIFF文件中,数据保存的基本单元是CHUNK,可用于保存音视频数据或者一些参数信息,LIST相当于文件系统的目录,可以包含多个CHUNK或者多个LIST。
AVI文件的两种原子类型:
(1)LIST
typedef struct{
DWORD dwList;
DWORD dwSize;
DWORD dwFourCC;
BYTE data[dwSize -4]; //contain lists and chunks
}LIST;
(2)CHUNK
typedef struct{
DWORD dwFourCC;
DWORD dwSize;
BYTE data[dwSize ]; //contain headers or video/audio data
}CHUNK;
用LIST类型的只有 ‘RIFF’或’LIST’,类似树枝,而CHUNK类似叶子。下图中ID为RIFF、hdrl、strl、odml、INFO、movi均为LIST类型,hdrl LST块定义AVI文件的数据格式,movi LIST包含音视频序列数据,idx1 CHUNK存放索引数据,该块可选。
AVI Tree
文件结构图
2.1 RIFF LIST结构
dwList为’RIFF’,dwFourCC为’AVI’,当前不支持AVIX(open DML AVI),这是一个LIST,后续的filedata部分包含信息块、数据块和索引块,可以是LIST也可以是CHUNK,需要递归解析(注:Pv_avifile_parse.cpp中ParseFile函数采用循环解析所有的第一层LIST,但没有解析LIST中具体内容)。在这个解析过程中需要先解析出第一个chunktype为RIFF和第二个chunktype必须为AVI(与MPEG4Extractor.cpp类似,用压栈保存chunktype),后续的fileData采用递归解析。
2.2 HDRL LIST信息块
dwList为’LIST’,dwFourCC为’hdrl’,data部分包含了avih CHUNK和其他strl LIST,需要递归解析。Size部分是指从dwFourCC开始的字段大小。
2.2.1 AVIH CHUNK
AVIH CHUNK中data字段结构如下:
AVIH Data
Opencore 的PV_avifile_header.cpp代码中解析了上述所有字段iMicroSecPerFrame、iMaxBytesPerSec、iPadding、iFlags、iTotalFrames、iInitialFrames等,这些字段保存在PVAviFileMainHeaderStruct结构体中,其中iFlags字段用对应位表示5个标志:AVIF_COPYRIGHTED、AVIF_HASINDEX、AVIF_ISINTERLEAVED、AVIF_MUSTUSEINDEX、AVIF_WASCAPTUREFILE。
2.2.2 STRL LIST
这个LIST部分包括以下几个CHUNK:STRH、STRF、STRD、STRN、JUNK。iStreamListSize保存STRL LIST的data部分长度。
2.2.2.1 STRH CHUNK
STRH CHUNK中data字段结构如下:
注意:wPriority和wLanguage并不是四字节存储,解析时注意偏移位置,dWFlags有两种取值:AVISF_VIDEO_PALCHANGES、AVISF_DISABLED。
rcFrame包括上left、top、right、bottom四个值,分别用short int(占两个字节)保存。
帧率的计算:SamplingRate(samples/second)= dwRate/dwScale。
2.2.2.2 STRF CHUNK
Strf结构与strh的fccType定义的类型有关。AVIStream::fccType的可能取值有:PV_2_AUDIO、PV_2_VIDEO、MIDI和TEXT。
(1)AVIStream::fccType为视频PV_2_VIDEO时,STRF CHUNK的结构为BITMAPINFO:
typedef struct
{
BitmapInfoHhr BmiHeader; //header
uint32 BmiColorsCount; //number of RGBQuads actually present
RGBQuad BmiColors[MAX_COLOR_TABLE_SIZE];
} BitMapInfoStruct;
RGBQuad结构如下:
typedef struct
{
uint8 Blue;
uint8 Green;
uint8 Red;
uint8 Reserved;
} RGBQuad;
BitmapInfoHhr结构如下:
biClrUsed标识了BmiColorsCount,当这个字段值为0时,需要由biBiBitCount字段(每帧的比特数)决定color table的大小(BmiColorsCount):
biBiBitCount =1时,color table size = 2^1;
biBiBitCount = 2时,color table size = 2^2;
biBiBitCount = 4时,color table size = 2^4;
biBiBitCount = 8时,color table size = 2^8;
biBiBitCount = 16/32时,color table size由biCompression的值决定:
BiCompression= BI_BITFIELDS,color table size = 3;
BiCompression= BI_ALPHABITFIELDS,color table size = 4;
BiCompression= BI_RGB,color table size = 0;
biBiBitCount = 24时,color table size = 0;
BmiColors数组用来存放color table entry,详见ParseStreamFormat函数中的解析过程。
(2) AVIStream::fccType为音频PV_2_AUDIO时,STRF CHUNK结构为WAVEFORMAT:
2.2.2.3 STRD CHUNK(可选)
strd chunk为可选部分,紧跟在strf chunk后,保存编解码器需要的一些配置信息,文件规范中没有具体说明,参考opencore代码,包含size和data部分。
DWORD iCodecSpecificHdrDataSize; //大小
BYTE ipCodecSpecificHdrData[DataSize]; //数据
2.2.2.4 STRN CHUNK(可选)
DWORD strnSz; //大小
CHAR iStreamName [MAX_STRN_SZ]; //数据
保存流的名字,当strnSz<MAX_STRN_SZ时,iStreamName保存strnSz个BYTE;当strnSz>MAX_STRN_SZ,保存MAX_STRN_SZ 个BYTE。PVAviFileStreamlist函数中临时分配的空间strn似乎多此一举。
2.2.2.5 JUNK CHUNK
为了字节对齐填充的字段,不解析。
2.2.3 JUNK LIST
为了字节对齐填充的字段,可不解析。
2.3 MOVI LIST数据块
dwList为’LIST’,dwFourCC为’movi’。Size部分是指从dwFourCC开始的字段大小。movi列表保存的是真正的媒体流数据,其数据组织方式有两种。可以将数据块直接嵌在‘movi’列表里面,也可以将几个数据块分组成一个‘rec ’列表后再编排进‘movi’列表(读取数据时最好将整个rec读出)。每个数据块使用了一个四字符码来表征它的类型,这个四字符码由2个字节的类型码和2个字节的流编号组成。标准的类型码定义如下:‘db’(非压缩视频帧)、‘dc’(压缩视频帧)、‘pc’(改用新的调色板)、‘wb’(音缩视频)。比如第一个流(Stream 0)是音频,则表征音频数据块的四字符码为‘00wb’;第二个流(Stream 1)是视频,则表征视频数据块的四字符码为‘00db’或‘00dc’。对于视频数据来说,在AVI数据序列中间还可以定义一个新的调色板,每个改变的调色板数据块用‘xxpc’来表征,新的调色板使用一个数据结构AVIPALCHANGE来定义。(注意:如果一个流的调色板中途可能改变,应在这个流格式的描述中,也就是PVAviFileStreamHeaderStruct结构的dwFlags中包含一个AVISF_VIDEO_PALCHANGES标记)。另外,文字流数据块可以使用随意的类型码表征。
MOVI LIST的data部分每一个子chunk存放的是sample信息,sample所属的stream no可由indx的字节/16计算而来。
2.4 IDX1 LIST索引块(非必需)
dwList为’LIST’,dwFourCC为’idx1’。Size部分是指从dwFourCC开始的字段大小。结构如下(如下为AVI1.0的结构,AVI2.0不同):
索引块包含数据块在文件中的位置索引,能提高avi文件的读写速度,其中存放着一组AVIINDEXENTRY结构数据,这个块并不是必需的,也许不存在。dwChunkId包含了两字节的stream number(该sample所属的stream号)和两个字节的封装类型码,因此这个字段解析上有一个实现细节,dwChunkId值类似‘00dc’:十六进制30 30 64 63(转成十进制ASCII码为48 48 100 99对应字符“00dc”),chunkId前两个字节包含了streamno,并且用ASCII码表示,如存放的是’0’和’0’字符,最后转成十进制为0*10+0=0(PV_atoi),“01wb”(30 31 77 62)前两个字节转成十进制就是1。
iIndexTable是streams数组,每一个成员又是IndxTblVector类型的,即iIndexTable[aStreamNo]是一个IndxTblVector类型,IndxTblVector的每个成员是IdxTblType类型。每个iIndexTable[aStreamNo]又是一个数组,每个成员是一个sample。相当于二维数组,第一维大小sizeof(iIndexTable)= stream numbers,sizeof(IndxTblVector)=一个stream中的sample数,iIndexTable中的最小信息单元是一个sample。
2.5 INFO LIST
dwList为’LIST’,dwFourCC为’info’,Size部分是指从dwFourCC开始的字段大小。Opencore代码中没有解析这一部分。
2.6 JUNK LIST
为了字节对齐填充的字段,可不解析。
2.7 关键计算过程
2.7.1 计算时间戳
参考GetNextMediaSample中的计算部分
音频根据帧率计算:该帧大小arSize/每帧所占的字节数=samplecount
iTimeStampAudio = iTimeStampAudio+(sampleCount * 1000) / samplingRate
视频根据帧间隔计算:frameDurationInms =(iMainHeader.iMicroSecPerFrame) / 1000
arTimeStamp = (iStreamSampleCount[aStreamNo] * (frameDurationInms))
2.7.2 查找sample
(1) 确定时间,相对于媒体时间坐标系统;
(2) 查找对应该时间戳的sample序号(需要在解析的时候保存到iIndexTable表,如果不存在该索引表,需要在解析movi chunk时记录下来);
(3) 根据sample号查找该sample在文件中位移和大小(索引表iIndexTable或movi chunk);
2.7.3 查找关键帧
必须要用Idx1中的标志位??
3 Opencore代码分析
分析代码:Pv_avixxx.cpp、Pvmi_mio_avi_wav_file.cpp
3.1 数据结构存储方式
数据结构图
PVAviParser包括PVAviFileHeader和PVAviFileIdxChunk,包括HDRL LIST、IDX1 CHUNK。
PVAviFileHeader类包含PVAviFileStreamlist类型的vector—>iStreamList列表,一个strl list对应一个PVAviFileStreamlist对象,hdrl list中可以包含多个strl list(流)。
PVAviFileStreamlist(HDRL LIST的data部分)包含PVAviFileStreamHeaderStruct(avih chunk)和PVAviFileStreamFormatStruct,PVAviFileStreamFormatStruct存储的是strl list解析出来的data数据,其中的strf chunk结构取决于strh的fccType类型,对应有BitMapInfoStrunct和WaveFromatExStruct两种,因此在PVAviFileStreamFormatStruct中iVidBitMapInfo和iAudWaveFormatEx是以共用体定义的。
为了查找到sample等信息,这里有几个重要的字段:
Oscl_Vector < uint32,OsclMemAllocator > iStreamCount; 文件中的stream数
Oscl_Vector < uint32,OsclMemAllocator > iStreamSampleCount; 该数值从0开始,表示当前该stream中已读到的sample数,该字段随着读取文件的过程更新,因此该stream下次要读取的sample号 = 已经读取的sample数。
//stores current offset if index table is not present
uint32 iSampleOffset; 当前要读取的最新sample位移
//store latest sample offset if index table is not present
Oscl_Vector < uint32,OsclMemAllocator > iStreamSampleOffset; 链表,存放每个stream下次要读取的sample位移(位移相对文件开始,如果stream0的sample1已经读取,那么存放的是sample2的位移)
3.2 过程分析
解析过程图
3.2.1 文件解析入口
Pv_avifile_parse.cpp中ParseFile()函数采用循环解析所有的第一层LIST,同时递归解析子LIST或CHUNK。在这个过程中保存了以下信息:iFileSize (文件大小:从AVI开始的部分)、iHeaderChunkSize(hdrl list data部分的长度)、ipFileHeader(hdrl list的数据部分指针)、iMovieChunkSize(movi list data部分的长度)、iMovieChunkStartOffset(movi chunk在文件中的偏移,第一帧偏移,当indx table不存在时使用)、iSampleOffset(= iMovieChunkStartOffset,保存当前位移,当indx table不存在时使用)、iStreamCount、iStreamSampleCount、iIndxChunkSize、ipIdxChunk(Idx的数据部分指针) 、iIdxChunkPresent,递归解析PVAviFileHeader :OSCL_NEW(PVAviFileHeader..)、PVAviFileIdxChunk :OSCL_NEW(PVAviFileIdxChunk..)。
3.2.2 解析HDRL LIST
PVAviFileHeader解析HDRL LIST的数据部分,从AVIH Chunk开始,过程与ParseFile类似,调用ParseMainHeader解析AVIH Chunk,调用OSCL_NEW(PVAviFileStreamlist,..)递归解析STRL,JUNK部分可不解析。
ParseMainHeader()解析AVIH Chunk,具体解析字段内容见2.2.1。
PVAviFileStreamlist()解析STRL LIST,过程与PVAviFileHeader和ParseFile类似。ParseStreamHeader解析STRH Data部分,具体字段参考2.2.2.1。ParseStreamFormat解析STRF部分,strf结构与strh fccType定义的类型相关,参考2.2.2.2。
3.2.3 解析IDX1 CHUNK
PVAviFileIdxChunk解析IDX1 CHUNK,类似二维数组,第一维是stream,第二维是stream中的sample信息。
3.3 内部接口、对外接口及关键函数实现
入口:Pv_avifile.cpp
模块内部接口:
3.3.1 Pv_avifile_streamlist.cpp中的接口
3.3.1.1获取指定流的音频格式 GetAudioFormat(uint32 aStreamNo) -> GetAudioFormat()
HDRL LIST解析后保存了streamlist(PVAviFileStreamlist类型),通过strf chunk读取WaveFormatExStruct中的FormatTag字段。
3.3.1.2 获取指定流的声道数GetNumAudioChannels(uint32 aStreamNo)-> GetNumAudioChannels()。
HDRL LIST解析后保存了streamlist(PVAviFileStreamlist类型),通过strf chunk读取WaveFormatExStruct中的Channels字段。
3.3.2 Pv_avifile_streamlist.cpp中的接口,提供PVAviFileStreamlist中的相关信息
3.3.2.1获取当前流的类型GetStreamMimeType(),由iStreamTypeFCC决定,有四种类型:AUDS、VIDS、MIDS、TXTS。
3.3.2.2 GetHandlerType(uint8* aHdlr, uint32& aSize)获取strh中的fccHandler。
3.3.2.3 GetBitsPerSample()获取sample的bit数。
3.3.2.4 GetAudioFormat()获取音频格式,被PVAviFileHeader::tAudioFormat(uint32 aStreamNo)调用。
3.3.2.5 GetNumAudioChannels()获取声道数,被PVAviFileHeader::tNumAudioChannels(uint32 aStreamNo)调用。
3.3.2.6 GetVideoWidth()获取视频宽度。
3.3.2.7 GetVideoHeight()获取视频高度。
3.3.2.8 GetFormatSpecificInfo获取strf chunk解析的内容。
3.3.2.9 GetCodecSpecificData获取解码信息。
3.3.3 Pv_avifile_parser_utils.cpp 提供模块内公共函数
3.3.3.1 ReadNextChunkType读取下一个chunk的类型。
3.3.3.2 read32 读取四个字节,涉及到字节序转换。
3.3.3.3 read8读取一个字节,不考虑字节序。
3.3.3.4 read16读取两个字节,考虑字节序转换。
3.3.3.5 GetStreamNumber 将前两个字节存放的ASCII码值转换成十进制
模块的对外接口:(Pv_avifile.h)
3.3.4 Pv_avifile_parser.cpp提供读帧的对外接口
Pv_avifile_parse.h和Pv_avifile.h中有相关说明
3.3.4.1 PVAviFileParser::GetNextMediaSample(uint32& arStreamNo, uint8* aBuffer,
uint32& arSize, uint32& arTimeStamp)
从movi chunk中按顺序读取帧信息,arStreamNo表示当前sample所属的stream number,aBuffer返回media sample buffer,size是返回的sample大小,arTimeStamp是sample的时间戳信息。
具体实现过程:iSampleOffset从当前位移读取sample,CurrOff表示上次读取的sample位移。
3.3.4.2 PVAviFileParser::GetNextStreamMediaSample(uint32 aStreamNo, uint8* aBuffer, uint32& arSize, uint32& arTimeStamp) 获取stream中的下一个sample的buffer、大小和时间戳信息(iStreamSampleCount[aStreamNo]表示当前stream中要读取的下一个sample号,iStreamSampleOffset[aStreamNo]表示当前stream中要读取的下一个sample的位移)
具体实现过程:不存在indx时,直接通过movi chunk的起始位移、该stream最新的sample位移(即下一次要读取的sample位移)等信息进行计算;存在indx时,从ipIdxChunk中存放的每个sample信息获取。
问题:stream中的sample是连续存放的么?各个stream的sample之间应该是交错存放的
iIndexTable表中每一stream的sample是按序存放的,在movi chunk中同一stream的sample也是按时间顺序存放的,但其中夹杂着其他stream的sample信息,总的而言,按时间戳顺序存放。
3.3.4.3 PVAviFileParser::GetNextStreamSampleInfo(uint32 aStreamNo, uint32& arSize, uint32& arOffset)获取stream中的下一次要读取的sample位移和大小(以iStreamSampleOffset[aStreamNo]决定的位移为基准进行查找)
具体实现过程:不存在indx chunk,直接返回失败;存在indx chunk时,从ipIdxChunk中获取位移和大小,位移有两种情况:一是相对于文件开始,另一种是相对movi开始处的位移。ipIdxChunk->GetOffset(aStreamNo, iStreamSampleCount[aStreamNo])
4 参考资料
【1】http://blog.csdn.net/njuitjf/archive/2010/06/19/5680632.aspx
【2】http://www.jmcgowan.com/avi.html
【3】http://msdn.microsoft.com/en-us/library/ms779636.aspx
【4】http://msdn.microsoft.com/en-us/library/dd756808(v=VS.85).aspx
【5】http://pvdtools.sourceforge.net/aviformat.txt
【6】http://blog.csdn.net/ydfy6/archive/2009/09/18/4567230.aspx
【7】http://www.featheast.com/it/video-on-the-web-html5
【8】http://www.codesoso.com/default.aspx
【9】http://soft.cnzer.cn/view-17836-3.html
【10】http://doxygen.reactos.org/d1/dc8/avifile_8c_source.html#l00203
其他PDF文档在smb://192.168.9.200/ds/lei.yi/avi下
AVI spec here
http://www.wotsit.org/list.asp?page=2&fc=0&search=&al=
AVI player implementations
http://sourceforge.net/project/showf...group_id=91593
http://avifile.sourceforge.net/
http://freshmeat.net/
http://www.topshareware.com/avi-parser/downloads/1.htm
http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/4a284338-4809-4cdd-8cbd-50b4f440fdde/
http://doxygen.reactos.org/d1/dc8/avifile_8c_source.html
ffmpeg
5 工具
RIFFSpot.exe 该工具不能具体解析movi list中的内容,只能粗略解析文件结构
Linux下的ghex2显示文件的十六进制信息