OpenDML AVI 2.0 大文件扩展详解

OpenDML 解决什么问题

传统 AVI 建立在 RIFF 的 32 位 size 和 offset 字段之上。当视频码率较高、录制时间较长时,单个 RIFF 结构接近 4 GiB,idx1stco 一类 32 位偏移无法表示后续数据。OpenDML AVI 2.0 不是完全替换 AVI 1.0,而是在原有 hdrlstrlmovi 思路上增加大文件扩展:用 AVIX 分段保存媒体数据,用 indx 主索引和 ix## 标准索引描述每个分段中的 chunk。

因此 OpenDML 文件仍然可能以 RIFF('AVI ') 开头,也仍然有 avihstrhstrf 和视频/音频 chunk。区别在于:不能假设只有一个 movi,不能只寻找一个 idx1,也不能把所有偏移强制塞进 32 位整数。

文件总体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RIFF('AVI ')
├── LIST('hdrl')
│ ├── avih
│ ├── LIST('strl')
│ │ ├── strh
│ │ ├── strf
│ │ └── indx(该轨道的主索引)
│ └── LIST('odml')
│ └── dmlh
├── LIST('movi')
│ └── 第一段媒体 chunk
├── idx1(可选的传统索引)
├── RIFF('AVIX')
│ └── LIST('movi')
│ └── 后续媒体 chunk
└── RIFF('AVIX') ...

每个 RIFF('AVIX') 都是一个独立的 RIFF 段,但它不是一个新的完整 AVI 文件。它的 form type 是 AVIX,通常包含一个 LIST('movi'),用于承载超过第一段容量限制的连续媒体数据。读取器必须按照段出现顺序连接时间线。

odmldmlh

LIST('odml') 通常位于主 hdrl 中,里面的 dmlh(DML Header)提供扩展文件的总帧数等信息。传统 avih.dwTotalFrames 是 32 位字段,OpenDML 通过 dmlh 允许保存更大的帧数。两者不一致时,播放器应优先按 OpenDML 规范和实际索引判断,不能简单把较小值当成真实长度。

分段规则

写入器通常先建立一个主 RIFF('AVI '),当当前 RIFF 接近实现定义的安全上限时关闭它,回填 size,再开始新的 RIFF('AVIX')。每个段内部的 Chunk 仍使用 ckid + ckSize + payload + padding,因此旧播放器可能能播放第一段,也可能在遇到 AVIX 后停止。OpenDML 播放器则把所有段视为同一条轨道的连续数据。

分段边界不能切断一个 Chunk。写入器必须先写完整的音频/视频 sample,再判断是否需要切换段。如果一个视频帧特别大,即使剩余空间不足,也应将整个 chunk 放入下一段,而不是把一个 chunk 拆成两个具有相同 ckid 的普通 chunk。索引中的 dwOffsetdwSize 必须对应完整实体。

indx 主索引

每条轨道可在 strl 中放置一个 indx。它的作用不是直接列出每个媒体 sample,而是列出各个 OpenDML 子索引的位置、覆盖的 chunk 数量和相关时间信息。常见字段包括:

字段 作用
wLongsPerEntry 每个索引项包含多少个 32 位 long
indexType 表示标准索引或其他索引类型
nEntriesInUse 当前有效索引项数量
dwChunkId 对应轨道 chunk ID,如 00dc
dwReserved 保留字段
qwBaseOffset 子索引偏移计算基准
dwReserved3 保留字段

主索引的 entry 通常指向 ix## 子索引,具体偏移和大小按 OpenDML 结构解释。64 位字段必须使用无符号 64 位变量保存,不能先转成 long 或指针大小相关的类型。

ix## 标准索引

标准索引通常以 ix00ix01 等 FourCC 命名,前两位数字对应轨道编号。它的结构包括一个 Index Header 和若干 Index Entry。Header 用于说明索引版本、索引类型、chunk ID、时间基和 entry 数量;Entry 用于描述一个媒体 chunk 的偏移和长度。

典型 Entry 可抽象为:

字段 长度 作用
dwOffset 4 相对于 qwBaseOffset 的偏移
dwSize 4 chunk 数据大小及关键帧标志

某些实现会在 size 的高位使用标志位表示非关键帧,读取时必须先提取标志,再取低位真实长度。不能把包含标志的整个整数直接当作 payload 字节数。

64 位偏移计算

如果索引给出 qwBaseOffset 和 32 位 dwOffset,实际 chunk 地址通常按规范计算:

1
absolute_offset = qwBaseOffset + dwOffset

计算前要检查加法溢出,并确认结果小于文件长度。OpenDML 解决的是“文件位置可表达范围”,不代表每个字段都自动变成 64 位;dwSize、轨道编号和标志仍可能是 32 位,因此长度和偏移必须分别处理。

与传统 idx1 的关系

OpenDML 文件可以同时存在旧式 idx1。它可能只覆盖第一段,也可能由某个写入器提供兼容性索引。播放器不应认为存在 idx1 就可以忽略 indx/ix##。正确策略是:优先使用能够覆盖全部段的 OpenDML 主索引;如果缺失或损坏,再尝试传统索引和顺序扫描。

