AVI 传统格式(RIFF AVI 1.0)详解

格式定位

AVI(Audio Video Interleave)是 Microsoft RIFF 家族中的音视频容器格式。它规定的是文件如何组织音频流、视频流、元数据和索引,并不规定视频必须使用某一种编码。一个 AVI 可以承载 Motion JPEG、未压缩 RGB、H.264、MPEG-4 Part 2,也可以承载 PCM、MP3 等音频。判断文件中真正的媒体格式,必须读取 strf 中的 biCompressionwFormatTag,不能只根据 .avi 扩展名判断。

本文只讨论传统 AVI 1.0:文件由一个 RIFF AVI 主块组成,媒体数据通常集中在 LIST('movi') 中,索引通常使用 idx1。它的长度和偏移字段主要是 32 位,因此在接近 4 GiB 时会遇到限制。OpenDML 的大文件扩展另文说明。

RIFF 的基本单位

RIFF 中最基本的单位是 Chunk:

1
2
3
4
5
Chunk
├── ckid 4 字节 FourCC
├── ckSize 4 字节 uint32 little-endian
├── data ckSize 字节
└── padding 当 ckSize 为奇数时补 1 字节

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
2
3
4
LIST
├── ckSize
├── listType 例如 'AVI '、'hdrl'、'movi'
└── child chunks

因此 LISTckSize 包含 list type,但子 Chunk 解析范围必须从 list type 后面开始。

AVI 总体文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RIFF
├── 'AVI '
├── LIST 'hdrl'
│ ├── avih
│ └── LIST 'strl'(视频轨)
│ ├── strh
│ └── strf
│ └── LIST 'strl'(音频轨)
│ ├── strh
│ └── strf
├── LIST 'movi'
│ ├── 00dc / 00db 视频 sample
│ ├── 01wb 音频 sample
│ └── ...
└── idx1

文件中可以有 JUNKINFOodml 等可选块,但解析器不能假设 hdrlmoviidx1 一定紧邻。正确方法是遍历 RIFF 子块,根据 FourCC 建立索引,未知块按长度安全跳过。

RIFF Header 字段

偏移 长度 字段 作用
0 4 RIFF 文件是 RIFF 容器
4 4 riffSize 从偏移 8 开始计算的 RIFF 数据长度
8 4 AVI RIFF form type
12 不定 子 Chunk hdrlmoviidx1

文件总长度通常为 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 只是主视频时间间隔提示。更精确的轨道时间应读取对应 strhdwScaledwRatedwTotalFrames 也不能无条件当作 movi00dc chunk 数量,因为文件可能存在重复、损坏或多个视频轨道。

strl 轨道列表

每个媒体轨道都有一个 LIST('strl')。其中 strh 描述轨道类型、时间基和长度,strf 描述具体编码格式。视频轨道的 strh.fccTypevids,音频轨道是 auds。轨道编号由它们在 hdrl 中出现的顺序决定,并与 movi 中 chunk ID 的前两位对应。

strh AVIStreamHeader

字段 长度 作用
fccType 4 vidsauds
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 重要颜色数

如果 biCompressionMJPG00dc 的 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_samplenAvgBytesPerSec = sample_rate × nBlockAlign。若 wFormatTag 不是 PCM,不能用这些公式猜测每个 sample 的含义,应根据对应编码器规范解析。

movi 中的媒体 Chunk

媒体 chunk 的 ID 通常由两位轨道号和两位类型组成:

1
2
3
00dc  第一轨压缩视频
00db 第一轨未压缩或旧式视频
01wb 第二轨音频

chunk 的完整布局是 ckid + size + payload + paddingsize 不含 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 起点或文件某个约定位置。可靠解析器要用 ckiddwChunkLength 对候选位置做验证,而不是盲目套一个固定公式。

读取流程与写入流程

读取时先校验 RIFF/form type,再遍历 hdrl 建立轨道表,然后定位 moviidx1。播放第 N 帧时,从索引得到 chunk 偏移和长度,读取 payload,依据该轨道的 strf 交给 JPEG、H.264 或 PCM 解码器,最后使用 strh 时间基调度。没有 idx1 时,可以顺序扫描 movi,但随机 seek 会变慢。

写入时通常先写带占位长度的 RIFF、hdrlstrl,随后写每个媒体 chunk 并记录其位置,结束时生成 idx1,最后回填 avihstrh、LIST size 和 RIFF size。任何一个前置 Box 长度变化,都可能导致索引偏移需要重算。

十六进制识别示例

1
52 49 46 46 ?? ?? ?? ?? 41 56 49 20

其中 52 49 46 46 是 ASCII RIFF41 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
2
3
00  52 49 46 46    ckID = RIFF
04 xx xx xx xx ckSize, little-endian
08 41 56 49 20 formType = AVI<space>

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 数都不是该字段的替代值。

显示尺寸

dwWidthdwHeight 是电影级建议尺寸,视频 strf 中的 BITMAPINFOHEADER 还会给出该流宽高。二者不一致时,应优先保留轨道实际编码尺寸并报告头部不一致;不能为匹配 avih 而裁掉解码后的像素。

strh 流头字段

流类型与处理器

fccType 常见为 vidsaudsfccHandler 表示编码处理器或 codec FourCC。视频 strf.biCompression 也会声明压缩类型,两者应兼容。未知 handler 不代表容器损坏,读取器仍可以列出 chunk 和时间信息,只是无法解码 payload。

时间基和长度

dwScale/dwRate 定义每个流单位的时长,dwStart 给出起始位置,dwLength 给出流长度。视频通常一个 sample 对应一个 frame;PCM 音频的 sample 单位可能由 dwSampleSizenBlockAlign 决定。计算轨道时长时应使用 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 的定义解释扩展数据。

moviidx1 的闭环验证

顺序扫描 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。对损坏文件,首先保证不越界,再尝试顺序扫描恢复;不能为了“尽量播放”而读取到文件范围之外。