AVI 1.0 + OpenDML AVI 2.0 参考手册
AVI 1.0 + OpenDML AVI 2.0 参考手册
AVI(Audio Video Interleave)是基于 RIFF 的容器格式,不是编码格式,也不是网络协议。AVI 可以承载 MJPEG、H.264、PCM、MP3 等多种编码,但具体兼容性取决于 stream header、bitmap/audio format 和实现。
本文将传统 RIFF AVI 1.0 与 OpenDML AVI 2.0 放在同一个容器系列中比较说明;两者的公共结构先统一介绍,分段、扩展索引和大文件偏移单独说明。
第一章 RIFF 与 AVI 总体结构
RIFF 基础结构
AVI 文件以 RIFF header 开始:
1 | RIFF | file_size_minus_8 | AVI | LIST hdrl ... | LIST movi ... | idx1 ... |
RIFF chunk 通常为:
1 | FOURCC (4 bytes) + size (4 bytes) + data (size bytes) |
奇数字节长度的 chunk 后通常需要一个 padding 字节,以保持后续 chunk 对齐。读取时必须按 size 跳过 padding。
主要 LIST
| 区域 | 作用 |
|---|---|
LIST hdrl |
文件和各轨道的描述信息 |
LIST movi |
实际音视频 chunk |
idx1 |
传统 AVI 索引,可选但常见 |
LIST odml |
OpenDML 扩展,支持大文件 |
第二章 轨道描述与编码参数
avih 主头
avih(MainAVIHeader)描述整体时序和规模,常见字段包括:
dwMicroSecPerFrame:帧间隔微秒。dwMaxBytesPerSec:最大建议数据率。dwFlags:如是否含索引。dwTotalFrames:主视频轨道总帧数。dwStreams:轨道数。dwSuggestedBufferSize:建议缓冲区。dwWidth、dwHeight:主视频尺寸。
这些字段是提示信息,实际播放仍应以 stream header 和 chunk 数据为准。
strl、strh 与 strf
每条音视频轨道都在 LIST strl 中描述:
轨道头 strh
strh 是 AVIStreamHeader,包含 fccType(vids 或 auds)、fccHandler、时间基 dwScale/dwRate、起始位置、长度和质量等。
视频时间戳可近似为:
1 | time_seconds = sample_index × dwScale / dwRate |
格式块 strf
视频通常使用 BITMAPINFOHEADER,记录宽高、位深和 biCompression(如 MJPG、H264);音频通常使用 WAVEFORMATEX,记录格式码、声道、采样率、平均字节率和块对齐。
strf 不能只按轨道顺序猜测,必须结合 fccType 解析。
第三章 媒体数据与索引
movi 数据 chunk
传统 AVI 使用 4 字节 stream number + 2 字节 chunk type:
1 | 00dc / 01dc 视频帧 |
前两位表示轨道编号,后两位表示数据类别。chunk 的 size 是 payload 长度,不含 FOURCC 和 size 字段。
idx1 索引
传统 idx1 每项包含:
| 字段 | 长度 | 说明 |
|---|---|---|
| ckid | 4 | 对应 chunk ID |
| dwFlags | 4 | 关键帧等标志 |
| dwChunkOffset | 4 | 相对 movi 的偏移(实现可能存在变体) |
| dwChunkLength | 4 | payload 长度 |
不同软件对 offset 基准处理存在兼容性差异,解析器应结合实际文件和 movi 起点验证,而不是盲信单一偏移公式。
AVI 中的 MJPEG 与 PCM
MJPEG AVI 通常每个 00dc chunk 是一张完整 JPEG;PCM AVI 的 01wb chunk 是若干个交错 PCM 音频帧。dwSampleSize、块对齐和音频 stream 的时间基决定读取方式。
大文件与 OpenDML
传统 RIFF/AVI 使用 32 位大小字段,接近 4 GiB 会受限。OpenDML 通过 AVIX 分段和扩展索引支持更大文件。播放器必须处理多个 RIFF 段,不能只读取第一个 RIFF。
第四章 读取、写入与兼容性
解析流程
1 | 校验 RIFF/AVI → 遍历 hdrl → 建立轨道表 → 定位 movi → 读取 chunk → 使用索引/时间基调度 |
解析时应递归处理 LIST,保留未知 chunk,并根据 size 跳过而不是假定固定顺序。这样才能兼容 JUNK、INFO、应用私有 chunk 和扩展索引。
常见错误
- 将 AVI 当作一种视频编码。
- 忽略 RIFF 小端序。
- 忽略奇数 chunk 的 padding。
- 只支持
idx1,不支持 OpenDML 大文件。 - 用
dwTotalFrames代替实际 chunk 数量。 - 将视频 chunk 的 size 当成整文件偏移。
- 不检查
strf就把00dc当作 JPEG。
RIFF 字节布局
所有 RIFF 整数通常使用 little-endian。最小 chunk 读取逻辑为:
1 | 读取 4 字节 type |
RIFF 的 file_size_minus_8 不包含最初 8 字节;计算文件实际长度时不能直接把它当成总文件大小。
avih 字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| dwMicroSecPerFrame | uint32le | 主视频帧间隔 |
| dwMaxBytesPerSec | uint32le | 最大数据率建议 |
| dwPaddingGranularity | uint32le | 对齐/填充粒度 |
| dwFlags | uint32le | AVIF_HASINDEX 等标志 |
| dwTotalFrames | uint32le | 主视频帧数 |
| dwInitialFrames | uint32le | 交错初始帧 |
| dwStreams | uint32le | 音视频轨道数量 |
| dwSuggestedBufferSize | uint32le | 建议缓冲区 |
| dwWidth/dwHeight | uint32le | 主视频尺寸 |
后面保留字段通常为 0,但解析器应按结构长度跳过,不要因非零就拒绝文件。
strh 时间基字段
dwScale 和 dwRate 构成轨道时间基。视频每帧通常消耗 dwScale 个 time unit;音频则可能按样本块或块对齐消耗。dwLength 是轨道样本长度,不一定等于 movi 中所有 chunk 数量。
视频 strf 的 BITMAPINFOHEADER
常见字段:
| 字段 | 说明 |
|---|---|
| biSize | 结构大小 |
| biWidth/biHeight | 宽高;负高度可能表示 top-down |
| biPlanes | 通常为 1 |
| biBitCount | 位深 |
| biCompression | FOURCC,如 MJPG、H264 |
| biSizeImage | 图像数据大小提示 |
| biXPelsPerMeter/biYPelsPerMeter | 分辨率元数据 |
biHeight 的正负会影响图像行方向,尤其在未压缩 RGB/BGR 中不能忽略。
音频 strf 的 WAVEFORMATEX
| 字段 | 说明 |
|---|---|
| wFormatTag | 1 通常表示 PCM |
| nChannels | 声道数 |
| nSamplesPerSec | 采样率 |
| nAvgBytesPerSec | 平均字节率 |
| nBlockAlign | 一个音频块的字节数 |
| wBitsPerSample | 位深 |
| cbSize | 扩展字段长度 |
PCM 校验关系为 nAvgBytesPerSec = nSamplesPerSec × nBlockAlign,nBlockAlign = channels × bytes_per_sample。
idx1 offset 验证
解析器可尝试以下候选基准并校验对应 chunk 的 FOURCC 和 size:
1 | movi_data_start + offset |
实际使用哪一种必须由文件结构验证,不能依据单一厂商样例硬编码。
写入 AVI 的基本顺序
- 先写占位的 RIFF、
hdrl和 stream headers。 - 写
movi中的音视频 chunk,记录偏移、长度和关键帧标志。 - 回填
avih、strh的长度和统计信息。 - 写
idx1或 OpenDML 索引。 - 回填 RIFF/LIST size。
中途崩溃可能留下可恢复的 movi 数据,但没有完整索引;播放器应允许顺序扫描作为降级方案。
AVI 文件构成图
1 | RIFF ('AVI ') |
Chunk 的字节构成
1 | offset + 0 : ckid[4] 例如 00dc、01wb |
00dc 的前两位 00 是轨道编号,dc 表示压缩视频数据;01wb 的 01 是第二条轨道,wb 表示音频数据。它们只是 AVI chunk ID,不是网络协议字段。
AVI 字段关联
1 | avih.dwStreams → strl 数量 |
轨道时间与交错写入
AVI 的名字包含 Interleave,但交错并不是由一个单独的 Header 位控制,而是写入器按照轨道时间和缓冲策略安排 movi 中的 Chunk。视频轨道通常以帧为单位写入,音频轨道则可能一次写入多个 nBlockAlign 对齐的音频块。strh.dwScale 和 dwRate 让播放器能够把两个轨道换算到各自的时间轴;写入器若长时间只写视频而不写音频,即使最终文件有两条轨道,实时播放也可能出现缓冲欠载。
对于 PCM 音频,dwSampleSize 和 nBlockAlign 要共同决定一个可寻址单位。如果每个 01wb chunk 包含 4096 字节音频,而每个音频块是 4 字节,那么它包含 1024 个音频块。读取器不能把每个 chunk 无条件当作一帧音频,也不能把音频字节截在一个 block 的中间。对于可变码率音频,dwSampleSize 可能为 0,必须依靠每个 chunk 的长度和解码器输出采样数恢复时间。
关键帧、索引和随机定位
视频随机定位不是简单地跳到任意 00dc。idx1.dwFlags 中的关键帧标志用于告诉播放器哪些 chunk 可以作为解码入口;MJPEG 中每张 JPEG 通常可以独立解码,H.264 则还要结合 payload 内部的 IDR、SPS 和 PPS。播放器通常先在索引中寻找不晚于目标时间的最近关键帧,再从该位置顺序解码到目标帧。若索引标记与编码实际不一致,播放器可能从非关键帧开始并出现花屏,因此索引生成器应从编码器输出确认关键帧状态,而不是按固定间隔猜测。
传统 AVI 的局限
第五章 RIFF Chunk 的严格解析规则
Chunk 头、长度和偶数字节对齐
每个 RIFF chunk 由四字节小端 FourCC、四字节小端 ckSize 和 ckSize 个数据字节组成。当数据长度为奇数时,文件中还会追加一个填充字节,使下一个 chunk 从偶数偏移开始;这个填充字节不计入 ckSize。解析器推进游标时应使用:
1 | next = data_start + ckSize + (ckSize & 1) |
若把 padding 算进 chunk 长度,后续 FourCC 会整体错位;若忽略 padding,只有在所有前置 chunk 恰好为偶数长度时才会“看起来正常”。
LIST 的嵌套范围
LIST 和 RIFF 的数据区开头还包含一个四字节 list type,例如 hdrl、movi 或 odml。外层 ckSize 包含 list type,但不包含外层 chunk 头。递归解析时,子 chunk 的结束位置不能超过父 list 的 data_start + ckSize;遇到声明长度超出父范围的 chunk,应报告结构损坏并停止递归。
AVI 1.0 主头与轨道描述
avih 字段之间的约束
avih 是 AVI Main Header,常见字段包括 dwMicroSecPerFrame、dwMaxBytesPerSec、dwFlags、dwTotalFrames、dwInitialFrames、dwStreams、dwSuggestedBufferSize、dwWidth、dwHeight 和四个保留字段。dwStreams 必须与 hdrl 中的 strl 数量一致;dwTotalFrames 通常按主视频轨道定义,不能在包含音频后简单改成所有 chunk 的总数。
dwMicroSecPerFrame 是主视频的标称帧间隔,单位为微秒。它与视频 strh 的 dwScale/dwRate 应相互一致或至少能解释差异。dwFlags 中的 AVIF_HASINDEX 只表示文件声称存在索引,不保证索引真实可用;验证器仍需检查 idx1 的每一项。
strh 时间基
视频或音频 strh 使用 dwScale 和 dwRate 定义轨道时间单位,样本时间可写为:
1 | seconds = sample_position * dwScale / dwRate |
dwLength 是该轨道的样本数量或时间单位长度,具体解释取决于流类型和 dwSampleSize。音频 dwLength 不一定等于 chunk 数;PCM 中它通常对应音频块数量,压缩音频则可能依赖块长度和解码器规则。
视频 strf 与音频 strf
视频 strf 通常是 BITMAPINFOHEADER 或兼容结构,包含 biSize、宽高、平面数、位数、biCompression、图像大小和像素密度。负的 biHeight 表示顶向下位图方向;MJPEG/H.264 等压缩视频的 biSizeImage 可能是建议值而非每帧固定大小。音频 strf 通常是 WAVEFORMATEX,nBlockAlign 和 nAvgBytesPerSec 必须与编码器及轨道时间基相容。
movi 数据组织与 Chunk ID
两位轨道编号规则
普通 AVI chunk ID 的前两字节是十进制轨道编号的 ASCII 表示,后两字节表示数据类型,例如 00dc、01wb、00db。编号的前导零是字节内容的一部分;不能把 FourCC 当成以本机整数读取的 32 位数再依赖 CPU 端序比较。解析器应按四个字节逐字节匹配。
rec 子列表
交错录制器可以使用 LIST 类型为 rec 的记录组,把同一时间窗口的多个视频和音频 chunk 放在一起。rec 不是媒体 sample,它只是组织结构;播放器仍需根据其中每个 chunk 的 ID 和长度建立轨道样本。遇到未知 list type 时,只要长度和边界合法,读取器可以跳过而不破坏后续定位。
AVI 1.0 idx1 索引
索引项字段
每个 idx1 项为 16 字节:FourCC、flags、offset 和 size。flags 常包含关键帧标志;offset 的基准在不同 AVI 文件中可能是 movi 数据起点、chunk 头起点或兼容实现约定,读取器不能无条件假定单一基准。可靠做法是尝试按照文件声明和实际 FourCC/长度验证偏移,并在候选基准中选择能够闭合的解释。
1 | offset + 0 4 bytes ckid |
dwChunkLength 应与目标 chunk 的 ckSize 一致或符合该实现的索引约定。任何索引项指向文件外、指向父 list 外或指向错误 FourCC,都应标记为失效项。
没有索引时的恢复
没有 idx1 时,读取器可以扫描 movi,按 chunk 边界建立临时索引。扫描过程必须遵守奇数 padding、嵌套 LIST 和父范围,不能仅搜索 00dc 字节模式,因为压缩 payload 中也可能出现相同字节。恢复索引应标记为重建索引,不能回写原文件而不更新 dwFlags 和头部尺寸。
OpenDML AVI 2.0
AVIX 分段
OpenDML 用多个 RIFF 段突破传统 32 位 RIFF 大小限制。首段通常为 RIFF AVI ,后续段为 RIFF AVIX,每段有自己的 32 位段大小和内容范围。段与段之间不是普通 chunk 的嵌套关系;读取器应在当前 RIFF 段结束后读取下一个 RIFF header,并把所有 movi 数据逻辑上连接到同一文件时间线。
dmlh 与总帧数
LIST odml 中的 dmlh 提供 OpenDML 扩展信息,常见字段包括扩展总帧数。它与 avih.dwTotalFrames 可能存在不同语义或旧值;验证器应记录两者并通过实际视频样本数核对,而不是看到不一致就任选一个覆盖另一个。
indx 超级索引
OpenDML 的超级索引记录若干子索引的位置、大小和对应时间/样本范围。索引类型决定条目是指向标准索引 chunk 还是其他索引结构。64 位文件偏移通常由两个 32 位小端字段组合而成,组合时必须使用无符号 64 位整数:
1 | offset64 = low32 | (uint64(high32) << 32) |
ix## 标准索引
ix## 索引通常位于某个 movi 段附近,条目给出相对于该索引基准的偏移、大小和关键帧属性。解析器应先读取索引头中的 base offset、chunk ID、entry 数量和时间尺度,再逐项计算绝对文件位置。不要把 ix## 的相对偏移直接当作从文件零点开始的偏移。
AVI 写入和修复算法
写入顺序
安全写入器通常先写占位的 RIFF、hdrl 和轨道描述,再写 movi,同步收集样本和索引,结束时回填尺寸与索引。异常终止时,可能只留下可扫描的 movi 数据;修复器应以实际 chunk 结束位置重建 RIFF size,不能简单把文件长度写入所有 size 字段,因为每个父 list 的大小不同。
修复验证流程
1 | 读取 RIFF/AVIX 段 |
修复器应输出每个修改字段的原值、新值和依据。无法判断索引基准时,不应静默覆盖原索引;可以生成旁车索引或要求用户选择兼容模式。
AVI 兼容性与测试
编码格式边界
AVI 的容器结构不规定 MJPEG、H.264、PCM 等编码的全部码流语法。strf 只声明压缩类型和必要参数,实际解码仍由对应 codec 完成。尤其是 H.264 in AVI,可能存在 Annex-B、长度前缀、B 帧时间和非标准 extradata 组合,读取器必须把容器解析与编码码流解析分层处理。
测试矩阵
验证至少应覆盖小于 4 GiB 的 AVI 1.0、含奇数 chunk 的文件、无 idx1 文件、索引偏移以不同基准记录的文件、多 AVIX 段、indx/ix## 索引、视频单轨、音视频交错、负高度位图和截断文件。每个样本应核对 chunk 数、轨道时长、索引定位、关键帧和实际解码结果。
RIFF size、Chunk size 和 idx1 offset 的 32 位限制,使传统 AVI 不适合无限增长的高码率录制。即使某个播放器能读取超过 4 GiB 的私有扩展文件,也不能把这种行为当成 AVI 1.0 的标准能力。需要大文件、分段索引和 64 位偏移时,应使用 OpenDML AVI 2.0,或者选择更适合随机访问和时间戳表达的 MP4/ISO BMFF。
AVI 标识符和整数编码
FourCC 是四个字节
RIFF、LIST、AVI、hdrl、movi、avih、strh、strf、idx1 等标识符均由四个字节组成。源代码可用四字符常量辅助表示,但文件比较应按字节进行,避免 C/C++ 多字符常量的实现差异和 CPU 端序影响。
1 | 52 49 46 46 = "RIFF" |
尾部空格是 FourCC 的组成部分,例如 AVI 、fmt 、rec 。将字符串 trim 后再比较会把合法标识改成另一个值。
小端整数
RIFF/AVI 的 16、32 位字段通常使用 little-endian。一个声明为 10 00 00 00 的 chunk size 是 16 字节,不是 0x10000000。64 位 OpenDML 偏移也按低字节在前读取。解析器应提供 read_le16/read_le32/read_le64,不要把未对齐缓冲区直接转换为结构体指针。
对齐与结构体 Padding
磁盘字段连续排列,但 C 结构体可能在成员间插入 padding。即使使用 pack pragma,结构体端序、别名和未对齐访问仍可能不可移植。参考实现应逐字段读写;结构体仅用于保存解析结果,不用于直接映射不可信文件。
RIFF 树的完整边界计算
顶层 RIFF
顶层 12 字节包括 RIFF、size 和 form type。RIFF size 从 form type 开始计数,因此理论结束位置为 riff_start + 8 + riff_size。先检查加法溢出,再与文件长度比较。size 小于 4 无法容纳 form type,应立即判错。
LIST
LIST 的 size 同样包含四字节 list type。子 chunk 区域开始于 list type 之后,结束于 list_start + 8 + list_size。父 list 自身若为奇数 payload,也按 RIFF 规则在外层有一个 padding;子结构遍历不能越过未对齐结束位置。
未知 Chunk
AVI 允许应用扩展和填充 chunk。遇到 JUNK、INFO 或未知 FourCC,只要 size 合法就可跳过。跳过公式必须包含 padding。未知 chunk 不应被当作媒体 sample,也不应因无法识别就终止整个文件。
RF64 与 AVI 的区别
RF64 是面向 WAVE 等 RIFF 大文件的扩展,不等同于 OpenDML AVI。OpenDML 通过多个 RIFF/AVIX 段和索引解决 AVI 大文件问题。不能看到 RIFF 32 位上限就给 AVI 套用 WAVE 的 ds64 语义。
第六章 AVI Main Header 字段表
avih 的 56 字节 payload
经典 AVI Main Header 常见 payload 为 56 字节,由十四个 32 位 little-endian 字段组成:
| 偏移 | 字段 | 作用 |
|---|---|---|
| 0 | dwMicroSecPerFrame |
主视频帧的标称微秒周期 |
| 4 | dwMaxBytesPerSec |
建议最大吞吐率 |
| 8 | dwPaddingGranularity |
对齐粒度提示 |
| 12 | dwFlags |
索引、交错等标志 |
| 16 | dwTotalFrames |
主视频总帧数 |
| 20 | dwInitialFrames |
初始帧/预卷相关值 |
| 24 | dwStreams |
轨道数量 |
| 28 | dwSuggestedBufferSize |
建议缓冲大小 |
| 32 | dwWidth |
建议显示宽度 |
| 36 | dwHeight |
建议显示高度 |
| 40 | dwReserved[4] |
保留字段 |
avih chunk size 大于已知结构时,应在读取前 56 字节后跳过扩展;小于最低结构则无法安全解析。保留字段通常应为零,非零可报告兼容性警告。
dwFlags
常见标志包括 HASINDEX、MUSTUSEINDEX、ISINTERLEAVED、WASCAPTUREFILE 和 COPYRIGHTED 等。HASINDEX 只表明写入器声明索引存在;读取器仍要寻找并验证。MUSTUSEINDEX 不应被简单理解为没有索引就允许越界读取,而应在索引不可用时报告文件无法按声明方式播放。
速率一致性
若 dwMicroSecPerFrame 非零,可计算标称 fps 为 1,000,000 / value。视频 strh.dwRate/dwScale 也定义帧率。两者允许因整数近似略有差异,但若一个表示 25 fps、另一个表示 30 fps,应明确报告时间基冲突。
AVI Stream Header 字段表
固定字段布局
经典 strh 常见 payload 为 56 字节,包含:
| 字段 | 类型 | 作用 |
|---|---|---|
fccType |
FOURCC | vids、auds 等 |
fccHandler |
FOURCC | codec/handler |
dwFlags |
uint32 | 流状态标志 |
wPriority |
uint16 | 优先级 |
wLanguage |
uint16 | 语言标识 |
dwInitialFrames |
uint32 | 预卷相关值 |
dwScale |
uint32 | 时间单位分子 |
dwRate |
uint32 | 时间单位分母 |
dwStart |
uint32 | 流起点 |
dwLength |
uint32 | 流长度 |
dwSuggestedBufferSize |
uint32 | 建议缓冲 |
dwQuality |
uint32 | 质量提示 |
dwSampleSize |
uint32 | 固定 sample 大小,0 表示可变 |
rcFrame |
4×int16 | 视频目标矩形 |
dwScale 或 dwRate 为零会使时间无法计算。dwQuality 是编码/播放提示,不是 JPEG DQT 或 codec 质量的标准参数。rcFrame 使用 signed 16 位字段,解析时不能按 uint16 显示负坐标。
视频长度
普通固定帧视频通常 dwLength 等于轨道帧数,每个帧时间为 dwScale/dwRate。如果文件包含 dropped frame chunk 或特殊索引,实际媒体 chunk 数可能少于时间线 sample 数。验证报告应区分声明长度、索引样本数和实际非空 chunk 数。
音频长度
PCM 音频中 dwSampleSize 常等于 nBlockAlign,dwLength 表示音频块数;有些实现使用不同约定。最可靠的时长交叉检查来自 strh 时间基、WAVEFORMATEX 和 movi 中总音频字节数三者。压缩音频的每 chunk 解码采样数需由 codec 语法确定。
视频格式块详解
BITMAPINFOHEADER 字段
经典 40 字节结构包含:
| 偏移 | 字段 | 长度 |
|---|---|---|
| 0 | biSize |
4 |
| 4 | biWidth |
4 |
| 8 | biHeight |
4 |
| 12 | biPlanes |
2 |
| 14 | biBitCount |
2 |
| 16 | biCompression |
4 |
| 20 | biSizeImage |
4 |
| 24 | biXPelsPerMeter |
4 |
| 28 | biYPelsPerMeter |
4 |
| 32 | biClrUsed |
4 |
| 36 | biClrImportant |
4 |
biSize 决定可用字段和扩展数据起点。未压缩 paletted 格式可能在 header 后有颜色表,bitfields 格式可能有掩码,压缩 codec 可能附带 extradata。strf chunk size 必须覆盖这些数据。
高度方向
正 biHeight 常表示 bottom-up DIB,负值表示 top-down。对压缩视频,codec 自身决定扫描/图像方向,历史解码器对负高度支持不一。显示层应使用解码器输出 stride 与方向,不要仅按 sign 反转压缩字节。
biCompression
零值或特定常量可表示未压缩 RGB/bitfields,FourCC 可表示 MJPG、H264 等压缩格式。handler 与 compression 不一致时,需要结合文件生态判断,但验证器应保留冲突。不能仅根据扩展名 .avi 选择解码器。
音频格式块详解
WAVEFORMATEX 字段
音频 strf 常复用 WAVEFORMATEX:format tag、channels、sample rate、average bytes/sec、block align、bits/sample 和扩展长度。PCM 最小结构可能只有 16 字节,没有 cbSize;解析器应依据 chunk size 判断字段存在,而不是强制读取第 17、18 字节。
PCM 对齐
整字节 PCM 应满足 blockAlign = channels × bytesPerSample,audio chunk size 应是 blockAlign 的整数倍。若 chunk 在半个采样帧处结束,应作为截断处理;padding 是 RIFF chunk 外部对齐,不能补作 PCM payload。
压缩音频 Extradata
非 PCM format tag 可在 cbSize 后保存 codec 配置。其语法由具体编码标准定义,AVI 只提供边界。解析器即使不支持 codec,也应验证 18 + cbSize <= strf chunk size,并保留原始 extradata 供外部解码器使用。
Stream Name 与附加头部
strn
strl 可包含 strn 流名称。名称编码取决于生态,可能以零结尾,也可能包含本地代码页字节。日志应按长度读取并转义,不得调用无界 C string 函数。
vprp
视频属性头可描述场、帧率、纵横比和视频标准等额外信息。不是所有 AVI 都包含它。读取器未知时可按 size 跳过,专业验证器可交叉检查其显示比例与 avih/strf。
INFO LIST
LIST INFO 可含标题、作者、版权、软件等 RIFF 信息 chunk。它们不参与媒体 sample 定位。错误或超长元数据不应破坏 movi 解析,输出日志时还应防止控制字符和超大字符串。
第七章 movi 样本模型
Chunk 后缀
常见后缀 db 表示未压缩视频帧,dc 表示压缩视频帧,wb 表示音频波形数据,pc 等可用于 palette change。读取器应根据后缀和流类型判断,不合法组合如音频流出现 dc 应报告。
Palette Change
Paletted video 可通过 palette change chunk 更新颜色表。它影响后续视频呈现但不是普通图像帧。样本计数和时间推进需按对应规范处理,不能把 palette chunk 交给视频 decoder 当作一帧。
Zero Length Chunk
零长度媒体 chunk 可用于表示 dropped frame 或时间位置,具体取决于流和索引约定。它仍有 8 字节 chunk header,索引长度可为零。解析器不能陷入不推进的循环:即使 size 为零,下一偏移仍要越过 header。
Rec List
LIST rec 把同期附近的数据组织为记录。索引可能指向 rec 内的真实媒体 chunk,不应指向 rec list 后就把整个 list 当一个 sample。顺序扫描应递归展开并保持文件顺序。
AVI 1.0 索引详解
idx1 条目布局
idx1 payload 长度应为 16 的整数倍。每项依次是 4 字节 chunk ID、4 字节 flags、4 字节 offset、4 字节 length。所有整数 little-endian。余数字节表示索引截断或私有扩展,不能静默忽略。
标志位
常见 flags 表示列表、关键帧、无时间等属性。关键帧只对需要随机接入的媒体有意义;音频或 MJPEG 通常每个 sample 可独立处理,但仍应尊重写入器标志与 codec 实际语义。无时间 chunk 不应推进轨道时间。
Offset 候选验证
由于历史文件存在多种 offset 基准,兼容读取器可对首批 index entry 尝试有限候选:相对 movi list type、相对首媒体数据字节、绝对文件位置等。候选必须让多项同时指向匹配 FourCC 和合法 length,不能每项独立选择不同基准。
Index 与实际 Chunk Size
若 index length 小于 chunk header 声明 size,读取按哪个值会影响越界安全。严格模式报告不一致并拒绝该项;恢复模式可取两者与文件剩余范围中的安全下界,但必须标记 sample 截断,不能声称已修复。
第八章 OpenDML Header 扩展
LIST odml
OpenDML 头扩展常位于 hdrl 内,包含 dmlh。其扩展总帧数用于描述跨所有 RIFF/AVIX 段的主视频帧。旧 avih.dwTotalFrames 可能只覆盖首段或保留兼容值,播放器应理解 OpenDML 时优先结合 dmlh 和索引验证。
Super Index 在 strl
每条流可以在 stream list 中带 indx 超级索引,dwChunkId 指明它索引的媒体 chunk 类型。视频 00dc 与音频 01wb 应分别有各自索引链。轨道编号、chunk ID 和 strl 顺序必须一致。
OpenDML Index 通用头
OpenDML 索引基础字段
OpenDML 索引头常包括 wLongsPerEntry、bIndexSubType、bIndexType、nEntriesInUse、dwChunkId 和若干保留/基准字段。wLongsPerEntry 以 32 位 long 数描述 entry 宽度,不能误当字节数。类型决定后续是超级索引还是标准索引。
Entry Count
nEntriesInUse × entry_bytes 必须不超过 chunk payload 剩余长度。乘法先检查溢出。保留的预分配 entry 不一定已使用,读取器只能遍历 nEntriesInUse,不应把整个 chunk 剩余空间都当有效索引。
Super Index Entry
64 位偏移
超级索引 entry 常含 qwOffset、dwSize 和 dwDuration。offset 指向一个标准索引 chunk 的文件位置,必须读取到 ix## 等预期 FourCC。dwSize 用于限制子索引范围,dwDuration 表示该索引覆盖的流时长/样本量。
双向验证
从超级索引下钻到子索引后,检查子索引 dwChunkId 与父索引一致,并验证子索引 entry 指向真实媒体。再把所有子索引 duration 累加,与 strh/dmlh 比较。只验证第一层偏移不足以证明整条索引链有效。
Standard Index Entry
Base Offset
标准索引包含 qwBaseOffset,entry 的 dwOffset 相对该 base。二者相加需要 64 位溢出检查。不同 subtype 可能使 offset 指向 chunk header 或 payload,解析时应依据规范类型并通过 FourCC 反验。
Size 与关键帧位
entry 的 size 字段可能把最高位作为非关键帧标志,低位才是长度。实际掩码和极性由索引定义确定。先分离 flag,再检查 payload size;把含 flag 的原始 uint32 当长度会得到接近 2 GiB 的虚假值。
ix## 命名
ix00、ix01 的后两字符通常对应流编号。读取器仍应以索引头的 dwChunkId 和 strl 映射为最终依据,不能只解析 FourCC 字符。超过两位轨道编号的历史限制也需要实现明确支持边界。
AVIX 多 RIFF 段
文件结构
典型大文件结构为首个 RIFF AVI 包含 header 和第一段 movi,后续一个或多个 RIFF AVIX 包含额外 movi 和局部索引。每个 RIFF size 独立 32 位闭合,因此单段仍必须小于可表示上限。
1 | RIFF AVI |
顶层遍历
完成首 RIFF 后,解析器应在文件顶层继续寻找合法 RIFF header。不能把第二个 RIFF 当作首段 list 的未知子 chunk;也不能因首段 RIFF size 小于文件长度就一律报告尾部垃圾。
时间连续性
AVIX 段不重置 stream header、codec 配置或轨道 sample index。局部索引的 duration 和 sample 顺序延续前段。若编码参数确实改变,AVI 传统轨道描述未必能完整表达逐 sample 配置变化,这是兼容风险而不是段切换默认行为。
第九章 AVI 时间与同步算法
轨道 Sample Time
对于固定 sample 轨道,第 n 个时间单位的起点可写为 (dwStart + n) × dwScale / dwRate。实际 chunk 可以包含多个固定 sample,因此读取器需用 chunk bytes/dwSampleSize 推导增量。可变 sample size 为零时,通常一个视频 chunk 对应一个 sample,但音频需 codec-aware 处理。
主电影时间
AVI 没有 MP4 那样统一的 movie timescale 和 composition offset 表。主视频的微秒帧率与各轨道 scale/rate 共同驱动播放。B 帧和解码/显示顺序表达能力有限,因此某些现代 codec in AVI 依赖私有约定,兼容性较差。
Interleave 深度
交错质量可通过相邻音视频 chunk 的时间差和文件距离评估。若一分钟视频数据后才写对应音频,虽然轨道总时长一致,流式读取会需要巨大缓冲和 seek。写入器应按时间窗口交错,并保证音频 chunk 以 codec block 边界结束。
Drift 检查
分别由视频帧数×时间单位和音频总样本数/采样率计算尾部时间。如果差异超过一个合理 frame/packet,报告可能的丢帧、头字段错误或音频 padding。不要通过修改单个 rate 字段掩盖实际缺失数据。
第十章 AVI 写入器详细流程
初始化
确定轨道数、每轨 codec 描述、时间基、最大 chunk 大小和 OpenDML 策略。写入 RIFF、hdrl、strl、可回填的 avih/strh/dmlh/indx 区域。预留区必须以合法 JUNK 或规范方式占位,不能留下未定义随机字节。
媒体写入
每接收一个 sample,先验证轨道和 codec 边界,写 chunk header、payload、必要 padding,并记录绝对 header/payload 偏移、长度、时间增量和关键帧属性。只有整 chunk 成功写入后才提交索引记录,避免崩溃索引指向半写 payload。
段切换
预计下一个 chunk、padding 和必要索引将超过当前 RIFF 阈值时,先封闭当前 movi/list/RIFF 并写局部标准索引,再开始 AVIX。轨道状态与总计数保留。单个 chunk 超过段能力时应拒绝,而不是拆成非标准跨段片段。
结束回填
结束时生成/完成子索引和超级索引,回填 strh length、avih/dmlh total frames、所有 LIST/RIFF size 及 flags。随后重新以只读解析器扫描刚写文件,证明每个 offset 和 size 可反向解析。写入成功不等于结构正确。
AVI 读取器详细流程
发现阶段
从文件零点读取 RIFF AVI,递归解析 hdrl,建立 stream table 和格式描述。继续扫描各 RIFF/AVIX 段,记录 movi 范围、idx1、indx 和 ix##。发现阶段不立即信任索引,也不执行 codec 解码。
索引选择
完整 OpenDML 索引可优先用于大文件,传统 idx1 可用于兼容或首段,二者均损坏时顺序扫描 movi 重建内存索引。选择结果要记录来源和验证状态。混合使用两个不一致索引时,必须明确冲突解决规则。
Sample 读取
按索引取得绝对 offset 后,重新读取目标 chunk header,核对 ID 和 size,再限定 payload view。解码器只能访问该 view,不能把后续 RIFF padding/next header 暴露为 codec 数据。读取失败应丢弃该 sample 并保留时间 discontinuity。
十六进制实例
RIFF 与 hdrl
1 | 52 49 46 46 F4 03 00 00 41 56 49 20 |
第一行声明 RIFF size 为 0x000003F4,form type 为 AVI。第二行是 LIST,size 为 0xC0,list type 为 hdrl。第三行 avih size 为 0x38,即 56 字节。示例省略的 payload 必须仍在 LIST 声明范围内。
媒体 Chunk
1 | 30 30 64 63 05 00 00 00 AA BB CC DD EE 00 |
30 30 64 63 是 00dc,size 为 5,payload 是五字节 AA..EE,最后 00 是 RIFF padding。索引 length 应为 5,而不是 6。下一 chunk 从 padding 后开始。
idx1 Entry
1 | 30 30 64 63 10 00 00 00 04 00 00 00 05 00 00 00 |
entry 指向 00dc,flags 示例值含关键帧位,offset 为 4,length 为 5。offset 的绝对位置仍需结合该文件选用的基准验证。
AVI 修复证据链
可直接证明的字段
文件中完整 chunk header 可证明 payload 边界;strf 可证明声明 codec 参数;解码成功可证明部分 payload 语义;多个一致索引可提高 offset 可信度。任何单一来源都可能损坏,修复应使用交叉证据。
可推断字段
总帧数可由实际视频 chunk 与 dropped-frame 语义推断,最大 buffer 可由最大 chunk 估算,码率可由字节和时间统计。推断值应在修复报告标注来源和置信度,不能冒充原始标准字段。
不可可靠恢复字段
缺失 codec extradata、未知时间基、加密或私有 handler 语义,可能无法从 payload 唯一恢复。工具应停止在“容器结构已恢复但媒体参数未知”,而不是编造常见 25 fps、48 kHz 或 MJPEG 配置。
AVI 安全实现要求
整数溢出
所有 start + 8 + size + padding、entry_count×entry_size、base+offset、frames×sample_size 都使用宽无符号整数并先检查。读取 32 位值后直接在 32 位变量相加,会在接近 4 GiB 时环绕到文件开头。
递归和数量上限
RIFF LIST 可嵌套,恶意文件可构造极深层级或大量零长度 chunk。解析器应限制最大递归深度、chunk 总数、stream 数、索引项数和元数据总字节。零 payload chunk仍要推进 8 字节 header。
内存分配
dwSuggestedBufferSize、biSizeImage 和 chunk size 都是不可信建议或声明。分配前与文件剩余、实现最大 frame 和 codec 能力比较。可采用流式读取,避免一次把巨大 movi/idx1 全部加载到内存。
日志输出
FourCC 可能含不可打印字节,stream name 和 INFO 字段可能没有终止符。日志应按长度转义,并限制十六进制 dump,防止损坏文件造成日志注入或内存越界。
AVI 完整验证报告
容器层
列出每个 RIFF/AVIX 的起止、每个 LIST/chunk 的层级、声明/实际长度、padding 和错误。列出未知扩展但不要把它们自动判为损坏。
轨道层
列出 avih 总体参数,每条 strh/strf、时间基、声明长度、格式、实际 chunk 数和推导时长。显示 header 之间的冲突与采用的解释。
索引层
列出 idx1/OpenDML 索引类型、entry 数、基准、有效/无效项、关键帧统计和覆盖率。每个无效项报告期望 FourCC、计算 offset、实际字节和父范围。
编码层
抽样或逐 sample 调用 JPEG/H.264/PCM 等对应验证器,报告容器边界与 codec 边界是否一致。AVI 结构完全合法但 codec payload 损坏时,结论应明确为编码层错误。
AVI Parser 数据结构
文件范围
解析器内部用 64 位半开区间表示每个 RIFF、LIST、chunk 和 payload:[start,end)。半开区间便于计算 length=end-start 并避免“最后一个字节加一”混乱。所有来自 32 位 size 的值先提升再计算。
1 | struct Range { |
Chunk 记录
每个 chunk 记录 header offset、payload range、FourCC、声明 size、padding 字节数、父节点和解析状态。媒体 chunk 再关联 stream ID、sample index、时间与关键帧。未知 chunk 仍保留通用记录,方便生成完整树。
Stream 记录
每条流保存 strh 原始字段、strf 原始 payload、解析后的 codec description、movi chunk 列表和索引来源。不要用 stream_number 直接索引固定大小数组而不检查两位 ASCII 与 dwStreams。
Index Source
样本记录应标明来自 idx1、OpenDML、顺序扫描或修复推断。若多个来源指向同一 chunk,保存验证结果;若冲突,不要悄悄覆盖。随机定位可优先选已完整验证的来源。
RIFF Reader 伪代码
读取单个 Chunk
1 | read_chunk(parent_end): |
检查 size <= parent_end-pos 还不够,若 size 为最大值且随后加 padding,aligned_end 仍可能越界。应分别检查 padding 空间,所有算术使用 64 位。
解析 LIST
1 | parse_list(chunk): |
父 LIST payload 本身的外层 padding 已由调用者处理。子循环若剩余不足 8 字节但不是合法 padding,应报告尾部残缺。
顶层 RIFF 循环
首段要求 form type AVI 。完成其 aligned_end 后,如果还有文件数据,检查下一顶层结构;OpenDML 后续合法段为 RIFF AVIX。顶层未知数据可报告并按明确恢复策略处理,不能默认归入前一 RIFF。
Header 解析器伪代码
avih
1 | require payload >= 56 |
读取后检查 streams 在实现上限内、width/height合理、保留字段和帧率。不要在发现 width=0 时立即分配零画面;实际视频尺寸仍从 strf 获取,但主头矛盾要报告。
解析 strh
读取 fccType/handler、flags、16 位 priority/language、时间字段和 rcFrame。dwRate 为零导致不可换算,dwScale 为零同样非法或不可用。时间乘法使用 128 位/安全有理数避免大 length 溢出。
解析 strf
先根据 fccType 选择 video/audio parser,再依据内部 size/tag 决定结构版本。未知 fccType 的 strf 作为原始字节保留。不能由 strf 长度猜 stream type并覆盖 strh。
Stream Number 解析
ASCII 十进制
媒体 chunk ID 前两字符通常是 '0'..'9',流号为 (c0-'0')×10+(c1-'0')。字符超出数字范围时不是普通媒体 ID。两位表示通常限制到 00..99,但实现和 AVI 规范/生态对最大流数还有额外约束。
ix##
标准索引 FourCC 的后两字符也常表示流号,但其 header 中 dwChunkId 才给被索引媒体 ID。两者不一致时报告命名/内容冲突,不要只信文件名式字符。
Stream List 顺序
第一个 strl 通常对应 stream 00,第二个对应 01。解析器在 hdrl 中按 list 出现顺序编号,并核对 dwStreams。缺失/重复 strh 的 strl 不应计为完整可播放流。
第十一章 AVI PCM 承载
01wb Payload
PCM 音频 chunk 的 payload 是连续交错采样帧,不含 WAVE RIFF header。格式来自音频 strf。把 01wb 单独保存为 .wav 前必须新建 RIFF/WAVE fmt/data 结构,不能直接改扩展名。
Block 对齐
每个 PCM chunk size 应是 nBlockAlign 的整数倍。RIFF padding位于 payload 后,不计入音频。多个 chunk 逻辑连接时,不能把 padding 插入 PCM 数据。若某 chunk 不整除,后续 chunk 不能自动补齐半样本,除非存在明确私有约定。
音频时间推导实例
48 kHz、双声道、S16 的 blockAlign=4。一个 01wb payload 4096 字节包含 1024 个 sample frames,时长约 21.333 ms。若 strh.dwScale=1, dwRate=48000, dwSampleSize=4,该 chunk 推进 dwLength 1024,而不是 1。
PCM Seek
目标音频采样位置可根据每个 chunk 包含的 block 数在索引中累计,再定位到 chunk 内 relative_frame × blockAlign。位置必须落在 payload range。对齐到任意字节会破坏声道与样本边界。
PCM 音频 strf 的完整字段
16 字节 PCMWAVEFORMAT Payload
未压缩 PCM 的音频 strf 常保存 16 字节 WAVEFORMAT 基础结构:
| 偏移 | 长度 | 字段 | PCM 规则 |
|---|---|---|---|
| 0 | 2 | wFormatTag |
0x0001=WAVE_FORMAT_PCM |
| 2 | 2 | nChannels |
每个采样帧的声道数 |
| 4 | 4 | nSamplesPerSec |
每声道采样率 |
| 8 | 4 | nAvgBytesPerSec |
平均字节率,PCM 为精确值 |
| 12 | 2 | nBlockAlign |
一个完整采样帧的字节数 |
| 14 | 2 | wBitsPerSample |
每声道样本位数/容器约定 |
若使用 18 字节 WAVEFORMATEX,末尾 cbSize 对 PCM 通常为零;若是 WAVE_FORMAT_EXTENSIBLE,strf 更长并包含 valid bits、channel mask 和 SubFormat GUID。解析器根据 strf.ckSize 和 format tag 选择结构,不能无条件在 16 字节后读取 cbSize。
对传统字节对齐 PCM:
1 | bytes_per_sample = ceil(wBitsPerSample / 8) |
验证乘法溢出、声道和采样率非零、block align 与格式匹配、平均字节率闭合。对于 valid bits 少于容器位的 Extensible PCM,步长使用容器位,归一化使用 valid bits。
48 kHz Stereo S16 strf
参数:PCM tag 1、2 channels、48000 Hz、四字节 frame、192000 byte/s、16 bit:
1 | 01 00 # wFormatTag = PCM |
数值闭合:
1 | 2 channels * 2 bytes = 4 bytes/frame |
十六进制 0x0002EE00=192000,little-endian 为 00 EE 02 00。把该字段错误写成 48000 会使按平均字节率估算的缓冲和时长偏差四倍。
PCM Audio Stream Header
fccType 与 Handler
PCM 音频 strh 的 fccType 为 ASCII auds:
1 | 61 75 64 73 |
fccHandler 对普通 PCM 常为零,但生态中也可能保存格式标识。真正样本格式仍由 strf 的 format tag 和字段确定。rcFrame 对音频通常不承担视频目标矩形语义,应保持合理零值。
时间基的规范组合
AVI Stream Header 用有理数:
1 | seconds_per_stream_unit = dwScale / dwRate |
PCM 的常见规范化组合:
1 | dwScale = nBlockAlign |
本例:
1 | dwScale = 4 |
有些文件把等价比约分为 dwScale=1,dwRate=48000,时间单位仍为一帧。读取器用有理数计算而不是硬性要求某一表示;验证器可报告非典型但等价。绝不能只读 dwRate 并称其采样率,必须计算 dwRate/dwScale。
dwLength 在这一时间基下通常等于采样帧总数。若有 12000 帧:
1 | duration = 12000 * 4 / 192000 = 0.25 s |
或用约分时间基 12000*1/48000,结果相同。
固定样本大小
dwSampleSize 非零表示固定大小流样本。PCM 应与 nBlockAlign 一致。若为零,索引/播放器可能把每个 wb chunk 当一个可变样本,Seek 和 dwLength 解释发生差异。验证:
1 | dwSampleSize in {0, nBlockAlign} |
兼容读取器可根据 strf 和 chunk 整除恢复帧,但不能掩盖 Header 冲突。
##wb Payload 的精确实例
两帧 Stereo S16LE
假设音频是第二条 stream,Chunk ID 为 01wb。两帧:
1 | frame 0: L=0, R=32767 |
完整 Chunk:
1 | 30 31 77 62 08 00 00 00 |
目录:
| 相对偏移 | 长度 | 内容 |
|---|---|---|
| 0 | 4 | 01wb |
| 4 | 4 | ckSize=8 |
| 8 | 2 | Frame0 L=00 00 |
| 10 | 2 | Frame0 R=FF 7F |
| 12 | 2 | Frame1 L=00 80 |
| 14 | 2 | Frame1 R=FF FF |
8/4=2 帧,时间推进 2/48000 秒。Chunk payload 为偶数,无 RIFF pad。若 payload 是 10 字节,虽然 RIFF 结构允许偶数长度,但它不是四字节 blockAlign 的整数倍,应报告半帧尾部。
Chunk 之间的逻辑连接
每个 wb payload 从完整音频帧开始并在完整帧结束。Chunk 后的 RIFF pad 不进入逻辑 PCM。连接算法:
1 | for chunk in time_order: |
对 PCM blockAlign 通常可能为奇数,例如 mono U8 的一字节;奇数大小 chunk 会有 RIFF pad。该 pad 可能是 00,但绝不能当 U8 样本静音,因为 U8 静音应为 0x80,而且 pad 不属于 ckSize。
音频索引单位
idx1 Entry 只定位 Chunk
传统 idx1 一项定位一个 ##wb chunk,并给出该 chunk payload size。它通常不为内部每个 PCM frame 建索引。Seek 到具体帧要先累计前面索引项的:
1 | frames_in_entry = dwChunkLength / nBlockAlign |
找到包含目标帧的 entry 后:
1 | relative_frame = target_frame - cumulative_before_entry |
验证 byte_in_payload+nBlockAlign<=dwChunkLength。Index offset 仍按文件实际采用的 movi-relative候选基址解析,不能把内部 frame byte offset加到未确认的 idx1基址。
OpenDML Duration
OpenDML Standard Index/Super Index 的 duration 对音频要按 stream time units解释。固定 PCM 通常可由 chunk bytes/blockAlign 得到帧数并累计。不同写入器的 index duration 细节需要与 strh time base和实际 entries交叉验证,不能把视频的“一项一帧”规则套给音频。
PCM Seek 实算
1.234 秒目标
48 kHz 目标时间 1.234 s:
1 | target_frame = floor(1.234 * 48000) = 59232 |
假设每个 wb chunk 含 1024 帧,则:
1 | entry_index = floor(59232 / 1024) = 57 |
第 57 项 payload 至少应有 3456+4=3460 字节。若每项 nominal 4096 字节,则位置有效。时间映射采用整数有理数可避免浮点边界:
1 | target_frame = floor(target_time_num * sample_rate / target_time_den) |
若 Seek API 语义要求 nearest 或 ceil,应明确替换舍入方式。
与视频对齐
Seek 播放通常先找目标时间之前最近视频关键帧,再从对应时间附近找音频帧。若视频从较早关键帧开始预卷,音频可以:同步从该时刻输出、静音预卷后在目标点启用,或按播放器策略处理。不能仅让音频从目标点开始却把时间戳重置为视频关键帧时刻。
PCM Interleave 调度
以时间而非 Chunk 数轮转
写入器应比较每条轨道下一个样本的媒体时间,而不是机械写一个视频 chunk、一个音频 chunk。25 fps 视频每帧 40 ms;48 kHz PCM 每 40 ms 为 1920 帧、Stereo S16 为:
1 | 1920 * 4 = 7680 bytes |
可采用一视频帧后约 7680 字节音频,或把音频拆成更小块降低读取延迟。无论分块,累计音频时间应跟随视频:
1 | video_time = video_frames * video_scale / video_rate |
调度器把 drift 控制在配置的 interleave window 内。最后一块可短,但仍必须按 blockAlign 对齐。
最大 Chunk 与缓冲
音频 chunk 越大,Index 项越少但随机读取和交错延迟越大;越小则 RIFF/Index overhead 增加。观察到的最大 wb payload 应参与音频 dwSuggestedBufferSize,avih.dwSuggestedBufferSize 至少覆盖全文件最大样本需求的兼容值。
PCM 承载一致性审计
字段闭合
1 | format_frame_bytes = nBlockAlign |
检查:
1 | nBlockAlign == channels * container_bytes |
若等价时间基没有约成一帧一个 unit,则把 observed bytes/frames转换到 strh units 后比较,不能直接比较整数。
错误分类
| 现象 | 可能错误 |
|---|---|
| 播放速度错误但样本声音正常 | dwRate/dwScale 或 sample rate 错 |
| 左右交替噪声 | blockAlign/channels/位深错 |
| 每块边界点击 | Chunk 含半帧、错误插入 RIFF pad |
| Seek 后声道错位 | byte offset 未按 blockAlign 对齐 |
| 时长为实际四倍/四分之一 | 把 byte rate 当 frame rate或相反 |
| 大文件后 Seek 错 | idx1 基址/32 位 offset,而非 PCM 格式本身 |
修复时优先使用 strf、实际 wb 整除、样本统计和其他轨道同步建立证据。若多个采样率都能整除字节,单靠 payload 无法唯一恢复原 rate,不能无依据猜写。
AVI MJPEG 承载
Chunk 是 JPEG Frame
常见 MJPG 轨道一个 00dc 对应一幅 JPEG,但实际 FourCC profile 可能一个 chunk 含两个 field。通用验证先检查 SOI/SOF/SOS/EOI,再根据 handler 处理场扩展。RIFF padding 不属于 JPEG。
表缺失兼容
某些 AVI MJPEG 省略 DHT 并依赖默认 Huffman 表。只有在已知 MJPEG profile/兼容模式下才注入,报告注入位置和表。不能修改原 payload 后仍声称原始 JPEG 自包含。
JPEG 尺寸
JPEG SOF 宽高应与 BITMAPINFOHEADER 相容;场式编码可能高度关系不同。普通 progressive/baseline frame 不一致时,实际 decoder 输出以 SOF 为准,同时容器头应标记损坏或过时。
关键帧
独立 JPEG frame 通常均可随机访问,因此 idx1/OpenDML 可标关键帧。若 chunk 是 abbreviated JPEG 依赖外部表,仍需确保 seek 时表配置已可用;索引“关键帧”不自动携带表。
AVI H.264 承载
缺少统一映射的兼容风险
H.264 in AVI 存在多种历史 FourCC 和 extradata/payload 布局。有的 chunk 使用 Annex-B,有的使用长度前缀,有的重复 SPS/PPS。读取器必须由 handler、strf extradata 和首个 sample 共同判断,并允许用户指定 profile;不能仅搜索 00 00 01 后自动定论。
B Frame 时间
传统 AVI 主要表达单一顺序时间,缺少类似 MP4 ctts 的通用逐 sample composition offset 表。历史实现可能用 packed bitstream 或其他约定承载 B frame,兼容性不一致。重封装到 MP4 需要解析编码顺序/POC,不能直接按 AVI chunk 顺序给 PTS=DTS。
SPS/PPS
Extradata 若含参数集,应识别它是 Annex-B、avcC-like 还是私有结构。Chunk 中参数集更新也要处理。随机 seek 点不仅需要 IDR/恢复语义,还需要对应版本 SPS/PPS 可用。
Index Keyframe
AVI 索引标志可能由编码器错误设置。验证器解析每个候选 sample 的 H.264 access unit,比较 IDR、I slice、参数集和参考状态。I slice 不一定是安全随机入口,不能等同 keyframe。
AVI Uncompressed Video
DIB 行对齐
未压缩 RGB DIB 每行通常按 4 字节边界对齐,frame bytes 由 aligned row stride × absolute height 计算,而不是简单 width×height×bits/8。biSizeImage 可为零或建议值,实际 chunk size需与推导相容。
Bottom-up
正高度 DIB 的首存储行通常是图像底行,负高度为 top-down。播放时可通过负 stride/view 转换,不能把整帧字节反序;每行内部像素顺序仍保持。
YUV FourCC
未压缩/轻压缩 YUV FourCC 可能有 packed 或 planar layout,例如每像素/每平面排列不同。BITMAPINFOHEADER 位深字段不足以完整描述布局,必须按 FourCC 标准解释 stride、plane 和色度采样。
Palette
低位深 RGB 可在 strf header 后带颜色表,并用 palette change chunk 更新。Chunk 中像素是 palette index,不是直接 RGB。缺颜色表时无法正确呈色。
未压缩 DIB 帧的完整字节模型
轨道格式与媒体帧的边界
AVI 视频轨道的 strf 保存 BITMAPINFOHEADER 以及可能的颜色掩码、调色板或扩展字段;真正的未压缩图像位于 movi 的 ##db chunk。Header 不在每帧前重复。解码器先由轨道级 strf 建立格式,再用该格式解释每个媒体 chunk。
经典 40 字节 BITMAPINFOHEADER:
| 偏移 | 长度 | 字段 | 类型与作用 |
|---|---|---|---|
| 0 | 4 | biSize |
Header 大小,经典值 40 |
| 4 | 4 | biWidth |
有符号像素宽度 |
| 8 | 4 | biHeight |
有符号高度;正值常表示 bottom-up |
| 12 | 2 | biPlanes |
必须为 1 |
| 14 | 2 | biBitCount |
每像素位数 |
| 16 | 4 | biCompression |
BI_RGB=0、BI_BITFIELDS=3 或 FourCC |
| 20 | 4 | biSizeImage |
图像字节数或格式允许的零值 |
| 24 | 4 | biXPelsPerMeter |
水平像素密度 |
| 28 | 4 | biYPelsPerMeter |
垂直像素密度 |
| 32 | 4 | biClrUsed |
显式使用的调色板项数 |
| 36 | 4 | biClrImportant |
重要颜色数,零表示全部 |
所有整数 little-endian。宽高必须先按 int32 解释,不能把负高度读成巨大无符号分配。验证 biSize>=40、biPlanes=1、宽度大于零、绝对高度和像素数不超过实现上限。
DIB 行步长
32-bit 行对齐
经典未压缩 DIB 每行按四字节边界填充。宽 W、每像素 B bit:
1 | row_bits = W * B |
三像素 BGR24:有效行字节 9,stride 12,每行三字节 padding。1920×1080 BGR24 的有效行字节 5760,已经对齐,stride 仍为 5760。
防溢出实现:
1 | checked_dib_stride(width, bits, height): |
INT32_MIN 不能在 32 位有符号域直接取绝对值。biSizeImage=0 在某些 BI_RGB 文件中可见,读取器仍可推导,但应把“省略”与“字段匹配”分开报告。
两层 Padding
行末 padding 属于 frame payload,不是像素,值不保证为零。写入器应清零以获得确定输出并避免泄漏内存;读取器按 stride 跳过。若整个 frame payload 是奇数,##db chunk 后还会有一个不计入 ckSize 的 RIFF pad。DIB row padding 与 RIFF chunk padding不能混用。
BGR24 通道顺序
文件中是 Blue、Green、Red
BI_RGB 24-bit DIB 每像素三个独立八位分量:
| 显示颜色 | R,G,B | DIB 字节 |
|---|---|---|
| 红 | 255,0,0 | 00 00 FF |
| 绿 | 0,255,0 | 00 FF 00 |
| 蓝 | 0,0,255 | FF 00 00 |
| 白 | 255,255,255 | FF FF FF |
| 黑 | 0,0,0 | 00 00 00 |
| 灰 128 | 128,128,128 | 80 80 80 |
把它当 RGB 会交换红蓝。BGR 是分量排列,不等于把三字节作为整数执行 byte-swap。
3×2 BGR24 Bottom-up 实例
显示内容与格式
1 | top row: red, green, blue |
width=3、height=+2、24 bpp、BI_RGB。正高度表示首存储行是显示底行。每行 9 个颜色字节加 3 个 padding,Frame payload 24 字节。
40 字节 strf Payload
1 | 28 00 00 00 # biSize = 40 |
40 字节闭合,biSizeImage=0x18=12*2。
00db Frame Chunk
先写显示底行,再写顶行:
1 | 30 30 64 62 18 00 00 00 |
30 30 64 62="00db",18 00 00 00=24。Chunk 物理大小 32,payload 为偶数,不需要额外 RIFF pad。
逐行目录:
1 | stored row 0, display y=1: |
坐标:
1 | if biHeight > 0: |
检查 x/y 范围和受检乘加,且 pixel_offset+3<=chunk_size。Header 的 biSizeImage 不是读取外部输入的唯一硬边界,当前 chunk ckSize 才是。
Top-down DIB
负高度字节
biHeight=-2 的 little-endian 字节:
1 | FE FF FF FF |
此时首行就是显示顶行,相同图像的 payload:
1 | 00 00 FF 00 FF 00 FF 00 00 00 00 00 |
对 BI_RGB 可按 top-down 处理;对 FourCC 压缩/像素格式须查映射约定,不能假定所有 Codec 都接受负高度。播放器可用负 stride view 映射 bottom-up,无需反转每个字节。
8-bit Paletted DIB
RGBQUAD 数组
8-bit BI_RGB 的像素是调色板索引。BITMAPINFOHEADER 后跟 RGBQUAD,每项:
1 | blue, green, red, reserved |
若 biClrUsed!=0,采用该项数;否则默认上限常为 1<<biBitCount。验证:
1 | palette_count <= 1 << biBitCount |
四项表:
1 | index 0 black: 00 00 00 00 |
像素 01 02 03 表示红、绿、蓝,不是一个 BGR 像素。width=3、8 bpp 的有效行三字节、stride 四字节。
Palette Change 状态
AVI ##pc 可更新时间线上的调色板。它不是视频帧,不能计入视频 dwLength。顺序播放时在后续索引帧前应用;随机 seek 时恢复目标帧之前最后有效状态。修复工具若只保留 ##db 会丢失颜色变化。
BI_BITFIELDS 颜色掩码
Mask 的存放位置
biCompression=BI_BITFIELDS 时,经典 40 字节 Header 后通常跟 Red、Green、Blue 三个 little-endian 32-bit mask;扩展 DIB Header 可能把 masks 包含在 Header 内,并可增加 alpha。解析器先看 biSize 和具体 Header 类型,不能总是在 40 字节后额外读取十二字节。
典型 RGB565:
1 | red = 0xF800 -> 00 F8 00 00 |
16-bit 像素 0xF800 的 payload 字节为 00 F8,表示最大红。分量提取:
1 | shift = trailing_zero_count(mask) |
验证必需 RGB masks 非零、彼此不重叠、没有超出 biBitCount。常见 masks 是连续 bit 区间;若实现支持稀疏 mask,应把选中 bit 压缩后再归一化,不能只右移并把洞当有效位。乘 255 使用足够宽整数防溢出。
RGB555 与 RGB565 不可仅靠 16 bpp 区分
同样 biBitCount=16,无 mask 时生态可能默认 RGB555;BI_BITFIELDS 可明确 RGB565。两者绿色位数不同:
1 | RGB555: R=0x7C00 G=0x03E0 B=0x001F |
若按错格式,绿色亮度量化和红色最高位都会错误。格式相等比较必须包含 compression 与全部 masks,而不只是 width/height/bpp。
FourCC YUV Frame
Layout 由 FourCC 决定
YUY2、UYVY、NV12、I420 等 FourCC 使用不同 packed/planar 布局:
1 | YUY2 pair: Y0 U0 Y1 V0 |
不能对 YUV 机械套用 BGR DIB stride 公式。各映射决定偶数宽高约束、plane 顺序、plane stride、色度采样位置和方向。biBitCount 只是辅助描述,不足以重建 plane。
紧密偶数尺寸 NV12 的理论最小值:
1 | Y = W * H |
实际写入器可能把每个 plane 行 stride 对齐到 16/32/64 字节,frame chunk 会大于紧密值。解析器需要已知映射和 stride 约定;若 AVI Header 未明确额外 stride,只能按互操作约定、biSizeImage 和实际 chunk size交叉判断并报告歧义。
YUY2 两像素实例
一组 YUY2 四字节:
1 | 10 80 EB 80 |
表示 Y0=0x10,U=0x80,Y1=0xEB,V=0x80。两个像素共享 U/V。把它当 UYVY 会得到完全不同的亮度/色度。FourCC 字节在 BITMAPINFOHEADER 中为 ASCII YUY2:
1 | 59 55 59 32 |
FourCC 不做整数端序交换;文件 hexdump 就应看到字符顺序。代码若用 little-endian 多字符整数常量比较,需要明确构造方式,避免平台依赖。
未压缩视频的索引和验证
Keyframe
独立未压缩 Frame 通常都可标 AVIIF_KEYFRAME。Paletted DIB 仍依赖当前 palette 状态,因此随机访问不仅要找到 ##db,还要恢复 palette change。Keyframe 标志不改变 payload 的像素排列。
可交叉检查:
1 | derived_frame_size = stride * abs(height) |
Suggested buffer 偏小不授权读取器截断 Frame;安全读取器依据实际 ckSize 且受全局上限约束。写入器应回填观察到的最大样本大小。
黄金向量断言
前述 3×2 BGR24:
1 | stride = 12 |
测试矩阵覆盖 width 1/2/3/4 的 BGR24 padding、正负高度、奇数 payload 的 RIFF pad、8-bit palette 默认/显式数量、palette change、RGB555/565 masks、重叠或越界 mask、YUY2/UYVY/NV12、零 biSizeImage、Frame 小于/大于推导值和宽高乘法溢出。
第十二章 idx1 构建算法
扫描输入
遍历 movi/rec 子树,对每个媒体 chunk记录 header offset、payload offset、ID、size、轨道和 codec-derived flags。无时间/调色板 chunk按对应 flags处理。跳过未知非媒体 chunk,但保留文件树。
选择 Offset Base
新写文件应选定一种与目标兼容规范一致的基准,并对所有 entry统一使用。内部始终保存绝对 offset,序列化时减 base并检查落入 uint32。负值或超过 32 位说明传统 idx1 无法表达。
Entry 写入
写 ckid、flags、相对 offset 和原始 payload size。Size 不含 header/padding。完成 idx1 后回填 chunk size(16×entry count),若为奇数理论上再处理 RIFF padding,但 16 倍天然偶数。
回读验证
使用独立 reader按选定 base计算每项绝对位置,读取 FourCC/size比较。全部通过后才设置 HASINDEX。若文件追加移动 movi,必须重建 offset。
OpenDML Index 构建算法
子索引分组
按轨道和 RIFF/AVIX 段把媒体 chunk 分组,每组建立一个 standard index。选择 base offset使 entry relative offset 可表示,并记录 duration。一个子索引不应跨越无法用其基准/类型表达的数据范围。
Super Index
每条轨道的 super index entry 指向其各标准索引,保存子索引绝对 64 位 offset、size 和 duration。nEntriesInUse 只写真实子索引数,预留 entry 保持零/规范值。
预留容量
实时录制若在 hdrl 预留固定 super index 空间,需预测最大段数。超出时不能覆盖后续 header;可以提前采用更大预留、结束时重写文件或使用兼容的额外索引策略。
Duration 定义
视频标准索引 duration通常按 sample/time 单位累计,PCM 音频按 block/sample count。不要用 chunk 数统一填 duration。与 strh scale/rate 结合后应得到真实时间。
OpenDML 读取实例
Super Entry
假设 qwOffset=0x0000000100002000、dwSize=0x1000,读取器先验证 [offset,offset+size) 在文件内,再读 offset处 FourCC/size。64 位 offset 若截成 uint32 会错误落到 0x2000。
Standard Entry
若 base=0x100000000、entry offset=0x120,绝对位置=0x100000120。读取目标 00dc header,清除 entry size flag后比较 payload length。若实际 FourCC为 01wb,说明索引轨道或 offset损坏。
Duration Sum
同一视频轨道三个子索引各覆盖 1000、1000、500 frame,则 super durations总和为2500,并应与 dmlh/strh/实际样本接近。缺失中间子索引会造成 seek 时间洞,即便前后 offset都合法。
AVI Seek 算法
视频目标时间
把目标秒换成轨道 time unit,二分查找不晚于目标的最近合法关键 sample。H.264 等需要 codec-aware random access;MJPEG通常可直接选目标 frame。随后顺序解码到目标显示时间。
音频对齐
音频 seek 到与视频入口相同 presentation time,定位对应 sample frame/block。压缩音频可能需要前卷 decoder state;PCM 可直接 blockAlign定位。起始差异用静音/丢弃输出按同步策略处理。
OpenDML 层级查找
先用 super index duration找到覆盖目标的子索引,再在 standard entries中查 sample。若 duration不可信,可按累计验证样本表查找。不要每次 seek顺序扫描数 GiB movi。
索引失效回退
某个 entry验证失败时,可在相邻已知 chunk范围做有界扫描寻找下一个合法媒体 chunk,并标记 seek降级。不能全文件无界搜索相同 FourCC后把偶然 payload模式当目标。
AVI Duration Audit
主视频
分别计算 avih.dwTotalFrames × dwMicroSecPerFrame、视频 strh.dwLength × dwScale/dwRate 和实际时间样本累计。整数单位允许小误差;显著差异分类为主头、流头或数据/索引问题。
PCM 音频
由总 wb payload bytes/blockAlign/sampleRate计算,和 strh duration比较。若最后 chunk包含不足 block,完整 frame字节才计入可播放时长,尾部报告截断。
压缩音频
容器字节率不是可靠 duration。用 codec frame header或 decoder输出样本统计,并考虑 priming/padding。无法解析 codec时只报告声明 duration和chunk总字节,不编造实际时间。
AVIX 覆盖
确认所有段媒体都进入索引/时长统计。只读取首 RIFF会得到看似合法但过短 duration。dmlh总帧数应覆盖整个文件而非首段。
AVI Fuzz 与鲁棒性测试
Size 变异
对 RIFF/LIST/chunk size测试 0、1、最小合法、奇数、最大 uint32、恰好父边界和超父一字节。验证 parser不越界、不死循环,错误偏移准确。
Count 变异
对 streams、idx entries、OpenDML entries、颜色表数和 cbSize测试巨大值及乘法溢出。解析器在分配前使用 payload长度推导最大可能 count。
Offset 变异
索引测试负向(经无符号环绕)、文件外、指向 header中间、错误 FourCC、正确 FourCC错误 size和交叉轨道。任何 index失败不得直接驱动文件读取越界。
嵌套变异
构造深 LIST、零长度 chunk序列、rec内rec、AVIX中异常 hdrl和顶层垃圾。限制深度/数量且保留可恢复前段。
Codec Payload
容器合法但 JPEG/H.264/PCM损坏,容器 parser仍应列出样本并把 codec错误隔离。Codec parser不得读取 sample view之外寻找恢复字节。
LIST('rec ') 的完整嵌套模型
Record List 是媒体 Chunk 的父容器
AVI 可在 LIST('movi') 内把同一交错周期的一组媒体 Chunk 放入 LIST('rec ')。结构:
1 | LIST |
rec 的尾部空格是 FourCC 第四字节 0x20,不能写成三字节 rec。外层 list size 包含四字节 list type和所有 child的物理大小(含各自 pad),不包含 LIST 自身八字节 Header。
1 | rec_list_size = 4 + sum(8 + child.ckSize + (child.ckSize & 1)) |
由于每个 child 物理大小为偶数,四字节 type也是偶数,正常 rec list size通常为偶数。
36 字节结构实例
以下只演示 RIFF 嵌套,四字节视频 payload是占位媒体数据,不声称为完整 JPEG:
1 | 4C 49 53 54 1C 00 00 00 72 65 63 20 |
目录:
| 相对偏移 | 长度 | 结构 |
|---|---|---|
| 0 | 4 | LIST |
| 4 | 4 | list size 0x1C=28 |
| 8 | 4 | list type rec |
| 12 | 8+4 | 00dc, size 4, payload |
| 24 | 8+4 | 01wb, size 4, payload |
计算:
1 | list size = 4 + 12 + 12 = 28 |
解析器进入 rec 后把 parent end设为 list_start+8+28,逐 child 前进,最后游标必须恰好等于 parent end。不能把整个 rec payload作为一个视频 sample交给 Codec。
rec 与时间交错
Record 不等于统一时间单位
一个 rec 可包含一帧视频和覆盖相近时间区间的多个音频 Chunk,但标准结构本身不保证“一个 rec正好一帧”或固定时长。时间仍由各 stream Header和每个 sample duration推导。
25 fps视频每帧 40 ms,48 kHz PCM 每 40 ms为 1920 Frame。写入器可能:
1 | rec 0: video frame 0 + 1920 audio frames |
也可能在一个 rec 中放两个 960-frame音频 Chunk,或不用 rec直接交错。读取器不能用 rec count作为 video/audio dwLength。
Index 目标
传统 idx1 通常为 rec 内的实际 00dc、01wb child建立 entries;带 AVIIF_LIST 的索引项也可描述 LIST结构。通用读取器要按 flags和 ckid判断目标,不能假定所有 entry都直接是 Codec sample。
当 entry目标为 child,其 offset可能跨过 rec Header,反验仍应在目标处读到 child FourCC和 size。构造索引时保存 child绝对位置,不把父 rec起点代替。
传统 idx1 Flag 位
dwFlags 是位掩码
常见 flag:
| 位值 | 名称 | 含义 |
|---|---|---|
0x00000001 |
AVIIF_LIST |
entry引用 LIST而非普通 data chunk |
0x00000010 |
AVIIF_KEYFRAME |
可作为关键/随机访问样本候选 |
0x00000020 |
AVIIF_FIRSTPART |
分段数据的第一部分 |
0x00000040 |
AVIIF_LASTPART |
分段数据的最后部分 |
0x00000060 |
AVIIF_MIDPART |
中间部分组合语义 |
0x00000100 |
AVIIF_NO_TIME |
该项不推进 stream时间 |
Palette change等状态 Chunk可使用 NO_TIME;不能把它计入视频 Frame数。FIRST/LAST/MID用于历史分片语义,读取器若不支持必须明确拒绝或顺序重组,不能把每部分当独立帧。
未知 flags保留并报告。dwFlags==0x10 不保证 Codec层真的可随机访问,H.264等仍需语法验证;无标志也不一定证明 MJPEG帧非关键,可能只是写入器漏标。
idx1 Offset 的候选坐标系
历史歧义
AVI 生态存在不同写入器:offset可能相对 movi list type、首 child区域、LIST Header或其他历史基准,并可能指向 Chunk Header或 payload。安全读取器不能只尝试一个硬编码公式。
建立候选 base:
1 | file_start = 0 |
对 entry offset O,可测试:
1 | candidate_header = base + O |
实际采用哪些候选由实现兼容 profile确定。每个候选都要在父 movi/rec范围和文件范围内验证,不能让减八下溢。
Offset 候选评分
单项证据
候选 target得到分数:
| 条件 | 证据强度 |
|---|---|
| target 四字节等于 entry.ckid | 必需强证据 |
| target+4 的 ckSize等于 entry.dwSize | 强证据 |
| target位于解析出的 movi/rec child Header | 强证据 |
| stream编号存在且类型匹配 | 强证据 |
| payload Codec语法闭合 | 强证据 |
| target 为偶数字节对齐 | 辅助证据 |
| target顺序与相邻 entries一致 | 辅助证据 |
FourCC单独不足,因为 payload内可出现相同字节。Size相同也可能碰撞,因此最好用多个 entry和预先顺序扫描得到的 Chunk目录交叉。
多项选择
1 | choose_idx1_mapping(entries, candidate_modes, chunk_directory): |
采样应覆盖首、中、末和不同 stream,不只看第一项。若两个 mode对所有项都成立(例如某些特制布局),保留歧义并使用更强 Codec/父范围证据;不能随机选择。
受控文件实算
前述文件 movi list type在 0x26FA,第一项 dwOffset=4:
1 | 0x26FA + 4 = 0x26FE |
目标处为 00dc Header,size 896。第二项 offset 0x038C:
1 | 0x26FA + 0x038C = 0x2A86 |
目标处为 01wb Header,size 2048。连续两项已支持该 mode,完整验证再覆盖15项,全部命中才确认。
idx1 Payload 的容量和闭合
idx1 Entry 数量
每项固定16字节:
1 | require idx1.ckSize % 16 == 0 |
最大理论 entry数受 32-bit ckSize限制:
1 | floor(UINT32_MAX / 16) = 268435455 |
但单 RIFF根和内存/CPU上限会更早限制。解析器不按 entry_count一次性分配巨大数组,应流式读取或验证合理上限。
idx1.ckSize 通常为 16*N,天然偶数;Chunk物理大小 8+16*N。若有余数,尾部不是完整 entry,应报告损坏,不用零填充伪造。
Offset 表达范围
dwOffset 只有32位。若采用的 base到任何目标 Header差值超过 UINT32_MAX,Legacy idx1无法表达,应使用 OpenDML 64位 Super/Standard Index或重新分段/选择 base。不能截断:
1 | relative64 = target - base |
idx1 与顺序 Chunk 目录双向审计
Index 到媒体
对每项:
1 | resolve target with confirmed mode |
媒体到 Index
遍历所有媒体 child:
1 | if chunk should be indexed: |
报告 unindexed、duplicate-indexed、entry指向非媒体、同一 offset不同 size等。兼容文件可能有选择性索引,应根据写入 profile区分 warning/error,但不能声称覆盖完整。
顺序和重复
idx1常按 movi物理顺序排列,但读取器不要仅凭 out-of-order立即越界;先用 offset定位并建立时间语义。重复 entry可能造成重复播放/Seek错误,默认去重需保留诊断,不能静默把两个相同 offset当两个不同Frame计入 length。
idx1 重建算法
从目录生成
1 | build_idx1(parsed_media, chosen_base): |
Palette/no-time、rec LIST、分片 flags和 Codec keyframe都在 derive阶段处理。PCM Chunk是否 keyframe可按目标播放器兼容策略,但不得影响其 duration/frame计算。
写后反验
将新 idx1追加后更新 RIFF size和 avih HASINDEX。随后关闭写入器,使用独立 Reader重新解析:每项解析到实际 Chunk;覆盖数符合 profile;视频/音频观察时长未因 Index重复改变;逐媒体 payload hash与写 idx1前一致。
rec /idx1 测试矩阵
正常向量
覆盖无 rec、每 rec一视频一音频、一个 rec多个音频块、空 rec、奇数 child padding、idx1指 child、AVIIF_LIST指 LIST、NO_TIME palette和 FIRST/LAST分片。
负向量
1 | rec size小于4 |
Fuzz测试限制 LIST深度、child数量、索引项数和 Codec验证成本。即使 idx1损坏,顺序 Chunk目录仍受 movi/rec父边界保护;Index不能授权读取父范围之外的字节。
第十三章 OpenDML Super Index 字段布局
Super Index Header
Super Index通常置于对应strl内的indx chunk。Payload通用字段可表示为:
| Payload 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 2 | wLongsPerEntry |
| 2 | 1 | bIndexSubType |
| 3 | 1 | bIndexType |
| 4 | 4 | nEntriesInUse |
| 8 | 4 | dwChunkId |
| 12 | 12 | dwReserved[3] |
| 24 | 16×N | Super Index entries |
字段little-endian,FourCC按字节。Super Index的index type应匹配“index of indexes”定义;subtype/保留值按规范验证。
wLongsPerEntry
该字段以32位long为单位。常见super entry为4 longs,即16字节。Parser计算entry_bytes = wLongsPerEntry × 4并检查乘法、最小字段和兼容扩展;不能硬编码16后忽略声明。
Entry
常见entry布局:
| Entry 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 8 | qwOffset |
| 8 | 4 | dwSize |
| 12 | 4 | dwDuration |
qwOffset为绝对文件偏移,指向标准索引chunk header。dwSize通常覆盖索引chunk范围,必须至少容纳header+通用索引头。Duration按该stream时间单位/样本语义。
Payload 长度
24 + nEntriesInUse × entry_bytes不得超过indx payload。Chunk可预留更多unused entries,但只遍历有效数量。Unused区域是否全零按写入profile检查,不作为媒体数据。
Offset 反验
在qwOffset读取目标FourCC,确认类似ix##,再读其chunk size,验证与super entry dwSize的包含关系。目标standard index的dwChunkId需与父一致。
OpenDML Standard Index 字段布局
Standard Index Header
标准索引常用ix00等chunk,payload头包含:
| Payload 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 2 | wLongsPerEntry |
| 2 | 1 | bIndexSubType |
| 3 | 1 | bIndexType |
| 4 | 4 | nEntriesInUse |
| 8 | 4 | dwChunkId |
| 12 | 8 | qwBaseOffset |
| 20 | 4 | dwReserved3 |
| 24 | entry array | offsets/sizes |
常见entry为两个long(8字节):dwOffset和dwSize。类型/subtype决定准确语义,字段表用于最常见AVI_INDEX_OF_CHUNKS形式。
Entry Offset
绝对目标由base和entry offset组合。某些定义使目标指向chunk data或header附近,需按index subtype公式。实现封装成resolve_standard_index_entry(type,base,entry),并用FourCC/header反验,不在调用处散落+8/-8。
Size Flag
常见标准索引用dwSize最高位表示非关键帧,低31位为大小:
1 | is_keyframe = (dwSize & 0x80000000) == 0 |
具体解释必须确认索引类型。清除flag后size与目标chunk payload比较。空/dropped sample可为0。
Entry 数量
nEntriesInUse × wLongsPerEntry × 4先做溢出检查,并不超过payload-24。Entry count也受实现memory/sample上限。不要按chunk size分配后再验证。
OpenDML Super/Standard Index 联动实例
示例地址空间
下面构造一个内部完全一致的视频索引例子。轨道媒体 Chunk ID 为 00dc,三个媒体 chunk header 分别位于绝对偏移 0x1010、0x1200、0x1400,payload 分别从 0x1018、0x1208、0x1408 开始,长度分别为 0x100、0x180、0x80。Standard Index ix00 的 chunk header 位于绝对偏移 0x2000。
Standard Index 选择:
1 | wLongsPerEntry = 2 |
该例把 entry offset 解释为相对于 qwBaseOffset 的媒体 payload 偏移;校验 chunk header 时回退八字节。这个目标口径必须与所采用 OpenDML index subtype 及写入器约定一致,并通过 FourCC 与 size 反验。
ix00 完整十六进制
三项标准索引 payload 为 24 + 3×8 = 48 = 0x30 字节,连同八字节 RIFF chunk header 总长 0x38:
1 | 00002000 69 78 30 30 30 00 00 00 02 00 00 01 03 00 00 00 |
从 0x2000 开始拆解:
| 绝对偏移 | 字节 | 字段 |
|---|---|---|
0x2000 |
69 78 30 30 |
ix00 |
0x2004 |
30 00 00 00 |
payload size 48 |
0x2008 |
02 00 |
每 entry 两个 DWORD |
0x200A |
00 |
subtype 0 |
0x200B |
01 |
index of chunks |
0x200C |
03 00 00 00 |
三项有效 entry |
0x2010 |
30 30 64 63 |
目标 Chunk ID 00dc |
0x2014 |
00 10 00 00 00 00 00 00 |
base 0x1000 |
0x201C |
00 00 00 00 |
reserved |
第一项位于 0x2020,dwOffset=0x18、dwSize=0x100。目标 payload 是:
1 | 0x1000 + 0x18 = 0x1018 |
在 0x1010 应读取到 00dc 和 little-endian size 0x100。第二项 dwOffset=0x208,dwSize=0x80000180;清除最高位后 payload size 是 0x180,最高位一表示 delta/non-key 状态。第三项 dwOffset=0x408、size 0x80。
三项解析结果
| Entry | dwOffset |
原始 dwSize |
Key Frame | Payload Size | Payload 绝对偏移 | Header 绝对偏移 |
|---|---|---|---|---|---|---|
| 0 | 0x18 |
0x00000100 |
是 | 256 | 0x1018 |
0x1010 |
| 1 | 0x208 |
0x80000180 |
否 | 384 | 0x1208 |
0x1200 |
| 2 | 0x408 |
0x00000080 |
是 | 128 | 0x1408 |
0x1400 |
Key 判断和大小掩码分别为:
1 | non_keyframe = (dwSize & 0x80000000) != 0 |
不要在计算边界时使用带标志的原始 dwSize,否则第二项会被误认为超过 2 GiB。
对应 Super Index
在视频 strl 内放置一个 indx,令它位于示例偏移 0x0180,只包含一项指向 0x2000 的 Standard Index:
1 | 00000180 69 6E 64 78 28 00 00 00 04 00 00 00 01 00 00 00 |
indx payload size 为 0x28=40。wLongsPerEntry=4、subtype 0、type 0(index of indexes)、nEntriesInUse=1、dwChunkId=00dc。三个 reserved DWORD 全零。唯一 entry 为:
1 | qwOffset = 0x0000000000002000 |
qwOffset 精确指向 ix00 的 FourCC;dwSize=0x38 覆盖 Standard Index 的八字节 chunk header 和 48 字节 payload;duration 为三个视频时间单位,与三项 standard entry 对应。
Super 到媒体的证据链
完整验证不能只确认两个索引块“都在文件中”,而应串联全部约束:
1 | strl/indx.dwChunkId == '00dc' |
任何一环失败都要给出具体层级。比如 super offset 正确但 dwChunkId 不同,是轨道关联错误;标准索引结构正确但某 entry size 不匹配,是媒体或 entry 错误;不要统一报告为“AVI 索引损坏”而丢失定位信息。
跨 4 GiB 的 64 位偏移实例
AVIX 段中的 Base Offset
假设后续 RIFF/AVIX 段位于 4 GiB 之后,某视频 Standard Index 的绝对位置为 0x0000000100002000,其媒体基址为 0x0000000100001000。Super entry 使用完整 64 位 qwOffset:
1 | qwOffset bytes = 00 20 00 00 01 00 00 00 |
Standard Index 的 qwBaseOffset 写为:
1 | qwBaseOffset bytes = 00 10 00 00 01 00 00 00 |
若第一项 dwOffset=0x18,媒体 payload 的绝对位置为:
1 | 0x0000000100001000 + 0x00000018 |
32 位 entry offset 与 64 位 base 相加,使单个 Standard Index 能在局部段内使用紧凑条目,同时覆盖整个大文件的绝对空间。实现若先把 base 截成 32 位,会错误得到 0x1018,很可能命中首段中无关字节并造成危险的假验证。
Checked 64 位加法
即使使用 uint64,仍要防 base + offset 回绕:
1 | if dwOffset > UINT64_MAX - qwBaseOffset: |
回退八字节检查 chunk header 前,还要验证 absolute_payload >= 8。所有比较都在同一个绝对文件坐标系完成;不能把相对于映射窗口的指针值与绝对 qwOffset 混用。
OpenDML 索引构造算法
建立 Standard Index
写入器按 stream 和段收集样本。选择 qwBaseOffset 后,每个样本目标 payload 必须满足 absolute_payload >= base 且差值能放入 32 位 dwOffset。若超出,就结束当前 Standard Index、选择新 base 并建立下一块。
1 | start_standard_index(stream, base): |
样本 payload 大小必须小于 0x80000000,因为最高位被索引标志占用。媒体 chunk 的 RIFF size 仍为完整 32 位字段,但 Standard Index 的此种 entry 表达对单样本有更严格上限。
回填 Super Index
每关闭一个 Standard Index,就把其绝对 chunk header 偏移、含头总大小和覆盖 duration 加入对应 stream 的 Super Index。若 indx 预留区不足以容纳更多 super entries,写入器需要采用预先容量策略、重写头部或选择兼容扩展方案;不能覆盖后面的 strf/JUNK/movi。
跨段 Duration
视频通常每帧贡献一个或由 stream time base 定义的 duration;PCM 音频应按 sample frame 数累计,而不是按 wb chunk 数。Super Index 各 entry 的 dwDuration 之和应与相应 strh.dwLength 以及实际样本时间线相容。dmlh.dwTotalFrames 只针对主视频总帧,不能拿它校验音频 duration。
OpenDML Index 的容量公式
Standard Index 大小
Chunks 类型的 Standard Index Header 固定 24 字节,常见 wLongsPerEntry=2,每条 entry 为两个 DWORD,即 8 字节:
1 | standard_payload_size = 24 + entry_count * 8 |
Header 和 entry 都是偶数字节,因此这一常见结构无需额外 RIFF pad。若 subtype/longs per entry 不同,应使用:
1 | entry_bytes = wLongsPerEntry * 4 |
并验证乘法不溢出、payload 不超过 chunk ckSize、nEntriesInUse 不超过 payload 能容纳的数量。
8192 项的 chunks index:
1 | entries bytes = 8192 * 8 = 65536 |
写入器选择容量时要计入这些索引本身的空间,不能只累加媒体 chunk。
Super Index 大小
Super Index Header 同样为 24 字节,每条 AVISUPERINDEX_ENTRY 为 16 字节:
1 | super_payload_used = 24 + nEntriesInUse * 16 |
但位于 Header 中的 indx 通常要在开始写媒体前预留最大容量。假设预留 C 项:
1 | reserved_payload = 24 + C * 16 |
nEntriesInUse 只写实际有效项;未使用 entry 区清零或保留为未使用容量,不能让读取器把它们计入索引。
两小时录制的容量规划实例
视频轨道
25 frame/s、两小时:
1 | total_video_frames = 25 * 2 * 60 * 60 = 180000 |
若每个 Standard Index 最多 8192 Frame:
1 | standard_index_count = ceil(180000 / 8192) = 22 |
精确分组为前 21 组各 8192,最后:
1 | 180000 - 21 * 8192 = 7968 entries |
Super Index 实际 payload:
1 | 24 + 22 * 16 = 376 bytes |
如果预留 32 项:
1 | reserved payload = 24 + 32 * 16 = 536 |
实际 nEntriesInUse=22,还有十项增长空间。预留不足时不能把第 23 项写进紧邻 strf 或 movi;应在建文件时增加容量、用更小录制上限、重写新文件或采用有明确兼容语义的扩展。
音频轨道
48 kHz PCM 每 40 ms 写一个 chunk:
1 | frames_per_chunk = 48000 * 0.040 = 1920 |
若每个音频 Chunk 一条 Standard Entry,数量也为 22 组。但每个 entry 的 duration 是 1920 个音频时间单位,而不是 1:
1 | first_full_index_duration = 8192 * 1920 = 15728640 frames |
全部 Super Entry duration 之和应为:
1 | 180000 * 1920 = 345600000 frames |
若错误按 chunk 数写 duration,总音频时长会变成 180000/48000=3.75 s。
RIFF/AVIX 分段预算
为什么阈值必须低于字段极限
每个 RIFF/AVIX 根的 size 只有 32 位,理论内容上限受 UINT32_MAX 限制。但实际写入阈值还受旧读取器、文件系统、索引相对偏移和回填安全影响。OpenDML 写入器通常选择明显低于 4 GiB 的 segment target,例如约 1 GiB 量级;这是一项互操作策略,不是把所有文件强制限定为同一精确值。
段预算应包含:
1 | RIFF form type |
不能只在 media_payload_sum 达阈值后切段,因为八字节 Chunk Header、奇数 pad 和索引会使最终 RIFF size 超限。
写入前预测
在写下一个 sample 前计算其物理增长:
1 | sample_physical = 8 + sample_size + (sample_size & 1) |
若 projected content 超过 target 或 32 位上限,则先关闭当前 movi/RIFF、写完其 index,再在新 RIFF('AVIX') 中写 sample。一个单独 sample 若本身超过允许 segment,应拒绝或改变编码分片策略,不能生成跨 RIFF 根的半个 media chunk。
Segment 边界不切 PCM Frame
视频 sample/音频 chunk 是切段原子单位。PCM wb chunk 必须按 blockAlign 闭合;如果目标剩余空间只够半个音频 chunk,可在更早处关闭当前 chunk并写完整帧数,剩余帧放下一段。Chunk payload 可调整大小,但不能把一个 RIFF chunk Header 写在前段、payload 续到 AVIX。
Standard Index Base Offset 规划
Base 指向同一坐标系
每个 entry 的目标 payload:
1 | absolute_payload = qwBaseOffset + dwOffset |
常见写入策略把 qwBaseOffset 设为当前组附近的已知绝对位置,使所有 dwOffset 非负且不超过 UINT32_MAX。无论选择 media chunk header 后八字节、movi 数据区域或其他规范允许的基准,写入器和验证器必须用同一公式反验到真实 payload。
记录样本时同时保存:
1 | chunk_header_offset |
构建 Standard Entry 时用 payload_offset-base,并在文件中检查目标前八字节的 Header ID/size与 index 的 dwChunkId/size一致。不能把 idx1 的历史 offset base 规则直接复用到 OpenDML。
Base 切换
即使 segment 本身低于 4 GiB,若一个 index 跨越很远文件范围,relative 仍可能超 32 位。遇到:
1 | payload_offset < base |
就关闭当前 Standard Index,选择新 base。不得用 32 位截断差值;截断后目标可能落到另一个合法 Chunk,造成难以发现的错误 Seek。
Super Entry 的精确回填
qwOffset
Super Entry 的 qwOffset 是 Standard Index Chunk 的绝对文件偏移,指向它的 FourCC Header:
1 | at qwOffset: |
dwSize 保存该 Standard Index Chunk 的总大小,包含八字节 RIFF Chunk Header。验证:
1 | read_fourcc(qwOffset) == expected ix## |
若 Standard Index payload 为 65560,则 Super dwSize=65568,不是 65560。
dwDuration
Duration 是该子索引覆盖的 stream time units。视频一 entry 一帧且 scale/rate表示一帧时,通常等于 entry 数;PCM 按每 entry 包含的采样帧累计。Variable audio 需要从 sample duration/packet语义得到,不能用 bytes 猜,除非 Codec 规则明确。
Super 中所有有效 duration 用 64 位累加进行审计,虽然单 entry 字段是 DWORD:
1 | total_duration64 += super_entry.dwDuration |
单个 Standard Index 覆盖 duration 超 UINT32_MAX 时,应更早分组,不能截断。
JUNK 预留转为 indx
等尺寸原位替换
Header 写入阶段可以放一个足够大的 JUNK 作为未来 Super Index 空间。完成格式和容量后,将其原位改为 indx,但必须保持后续 Chunk 偏移不变。若预留物理区域恰好 R 字节,目标 indx 使用 U 字节:
1 | require U <= R |
只有 remaining 足以形成合法 RIFF Chunk(至少八字节 Header,加合法 payload/pad)时,才可把剩余写成新的 JUNK。若差值过小,应让 indx 的 ckSize 吸收整个预留 payload,并把未使用 entry 区清零,而不是留下 2/4/6 字节无法解析的裂缝。
Chunk size 变化会影响父 LIST/RIFF size;等物理长度替换避免移动后续数据和重写所有绝对索引。开始录制前就确定每条 stream 的最大 super capacity 是最稳妥方案。
OpenDML 写入状态机
OpenDML Header 初始化
1 | write RIFF('AVI ') |
所有 placeholder 保存文件偏移和预期长度。写入前验证 reserve 不使 Header 自身超过 segment 策略。
流式媒体阶段
每写一个 media chunk:
1 | validate stream id and sample size |
长时间录制不能把所有 per-sample record 永远放 RAM,可将临时 index records 写入旁路文件/保留区,或按 Standard Index 容量及时封闭索引。旁路状态属于写入恢复实现,不进入最终 AVI 语法。
关闭一个 Segment
1 | close LIST/movi size |
关闭顺序确保 Standard Index 的绝对 offset和总 size 已知后,才建立 Super Entry。最终结束时回填每条 stream 的 indx、strh length、suggested buffer,回填 dmlh total video frames、avih兼容字段和首 RIFF size。
崩溃安全与回填顺序
不可把 Placeholder 当完成证据
录制中断可能留下 RIFF/LIST size 为零或预估值、strh length 未更新、dmlh 为零、Super Index 未回填。恢复器要扫描实际 Chunk 证据,不能因为 Header 看起来完整就信任计数。
较安全的结束事务:
1 | 1. finish last media payload and pad |
系统掉电不保证普通 write 顺序持久化,关键录制应用需使用平台的 durable flush 和临时文件策略。不能把“调用 fclose”当作所有存储介质必然已经落盘。
回填必须验证原位置
每次 seek 回 placeholder 前,先读回 FourCC/预留长度确认文件没有发生意外布局变化:
1 | require read_fourcc(saved_offset) == expected_id |
禁止根据重新搜索到的第一个 indx 猜回填目标,因为可能有多条 stream。保存 stream identity、parent strl范围和绝对 offset。
超出预留容量
发现 Super Entry 超过容量时,最终化必须明确失败或重写新文件,不能:
- 覆盖后续 strf/JUNK;
- 让 nEntriesInUse 大于 payload capacity;
- 丢弃后面的 AVIX 索引却仍声明完整 duration;
- 把 64 位绝对 offset截成 32 位 idx1。
可以生成“媒体可顺序扫描但索引不完整”的恢复文件,但必须调整 Header/报告状态,不能伪称 OpenDML 索引完整。
分段写入验证报告
每段报告
1 | RIFF ordinal and form type AVI/AVIX |
每条 Stream 报告
1 | Super capacity and nEntriesInUse |
全文件不变量
1 | all RIFF roots are non-overlapping and ordered |
只有 Header、媒体 Chunk、Standard Index、Super Index 和时间线五层互相闭合,才可判定 OpenDML 文件结构完整。
Field Index Subtype
场索引用途
OpenDML可使用field相关index subtype,为一个视频sample记录两个field的偏移/大小关系。它适合每帧含两个独立field数据的格式。普通chunks索引parser不能只读前两个long后丢弃field信息。
Entry 宽度
Field index entry宽度和字段由subtype规定,wLongsPerEntry会反映更多long。Parser依据subtype选择结构,并检查两个field范围均在目标chunk/文件内。
Seek
容器sample仍可能对应一帧,而field offset用于解码/显示顺序。播放器seek到sample后按field order处理;不能把两个field各计一个dwLength frame,除非stream time定义如此。
dmlh 字段
Extended Total Frames
dmlh位于LIST odml,核心dwTotalFrames为32位little-endian,表示跨AVI/AVIX的主视频总frame。Chunk可能含保留扩展,至少检查4字节。
与 avih
OpenDML读取器比较dmlh、avih、视频strh和索引实际count。Avih可能为兼容值或首RIFF限制,但不应无条件忽略。报告标明每个来源。
超过32位
dmlh核心仍是32位frame count;极端长文件在frame数量层面也有表示/实现边界。字节偏移的64位扩展不意味着所有计数字段无限。写入器达到上限前分卷/选择其他容器。
dmlh 的精确读取与回填
最小字段
dmlh payload 的首四字节是 little-endian dwTotalFrames,统计整个 OpenDML 文件所有 AVI/AVIX 段中的主视频 Frame:
1 | 64 6D 6C 68 04 00 00 00 20 BF 02 00 |
解析:
1 | chunk id = "dmlh" |
某些文件写更长 payload并把后续区域保留为零。读取器要求 ckSize>=4,读取首DWORD,剩余字节按扩展/保留数据保存;不能要求所有实现恰好四字节,也不能把保留DWORD累加成额外Frame。
两小时25fps实例
1 | 25 * 2 * 60 * 60 = 180000 frames |
little-endian 180000为:
1 | decimal 180000 = 0x0002BF20 |
验证器比较:
1 | dmlh.dwTotalFrames |
各来源不一致时逐项报告,不盲目以 dmlh覆盖实际媒体。修复写入器在所有Segment和Index完成后,从确认主视频Sample模型计算并回填。
32 位上限
dwTotalFrames 最大 4294967295。25fps下约5.44年,120fps下约414天。达到字段上限前应分卷或采用能表达更大计数的容器;饱和写 0xFFFFFFFF 会丢失精确总数,回绕则产生更严重错误。
解析端使用64位累计观察Frame,再比较:
1 | if observed_frames > UINT32_MAX: |
OpenDML的64位 qwOffset 解决文件位置,不自动扩大 dmlh、单个Index duration等所有DWORD字段。
vprp Video Properties Header
vprp 基础字段
Video Properties Header可描述视频格式token、标准、垂直刷新、总/活动每行sample、纵横比和field数量,随后有每field信息。字段用于更准确表达模拟/场式视频属性。
Aspect Ratio
Frame aspect ratio通常以打包分子/分母或规范结构表示,不能按普通float读取。分母为0非法。它与BITMAPINFOHEADER宽高和像素aspect共同决定显示。
Field Count
nbFieldPerFrame控制后续field info数组数量。Count乘entry size不得越过vprp chunk。Progressive常见1,interlaced可能2,实际含义按规范。
Field Info
每field可描述compressed BM height/width、valid region和offset。矩形坐标需在编码范围,signed/unsigned按字段定义。未知扩展按biSize/chunk边界跳过。
vprp 的 36 字节主结构
VIDEO_PROPERTY_HEADER
Video Properties Header 的固定部分由九个 little-endian DWORD组成,共36字节:
| Payload偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 4 | dwVideoFormatToken |
视频格式Token,按标准/生态解释 |
| 4 | 4 | dwVideoStandard |
视频标准标识 |
| 8 | 4 | dwVerticalRefreshRate |
垂直刷新率提示 |
| 12 | 4 | dwHTotalInT |
每行总采样/时序单位 |
| 16 | 4 | dwVTotalInLines |
每帧总行数 |
| 20 | 4 | dwFrameAspectRatio |
两个16位量打包的比例 |
| 24 | 4 | dwFrameWidthInPixels |
Frame像素宽度 |
| 28 | 4 | dwFrameHeightInLines |
Frame行高 |
| 32 | 4 | nbFieldPerFrame |
后续Field描述项数 |
最小 vprp.ckSize 为36,实际大小:
1 | expected_min = 36 + nbFieldPerFrame * 32 |
字段数量乘法使用受检64位。Count异常巨大时不能按它分配数组;先由 ckSize反推最多可容纳项:
1 | max_fields_by_size = (ckSize - 36) / 32 |
Frame Aspect Ratio 打包
两个 WORD 而非 Float
Frame Aspect通常在一个 DWORD中打包两个16-bit无符号值。按常见 little-endian/MAKELONG表达,低WORD为水平量,高WORD为垂直量:
1 | aspect_x = value & 0xFFFF |
4:3:
1 | value = 0x00030004 |
不能把 04 00 03 00 bit-cast成 IEEE float。aspect_y=0 无法形成比例,应报告无效/未定义,而不是除零。
与像素尺寸交叉:32×24 的存储宽高本身为 4:3,因此 SAR约为1:1。若显示比例16:9而存储720×576,则像素并非正方形,播放器需要结合 Frame Aspect与尺寸得到显示变换。vprp、BITMAPINFOHEADER、容器/Codec SAR冲突时保留各来源并按目标Profile决定优先级,不能静默覆盖。
VIDEO_FIELD_DESC 的32字节
每Field八个DWORD
| Field相对偏移 | 字段 | 作用 |
|---|---|---|
| 0 | CompressedBMHeight |
压缩Bitmap高度 |
| 4 | CompressedBMWidth |
压缩Bitmap宽度 |
| 8 | ValidBMHeight |
有效图像区域高度 |
| 12 | ValidBMWidth |
有效图像区域宽度 |
| 16 | ValidBMXOffset |
有效区域水平偏移 |
| 20 | ValidBMYOffset |
有效区域垂直偏移 |
| 24 | VideoXOffsetInT |
视频时序水平偏移 |
| 28 | VideoYValidStartLine |
有效视频起始行 |
第 i项起点:
1 | field_offset = 36 + i * 32 |
有效矩形检查使用受检加法:
1 | ValidBMXOffset + ValidBMWidth <= CompressedBMWidth |
字段的具体时序语义与VideoStandard相关;通用容器验证至少保证几何范围和数组边界,不应对未知标准强行套一个模拟制式。
68 字节 Progressive vprp 实例
参数
构造32×24、2 Hz刷新、4:3显示、每帧一个progressive field描述:
1 | dwVideoFormatToken = 0 |
Field覆盖完整32×24区域,所有offset为零。
完整Payload
1 | 00 00 00 00 # dwVideoFormatToken |
长度:
1 | 9 * 4 + 1 * (8 * 4) = 36 + 32 = 68 |
完整 Chunk Header:
1 | 76 70 72 70 44 00 00 00 |
44 00 00 00=0x44=68。Chunk物理大小76,均为偶数,无pad。前述受控AVI目录中的 vprp size也为68,说明其结构容量恰好是固定Header加一个Field描述;具体字段值仍应从文件原字节解析,不能仅由长度替换为本合成实例。
两Field Interlaced 结构
数组容量
nbFieldPerFrame=2 时最小payload:
1 | 36 + 2 * 32 = 100 bytes |
两个Field可以描述各自压缩/有效高度和起始行。它们仍共同属于一个视频Frame属性描述,不自动使 strh.dwLength 翻倍。实际媒体Chunk是一Frame含两Field、两个Chunk还是Codec私有布局,由Handler和Field Index等共同定义。
若读取器只解析第一项并忽略第二项,隔行视频的有效区域/offset可能错误。若把 Count=2 当两个时间样本,又会将时长翻倍。
vprp 与其他Header交叉校验
尺寸
1 | vprp.dwFrameWidthInPixels vs BITMAPINFOHEADER.biWidth |
不一致可能源于裁剪/模拟blanking语义,并非所有差异都应自动改写。报告原始值和推导关系,由支持该VideoStandard的模块判断。
刷新率
1 | vprp refresh hint |
本合成实例三者均为2 Hz。字段存在舍入时使用有理数比较并设容差;若 vprp写60而stream是30000/1001≈29.97,要先确认vprp字段是否表达field refresh而非Frame rate,不能立即判错。
Aspect
计算:
1 | storage_aspect = width / height |
32×24、DAR4:3得到SAR1。若分母零、宽高零或乘法溢出,不执行换算。用约分有理数避免浮点误差。
vprp 有界解析算法
1 | parse_vprp(payload): |
不要要求 payload恰好等于 known_end,未来/私有扩展可在Chunk边界内保留;严格Profile可另行限制。读取任何Field前先做容量检查,不根据 Count越界。
vprp 错误测试
结构向量
1 | ckSize < 36 |
一致性向量
1 | vprp width differs from strf |
未知或损坏 vprp不应阻止容器跳到后续Chunk:其 ckSize仍提供边界。播放器可退回 strh/strf,但验证报告保留 vprp错误。
strd Stream Data
Codec 私有数据
Stream list可含strd供handler私有数据。它与strf extradata不同但都可能参与decoder初始化。语法由handler定义,容器parser按长度保留。
安全边界
未知strd不执行、不当字符串、不根据内容访问外部资源。传给codec plugin时使用受限byte view和明确FourCC。超大strd设metadata上限。
重封装
不知道语义时移动到同轨通常可原样保留,但更改handler/codec后旧strd可能无效。转换工具应丢弃并警告或要求codec模块重建,不能盲目绑定新格式。
strn 与 INFO 文本解析
长度优先
文本chunk可能带NUL,也可能填满长度。先按chunk size读取,找到首NUL作为显示终止,但原始字节保留。不要strlen越界。
编码
RIFF历史文本常用本地代码页,无法统一假定UTF-8。API可返回raw bytes+可选检测结果。写新文件选择目标生态支持编码并明确策略。
Padding
奇数字符payload后的RIFF padding不属于字符串。编辑文本后重新计算size和padding。NUL若是payload一部分计入size。
INFO Metadata 常见 Chunk
标识符
INFO list可包含名称、艺术家、版权、软件、注释、日期等FourCC。它们是元数据,不影响stream count/time。未知INFO child同样按通用chunk跳过。
重复值
同一FourCC可能重复。Reader保留顺序和全部值,不用字典静默覆盖。UI可选首/末但报告注明。
安全输出
文本做长度限制、换行/control转义。Metadata不能影响日志格式或HTML渲染。Hexo文章示例中也应使用code span/dump展示原始值。
第十四章 AVI 1.0 文件布局实例
结构
1 | RIFF('AVI ') |
顺序不是所有扩展的唯一排列,但hdrl需在解码媒体前可得。Info/JUNK可穿插,parser按类型和父list处理。
Size 自底向上
写入时先确定叶chunk payload/padding,再求strl list size、hdrl size、movi size,最后RIFF size。父size包含child header与padding。不能把所有payload size简单相加漏掉8字节chunk header。
idx1 在 movi 后
常见idx1位于movi LIST结束之后,仍在同RIFF内。搜索movi子chunk循环应在LIST边界停止,不能继续把idx1当媒体chunk。
受控 AVI 1.0 测试文件
媒体参数
下面的测试文件由三帧 MJPEG 视频和一条 PCM 音频轨组成。视频为 32×24、2 frame/s,时长 1.5 s;音频为 8000 Hz、单声道、16 bit little-endian PCM,共 12000 个采样帧。文件总长 37036 字节,即十六进制 0x90AC。
文件根部前 32 字节为:
1 | 00000000 52 49 46 46 A4 90 00 00 41 56 49 20 4C 49 53 54 |
52 49 46 46 是 RIFF,little-endian size A4 90 00 00 等于 37028。按 RIFF 规则,根块总长是 8+37028=37036,恰好等于文件长度。Form Type 41 56 49 20 是 AVI 。偏移 0x0C 开始 LIST,其 size 为 0x22BC=8892,list type 为 hdrl。
完整 RIFF 目录
用通用 RIFF 边界公式逐层遍历,得到以下目录。end 均为左闭右开区间的结束偏移,已经包含奇数 payload 的 padding:
1 | RIFF/AVI off=0x0000 size=37028 end=0x90AC |
JUNK 是合法的预留/对齐块,不是损坏数据。解析器必须按声明长度跳过。此文件的 idx1 正好从 movi 的结束位置开始,idx1 结束又正好等于 RIFF 根结束,三个父子边界全部闭合。
avih 主头实例
56 字节字段值
avih payload 位于 0x20,由 14 个 little-endian DWORD 组成:
| 字段 | 十进制值 | 十六进制值 | 解释 |
|---|---|---|---|
dwMicroSecPerFrame |
500000 | 0x0007A120 |
每视频帧 0.5 s,即 2 frame/s |
dwMaxBytesPerSec |
41000 | 0x0000A028 |
估计最大读取速率 |
dwPaddingGranularity |
0 | 0x00000000 |
未声明固定 padding 粒度 |
dwFlags |
2320 | 0x00000910 |
包含索引等标志组合 |
dwTotalFrames |
3 | 0x00000003 |
主视频帧数 |
dwInitialFrames |
0 | 0x00000000 |
无初始交错帧偏移 |
dwStreams |
2 | 0x00000002 |
视频与音频两条 stream |
dwSuggestedBufferSize |
1048576 | 0x00100000 |
建议缓冲上限 |
dwWidth |
32 | 0x00000020 |
主视频宽度 |
dwHeight |
24 | 0x00000018 |
主视频高度 |
dwReserved[4] |
全零 | 全零 | 保留字段 |
由 dwMicroSecPerFrame 推导的帧率是 1,000,000/500,000=2。由主头推导的时长是 3×500000 µs=1.5 s,与视频 stream header 和实际三帧一致。
dwFlags=0x0910 应按位掩码分别解释,不能当成一个枚举值。验证器至少确认索引存在标志与实际 idx1 相符,并对未理解的置位保留原始十六进制值。
视频 Stream Header 与 Format
strh 字段
第一条 strl 的 strh payload 位于 0x6C:
| 字段 | 值 |
|---|---|
fccType |
vids |
fccHandler |
MJPG |
dwFlags |
0 |
wPriority / wLanguage |
0 / 0 |
dwInitialFrames |
0 |
dwScale |
1 |
dwRate |
2 |
dwStart |
0 |
dwLength |
3 |
dwSuggestedBufferSize |
896 |
dwQuality |
0xFFFFFFFF |
dwSampleSize |
0 |
rcFrame |
(0,0)-(32,24) |
时间基为 dwScale/dwRate=1/2 s,dwLength=3 给出 1.5 s。dwSampleSize=0 表示样本大小可变,符合三帧 MJPEG payload 分别为 896、894、896 字节。不能由 dwSuggestedBufferSize=896 推断每帧永远固定 896;它只是缓冲建议,本例第二帧已经反证固定长度假设。
BITMAPINFOHEADER
视频 strf payload 位于 0xAC,长度 40,正好是一份基础 BITMAPINFOHEADER:
| 字段 | 值 |
|---|---|
biSize |
40 |
biWidth |
32 |
biHeight |
24 |
biPlanes |
1 |
biBitCount |
24 |
biCompression |
MJPG |
biSizeImage |
2304 |
biSizeImage=32×24×3=2304 是未压缩图像尺寸语义/建议值,不等于每个压缩 MJPEG chunk 的字节数。索引中的 896、894、896 才是各压缩帧的真实 payload 长度。
PCM Stream Header 与 Format
音频时间基
第二条 strh payload 位于 0x1154,fccType=auds、dwScale=1、dwRate=8000、dwLength=12000、dwSuggestedBufferSize=2048、dwSampleSize=2。因此一个 stream 时间单位是一个 PCM 采样帧,时长为:
1 | dwLength * dwScale / dwRate |
dwSampleSize=2 与单声道 S16 的 nBlockAlign=2 一致。它不是“每个 AVI 01wb chunk 两字节”;每个音频 chunk 可以包含很多采样帧。
WAVEFORMAT 字段
音频 strf payload 位于 0x1194,长度 16:
| 字段 | 值 |
|---|---|
wFormatTag |
1,PCM |
nChannels |
1 |
nSamplesPerSec |
8000 |
nAvgBytesPerSec |
16000 |
nBlockAlign |
2 |
wBitsPerSample |
16 |
交叉校验:8000×1×16/8=16000 byte/s,1×16/8=2 byte/frame。所有 01wb payload 之和为 24000 字节,除以 block align 得到 12000 帧,与 strh.dwLength 完全一致。
受控文件 Header 的完整序列化
avih 的 56 字节
按前述字段值逐 DWORD little-endian 写出:
1 | 20 A1 07 00 # dwMicroSecPerFrame = 500000 |
14 个 DWORD 共 14*4=56 字节。完整 Chunk Header 还需:
1 | 61 76 69 68 38 00 00 00 |
即 avih 和 payload size 56。38 00 00 00 是十六进制 0x38,不是十进制字符“38”。Chunk 物理大小 8+56=64。
dwFlags=0x0910 的位解释
按位测试而非 switch 枚举:
1 | flags = 0x00000910 |
验证器保留未知置位。Header 声明 HASINDEX 时,应找到并验证 idx1/OpenDML index;找到索引但 flag 未置位也应报告不一致,而不是丢弃可验证索引。
MJPEG 视频 strh 完整字节
56 字节 Payload
1 | 76 69 64 73 # fccType = "vids" |
wPriority 和 wLanguage 各 16 位,共占一个四字节区;rcFrame 四个值也是 little-endian 16-bit signed,而非四个 DWORD。若按 DWORD 读取 rcFrame,既会越过 56 字节边界,也会错读下一个 chunk。
时间:
1 | frame duration = dwScale/dwRate = 1/2 s |
dwSampleSize=0 对应可变压缩帧。本例 Chunk 896、894、896,最大值 896 与 suggested buffer 一致。
strh Chunk 外壳
1 | 73 74 72 68 38 00 00 00 [56-byte payload] |
FourCC strh 的 H 不是 Header 长度的一部分。解析 LIST/strl 时,必须把这 64 个物理字节完整包含在父 LIST 范围内。
MJPEG BITMAPINFOHEADER 完整字节
1 | 28 00 00 00 # biSize = 40 |
0x900=2304=32*24*3 是未压缩图像缓冲参考,不是 JPEG payload size。对压缩视频,逐帧大小来自 00dc.ckSize/Index。Header 的 handler 和 compression 都为 MJPG,应一致;若两者冲突,解码器选择存在歧义。
PCM 音频 strh 的完整序列化
8000 Hz Mono S16
使用本文件已解析的时间字段,其规范字节布局:
1 | 61 75 64 73 # fccType = "auds" |
若原文件某些非时间兼容字段采用不同零/default 策略,重封装器应保留原始值;上表展示由已解析语义得到的完整规范序列化。关键闭合字段是 scale/rate/length/sample size。
0x1F40=8000 little-endian 为 40 1F 00 00;0x2EE0=12000 为 E0 2E 00 00。把十进制文本直接写入 Header 是常见手工构造错误。
PCM WAVEFORMAT 完整字节
1 | 01 00 # WAVE_FORMAT_PCM |
总计 16 字节。完整 strf Chunk:
1 | 73 74 72 66 10 00 00 00 |
80 3E 00 00=0x3E80=16000。这一块不含 WAVEFORMATEX 的 cbSize,因为 ckSize 明确为 16。若解析器无条件再读两字节,会吞掉下一 Chunk ID 开头。
Header 与媒体的五路闭合
视频 Header 与媒体闭合
1 | avih total frames = 3 |
MJPEG payload sizes:
1 | 896 + 894 + 896 = 2686 bytes |
音频 Header 与媒体闭合
1 | observed wb bytes = 24000 |
视频、音频、Main Header 三条时长都为 1.5 s。这样的多路闭合比“文件能播放”更强;若任一路不一致,报告具体证据,不立即选择一个字段覆盖其他字段。
movi 交错序列实例
15 个媒体 Chunk
LIST/movi 位于 0x26F2,list size 26810。List Type movi 位于 0x26FA,第一个媒体块头从 0x26FE 开始。按 RIFF chunk 步长遍历:
| 顺序 | Chunk | Header 偏移 | Payload 偏移 | Payload 长度 |
|---|---|---|---|---|
| 0 | 00dc |
0x26FE |
0x2706 |
896 |
| 1 | 01wb |
0x2A86 |
0x2A8E |
2048 |
| 2 | 01wb |
0x328E |
0x3296 |
2048 |
| 3 | 01wb |
0x3A96 |
0x3A9E |
2048 |
| 4 | 01wb |
0x429E |
0x42A6 |
2048 |
| 5 | 00dc |
0x4AA6 |
0x4AAE |
894 |
| 6 | 01wb |
0x4E2C |
0x4E34 |
2048 |
| 7 | 01wb |
0x5634 |
0x563C |
2048 |
| 8 | 01wb |
0x5E3C |
0x5E44 |
2048 |
| 9 | 01wb |
0x6644 |
0x664C |
2048 |
| 10 | 00dc |
0x6E4C |
0x6E54 |
896 |
| 11 | 01wb |
0x71D4 |
0x71DC |
2048 |
| 12 | 01wb |
0x79DC |
0x79E4 |
2048 |
| 13 | 01wb |
0x81E4 |
0x81EC |
2048 |
| 14 | 01wb |
0x89EC |
0x89F4 |
1472 |
视频 stream number 是 00,后缀 dc 表示 compressed video;音频 stream number 是 01,后缀 wb 表示 waveform bytes。本例实际共有 12 个音频块,其中前 11 个长度 2048,最后一个 1472:
1 | 11 * 2048 + 1472 = 24000 bytes |
表中共有 15 个块,即 3 个视频加 12 个音频。每个长度均为偶数,所以本例媒体块没有额外一字节 pad;通用解析器仍必须保留奇数长度对齐公式。
idx1 完整实例
Chunk Header 与条目数量
idx1 从 0x8FB4 开始:
1 | 69 64 78 31 F0 00 00 00 |
FourCC 是 idx1,payload size 0xF0=240。每条 AVIOLDINDEX_ENTRY 固定 16 字节,因此:
1 | entry_count = 240 / 16 = 15 |
条目数量与 movi 中 15 个媒体块完全一致。
偏移基址实测
本文件的 dwOffset 相对于 movi List Type 字段的起点 0x26FA,并指向媒体 chunk header。例如第一项 offset 为 4:
1 | base + dwOffset = 0x26FA + 0x00000004 = 0x26FE |
0x26FE 的四字节确实为 00dc,后续 DWORD 长度为 896。第二项:
1 | 0x26FA + 0x0000038C = 0x2A86 |
0x2A86 是第一个 01wb chunk header,长度 2048。这个反验同时确定了基址和目标指向 chunk header,而不是 payload。其他 AVI 写入器可能采用另一历史基准,因此通用修复器应通过多个条目的 FourCC/size 一致性选择基址,不能把本例的 movi+4 约定写死为全部文件唯一规则。
15 项索引表
| 项 | ckid |
dwFlags |
dwOffset |
dwSize |
目标 Header |
|---|---|---|---|---|---|
| 0 | 00dc |
0x10 |
0x0004 |
896 | 0x26FE |
| 1 | 01wb |
0x10 |
0x038C |
2048 | 0x2A86 |
| 2 | 01wb |
0x10 |
0x0B94 |
2048 | 0x328E |
| 3 | 01wb |
0x10 |
0x139C |
2048 | 0x3A96 |
| 4 | 01wb |
0x10 |
0x1BA4 |
2048 | 0x429E |
| 5 | 00dc |
0x10 |
0x23AC |
894 | 0x4AA6 |
| 6 | 01wb |
0x10 |
0x2732 |
2048 | 0x4E2C |
| 7 | 01wb |
0x10 |
0x2F3A |
2048 | 0x5634 |
| 8 | 01wb |
0x10 |
0x3742 |
2048 | 0x5E3C |
| 9 | 01wb |
0x10 |
0x3F4A |
2048 | 0x6644 |
| 10 | 00dc |
0x10 |
0x4752 |
896 | 0x6E4C |
| 11 | 01wb |
0x10 |
0x4ADA |
2048 | 0x71D4 |
| 12 | 01wb |
0x10 |
0x52E2 |
2048 | 0x79DC |
| 13 | 01wb |
0x10 |
0x5AEA |
2048 | 0x81E4 |
| 14 | 01wb |
0x10 |
0x62F2 |
1472 | 0x89EC |
dwFlags=0x10 包含关键帧标志。MJPEG 每帧独立,因此三个视频项都是安全随机访问入口;PCM 音频项也不依赖压缩预测状态。对于 H.264 AVI,不能看到所有项都写 0x10 就盲信,仍应检查 sample 是否包含 IDR/恢复所需参数集。
索引原始字节开头
1 | 00008FB4 69 64 78 31 F0 00 00 00 30 30 64 63 10 00 00 00 |
从 0x8FBC 才是第一条 entry。其四个字段依次为 30 30 64 63、10 00 00 00、04 00 00 00、80 03 00 00。若把 idx1 的八字节 chunk header 也算进第一项,后续所有字段都会错位。
idx1 校验算法
1 | validate_idx1(idx, movi_candidates): |
修复器不应为了让某项匹配而对不同 entry 使用不同基址;同一 idx1 应形成统一坐标系。对于 LIST rec 嵌套等情况,目标验证还要确认 chunk 最终位于合法 movi 子树内,而不是只要文件某处 FourCC 相同就接受。
OpenDML 文件布局实例
首段
1 | RIFF('AVI ') |
后续段
1 | RIFF('AVIX') |
Standard index物理位置可依写入策略,但super index offset必须准确。每段独立闭合。
兼容 idx1
写入器可在首段提供传统idx1帮助旧reader播放首部分,同时完整播放器使用OpenDML覆盖全文件。两个索引重叠部分应一致。旧reader忽略AVIX是能力限制,不说明文件损坏。
AVIX 十六进制识别
AVIX Header
1 | 52 49 46 46 xx xx xx xx 41 56 49 58 |
表示RIFF、段size、AVIX。段size为little-endian,从form type到段末。找到字节串后仍需确认它位于上一顶层RIFF aligned_end,不能在movi payload中盲搜。
后续 LIST
AVIX内常见:
1 | 4C 49 53 54 yy yy yy yy 6D 6F 76 69 |
即LIST size和movi type。子媒体chunk按普通RIFF规则。AVIX不需要重复完整hdrl;轨道描述来自首段。
Index Duration 实例
视频
视频stream rate=25、scale=1,一个standard index覆盖250 frame,则duration通常250 time units,对应10秒。Super entry的dwDuration累加到strh length应约为总frame。
PCM
48kHz PCM scale=1、rate=48000,一个子索引覆盖10个chunk,每chunk1024 sample frames,则duration=10240,而不是10。时间约0.213333秒。
Variable Audio
压缩audio若dwSampleSize=0,每chunk duration需由codec/AVI约定推导。若standard index只能存聚合duration,写入器从实际packet sample count累计,不能用bytes。
第十五章 AVI Header 修复事务
只读扫描
第一阶段不修改原件:解析所有可闭合RIFF/LIST/chunk,记录最后完整offset、stream headers、媒体sample和索引。损坏点后不假设连续,按有界顶层恢复策略寻找AVIX。
建立候选模型
从媒体扫描生成轨道sample表、时长、最大chunk和关键帧。比较原idx1/OpenDML,标记可复用entry。缺codec参数时模型保留unknown,不填默认值。
写新文件
按候选模型写全新AVI/OpenDML,复制验证后的payload range,生成新header/index。原件不原地改,避免修复中断导致二次损坏。
独立验证
用另一parser从新文件零点全量检查,逐sample hash与源range比较,解码抽样/全量,核对duration。通过后再交付。
中断录制恢复
未回填 RIFF Size
录制崩溃常留下占位size。若chunk序列完整到EOF,可按扫描结束重算各父size。若最后chunk只写部分payload,截到其header前或保留完整前缀,不能把短payload假装声明长度完整。
缺 idx1
扫描movi重建。视频关键标志需codec parser,音频duration按format。设置avih HASINDEX并追加idx1/OpenDML后更新所有父size。
缺 hdrl
只有movi字节通常无法唯一恢复分辨率、codec、采样率、时间基。JPEG frame可提供图像尺寸,PCM无法自描述采样率。需要旁车/用户配置,报告推断而非标准恢复。
多 AVIX 部分丢失
Super index可能指向未完成子索引;忽略失效entry,扫描现存段媒体。dmlh总frame重算为实际恢复数据。时间洞是否保留需由录制时间戳/业务选择。
恢复扫描的信任层级
正常解析与恢复解析必须分开
标准读取器把 RIFF/LIST/Chunk size 当硬边界;恢复器在证据表明父 size 未回填时,才可在受控范围内尝试超出声明边界的扫描。两种模式不能混在一起,否则普通恶意文件可用伪 FourCC 诱导越界。
建议证据等级:
| 等级 | 证据 | 可执行行为 |
|---|---|---|
| Confirmed | Header、size、父范围、目标 Codec语法全部闭合 | 直接保留 |
| Structurally valid | RIFF Chunk闭合但 Codec尚未深验 | 保留并标记待验证 |
| Recovered candidate | 父 size失真,通过多项启发式找到 | 仅复制到候选模型,必须二次验证 |
| Truncated | Header存在但 payload不足 | 按媒体类型决定截短或丢弃 |
| Unknown | 无法证明边界/格式 | 不作为标准媒体输出 |
恢复日志对每个 Chunk 保存来源等级。最终文件只写通过策略的范围,不能把启发式候选伪装成原始标准结构。
父 Size 未回填
RIFF Root 占位值
崩溃文件可能有:
1 | RIFF ckSize = 0 |
恢复器先验证最小 Header 和 form type,然后以文件末尾为临时外层上限,但仍优先解析已闭合 child。若存在多个 RIFF/AVIX 根,只能在找到可信下一根边界后结束前一段;不能把整个文件尾都归给第一个 RIFF。
重算公式:
1 | riff_ckSize = recovered_riff_end - (riff_start + 8) |
LIST size 包含四字节 list type。结果必须放入 uint32;否则该范围不能写成单个 RIFF 根,应按 OpenDML 重新分段。
Stale Size 指向媒体中间
若声明 movi end 落在某个已验证 media chunk 中间,说明 size 与物理序列冲突。恢复器不能在声明 end 处把 payload 当新 Chunk。应从 movi 数据起点顺序验证 chunk:
1 | next = header + 8 + ckSize + (ckSize & 1) |
只要每一步都闭合并且 FourCC stream编号/后缀与已知轨道一致,就可建立 recovered end候选。遇到无法闭合处停止,不在任意 payload 内无限搜索。
Chunk Header 重同步
为什么只搜索 FourCC 不可靠
MJPEG、PCM、H.264 payload 都可能含 30 30 64 63(00dc)等四字节。若简单扫描字节串,会把媒体内部误判为 Chunk Header。候选必须同时满足:
1 | candidate offset is compatible with RIFF word alignment |
单个候选分数不足时,要求连续两到三块闭合。PCM 候选 size 还应整除 blockAlign;MJPEG候选应有合法 SOI/SOF/SOS/EOI关系;未压缩 DIB 应满足推导 frame size;H.264 需按已知映射验证 NAL边界。
有界搜索窗口
丢失一个 Chunk size 或局部损坏时,可以从最后可信 end向后在有限窗口搜索下一个候选,例如若干 MiB或应用配置上限。窗口耗尽则停止当前 segment。不得对数百 GiB 文件进行 O(n²) 回溯评分。
候选选择记录:偏移、对齐、FourCC、size、下一边界、Codec验证、Index匹配和总分。若两个候选同样可信,恢复结果有歧义,要求外部选择或截在前一可信点。
最后一个 Chunk 截断
通用判定
假设 Header 声明 payload N 字节,文件仅剩 R:
1 | available = file_end - payload_start |
不能把 Header 的 N 改成 R 后无条件保留,因为 R 可能终止在半样本、熵编码中间或一个未完成磁盘写扇区。
PCM 截断
已知 PCM blockAlign=A 时,可恢复完整前缀帧:
1 | recoverable = floor(R / A) * A |
例如 stereo S16,A=4,声明 2048,实际 1502:
1 | recoverable = floor(1502/4)*4 = 1500 |
新文件写一个 ckSize=1500 的 wb chunk并更新 stream length/index。丢弃两字节要在报告中明确;不能与下一物理区域拼成一帧,除非有强证据证明只是 Chunk分界写错。
MJPEG 截断
JPEG Frame 只有在 SOI 到 EOI 及 Marker长度/Scan语法闭合时才作为完整 dc 保留。文件剩余中存在 EOI 不一定证明完整,因为压缩数据中的转义和 Marker结构仍需解析。如果 EOI 在声明范围的完整前缀内,且后续仅为未写满/垃圾,可按已验证 EOI 结束位置裁剪 Frame;否则丢弃该 Frame。
MJPEG 独立帧被丢弃不会破坏后续 Frame预测状态,但时间线上会少一帧。是否保留时间空洞需要外部录制时间信息;单纯 AVI 顺序扫描通常只能生成连续恢复时间线并报告丢失。
H.264/预测视频
截断 VCL NAL/访问单元通常丢弃整个 sample。即使后续 Chunk完整,直到下一个可验证随机访问点之前的预测链可能受污染。恢复器保留字节与“安全解码起点”是两个不同判断:可保留后续非 IDR Sample用于高级错误恢复,但 Index Keyframe只能标记真实可随机访问样本。
缺失或损坏 idx1 的重建
从 movi 生成记录
对每个确认 media chunk记录:
1 | stream number |
传统 idx1 offset基址在历史文件中存在变体。新写恢复文件时选择一个明确兼容 profile,并从新文件的 movi坐标重新计算,不能复用原损坏文件 offset。每项 dwSize 用新 payload size,尤其最后截短 PCM chunk。
Keyframe 恢复
| 媒体 | 判定 |
|---|---|
| PCM | 每块独立,但 Seek 仍需 frame对齐 |
| MJPEG | 完整 JPEG Frame独立,通常 keyframe |
| 未压缩 DIB | 独立 Frame;paletted模式需恢复调色板状态 |
| H.264 | 解析 NAL/Slice/参数集,I Slice不自动等于安全入口 |
| 未知 Codec | 不猜;Index Flag标记未知/保守值 |
不能把原 idx1 所有 0x10 原样复制到新文件而不验证。索引损坏可能正是恢复原因。
OpenDML 重建
大于 Legacy profile上限或包含 AVIX 时,为每条 stream/segment 重新分组 Standard Index,再构造 Super Index和 duration。所有 qwOffset 以新文件绝对位置写入。dmlh 使用恢复的主视频 Frame数;strh length按各轨道恢复 duration。
Header 计数恢复
可直接重算
从确认媒体可重算:
1 | observed_video_frames |
不能仅靠 payload 唯一恢复
1 | PCM sample rate without strf/sidecar |
例如 24000 字节 PCM 可以是 12000 mono S16帧,也可为 6000 stereo S16帧或 24000 mono U8帧。没有 Header/旁车,不能宣称其中一个是标准恢复结果。
修复输出事务
原件只读
恢复器默认不原地改损坏文件。创建新文件,按候选模型写 Header、媒体和索引。媒体 payload复制前确认源范围:
1 | source_offset + source_length <= source_file_size |
复制后对每个保留 payload计算 hash,确认新文件解析出的 payload与源确认范围相同。行 padding/RIFF pad是否包含在 hash要明确:媒体 hash通常只含 ckSize payload。
自底向上写 Size
先写 child payload/Chunk,关闭 LIST,再关闭 RIFF。最终回填 Header计数与 Index。写完后用独立解析器从零开始,禁止复用恢复扫描内存树作为唯一验证,否则同一个边界 Bug可能同时影响写入与“验证”。
恢复清单
最终报告至少包括:
1 | source file hash and size |
任何使用外部配置的字段都标记来源,不能写成“由标准自动恢复”。
AVI Compatibility Decision
AVI 1.0 Reader
要求首RIFF AVI、hdrl、movi,通常依赖idx1且只处理首段/32位offset。遇AVIX可能忽略。文件目标若是旧设备,应限制size、codec、stream数量和索引基准。
OpenDML Reader
识别odml/dmlh、super/standard index和后续AVIX,使用64位file offset。仍可读普通AVI 1.0作为子集。不能因没有odml就拒绝小文件。
顺序容错 Reader
即使索引损坏,可扫描movi/AVIX顺序播放,但seek慢且keyframe未知。该模式输出warning,不能用成功播放证明索引正确。
写入 Profile
写入前明确目标:Legacy AVI 1.0、OpenDML with legacy idx1或OpenDML-only。Profile决定段阈值、索引、codec约束和测试播放器集合。
AVI Conformance Checklist
RIFF
所有size闭合、奇数padding存在、LIST有type、顶层form合法、AVIX从顶层边界开始。Unknown chunk安全跳过。
Header 一致性
avih streams与strl一致;strh type/rate/scale/length合法;strf结构与handler相容;视频尺寸/音频block字段一致;保留字段符合要求。
Media
每媒体ID映射有效stream,payload在movi父范围,PCM对齐、JPEG/H.264边界合法,零/无时间chunk语义明确。
Index
idx1长度16倍、统一base、目标FourCC/size匹配;OpenDML类型/subtype/entry width/count合法;64位offset、duration和super→standard→media链闭合。
Timeline
avih/strh/dmlh/index/实际sample duration相容,音视频尾差解释,interleave满足目标buffer,seek落在安全入口。