avilib源码解析:AVI容器读取、帧抽取与VS2019移植实战指南
2026/9/9 17:47:51 网站建设 项目流程

简介:这是国外开发者早期发布的avilib C++库,包含avilib.h与avilib.cpp两个原始文件。avilib是处理AVI视频格式的轻量级工具,提供打开文件、读取帧率分辨率等元数据、逐帧解析音视频数据以及创建并写入AVI流的基本接口,适合C++学习者通过阅读源码理解AVI容器结构,也便于在Qt项目中集成或二次改造。压缩包仅2个文件,1个头文件负责函数与结构体声明,1个源文件承载具体实现,整体大小约10KB,代码量精简、无多余封装,利于逐行研读。目前已有373人学习参考,在开发调试中积累了一定认可度。通过这份初版代码,读者可以掌握声明与实现分离的工程组织方式,并借助清晰的接口调用流程,快速上手AVI文件的读写与基础编辑,为后续从事音视频处理相关工作打下扎实基础。 老外最初版的 avilib,也就是后来无数 AVI 读写项目里那个根目录下的avilib.havilib.c,是很多人接触视频容器解析的第一份源码。当年做嵌入式播放器、做视频缩略图生成、甚至早期不少开源播放器的 AVI 解复用模块,都是从这套代码改出来的。现在 GitHub 上能搜到的版本大多是后续维护者加了各种补丁的 fork,但最初那一版的设计思路和代码风格,反而最能让人把 AVI 容器格式看明白。

这篇文章我结合自己在 VS2019 下编译、移植、改造这套老代码的实战经验,把这套代码的核心结构、编译踩坑点、常见误用以及和 OpenCV 搭配的姿势梳理一遍,给准备用它做项目或读源码的人做个参考。

1. avilib 的封装逻辑:老 C 风格里的经典套路

avilib 这一版代码,核心就两个文件:头文件avilib.h定义对外接口和数据结构,实现文件avilib.c负责 AVI 容器解析和帧读取。整个库没有任何第三方依赖,底层就是标准 C 的文件操作和内存管理,这也是它能在各种平台和编译环境下被反复移植的根本原因。

它的对外接口围绕avi_t这个不透明结构体展开。AVI_open_input_file打开一个 AVI 文件,解析 RIFF 头、hdrl列表、avih主头、strl流列表、strh流头、strf流格式,最后构建出avi_t内部的帧索引表。之后所有操作,比如读帧、定位、获取视频宽高帧数,都是对这个结构体的字段进行操作。这种“句柄式”设计在当时很常见,把复杂的数据结构隐藏在 opaque 指针后面,外部调用者只需要关心AVI_xxx这一系列函数就行。

avilib.h里值得细看的是那些宏和内联函数,比如AVI_video_framesAVI_video_widthAVI_video_height,它们本质上就是直接读取结构体字段,连函数调用开销都省了。这种“明明可以直接读字段,偏要包一层宏”的做法,在现在的 C++ 代码里看着有点多余,但在那个年代,这是一种封装习惯,方便后续如果字段名变了,只改宏定义就行,调用方代码不用动。

avilib.c里核心的数据结构是帧索引表,它记录每一帧在文件中的偏移量、长度、是否为关键帧。AVI_set_video_position做随机定位时,就是查这张表然后fseek到对应位置。理解了这个逻辑,后面遇到定位错乱的问题时排查方向就很清晰了。

2. VS2019 编译老代码的三座大山:编码、运行库、文件模式

用 VS2019 打开这份老掉牙的 C 代码,运气好能一次过,运气不好会连环踩坑。我把我实际遇到的和帮别人解决的编译问题集中列一下,这几条是最高频的。

2.1 中文注释报错的根源:BOM 和代码页不匹配

最近很多人在 VS2019 里往 avilib 的源文件加中文注释后编译报错,错误码一般是C2001C2143C2059这类“语法错误”,但你看代码怎么都看不出语法问题。这其实是编码冲突:avilib 原版文件是 UTF-8 无 BOM 编码,VS2019 默认按当前系统代码页(中文系统是 GBK/GB2312)去读源文件,于是多字节字符被拆错,导致编译器在注释文本里“看到”了引号、反斜杠之类的东西,直接引发误报。

解决办法有三条路,按优先级从高到低:

  • 用 VS 的“文件 -> 另存为 -> 保存选项”,把编码改成“Unicode (UTF-8 带签名) - 代码页 65001”,保存后再编译。加 BOM 后 MSVC 会正确识别 UTF-8 编码,一举解决乱码和报错。
  • 在解决方案里加编译选项/source-charset:utf-8,告诉编译器源文件是 UTF-8。注意这个选项在“属性 -> C/C++ -> 命令行 -> 附加选项”里加,实测对 VS2019 有效。
  • 最省心的是不往源文件里写中文注释,统一用英文注释或把设计说明挪到独立的 .md 文档里。老代码本来就是英文注释风格,硬塞中文字符进去,后续 git diff、跨平台编译都可能引入额外风险。

