日常生活中,看到的视频文件的后缀名如 .mp4、.avi、.rmvb 都是属于视频文件的封装格式。所谓封装格式,就是以怎样的方式将视频轨、音频轨、字幕轨等信息组合在一起。说得通俗点,视频轨相当于饭,而音频轨相当于菜,封装格式就是一个碗或者一个锅,是用来盛放饭菜的容器。
视频文件的封装格式并不影响视频的画质,影响视频画面质量的是视频的编码格式。
下面介绍常见的视频封装格式 -AVI。
容器 AVI(Audio Video Interleaved)即音视频交错格式是一门成熟的老技术,尽管国际学术界公认 AVI 已经属于被淘汰的技术,但是简单易懂的开发 API,还在被广泛使用。
AVI 符合 RIFF(Resource Interchange File Format)文件规范,使用四字符码 FOURCC(four-character code)来表征数据类型。AVI 的文件结构分为头部、主体和索引三部分。 主体中图像数据和声音数据是交互存放的,从尾部的索引可以索引跳到自己想放的位置。
AVI 本身只是提供了这么一个框架,内部的图像数据和声音数据格式可以是任意的编码形式。因为索引放在了文件尾部,所以在播网络流媒体时已属力不从心。一个很简单的例子,从网络上下载 AVI 文件,如果没有下载完成,是很难正常播放出来。
AVI 中有两种最基本的数据单元,一个是 chunk,一个是 list。这两种结构如下:
Chunks
typedef struct {
DWORD dwFourCC
DWORD dwSize //data
BYTE data[dwSize] // contains headers or video/audio data
} CHUNK;
Lists
typedef struct {
DWORD dwList
DWORD dwSize //dwFourcc + data
DWORD dwFourCC
BYTE data[dwSize-4] // contains Lists and Chunks
} LIST;
如上可知,Chunks 数据块由一个四字符码、4 字节 data size(指下面的数据大小)以及数据组成
List 由四部分组成,四个字节四字符码(“list”)、4 字节数据大小(指后面列的两部分数据大小)、四字节 list 类型以及数据组成,与 Chunk 数据块不同的是,List 数据内容可以包含字块(Chunk 或 List)。
AVI 文件采用 RIFF 文件结构方式,使用四字符码 FOURCC(four-character code)来表征数据类型,比如 ‘RIFF’、‘AVI’、‘LIST’ 等,通常我们称四字符码为数据块 ID。因此首先我们需要了解一个标准 RIFF 文件结构。
有了 RIFF 文件结构的了解,下面这张 AVI 文件结构图就比较好理解了。需要说明的是,一个 AVI 通常都包含以下几个字块:
图:AVI 文件结构
AVI 文件直接用文本编辑如(UltraEdit)打开分析其结构即可,下面以一个 AVI 文件为例,逐字节往下分析。
用 UltraEdit 打开一个 AVI 文件
可以看到前四个字节为 “RIFF”,在接着四个字节为 RIFF 文件大小(0x01526E34 即 22176380 字节),22176380 字节只包含 "AVI " RIFF 文件类型四字符码及 RIFF 块数据长度,因此 22176380 + 8 是整个 RIFF 文件的大小。再接着 4 字节为 RIFF 文件类型 "avi "。
1)hdrl list 头部
AVI 文件中必需的第一个 list 就是 hdrl list,用于描述 AVI 文件中各个流的格式信息(AVI 文件中的每一路媒体数据都称为一个流)。hdrl list 中嵌套了一系列块和子列表,首先是一个 “avih” 块,用于记录 AVI 文件的全局信息,比如流的数量、视频图像的宽和高等。
可以看到首先是 4 字节的 “list”,然后是 4 字节的 list size,接着是 4 字节 list 类型 “hdrl”,接着是 list 数据内容。
2)avih 块
avih 即 avi_header,该数据块是主信息头,这意外着该 header 数据块之后就是文件多媒体流信息。
4 字节的 “avih” 标识码,4 字节大小(0x38 即 56),接下来是 56 个字节数据。该块可以用如下结构体表示:
typedef struct {
FourCC fcc; // "avih" 特征码
DWORD cb; // 数据大小
DWORD dwMicroSecPerFrame; // 视频帧间隔时间(以毫秒为单位)
DWORD dwMaxBytesPerSec; // AVI 文件的最大数据率
DWORD dwPaddingGranularity; // 数据填充的粒度
DWORD dwFlags; // AVI 文件的全局标记,比如是否含有索引块等
DWORD dwTotalFrames; // 总帧数
DWORD dwInitialFrames; // 为交互格式指定初始帧数(非交互格式应该指定为 0)
DWORD dwStreams; // 本文件包含的流的个数
DWORD dwSuggestedBufferSize; // 建议读取本文件的缓存大小(应能容纳最大的块)
DWORD dwWidth; // 视频图像的宽(以像素为单位)
DWORD dwHeight; // 视频图像的高(以像素为单位)
DWORD dwReserved[4]; // 保留
} AVIMainHeader;
3)strl list 头部
一个 strl list 中至少包含一个 strh 块和一个 strf 块。文件中有多少个流,就对应有多少个 strl list。
上图可知,依次为 4 字节 “list”,4 字节数据大小,4 字节 “strl” 四字符码标识符。
4)strh 块
用于描述流的头信息
4 字节 “strh”,4 字节 “strh” 块大小(0x38 即 56),后面是 56 字节大小数据。该块用如下结构体表示,由上面 strh 块可知,该 AVI 第一个流是视频流(vids)。
// AVI流头部
typedef struct
{
FourCC fcc; // "strh"
DWORD cb; // 数据大小
FourCC fccType; // 流类型: auds(音频流) vids(视频流) mids(MIDI流) txts(文字流)
FourCC fccHandler; // 指定流的处理者,对于音视频来说就是解码器
DWORD dwFlags; // 标记:是否允许这个流输出?调色板是否变化?
WORD wPriority; // 流的优先级(当有多个相同类型的流时优先级最高的为默认流)
WORD wLanguage; // 语言
DWORD dwInitialFrames; // 为交互格式指定初始帧数
DWORD dwScale; // 每帧视频大小或者音频采样大小
DWORD dwRate; // dwScale/dwRate,每秒采样率
DWORD dwStart; // 流的开始时间
DWORD dwLength; // 流的长度(单位与 dwScale 和 dwRate 的定义有关)
DWORD dwSuggestedBufferSize;// 读取这个流数据建议使用的缓存大小
DWORD dwQuality; // 流数据的质量指标(0 ~ 10,000)
DWORD dwSampleSize; // Sample的大小
RECT rcFrame; // 指定这个流(视频流或文字流)在视频主窗口中的显示位置
} AVIStreamHeader;
5)strf 块
该块用于描述流的具体信息。如果是视频流(vids,由 strh 块得知),用一个 BitmapInfo 结构体表示,如果是音频流(auds),用 WaveFormatEx 结构体表示。
// 位图头
typedef struct
{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BitmapInfoHeader;
// 位图信息
typedef struct
{
BitmapInfoHeader bmiHeader; // 位图头
RGBQUAD bmiColors[1]; // 调色板
} BitmapInfo;
// 音频波形信息
typedef struct
{
WORD wFormatTag;
WORD nChannels; // 声道数
DWORD nSamplesPerSec; // 采样率
DWORD nAvgBytesPerSec; // 每秒的数据量
WORD nBlockAlign; // 数据块对齐标志
WORD wBitsPerSample; // 每次采样的数据量
WORD cbSize; // 大小
} WaveFormatEx;
该块首先 4 字节 “strf” 标识,4 字节数据大小(0x28 即 40个字节),接着的数据用一个 40 字节大小的 BitmapInfo 结构体表示。
6)strd 块与strh 块
这两个块是可选的,一个 strl list 可以不包含,不加分析。
info list 用于描述编码该 AVI 文件的程序信息,包含一个 isft 块
movi list 用于保存真正的媒体流数据,音视频数据块在该 list 中交错方式存放着。
当 AVI 文件中包含有多个流时,数据块与数据块之间如何来区别呢?
同样的,这些数据块使用了一个四字符码来表征它的类型,这个四字符码由 2 个字节的类型码和 2 个字节的流编号组成。
可用类型码有:
比如第一个流(Stream 0)是音频,则表征音频数据块的四字符码为 “00wb” ,第二个流(Stream 1)是视频,则表征视频数据块的四字符码为 “01db” 或 “01dc”。
最后,紧跟在 hdrl list 和 movi list 之后的,就是 AVI 文件的索引块(可选),这个索引块为 AVI 文件中每一个媒体数据块进行索引,并且记录它们在文件中的偏移(可能相对于 movi list,也可能相对于 AVI 文件开头)。索引块使用一个四字符码 “idx1” 来表征,索引信息使用一个数据结构来 AVIOLDINDEX 定义。
typedef struct _avioldindex {
FOURCC fcc; // "idx1"
DWORD cb; // 数据大小
struct _avioldindex_entry {
DWORD dwChunkId; // 数据块 id
DWORD dwFlags;
DWORD dwOffset; // 数据块偏移
DWORD dwSize; // 数据块大小
} aIndex[];
} AVIOLDINDEX;
在 AVIMainHeader 的 dwFlags 中指出是否包含索引块。有了索引块可以方便文件快进,如果没有索引块,在对 AVI 进行快进时需要计算位置,会很耗时。
RIFF ('AVI'
LIST('hdrl'
'avih'(主 AVI 信息头数据)
LIST('strl'
'strh' (流的头信息数据)
'strf' (流的格式信息数据)
['strd' (可选的额外的头信息数据)]
['strn' (可选的流的名字) ]
)
... // 其他流信息
)
LIST('movi'
{
// 媒体流数据
SubChunk | LIST ('rec'
SubChunk1
SubChunk2
...
}
)
['idx1' (可选的 AVI 索引块数据) ]
)