ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

avilib源码解析:AVI容器读取、帧抽取与VS2019移植实战指南

avilib源码解析:AVI容器读取、帧抽取与VS2019移植实战指南 简介这是国外开发者早期发布的avilib C库包含avilib.h与avilib.cpp两个原始文件。avilib是处理AVI视频格式的轻量级工具提供打开文件、读取帧率分辨率等元数据、逐帧解析音视频数据以及创建并写入AVI流的基本接口适合C学习者通过阅读源码理解AVI容器结构也便于在Qt项目中集成或二次改造。压缩包仅2个文件1个头文件负责函数与结构体声明1个源文件承载具体实现整体大小约10KB代码量精简、无多余封装利于逐行研读。目前已有373人学习参考在开发调试中积累了一定认可度。通过这份初版代码读者可以掌握声明与实现分离的工程组织方式并借助清晰的接口调用流程快速上手AVI文件的读写与基础编辑为后续从事音视频处理相关工作打下扎实基础。 老外最初版的 avilib也就是后来无数 AVI 读写项目里那个根目录下的avilib.h和avilib.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_frames、AVI_video_width、AVI_video_height它们本质上就是直接读取结构体字段连函数调用开销都省了。这种“明明可以直接读字段偏要包一层宏”的做法在现在的 C 代码里看着有点多余但在那个年代这是一种封装习惯方便后续如果字段名变了只改宏定义就行调用方代码不用动。avilib.c里核心的数据结构是帧索引表它记录每一帧在文件中的偏移量、长度、是否为关键帧。AVI_set_video_position做随机定位时就是查这张表然后fseek到对应位置。理解了这个逻辑后面遇到定位错乱的问题时排查方向就很清晰了。2. VS2019 编译老代码的三座大山编码、运行库、文件模式用 VS2019 打开这份老掉牙的 C 代码运气好能一次过运气不好会连环踩坑。我把我实际遇到的和帮别人解决的编译问题集中列一下这几条是最高频的。2.1 中文注释报错的根源BOM 和代码页不匹配最近很多人在 VS2019 里往 avilib 的源文件加中文注释后编译报错错误码一般是C2001、C2143、C2059这类“语法错误”但你看代码怎么都看不出语法问题。这其实是编码冲突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 风格的函数默认藏起来了老代码里如果调用了fileno、strdup这类函数链接时就找不到对应符号。解决方案是在链接器输入里附加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 上如果你打开文件时用的是默认文本模式没加bfread读到0x1ACtrlZ会被当成文件结束符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 * 324 位 RGB或 432 位而不是原压缩帧大小。如果你想拿的是压缩数据那这个库就不适合你得上别的方案。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_t调AVI_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 编译基本一次过但要注意有些版本用了index、rindex这类被 POSIX 标记为过时的字符串函数在高版本 glibc 下可能要加_GNU_SOURCE宏规避警告。Clang 则可能对隐式函数声明更敏感建议编译时统一加-stdc99或-stdgnu99避免老式 KR 风格的声明方式引发报错。avilib 这套代码不长但信息密度很高适合拿来当 AVI 容器格式的入门教材。它的价值不在于代码风格多现代、功能多健全而在于你照着源码走一遍就能把 RIFF 结构、chunk 解析、索引表定位这些底层机制彻底搞明白。相比直接调 FFmpeg 那套大而全的库这份几百 KB 的老代码反而更像一张能看懂原理的电路图。如果你只是需要快速抽帧或做格式兼容层大可直接拿它当轮子用遇到本文提到的坑时回来对照排查就行。本文还有配套的精品资源点击获取
返回列表