另外补充一句,#pragma execution_character_set("utf-8")只影响执行字符集,管不到源文件字符集的解析,别指望靠它解决编译报错。

2.2 报 unresolved external symbol,先检查运行库

在 VS2019 里编译 avilib.c 可能出现链接错误,比如unresolved external symbol __imp__fileno之类的。这是因为新版 MSVC 的 C 运行库把一些 POSIX 风格的函数默认藏起来了,老代码里如果调用了filenostrdup这类函数,链接时就找不到对应符号。解决方案是在链接器输入里附加legacy_stdio_definitions.lib,这个库就是为了兼容老代码准备的。

另一个相关的坑是 C 和 C++ 混合编译时,如果 avilib.c 用 C 编译器编译,但被 C++ 代码通过extern "C"包含,容易因为函数调用约定不一致出问题。最省事的办法是把 avilib.c 直接改成 .cpp 扩展名参与编译,或者保证所有包含 avilib.h 的地方统一用extern "C"包起来。

2.3 千万别忘了二进制模式

avilib 底层用fopen/fread/fseek读写文件,在 Windows 上如果你打开文件时用的是默认文本模式(没加"b"),fread读到0x1A(Ctrl+Z)会被当成文件结束符,AVI 文件的帧数据里又偏偏可能大量出现这个字节,于是出现“帧读到一半就 EOF”的诡异情况。使用 avilib 打开文件时,确保内部fopen的 mode 参数是"rb",如果你自己封装了文件读取层,同理。这个坑在 Linux 下不存在,但在 Windows 下几乎是必现的,务必检查。

3. 抽帧转换的完整操作流程:几行代码干完核心活

avilib 的读帧逻辑非常简单,核心就是AVI_open_input_file->AVI_read_frame->AVI_close这条链。下面给出一段可用的抽帧代码骨架,并顺带解释几个重要的细节。

#include "avilib.h" #include <cstdio> int extract_frames(const char* path) { avi_t* pav = AVI_open_input_file(path, 1); if (!pav) { fprintf(stderr, "open failed\n"); return -1; } int width = AVI_video_width(pav); int height = AVI_video_height(pav); long total = AVI_video_frames(pav); int bpp = (AVI_video_bpp(pav) + 7) / 8; unsigned char* frame_buf = nullptr; long frame_size = 0; long frame_num = 0; for (long i = 0; i < total; i++) { if (AVI_read_frame(pav, &frame_buf, &frame_size, &frame_num) != 0) { fprintf(stderr, "read frame %ld failed\n", i); break; } // frame_buf 指向解码后的 RAW 数据,frame_size 是实际大小 // 在这里做你要做的事:存 BMP、分析、缩略图... } AVI_close(pav); return 0; }

这里有个新手常误解的点:AVI_read_frame返回的frame_buf解码后的原始图像数据,不是压缩后的 JPEG 或 MJPEG 字节流。也就是说,这个库读取 AVI 时,内部已经把 MJPEG 帧解压成 RGB 位图了。所以frame_size通常等于width * height * 3(24 位 RGB)或 4(32 位),而不是原压缩帧大小。如果你想拿的是压缩数据,那这个库就不适合你,得上别的方案。

AVI_read_frame内部会自行管理缓冲区,frame_buf指向的内存不需要你手动释放,下一次调用时会被自动覆盖。所以你只要在每次循环里及时处理数据即可。

把帧数据复制到 OpenCV 的cv::Mat里做后续图像处理也很方便,代码很短:

cv::Mat img(height, width, CV_8UC3, frame_buf); // 注意:如果 AVI 里存的是 BGR 顺序,而你的处理逻辑按 RGB 来, // 用 cv::cvtColor(img, img_rgb, cv::COLOR_RGB2BGR) 做一次转换

这个组合方式很实用,因为 avilib 不依赖系统解码器,不像 OpenCV 的VideoCapture在 Windows 上一遇到 MJPG 以外的编码就容易打不开。avilib 是自己解析容器、自己解 MJPEG,完全可控,非常适合嵌入式设备或需要纯净依赖链的桌面工具。

4. 随机定位和索引表的坑:定位成功但读帧返祖

avilib 的随机访问依赖文件头部的索引块(idx1),每个索引项记录了帧的偏移和大小。AVI_set_video_position先查找目标帧在索引表里的位置,然后fseek到对应偏移。这个逻辑在索引完全正常时没问题,但 AVI 这个容器格式有“坏索引”或者说“非标准索引”的情况,也就是某些工具生成的 AVI 索引并不完整。

