资讯动态

深入解析avilib:轻量级AVI读写库源码与VS2019编译避坑指南

发布时间:2026/9/8 11:09:31 来源:尧图企业网站定制
简介这份压缩包收录了avilib早期版本的C源码文件包含一个头文件与一个C源文件总大小仅10KB面向需要在C或Qt环境中处理音视频交错AVI数据的开发者。头文件集中声明了打开AVI、读取帧率与分辨率等元数据、逐帧解析音视频流、创建并写入帧数据等接口源文件则实现了全部底层逻辑并对帧索引、流信息管理、文件头解析等关键细节给出了可读性较高的示例代码两类文件分工明确可直接嵌入工程使用。库支持从已有视频中分析帧数据也能初始化空白AVI文件、添加音视频流、逐帧写入并最终完成文件索引适配视频读写、格式分析、多媒体编辑等场景同时可为理解容器封装原理提供参考便于在此基础上继续扩展。代码结构简洁、无多余依赖所有功能集中在两个源文件中便于阅读、调试与按需裁剪。已有373人学习对希望深入理解AVI文件结构、学习C声明与实现分离设计或计划在Qt项目中集成视频处理能力的开发者来说是一份很有价值的轻量经典资源。 说到 AVI 文件的底层读写很多人第一反应就是上 FFmpeg但老项目里经常还躺着一个轻量级的老朋友avilib。这套由国外开发者最初发布的 avilib.h 和 avilib.c在 C 工程里常被直接拿来做 .cpp 文件使用源码一直是我处理简单 AVI 封装格式时的首选。它没有庞大依赖核心就七八个 API打开、读帧、写帧、关闭功能刚好覆盖最基本的 AVI 容器操作特别适合做视频采集端的快速封装或者教学演示 RIFF 结构。这篇文章我打算把这套经典源码的核心文件从头过一遍说清楚 h 文件里的数据结构怎么用cpp 文件里那些偏移量计算和索引构建是在干什么。另外很多人在 VS2019 里把 avilib.c 改名成 .cpp 加入工程后一编译就报警告甚至直接报错而且只要在源文件里加了中文注释就必现。这个坑我会单独拎出来讲透顺便给出我实际用过的几种解决办法。内容偏向实践适合正在读这套源码、或者想了解老式 AVI 读写库原理的朋友。1. 这套老代码到底解决什么问题1.1 什么是 avilib为什么现在还值得看avilib 最早是伴随 Linux 下的视频采集工具链出现的定位非常明确把 AVI 文件的封装和拆封做到最简。AVI 本质上是 RIFF 格式的一种具体化而 RIFF 可以理解为一种“带标签的块状容器”。你往里头放视频流、音频流每个流的数据用 FourCC 标识最后再写一个索引块方便随机定位。FFmpeg 对这种容器的处理是全功能级的但如果你的需求只是“把 YUV 帧压缩成 MJPEG 后按 AVI 存盘”或者“从 AVI 里逐帧读出来做二次处理”avilib 这种小工具反而更好。最初版 avilib 的代码风格和现在动辄抽象出各种 Context 结构的库完全不同它是典型的 C 风格过程式设计。一个核心结构体avi_t把文件句柄、流参数、帧索引全包在一起函数签名直来直去几乎不需要额外学习成本。这一点在嵌入式设备或者老平台编译时尤其占便宜整套代码拉下来就能编不用考虑依赖链问题。我现在回过头去看这套 h 和 cpp 文件最大的感受是它把“读 AVI”和“写 AVI”两种模式统一在一个抽象层里。同样的结构体打开输入文件时填充的是解析信息创建输出文件时填充的是写入参数和索引缓冲区。这种设计现在看有些粗糙但逻辑非常直白读源码就像看一张摊开的地图。1.2 它和你自己写的 AVI 解析代码有什么不同很多人第一次尝试写 AVI 解析都是从读 12 字节的 RIFF 头开始的先读RIFF四个字节再读文件大小然后读AVI类型标识接着进入hdrl列表找avih和strl。这套流程手工实现并不难但坑都在后面AVI 的 chunk 有对齐填充字节奇数长度块尾补一个 0x00movi列表里的rec列表可能嵌套idx1索引项的 offset 字段在不同版本编码器下含义不一致。avilib 的价值就在于它把这些边角问题都处理完了。你调用AVI_read_frame的时候它内部会通过frame_offset和frame_size从索引里定位根据上次读取的位置决定是顺序读还是 seek并且辅助处理了音视频交错带来的偏移问题。这些逻辑自己写一遍也能跑但往往要踩几个真实文件的坑才能稳定下来。直接参考老外最初版的实现能省掉很多测试样本的积累过程。而且这套源码对“只写一个临时 AVI 文件”这种场景做了专门优化。AVI_write_frame并不要求你预先知道每一帧的大小它会动态扩展索引数组最后关闭文件时统一计算偏移并追写idx1块。这点对实时视频采集尤为实用因为编码器出来的帧大小本身就有波动一次性写死的固定表根本行不通。2. 动手前先拆解avilib.h 的关键数据结构2.1 avi_t 结构体一切操作的中枢打开 avilib.h排在前面的一定是avi_t这个结构体定义。老外最初版里的写法很朴素成员命名也直白我这里挑几个关键字段说方便你对照源码阅读。avi_t里有两类核心成员。一类是文件级的状态信息比如int mode标识当前是读模式还是写模式int fdes保存文件描述符long int size是文件总长度long int movi_start和long int movi_end记录movi列表在文件中的起止位置long int last_pos用于记录上一次读写位置。另一类是流参数信息包括int width, height、double fps、int video_frames、int audio_bytes以及面向音频的int audio_chunks、audio_rate、audio_bits等。读模式时movi_start和movi_end是从hdrl里的偏移推算的写模式时这两个值是在写完hdrl列表后动态记录的后续写帧数据都从movi_start往后排。理解了这个设计你再看AVI_seek这类函数就能猜到它内部是拿帧号去索引表里找偏移而不是真的去扫描movi块。索引表就是结构体里的long int *index数组每一项对应video_frames里的一个帧记录。2.2 对外 API 的声明细节avilib.h 中函数声明不多但每一个都有明确分工。常用的有这么几个AVI_open_input_file(const char *filename, int dbg)打开已有 AVI 文件并解析头部信息。第二个参数是调试级别传 0 表示静默传大于 0 的值会把解析过程中的关键偏移量打印出来。AVI_close(AVI_t *AVI)关闭文件。写模式下会先完成索引落盘再释放索引内存。AVI_read_frame(AVI_t *AVI, char *vidbuf, int *keyframe)读取当前视频帧数据到vidbufkeyframe返回是否为关键帧。AVI_write_frame(AVI_t *AVI, char *data, long bytes, int keyframe)写入一帧视频数据。AVI_set_video_params(AVI_t *AVI, int w, int h, double fps, const char *compressor)写模式前设置视频参数。AVI_set_audio_params(AVI_t *AVI, int channels, int rate, int bits, int format)写模式前设置音频参数。AVI_seek(AVI_t *AVI, long frame)按帧号定位下次调用AVI_read_frame就能读到指定帧。AVI_frame_size(AVI_t *AVI)获取当前帧字节数。这些函数在 h 文件里都有注释说明但老版本的注释是英文且没有讲调用顺序。我补充一下典型的调用序列写模式必须先AVI_open_output_file然后AVI_set_video_params和AVI_set_audio_params之后反复调AVI_write_frame最后AVI_close。音频帧的写入还有一个配套的AVI_write_audio这个函数在源码里走的是和视频不同的缓冲区逻辑直接写入movi块而不参与帧索引。2.3 宏定义和一些容易忽略的常量avilib.h 里还定义了一批宏最显眼的是AVI_READ和AVI_WRITE分别取值 0 和 1用于avi_t-mode。还有AVIERR_OK、AVIERR_BADFORMAT、AVIERR_MEMALLOC这类错误码全部是宏常量。这套代码的错误处理很原始函数返回 0 表示成功返回负值表示错误码没有 errno 那种全局状态。DBG宏也值得注意。如果你在编译时定义了DEBUG函数内部会有大量fprintf(stderr, ...)输出把 RIFF chunk 的 id、大小、偏移逐个打印出来。我调试实际文件时非常依赖这个输出它能直接看出 avilib 在哪个块上解析失败。对于自己改过源码的人建议保留这个宏排查问题能省一半力气。还有一个容易被忽略的细节avilib 支持的最大帧索引数由AVI_MAX_INDEX宏限制最初版里这个值通常设为 65536。也就是说最多缓存 65536 个索引项对普通视频足够但如果做长时间无人值守采集就可能爆。这种场景下你需要自己调大这个宏并重新编译提醒一下是因为很多人不看 h 文件顶部的宏定义等到运行时报内存错误才回头查。3. 核心实现拆解cpp 文件里的那些关键函数3.1 RIFF 块读取与头部解析流程avilib 的解析逻辑是从AVI_open_input_file开始的它会调用内部函数ReadAviHeader。这个函数的核心是一个getc/fread驱动的状态机循环读取 RIFF chunk 头先读 4 字节的 chunk id再读 4 字节的 chunk size然后根据 chunk id 分发处理。这里有几个实现技巧值得说说。首先是字节序问题AVI 文件强制使用小端little-endianavilib 在读取时没有用windows.h那套ntohs/ntohl而是直接用小端解码函数GetLong它从缓冲区里取四个字节按低地址到高地址拼成一个long。在 x86 架构上这个拼法就是直接内存拷贝但在大端 CPU 上你得小心代码里专门做了字节交换处理移植到嵌入式平台时注意别把这段优化掉。其次是LIST块的处理。RIFF 中的LIST块后面还会跟一个 4 字节的列表类型例如hdrl、movi、strl。avilib 内部用递归或循环嵌套来处理这些列表遇到hdrl就继续往里找avih和strl遇到strl就解析strh和strf。每次进入一个列表块都要记录当前文件偏移等子块解析完再回到列表末尾继续往后找。这个“块边界恢复”的逻辑如果写错解析就会错位avilib 处理得比较稳妥但你自己改代码时一定不要在中间直接fseek很容易破坏上下文。3.2 写模式下的索引构建机制写模式的核心是AVI_write_frame但真正收尾工作在AVI_close里。AVI_write_frame做的事情很简单把上一帧的索引项记录到avi_t-index数组记录项包含四字节的00dc/00db标识、帧长度、帧偏移、是否为关键帧然后写入当前帧数据到movi区。这里用的是延迟记录策略也就是写完当前帧才登记上一帧的索引因为当前帧的movi偏移在写入前是未知的。随后在AVI_close里索引被统一转成idx1块格式写回文件末尾。idx1每一项是 16 字节4 字节 chunk id4 字节标志位4 字节帧偏移相对于movi列表起始位置的偏移量4 字节帧大小。avilib 在转换时会从movi_start减去文件起始偏移这保证生成文件的idx1偏移能被主流播放器正确识别。这里曾经是个经典坑位直接写绝对偏移会导致某些播放器尤其是老版本 DirectShow 的 AVI Splitter无法 seek只能顺序播放。音频帧的写入走的是AVI_write_audio它不参与视频帧索引而是直接写入movi块同时在avi_t里维护audio_bytes和audio_chunks两个计数。关闭文件时这两个值会被填到strl列表的strh结构里保证播放器能拿到正确的音频流时长。如果你的 AVI 文件只写了视频没写音频audio_chunks保持为 0avilib 会在写hdrl时跳过音频流信息块整体结构依然合法。3.3 读帧时的 seek 策略与缓冲管理AVI_read_frame的实现值得详细说一下因为它涉及频繁的 seek 和缓冲判断。函数内部先检查当前帧号是否等于上次读取帧号加一如果是就直接顺序读下一个movi块否则调用AVI_seek定位到指定帧的偏移再读取。这种“顺读优先异常才 seek”的策略极大减少了磁盘寻道在硬盘还靠物理磁头的年代非常有效放到今天做本地文件读取依然是合理优化。帧数据读入后avilib 还会根据keyframe参数判断是否为关键帧。当调用者传入的vidbuf缓冲区不够大时函数会返回AVIERR_BUFFERTOOSMALL需要调用者根据AVI_frame_size重新分配。初次接触的人容易忽略AVI_frame_size是在AVI_read_frame成功后才更新的所以不要试图用上一帧的大小提前判断这一帧需要多少缓冲老老实实按返回值处理。还有一点是老版本里比较隐蔽的 bugAVI_read_frame在读取到异常块时有可能会停在错误偏移上导致后续连续读全部错位。我在源码里看到新一点的补丁版本已经修正了这一点会在块类型不匹配时尝试跳过当前块并继续。但最初版没有这个处理遇到损坏的 AVI 文件时表现就是读到一半突然失败。如果你要把这套代码用到自己的工具里建议给AVI_read_frame增加一个容错分支遇到非法块 id 时至少打印警告而不是直接 fatal。4. VS2019 工程里的一个常见问题中文注释引发编译报错4.1 问题根因文件编码与编译器默认代码页不一致这可能是 avilib 的 cpp 文件在 Windows 上最让人恼火的问题。你把老外的源文件加进 VS2019 工程本来编译好好的手一抖在文件头部加了一行中文注释比如“// 打开文件”再编译就报C2001: 常量中有换行符或者C4819: 该文件包含不能在当前代码页(936)中表示的字符。这些报错的根源不在代码逻辑而是源文件的编码格式和 MSVC 的源码字符集解释方式不匹配。VS2019 的默认行为是如果源文件没有 BOMByte Order Mark它就按当前系统 ANSI 代码页去解码文件内容。中文 Windows 默认代码页是 936GBK但 avilib 的老文件通常是以无 BOM 的 UTF-8 或者纯 ASCII 保存的。纯 ASCII 文件本身没问题因为 ASCII 字符在 GBK 和 UTF-8 下一致可一旦你改成 UTF-8 无 BOM 并写入中文注释MSVC 如果把它当 GBK 解码一个 UTF-8 中文字符通常三个字节就会被解析成 1.5 个 GBK 字符后面的引号、换行符就可能被“吃”进字符串里于是报出各种莫名其妙的 C2001。这个问题的恶心之处在于它报错的位置通常不在注释行而在注释行后面的代码行甚至成了“常量”的一部分。我曾经在一个 cpp 文件头部加了四行中文注释结果报错定位到第 50 行的函数定义处排查的时候一度以为是语法写错了。4.2 三种可靠解决方式从根上避免踩坑我在 VS2019 里处理这个问题试过三种方法都有效但适用场景不同。第一种是改文件编码为 UTF-8 with BOM。在 VS 里用“文件 - 另存为 - 编码保存 - Unicode (UTF-8 带签名) - 代码页 65001”保存后再编译MSVC 会优先读取 BOM 并按 UTF-8 解码中文注释不会再触发 C4819 或 C2001。这个方案最简单缺点是无法用在某些跨平台构建系统里因为很多 Unix 工具链对 BOM 支持不好会把 BOM 当成非法字符。第二种是给工程加/utf-8编译选项。在项目属性 - C/C - 命令行 - 其他选项里加上/utf-8它的作用是同时把源文件字符集和执行字符集设定为 UTF-8这样即使源文件是无 BOM 的 UTF-8MSVC 也能正确解释中文注释。这个方案对跨平台项目最友好因为源码文件本身不需要改动只需工程层面的配置修改。需要注意/utf-8是 VS2015 Update 2 之后才加入的VS2019 完全支持。第三种是治标不治本的办法就是别在 cpp 文件里写中文注释统一用英文注释或者把中文说明放到单独的 README 文档里。这个方法对 avilib 这种老代码尤其合适因为源码里本来就全英文没必要为了注释引入编码兼容问题。4.3 实操检查清单怎么快速定位是不是编码问题如果你在 VS2019 编译旧 cpp 文件时遇到 C4819 或 C2001别急着改代码先按下面几步检查用 VS 打开文件看右下角“编码”显示。如果是“Unicode (UTF-8 无签名)”并且文件里有非 ASCII 字符那基本可以确定是编码问题。把报错行附近的所有中文注释先删掉编译看是否恢复正常。如果错误消失就坐实了编码原因。查看项目属性里是否已经设置/utf-8。如果没有可以先加这个选项再编译。如果项目里还有其他第三方库的源文件也需要一并检查因为/utf-8是工程级别的对工程下所有文件生效。检查是否误把avilib.c改名为avilib.cpp后原来能编译过的代码变得告警变多。这是因为 MSVC 对 .c 文件默认按 C 模式编译对 .cpp 文件按 C 模式编译两者对类型转换、字符字面量等处理有差异如果源码有隐式转换等 C 风格写法会出现新告警。这种情况下要么保持 .c 拓展名要么严格按 C 标准修正代码。以上的关键点其实就一句话源码字符集和执行字符集的匹配决定了 MSVC 如何解析你的中文注释。理解了这一点VS2019 报错就不再是玄学而是可以预期、可复现、可通过工程选项控制的固定行为。5. 常见问题与排查技巧实录5.1 真实项目里最常见的 5 个坑我在自己的采集工具里改造 avilib 的过程中遇到过不少问题汇总成一张速查表方便你直接对照排查。现象可能原因解决办法生成的 AVI 文件播放器无法 seekidx1块偏移写错或未写检查AVI_close是否正确落盘索引确认movi_start计算无误读取非标准 AVI 时读到一半失败遇到不支持的压缩格式或 RIFF 结构打开调试输出看停在哪个 chunk id必要时手工扩展解析逻辑写视频时丢帧AVI_MAX_INDEX达到上限调大宏定义的索引数量重新编译中文注释导致编译报错源文件编码与 MSVC 代码页不匹配设置/utf-8或另存为 UTF-8 BOM读取时首帧能读、后续帧全部错乱上一帧解码失败导致偏移未同步在每次AVI_read_frame后检查返回值失败时重新AVI_seek5.2 调试手段用好 avilib 自带的日志输出老外最初版里其实留了不少调试口子只是没有系统文档。打开 avilib.h 后你会看到类似#define DBG printf这种宏如果你的工程在编译时定义了DEBUG宏ReadAviHeader会把解析到的每个 chunk id、大小、偏移都打出来。这在面对打不开的 AVI 文件时简直是指路明灯。有一次我拿一个从监控设备导出的 AVI 文件被播放器识别为 0 帧怀疑是索引问题。打开DEBUG后才发现它的movi列表并不是从传统偏移开始的文件的头部写入方式采用了超大JUNK块填充导致movi_start的值异于常规。avilib 解析时把这个块当成了普通数据跳过但后来索引定位就全错了。我在代码里针对这种布局做了个补丁在进入movi列表前强制记录当前偏移问题就解决了。这个经验说明遇到解析类 bug第一件事不是改代码逻辑而是把解析过程完整打出来看到底停在哪一步。avilib 的日志在这个场景下比断点调试高效得多因为问题往往是数据层面的不是状态机逻辑层面的。5.3 一个容易忽略的跨平台编译注意点最后提醒一个老生常谈但很容易踩的点avilib 的 cpp 文件如果用 C 编译器编译要注意头文件里有没有被extern C包裹。老版本 avilib.h 绝大多数实现是纯 C 的没有extern C处理。你如果把它编译成 .cpp并在其他 C 文件里直接#include avilib.h链接时就会报 undefined reference因为 C 编译器生成的符号名和 C 编译器产生的修饰名不一致。解决方式要么在源码中加入#ifdef __cplusplus extern C { #endif /* avilib.h 内容 */ #ifdef __cplusplus } #endif要么在调用文件里用extern C { #include avilib.h }包裹。我个人更推荐前者因为一旦包装好后期迁移到其他 C 工程都不用再重复处理。另外老代码在 Windows 上编译还要注意fopen打开文件时的二进制模式。AVI 是二进制文件如果在 Windows 上用了文本模式打开默认fopen(xxx.avi, r)fread时会把0x1A当成 EOF 提前截断导致文件读不完整。必须显式用rb和wb打开。这是很多从 Linux 迁移到 Windows 的代码最容易出的问题avilib 最初版的源码里已经正确使用了rb/wb不过程序里如果有二次打开文件的逻辑记得自己检查一遍。6. 这套代码后续还能怎么扩展在我自己的项目里基于 avilib 做的最多一件事就是加一个简单的时间戳记录。原本的idx1索引只记录了帧大小和偏移没有时间戳信息导致我做帧率统计时还得靠frame_count / fps反推。我给avi_t结构体加了一个long *timestamp数组写帧时用QueryPerformanceCounter记录每个采集时刻关闭文件时把时间戳按固定格式追加到自定义 chunk 里。这样后续播放或分析时就能精确拿到每帧的真实采集时刻比反推可靠得多。另一个可扩展方向是支持多音视频流。avilib 最初版只支持一路视频和一路音频如果你面对的是带双声道独立录音的采集设备就需要在strl解析循环里加入对第二个strh的处理。代码本身并不复杂核心是在avi_t里增加另一组音频流参数和对应索引但要注意写模式时movi块的交错写入顺序avilib 已有的音视频交错逻辑是按固定顺序写的多流时必须你自己维护一个发送顺序表。如果你只是想学习 AVI 封装原理把这套源码读一遍会比看任何文档都直观。RIFF 的嵌套结构、chunk 对齐、索引构建所有这些概念最终都体现在几十行核心代码里。老外的注释和变量命名并不花哨但对理解 Linux 早期视频工具链的设计思路很有帮助。我在看这些代码时最大的收获就是明白了“一个格式解析器最该关心的不是格式本身而是数据在文件里的物理布局和读写时机”这个认知放到今天处理 MP4、MKV 依然适用。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价