AVI 传统格式(RIFF AVI 1.0)详解
AVI 传统格式(RIFF AVI 1.0)详解
格式定位
AVI(Audio Video Interleave)是 Microsoft RIFF 家族中的音视频容器格式。它规定的是文件如何组织音频流、视频流、元数据和索引,并不规定视频必须使用某一种编码。一个 AVI 可以承载 Motion JPEG、未压缩 RGB、H.264、MPEG-4 Part 2,也可以承载 PCM、MP3 等音频。判断文件中真正的媒体格式,必须读取 strf 中的 biCompression 或 wFormatTag,不能只根据 .avi 扩展名判断。
本文只讨论传统 AVI 1.0:文件由一个 RIFF AVI 主块组成,媒体数据通常集中在 LIST('movi') 中,索引通常使用 idx1。它的长度和偏移字段主要是 32 位,因此在接近 4 GiB 时会遇到限制。OpenDML 的大文件扩展另文说明。
RIFF 的基本单位
RIFF 中最基本的单位是 Chunk:
1 | Chunk |
ckSize 只表示 data 的长度,不包括前面的 8 字节 Header,也不包括为了偶数字节对齐而补的 padding。读取器必须在读取 payload 后检查 ckSize & 1,否则下一个 FourCC 会错一个字节。RIFF 的整数采用 little-endian,例如字节 18 00 00 00 表示十进制 24,而不是 0x18000000。
LIST 是一种特殊 Chunk。它的 data 开头又有一个 4 字节 list type,后面才是子 Chunk:
1 | LIST |
因此 LIST 的 ckSize 包含 list type,但子 Chunk 解析范围必须从 list type 后面开始。
AVI 总体文件结构
1 | RIFF |
文件中可以有 JUNK、INFO、odml 等可选块,但解析器不能假设 hdrl、movi、idx1 一定紧邻。正确方法是遍历 RIFF 子块,根据 FourCC 建立索引,未知块按长度安全跳过。
RIFF Header 字段
| 偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 4 | RIFF |
文件是 RIFF 容器 |
| 4 | 4 | riffSize |
从偏移 8 开始计算的 RIFF 数据长度 |
| 8 | 4 | AVI |
RIFF form type |
| 12 | 不定 | 子 Chunk | hdrl、movi、idx1 等 |
文件总长度通常为 riffSize + 8,但损坏文件可能存在不一致。安全读取器应同时检查实际文件长度,并以不越界为最高优先级。
avih 主 AVI Header
avih 的 data 是一个 56 字节的 MainAVIHeader,字段均为 little-endian:
| 偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 4 | dwMicroSecPerFrame |
主视频轨的帧间隔,单位微秒 |
| 4 | 4 | dwMaxBytesPerSec |
最大建议数据率 |
| 8 | 4 | dwPaddingGranularity |
数据填充粒度 |
| 12 | 4 | dwFlags |
AVIF_HASINDEX 等标志 |
| 16 | 4 | dwTotalFrames |
主视频轨的帧数 |
| 20 | 4 | dwInitialFrames |
交错播放的初始帧数 |
| 24 | 4 | dwStreams |
轨道数量 |
| 28 | 4 | dwSuggestedBufferSize |
建议读取缓冲大小 |
| 32 | 4 | dwWidth |
主视频宽度 |
| 36 | 4 | dwHeight |
主视频高度 |
| 40 | 4×4 | dwReserved |
保留字段,通常为 0 |
dwMicroSecPerFrame 只是主视频时间间隔提示。更精确的轨道时间应读取对应 strh 的 dwScale 和 dwRate。dwTotalFrames 也不能无条件当作 movi 中 00dc chunk 数量,因为文件可能存在重复、损坏或多个视频轨道。
strl 轨道列表
每个媒体轨道都有一个 LIST('strl')。其中 strh 描述轨道类型、时间基和长度,strf 描述具体编码格式。视频轨道的 strh.fccType 是 vids,音频轨道是 auds。轨道编号由它们在 hdrl 中出现的顺序决定,并与 movi 中 chunk ID 的前两位对应。
strh AVIStreamHeader
| 字段 | 长度 | 作用 |
|---|---|---|
fccType |
4 | vids 或 auds |
fccHandler |
4 | 编码器 FourCC,如 MJPG |
dwFlags |
4 | 轨道标志 |
wPriority |
2 | 播放优先级 |
wLanguage |
2 | 语言标识 |
dwInitialFrames |
4 | 初始交错帧 |
dwScale |
4 | 时间单位分子 |
dwRate |
4 | 每秒时间单位数 |
dwStart |
4 | 起始时间单位 |
dwLength |
4 | 轨道长度,单位由时间基定义 |
dwSuggestedBufferSize |
4 | 建议缓冲区 |
dwQuality |
4 | 质量提示 |
dwSampleSize |
4 | 固定样本大小,0 表示可变 |
rcFrame |
16 | 显示矩形 |
轨道时间计算为 seconds = sample_time × dwScale / dwRate。视频 25 fps 可以使用 dwScale=1,dwRate=25;音频轨则可能使用 dwScale=block_align,dwRate=avg_bytes_per_sec。因此不能把所有轨道都按“一个 chunk 一帧”处理。
视频 strf:BITMAPINFOHEADER
视频 strf 通常以 BITMAPINFOHEADER 开头:
| 字段 | 长度 | 作用 |
|---|---|---|
biSize |
4 | 结构大小,常为 40 |
biWidth |
4 | 图像宽度 |
biHeight |
4 | 高度;负值可表示 top-down |
biPlanes |
2 | 必须通常为 1 |
biBitCount |
2 | 位深 |
biCompression |
4 | 编码 FourCC,如 MJPG |
biSizeImage |
4 | 图像数据大小提示 |
biXPelsPerMeter |
4 | 水平分辨率元数据 |
biYPelsPerMeter |
4 | 垂直分辨率元数据 |
biClrUsed |
4 | 使用的颜色数 |
biClrImportant |
4 | 重要颜色数 |
如果 biCompression 为 MJPG,00dc 的 payload 通常是一张完整 JPEG;如果为 H264,payload 的具体是 Annex-B 还是长度前缀仍需根据编码器约定确认。AVI 的 strf 只声明编码器,不会替代编码码流内部的 SPS/PPS 或 JPEG Header。
音频 strf:WAVEFORMATEX
| 字段 | 长度 | 作用 |
|---|---|---|
wFormatTag |
2 | 1 通常为 PCM |
nChannels |
2 | 声道数 |
nSamplesPerSec |
4 | 采样率 |
nAvgBytesPerSec |
4 | 平均字节率 |
nBlockAlign |
2 | 一个音频块字节数 |
wBitsPerSample |
2 | 位深 |
cbSize |
2 | 扩展字段长度 |
PCM 的一致性检查是 nBlockAlign = channels × bytes_per_sample,nAvgBytesPerSec = sample_rate × nBlockAlign。若 wFormatTag 不是 PCM,不能用这些公式猜测每个 sample 的含义,应根据对应编码器规范解析。
movi 中的媒体 Chunk
媒体 chunk 的 ID 通常由两位轨道号和两位类型组成:
1 | 00dc 第一轨压缩视频 |
chunk 的完整布局是 ckid + size + payload + padding。size 不含 Header。一个 00dc 可以是一帧,也可能是一个编码块,具体取决于 dwSampleSize 和编码器;读取器应使用轨道描述和索引共同判断。
idx1 索引项
传统 idx1 的每项为 16 字节:
| 偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 4 | ckid |
对应 movi chunk ID |
| 4 | 4 | dwFlags |
AVIIF_KEYFRAME 等标志 |
| 8 | 4 | dwChunkOffset |
chunk 偏移,基准存在实现差异 |
| 12 | 4 | dwChunkLength |
payload 长度 |
偏移可能相对 movi list 起点、movi data 起点或文件某个约定位置。可靠解析器要用 ckid 和 dwChunkLength 对候选位置做验证,而不是盲目套一个固定公式。
读取流程与写入流程
读取时先校验 RIFF/form type,再遍历 hdrl 建立轨道表,然后定位 movi 和 idx1。播放第 N 帧时,从索引得到 chunk 偏移和长度,读取 payload,依据该轨道的 strf 交给 JPEG、H.264 或 PCM 解码器,最后使用 strh 时间基调度。没有 idx1 时,可以顺序扫描 movi,但随机 seek 会变慢。
写入时通常先写带占位长度的 RIFF、hdrl 和 strl,随后写每个媒体 chunk 并记录其位置,结束时生成 idx1,最后回填 avih、strh、LIST size 和 RIFF size。任何一个前置 Box 长度变化,都可能导致索引偏移需要重算。
十六进制识别示例
1 | 52 49 46 46 ?? ?? ?? ?? 41 56 49 20 |
其中 52 49 46 46 是 ASCII RIFF,41 56 49 20 是 ASCII AVI 。在后续数据中搜索 4C 49 53 54 可以发现 LIST,搜索 6D 6F 76 69 可以定位 movi。但搜索字符串只能帮助定位,最终仍必须按 size 递归解析,不能把任意出现的字符当作有效 Chunk。
兼容性和错误处理
RIFF AVI 1.0 的字节级模型
RIFF 头
文件开头 12 字节依次为 RIFF、四字节小端 size 和 AVI 。RIFF size 从 form type 开始计算到当前 RIFF 结束,因此常见完整文件中它等于文件长度减 8,而不是文件长度。若文件尾还有应用私有数据,验证器应区分“RIFF 外附加数据”和“RIFF size 错误”。
1 | 00 52 49 46 46 ckID = RIFF |
Chunk 和 LIST
普通 chunk 的 ckSize 只统计 payload,奇数 payload 后的 padding 不计入长度。LIST 的 payload 先有四字节 list type,再有子 chunk。递归读取器应把父结束偏移作为硬边界,每读取一个子结构都检查 offset + 8 + aligned_size 是否仍在父范围内。
avih 主头字段
帧时长和最大速率
dwMicroSecPerFrame 给出主视频的标称帧周期。25 fps 常见值为 40000,30 fps 可能用近似值 33333;精确时间仍可由视频 strh.dwScale/dwRate 表达。dwMaxBytesPerSec 是建议吞吐量,不应当成媒体数据精确码率。修复器无法从一两个帧可靠推导最大速率时,可以保留原值或根据完整文件统计并标记为估算。
标志和总帧数
dwFlags 可声明存在索引、需要交错播放或其他行为。标志是能力/状态提示,必须与实际内容核对。dwTotalFrames 通常以主视频轨道帧数为准;音频 chunk 数、rec list 数和全部 chunk 数都不是该字段的替代值。
显示尺寸
dwWidth 和 dwHeight 是电影级建议尺寸,视频 strf 中的 BITMAPINFOHEADER 还会给出该流宽高。二者不一致时,应优先保留轨道实际编码尺寸并报告头部不一致;不能为匹配 avih 而裁掉解码后的像素。
strh 流头字段
流类型与处理器
fccType 常见为 vids 或 auds,fccHandler 表示编码处理器或 codec FourCC。视频 strf.biCompression 也会声明压缩类型,两者应兼容。未知 handler 不代表容器损坏,读取器仍可以列出 chunk 和时间信息,只是无法解码 payload。
时间基和长度
dwScale/dwRate 定义每个流单位的时长,dwStart 给出起始位置,dwLength 给出流长度。视频通常一个 sample 对应一个 frame;PCM 音频的 sample 单位可能由 dwSampleSize 和 nBlockAlign 决定。计算轨道时长时应使用 64 位有理数,避免先做整数除法丢失精度。
1 | duration_seconds = dwLength × dwScale / dwRate |
建议缓冲和 sample size
dwSuggestedBufferSize 只是一项建议,不保证所有 chunk 都不超过该值。读取器必须以实际 ckSize 分配或扩容,并设置实现自己的安全上限。固定 sample 流可以使用 dwSampleSize,可变大小视频通常为 0。
strf 格式块
BITMAPINFOHEADER
视频格式块至少包含结构长度、宽高、平面数、位深、压缩 FourCC、图像大小和像素密度。biSize 允许结构扩展,解析器只读取自己理解的字段并按 biSize 跳过剩余扩展,不能假定 strf 永远正好 40 字节。biHeight 为负时通常表示 top-down 存储,对压缩视频的意义还应结合 codec 约定。
WAVEFORMATEX
音频格式块包含 format tag、通道数、采样率、平均字节率、块对齐、每样本位数和扩展长度。PCM 应检查 nBlockAlign = channels × bits_per_sample / 8(整字节 PCM),nAvgBytesPerSec = sample_rate × nBlockAlign。压缩音频不能套用同一公式,必须按 format tag 的定义解释扩展数据。
movi 与 idx1 的闭环验证
顺序扫描 movi
扫描时按 chunk header 和 aligned size 前进,遇到 LIST rec 时递归读取其中媒体 chunk。chunk ID 前两位映射到流号,必须小于 dwStreams,后两位决定压缩视频、未压缩视频、音频或其他数据。payload 中偶然出现的 FourCC 不参与扫描。
验证 idx1
每个索引项由 ckid、flags、offset、length 组成。因为历史实现对 offset 基准存在差异,读取器可以准备有限的候选基准:文件零点、movi list type 后、movi 数据首 chunk。对每个候选计算绝对位置,并验证 FourCC 与 size;只有能闭合的候选才可接受。不能在整个文件中无限搜索 FourCC 来“修正”索引,因为同名 chunk 数量可能很多。
关键帧标志
MJPEG 的每帧通常可以随机解码,H.264 等编码则取决于码流。写入 AVIIF_KEYFRAME 前应查询编码器产生的 sample 属性或解析 payload;按固定帧号打标可能让 seek 从不可解码点开始。修复器若无法可靠判断,应保留未知状态而不是把所有视频 chunk 标为关键帧。
AVI 1.0 修复实例
头部尺寸修复
若文件正常结束但 RIFF size 未回填,可先递归扫描所有完整 chunk,找到最后一个合法结束偏移。新的 RIFF size 为该偏移减 8;LIST movi size 为其 list type 起点到最后媒体子 chunk 结束的长度。每一层都独立计算,不能把同一个文件长度写入 RIFF、LIST 和媒体 chunk。
重建索引
顺序扫描得到每个媒体 chunk 的绝对位置、ID、大小和轨道内 sample 序号后,可以生成新的 idx1。写入索引前必须选择并记录 offset 基准;生成后的每一项要反向读取目标 chunk 验证。若追加 idx1 改变文件尾,还要更新 RIFF size 和 AVIF_HASINDEX 标志。
AVI 1.0 测试与验证
最小测试矩阵
测试样本应覆盖单视频、音视频交错、多音轨、rec 子列表、奇数 chunk、无索引、错误索引、负高度位图、未知扩展 chunk、截断媒体 chunk 和错误父 size。每个测试都应验证读取器不会越界,且轨道数量、样本数量、时长、chunk 偏移和关键帧定位符合预期。
外部工具与人工核对
工具输出只能作为一层证据。应同时查看十六进制头部、递归 chunk 树、索引目标和实际解码结果。能播放不等于容器完全合法:播放器可能忽略错误索引并顺序扫描;同样,某个严格工具拒绝文件也不一定意味着媒体 payload 已损坏。诊断报告应明确错误所在层级。
解析器必须处理未知 Chunk、奇数长度 padding、缺失索引、错误 RIFF size、负高度、多个音频轨道和不连续 chunk。遇到未知编码 FourCC 时可以保留轨道元数据并跳过 payload,不应把它误判为 MJPEG。对损坏文件,首先保证不越界,再尝试顺序扫描恢复;不能为了“尽量播放”而读取到文件范围之外。