上一篇把「画面接进来」打通了------VideoCapture 三种方式读帧、采集循环、测帧率。这一篇把另一半补上:画面「写出去」。采集到的帧只是内存里的 Mat,要变成能回放、能归档、能交付的视频文件,靠的是 VideoWriter。官方 samples 里的 videowriter_basic 就干这一件事:把摄像头帧实时编码成 .avi 文件,同时窗口预览。麻雀虽小,五脏俱全,编码器选择、帧率、尺寸、写入循环一次讲透。
一、效果先行
先看结果。videowriter_basic 打开默认摄像头,一边在「Live」窗口预览,一边把帧写进 live.avi,按任意键结束:

退出后盘上多出一个 live.avi,用 ffprobe 一验,编码、尺寸、帧率全对上了:
codec_name=mjpeg # Motion JPEG 编码
width=768 height=576 # 尺寸跟摄像头一致
r_frame_rate=25/1 # 25 帧每秒
pix_fmt=yuvj420p
记住两件事:VideoWriter 一个类就能把帧编码成视频文件;fourcc 决定用什么编码器,fps 和尺寸要在写入前定死。
二、FourCC:四个字符选一个编码器
视频编码的入口是一个叫 fourcc 的东西------Four Character Code,四个 ASCII 字符唯一标识一种编码器。fourcc('M','J','P','G') 把 M、J、P、G 四个字符打包成一个整数,交给底层编码库选编码器。
| FourCC | 编码器 | 特点 | 容器 |
|---|---|---|---|
| MJPG | Motion JPEG | 逐帧独立压缩,兼容性最好 | .avi |
| XVID / DIVX | MPEG-4 Part 2 | 有帧间压缩,体积小 | .avi |
| X264 / H264 | H.264 | 压缩率最高,工业主流 | .mp4 |
| MP4V | MPEG-4 | 通用 | .mp4 |
一个关键点:编码器必须运行时可用,否则 open 会失败。示例选 MJPG 就是因为它在几乎所有平台都内置,最不容易翻车。真要用 H264,得先确认系统装了对应编码库。
三、VideoWriter 的五个参数
初始化一个写入器,官方给的姿势是默认构造再 open,一共五个参数:
cpp
VideoWriter writer;
int codec = VideoWriter::fourcc('M', 'J', 'P', 'G'); // 编码器 ID
double fps = 25.0; // 目标帧率
string filename = "./live.avi"; // 输出文件
writer.open(filename, codec, fps, src.size(), isColor);
if (!writer.isOpened()) { // 打开失败要查
cerr << "Could not open the output video file for write\n";
return -1;
}
五个参数分别是:文件名、编码器 ID、帧率、帧尺寸、是否彩色。前两个决定「存成什么样」,后三个决定「存的数据长什么样」。尺寸和色彩必须跟实际写入的帧严格一致,不一致编码就出问题。
四、先读一帧,再初始化写入器
尺寸和色彩这两个参数,摄像头没交出一帧之前谁也不知道。所以官方用了「先读一帧探测」的范式:
cpp
cap >> src; // ① 先抓一帧
if (src.empty()) return -1;
bool isColor = (src.type() == CV_8UC3); // ② 从帧里问出类型
writer.open(filename, codec, fps, src.size(), isColor); // ③ 用它初始化
src.size() 问出尺寸,src.type() == CV_8UC3 判断是不是 BGR 彩色。这套「探测 → 初始化 → 循环写入」的顺序,是所有采集类程序的固定套路------不先拿到一帧,你根本没法告诉写入器帧长什么样。
五、采集-写入循环
初始化完之后就是主循环,在上一篇的采集循环中间加一步 write:
cpp
for (;;) {
if (!cap.read(src)) break; // 读一帧,读失败或流结束就退出
writer.write(src); // 把这一帧编码进视频流
imshow("Live", src); // 同时实时预览
if (waitKey(5) >= 0) break; // 任意键终止
}

注意 cap.read(src) 和上一篇的 cap >> src 是同一个动作,差别只在返回值------read 返回 bool,适合放进 if 里判断流是否结束。循环结束后没有显式 writer.release(),因为 VideoWriter 的析构函数会自动关闭文件、刷新缓冲,这是 RAII 的体现。
六、实测
这个示例硬编码 VideoCapture(0),VM 里没有物理摄像头。我用 v4l2loopback 造一个虚拟相机、再用 ffmpeg 把测试视频灌进去,成功跑通:
Writing videofile: ./live.avi
Press any key to terminate
12 秒后终止,生成 8.9MB 的 live.avi。ffprobe 对账:mjpeg 编码、768×576、25fps、yuvj420p,和源码里 fourcc('M','J','P','G') 加 fps=25 加 src.size() 完全吻合。
但这一趟并不顺利。第一天搭虚拟相机时踩了个硬坑:Ubuntu 24.04 自带的 v4l2loopback 0.12.7 是 2021 年的老版本,在 kernel 7.0.0 上运行时直接内核崩溃------vidioc_reqbufs 里有个空指针访问,内核 OOPS 后把进程强制杀死,连第一行输出都打印不出来。换成 GitHub 最新的 0.15.4 版,一次编译通过、再无崩溃,采集和写入全链路跑通。
七、踩坑记录
| 坑 | 现象 | 正确姿势 |
|---|---|---|
| 编码器不可用 | open 返回 false | 用 MJPG 兜底,用 H264 先查可用性 |
| 把 fps 当节拍器 | 播放快进或慢放 | fps 只是文件头元数据,节奏由采集循环定 |
| 尺寸类型不一致 | 编码失败或视频损坏 | write 的帧和 open 声明严格对齐 |
| 容器编码器不配 | 打不开或异常 | .avi 配 MJPG/XVID,.mp4 配 H264/MP4V |
| 老版虚拟相机崩溃 | 进程被 SIGKILL 无输出 | v4l2loopback 升到 0.15.4 |
八、AI 与 LLM Wiki:这一课沉淀了什么
这一课照例新增「视频写入」概念页,两条硬性要求一个不少------「API 汇总表」列了当天用到的 16 个接口(VideoWriter 的构造、fourcc、open、write、isOpened,加上配套的 VideoCapture 和 Mat 接口),功能、参数、用法三列逐条记清;「demo 语法分析」把 67 行源码拆成五块讲透,从头文件到主循环,重点拆了 fourcc 的静态成员调用、五参数 open、先读帧再初始化的范式。

更值钱的是把那个 v4l2loopback 0.12.7 崩溃的坑固化进了技能。AI 学东西不是记在脑子里就算完,而是把「现象 → 根因 → 解法」写进可复用的技能条目,下次再遇到无摄像头环境跑采集代码,直接照着升级到 0.15.4,不用再从头踩一遍。
进度表上 videowriter_basic 勾选完成,从 38 个变成 39 个,进度 40%。第三阶段「图像读写与视频 I/O」已经走过图片、采集、写入三站。
写在最后
97 个实例,今天完成第 39 个,进度 40%。这一站把「写出去」补上了:VideoWriter 一个类管编码,fourcc 选编码器,五个参数定输出,先读帧再初始化,循环里 write 就完事。下一篇往视频的边角料走------videocapture_image_sequence 把图像序列合成视频,videocapture_audio 讲音视频同步,看看视频文件里除了画面还有什么。
本文示例代码均出自 OpenCV 官方 samples,遵循 Apache 2.0 协议。