这时候AVI_set_video_position往往返回成功(因为它只检查索引项是否存在),但实际读到的那一帧可能不是你要的位置。我遇到最典型的表现是:定位到第 300 帧,读完发现画面内容还是第 250 帧附近的。排查半天,最后发现就是索引表不完整导致的。

如果遇到这个情况,备选方案有两条:

  • 定位后手动调用一次AVI_read_frame丢弃,再读下一帧才是真实目标,这个手法在部分 fork 版本里有用。
  • 彻底一点,定位前先AVI_close,重新AVI_open_input_file,再用AVI_set_video_position。虽然多了一次文件打开开销,但内容是稳的。

另外一个和索引相关的点是AVI_malloc这个内部内存分配函数。它在内部做malloc,但调用方并不总检查返回 ptr 是否为 NULL,尤其当请求大小为 0 时,某些平台上返回值可能是 NULL,后续往里写数据直接踩坏内存。如果你是在 C++ 项目里用这份老代码,建议在包含头文件前做宏覆盖,比如#define AVI_MALLOC(n) new uint8_t[n],或者在使用前给所有AVI_read_frame传入的缓冲区判空。

5. 多线程使用和性能优化的实操建议

avilib 整个实现不是线程安全的,它内部有大量共享的偏移量、索引游标状态,多个线程同时对同一个avi_tAVI_read_frame一定会出问题。如果你有多路并行解码的需求,推荐按“一路一个 avi_t 实例”的方式来拆,每个线程各自打开文件句柄,各读各的。如果必须共享同一个文件,那就在外层加锁,一次只允许一个线程进入读取函数。别指望在 avilib.c 内部加锁,老代码的全局状态太多,补锁的工程量不如直接改架构。

在性能方面,avilib 默认用fread走标准库缓冲,对于逐帧读取这种高频率小规模 I/O,默认 4KB 缓冲区会导致大量底层read系统调用。如果你做的是整段视频批量抽帧,打开文件后自己调一次setvbuf能把缓冲提到 1MB 甚至更大,实测在连续读数百帧的场景下,耗时有肉眼可见的下降。这个优化在原版代码里没有,属于你加在自己的封装层里的小改进。

FILE* fp = pav->fp; // 取决于具体版本结构体字段名 setvbuf(fp, nullptr, _IOFBF, 1 << 20);

注意:如果你拿到的 fork 版把fp封装得更深,可能需要自己保留文件指针,或者用AVI_open_input_file之后通过fileno相关方式再拿到底层句柄操作,原理都一样,目的是减少小读写的系统调用次数。

6. 原版代码的几个冷门注意事项,文档里绝对没写

最后分享几个容易被忽略的细节,这些在原版 README 和注释里都找不到,是我实际使用中总结出来的。

关于video_codec字段,老版本 avilib 在AVI_open_input_file后未必正确设置这个字段。如果你要靠它判断文件是 MJPEG 还是其他编码来走不同处理分支,建议自己解析strh里的fccHandler字段,或者干脆在打开后用几个样本帧的颜色布局反推。不要盲目相信这个字段,尤其在处理非标准编码类型时。

关于帧缓冲区大小AVI_frame_size返回的是当前读取到的帧的实际大小,但在某些版本里,如果你先调AVI_set_video_position再查AVI_frame_size,返回值可能还是上一帧的大小。保险做法是拿width * height * bpp自己算一个最大缓冲,再根据实际返回的frame_size做边界处理,避免越界写。

关于AVI_video_bpp,它返回的是 AVI 文件头里声明的位深度,不一定等于实际帧数据每像素字节数。比如有些文件头写 24,但内部存储是 32 位对齐的,读出来的 frame_size 会比宽*高*3大,按 24 位去解析就会错位。处理策略是优先用frame_size / (width * height)反推实际字节数,再去初始化cv::Mat的 type。

关于 GCC/Clang 下的编译,这份老代码在 Linux 下用 GCC 编译基本一次过,但要注意有些版本用了indexrindex这类被 POSIX 标记为过时的字符串函数,在高版本 glibc 下可能要加_GNU_SOURCE宏规避警告。Clang 则可能对隐式函数声明更敏感,建议编译时统一加-std=c99-std=gnu99,避免老式 K&R 风格的声明方式引发报错。

avilib 这套代码不长,但信息密度很高,适合拿来当 AVI 容器格式的入门教材。它的价值不在于代码风格多现代、功能多健全,而在于你照着源码走一遍,就能把 RIFF 结构、chunk 解析、索引表定位这些底层机制彻底搞明白。相比直接调 FFmpeg 那套大而全的库,这份几百 KB 的老代码反而更像一张能看懂原理的电路图。如果你只是需要快速抽帧或做格式兼容层,大可直接拿它当轮子用,遇到本文提到的坑时回来对照排查就行。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询