时间和帧号连续性

AVIX 分段时,轨道时间不能从零重新开始。每个段只是文件存储边界,不是媒体时间线边界。读取器应将索引 Entry 按轨道顺序合并,依据 strh.dwScale/dwRate 计算统一时间。音频和视频仍然分别拥有自己的 sample 数和时间基,不能因为它们出现在同一个 AVIX 段就假设一一对应。

大文件写入流程

写入器首先确定视频编码、音频编码和轨道参数,写入主 hdrl 以及每条轨道的 strhstrf 和主 indx 占位。写入 movi 时为每个 Chunk 记录:所属轨道、文件绝对偏移、payload 长度、关键帧状态和媒体时间。当段达到安全阈值时,关闭当前 LIST/RIFF,生成该段的 ix##,再开始新的 AVIX。所有段完成后,回填 dmlh、主 indx、avih 统计值和每个 RIFF/LIST 的 size。

读取流程

读取器先遍历主 RIFF 的 hdrl,建立轨道和编码描述;再查找 odml/dmlh、每条轨道的 indx、主 movi 以及所有 AVIX 段。随后解析主索引指向的 ix##,把每个索引项转换为绝对文件偏移。读取 chunk 时校验 FourCC、size、轨道号和关键帧标志,再把 payload 交给对应解码器。索引项指向段外、chunk size 超过父 RIFF 或时间出现倒退时,应报告文件损坏。

十六进制识别思路

主段通常以:

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

后续段以:

1
52 49 46 46 ?? ?? ?? ?? 41 56 49 58

其中 41 56 49 58 是 ASCII AVIX。发现 AVIX 后,解析器应重新建立当前 RIFF 段边界,但不能重置轨道时间、sample 序号或全局统计。ix##indx 的位置不能靠字符串搜索直接解释,必须遵守索引 Header 的 size 和 entry count。

损坏恢复和兼容性

indx 损坏但 movi 数据仍在时,可以逐段扫描 00dc00db01wb 并重新构建索引。扫描必须使用 Chunk size 跳过 payload 和 padding,并限制单个 Chunk 最大长度。若某个 Chunk 的 size 越过当前 RIFF 边界,应停止当前段扫描,不能跨越到下一个 AVIX 猜测数据。对关键帧标志缺失的视频,可以通过解码器探测或编码码流内部标志进行降级判断,但结果应标记为恢复索引而不是原始索引。

AVI 1.0 与 OpenDML 对比

项目 AVI 1.0 OpenDML AVI 2.0
主容器 RIFF('AVI ') 主 AVI + RIFF('AVIX')
媒体列表 一个 movi 为主 多个分段 movi
索引 idx1 indx + ix##,可兼容 idx1
大小能力 受 32 位限制 支持大文件和 64 位基准偏移
播放兼容性 老设备较好 需要 OpenDML 支持
恢复方式 顺序扫描 movi 按段扫描并重建子索引

实现时的边界条件

OpenDML 的实现难点不在于识别 AVIX 四个字符,而在于把容器边界、索引边界和媒体时间线同时维护正确。写入器应给每个 RIFF 段保存独立的起始文件偏移和当前写入位置;给每条轨道保存当前 sample 序号、当前时间、当前子索引 entry 数量以及关键帧状态。当段切换发生时,只关闭存储段,不重置轨道状态。这样下一个 AVIX 中的第一帧仍然可以沿用前一段之后的时间戳和 sample 序号。

读取器需要区分三种长度:RIFF 段长度、LIST 长度和媒体 Chunk 长度。RIFF 段长度限制搜索范围,LIST 长度限制子 Chunk 遍历范围,Chunk 长度限制 payload 读取范围。任何一个长度字段发生整数溢出,都可能使解析器跳出父容器;因此先做无符号加法溢出判断,再把结果转换成文件偏移。索引中的 dwSize 如果高位带有关键帧标志,必须先清除标志位再进行边界检查。

索引重建实例

假设主段中有两个视频 Chunk,第一段 movi 的绝对起点为 0x1000,扫描得到 00dc 位于 0x1100、长度 0x2000,第二个 00dc 位于 0x3108、长度 0x1A00;第二个位置还要考虑前一个 Chunk 为偶数长度而无需 padding。写入器可以为它们建立两个 ix00 entry,并把主 indx 的一个 entry 指向这份 ix00。当第二段 AVIX 从 0x10000000 开始时,子索引可以把 base offset 设为该段的 movi 数据起点,entry 只保存相对偏移。读取时用 base 加 offset 得到绝对地址,再验证该地址的 FourCC 是否与 dwChunkId 一致。这个“索引指向数据、数据反向验证索引”的闭环,是防止损坏索引误读的关键。

与播放器的兼容策略

OpenDML 索引的字段级解释

indx 超级索引头

超级索引是一个 OpenDML 标准索引结构,头部首先说明索引类型、条目大小、条目数量、chunk ID、时间基以及可选的 base offset。索引类型决定条目是指向标准索引 chunk,还是指向其他索引层级;条目大小不能被实现写死为某一个版本的常量。读取器应依据 wLongsPerEntrybIndexSubTypebIndexType 计算每个 entry 的有效字段,并跳过标准允许的保留区域。

每个超级索引 entry 通常包含子索引文件偏移、子索引大小以及该子索引覆盖的时间/样本数量。偏移是 64 位 little-endian 数值,大小字段可能包含子索引 chunk 的头部。验证时应同时检查偏移指向的 FourCC、子索引的 dwChunkId 和 entry 数量,不能只检查地址落在文件范围内。

ix## 标准索引头

标准索引通常存放在对应 movi 段附近,FourCC 形式为 ix00ix01 等。索引头给出被索引的媒体 chunk ID、时间基、entry 数和基准偏移。entry 的偏移通常是相对 base 的偏移,大小字段的最高位可能用于表示关键帧;计算实际 payload 长度前必须清除该标志位。

1
2
absolute_chunk = base_offset + (entry_offset & offset_mask)
payload_size = entry_size & ~keyframe_flag

具体标志位取值必须依据 OpenDML 规范和索引类型处理,不能将所有格式的最高位都无条件当成关键帧位。对音频索引,关键帧语义可能不存在,读取器应根据 dwChunkId 和轨道类型选择解释。

跨 AVIX 段的时间线

段切换与轨道连续性

AVIX 只是存储段边界,不是新的媒体文件。视频 sample 序号、音频采样位置和轨道时间应跨段连续。切换段时,写入器需要关闭当前 RIFF size、写入必要的索引或索引占位,然后开始新的 RIFF AVIX;不能重置 dwLength、frame number 或时间戳,否则播放器会在段边界重复播放或发生音视频跳变。

分段损坏的恢复

如果中间某个 AVIX 段被截断,读取器仍可尝试播放前面的完整段。恢复器应以 RIFF size 和实际 EOF 中较小者作为该段上界,扫描至最后一个完整 chunk,并将后续段标记为未知。不能把下一个 RIFF AVIX header 当成上一个媒体 chunk 的 payload。修复后若把截断段重新封闭,必须同步更新总帧数和索引覆盖范围。

OpenDML 写入器设计

段容量与切换阈值

写入器应在接近 32 位 RIFF 或 LIST size 上限前主动切换段,而不是等写入失败。阈值需要为 RIFF header、索引、padding 和可能的最后一个完整 chunk 预留空间。每条轨道的 chunk 不应被拆成跨段的半个 chunk;如果下一帧无法完整写入当前段,应先关闭当前段,再把整帧写入新的 AVIX。

索引延迟写入

超级索引可能需要在文件末尾才能知道全部子索引的位置,实时写入时可以先保留可回填区域,或在结束时追加索引并更新头部引用。异常断电时,已写入媒体数据和部分子索引仍可能可恢复。工具应允许“扫描媒体重新生成索引”,而不是要求原有超级索引完整才能读取。

OpenDML 解析伪代码

1
2
3
4
5
6
7
8
9
read RIFF header
require type == AVI or AVIX
while segment_end has bytes:
read top-level chunk/list with RIFF alignment
if LIST type == hdrl: parse headers and track descriptions
if LIST type == movi: scan media chunks and record absolute offsets
if LIST type == odml: parse dmlh and index metadata
if type == indx or ix##: parse index and validate references
at next RIFF header: start a new segment without resetting tracks

扫描得到的媒体样本表是验证索引的独立依据。索引声明的样本数、偏移、大小和关键帧标志都应与扫描表逐项比较;若二者不一致,应分别报告“索引错误”和“媒体数据错误”,避免用错误索引反向修改原始媒体。

OpenDML 的错误分类

结构错误

结构错误包括 RIFF size 小于 header、LIST 越界、chunk size 越过段边界、奇数 padding 缺失以及 AVIX header 不完整。这些错误发生在容器层,不应归因于视频编码器。

索引错误

索引错误包括超级索引指向不存在的子索引、ix## 的 chunk ID 与轨道不匹配、entry 偏移越界、大小标志未清除和样本数不一致。顺序扫描通常可以绕过这类错误,但随机定位结果不可信。

编码数据错误

编码数据错误包括 JPEG 缺少 SOI/EOI、H.264 NAL 截断、音频块不满足 nBlockAlign 或 codec extradata 缺失。即使所有 OpenDML 索引都正确,编码错误仍可能导致解码失败,因此验证必须同时包含容器和 codec 层。

播放器可以按能力分级:第一层支持单 RIFF AVI 和 idx1;第二层支持 AVIX 但只做顺序扫描;第三层完整支持 indx/ix##、随机定位和关键帧搜索。遇到 OpenDML 文件时,不能因为传统 idx1 缺失就直接判定文件不可播放;可以先检查 odmlindxAVIX,再选择索引播放或顺序扫描。对外报告错误时,应区分“格式不支持”“索引损坏”“媒体 Chunk 越界”和“编码器不支持”,否则调用方无法决定是修复容器还是更换解码器。