
手上正好有一块树莓派4B想接个USB摄像头做图像采集很多人第一反应是去翻内核源码、写驱动模块结果折腾一星期连图像都没看到。说实话在树莓派上用V4L2驱动USB摄像头这件事绝大部分情况压根不需要你动内核UVC驱动早就集成在系统里了你真正要干的其实是两件事确认设备被正确枚举然后通过V4L2用户态接口把图像数据拿回来。这篇文章我就把完整链路掰开揉碎讲清楚从硬件接线到系统配置从V4L2的核心机制到可编译运行的完整C代码再到我自己踩过的各种坑一次性给你打通。适合刚接触树莓派、Linux下视频采集或者想弄明白V4L2框架到底怎么用的朋友照着操作就能跑出第一张照片。1. 项目背景与整体思路1.1 为什么选树莓派4B加USB摄像头树莓派4B是目前玩嵌入式视觉最顺手的平台之一四核Cortex-A72处理器1GB到8GB内存可选算力足够跑常规的图像处理任务而且GPIO、CSI、USB、HDMI这些外设接口一应俱全。搭配USB摄像头更是省事不用拆排线、不用对齐引脚插上就能用。相比CSI接口的官方摄像头模组USB摄像头在成本、通用性、焦距选择上都更灵活尤其适合快速原型验证和教学演示。不过USB方案的代价就是数据要走USB总线带宽和延迟都比CSI差一些。树莓派4B的USB 2.0和USB 3.0接口同时存在但绝大多数USB摄像头都是USB 2.0的满速480Mbps实际可用带宽大约40MB/s。以720p分辨率、YUV422格式、30帧来算一帧数据量大约是1280×720×2字节约1.76MB30帧就是52.8MB/s已经超过USB 2.0的可用带宽了。所以实际部署时通常选MJPEG格式压缩后每帧只有几十到几百KB带宽压力大幅降低。这个选型思路后面代码里也会体现。1.2 到底要不要自己写“驱动”标题里带了“驱动”两个字这是最容易误导新手的地方。实际上Linux内核里对USB摄像头的支持非常成熟UVCUSB Video Class驱动已经是内核标准模块树莓派官方系统里默认编译进去了。当你插入摄像头时内核会自动识别设备并加载uvcvideo模块你压根不需要自己写内核驱动。那为什么网上还有那么多“写驱动”的教程一种情况是某些非标摄像头没有遵循UVC协议需要厂商提供专用驱动另一种情况是作者把“调用V4L2接口”也叫做“驱动”严格来说这是用户态编程不是内核态驱动开发。我建议你先把用户态的V4L2流程跑通如果确实遇到非UVC设备再去研究内核驱动模块的编写。这篇文章聚焦的是V4L2编程框架也就是应用层如何与内核驱动交互。1.3 V4L2在整个视频采集链路中的位置V4L2的全称是Video for Linux 2它是Linux内核里视频设备的标准抽象接口。摄像头驱动在内核里把硬件数据整理好V4L2作为中间层暴露一组标准API给用户态程序你只需要操作open、ioctl、mmap、poll这些通用系统调用就能控制摄像头采集、设置分辨率、读取图像数据。这个设计让应用层的代码可以跨设备复用今天用树莓派的USB摄像头明天换一块USB工业相机只要对方支持UVC或V4L2你的采集程序几乎不用改。换句话说V4L2就是Linux视频领域的“统一插座”驱动是插座里的电线你的程序是插头。理解了这层关系你再看网上一堆V4L2教程就会觉得脉络清晰很多。2. 硬件准备与系统环境搭建2.1 硬件清单与选型建议树莓派4B主板一块建议2GB内存以上版本。系统存储卡16GB以上Class 10的TF卡读写速度直接影响系统流畅度。5V 3A USB-C电源适配器树莓派4B对供电要求比较高。免驱USB摄像头优先选UVC协议支持的型号。键鼠、HDMI显示器或者直接用SSH远程登录也行。摄像头选型是很多人忽视的坑。建议不要买那种非常便宜、包装上只写“免驱”但没有品牌型号的摄像头它们往往用的是老旧的中星微芯片或者芯片方案不公开的型号虽然部分也能被UVC驱动识别但兼容性很不稳定。优先选罗技C270、C920这类经典型号或者任何标注支持UVC协议的工业摄像头它们在Linux下都是即插即用。分辨率方面如果你是做入门实验640×480或者1280×720足够了不要被4K宣传带跑树莓派4B处理4K图像会非常吃力。2.2 系统烧录与基础配置操作系统我用的是Raspberry Pi OS基于Debian的官方系统比较省心。烧录直接用官方Raspberry Pi Imager工具选择系统镜像后写入TF卡同时可以在Imager里预设SSH开启、WiFi连接、用户名密码这样板子开机后就能直接SSH进去不用再接键鼠和显示器。开机后用sudo raspi-config进配置界面确认Camera接口和I2C等外设状态但需要注意Camera接口对应的是CSI摄像头USB摄像头不受这个开关控制所以即使Camera选项为Disable也不影响USB摄像头的使用。接着跑一下系统更新sudo apt update sudo apt full-upgrade -y顺便装上后面编译代码需要的工具链和测试软件sudo apt install -y build-essential v4l-utils git cmakev4l-utils里的v4l2-ctl和v4l2-compliance是非常好用的调试工具后面排查问题会频繁用到。2.3 检查摄像头是否被系统识别插上USB摄像头然后在终端执行lsusb正常输出里会有一行类似Bus 001 Device 004: ID 046d:0825 Logitech, Inc. Webcam C270的记录这就是你的摄像头。如果这一行都没看到优先检查线材、接口、供电而不是看驱动。再查内核日志dmesg | grep -i uvc能看到uvcvideo: Found UVC 1.00 device这类信息说明UVC驱动已经加载成功。也可以通过lsmod | grep uvc确认模块状态。如果一切正常你会看到/dev/video0设备节点ls -l /dev/video*出现video0之后用v4l2-ctl快速验证一下摄像头能不能出图v4l2-ctl --list-devices v4l2-ctl --list-formats-ext -d /dev/video0第二条命令会列出摄像头支持的所有像素格式和分辨率、帧率组合。这一步拿到的东西很关键后面代码里设置的格式必须在这个列表里否则ioctl会直接报错。3. V4L2核心机制解析3.1 设备节点与驱动的关系当你插入USB摄像头内核里的UVC驱动会注册一个V4L2子设备用户态看到的抽象就是/dev/video0。这个节点不是普通的字符设备那么简单它有两大作用一是通过ioctl系统调用下发控制命令比如设置格式、申请缓冲区、启动采集二是通过read、write、mmap等接口传输图像数据。更复杂一点的路由架构里设备可能同时暴露/dev/video0和/dev/video1其中一个负责图像采集另一个负责输出或者元数据具体要看驱动实现。但对于普通USB摄像头/dev/video0就是你的采集入口。还有个小知识点/dev/v4l-subdev0这类节点属于内核子设备接口用户态程序通常不会直接操作它调试的时候偶尔会用到。3.2 像素格式与分辨率的选择V4L2里最常用的三种像素格式是YUYV、MJPEG和H264。YUYV是未压缩的原始YUV数据每像素2字节画质最好但数据量巨大MJPEG是Motion JPEG压缩每帧独立编码解码简单对CPU占用低非常适合树莓派H264压缩率最高但解码需要额外算力而且USB摄像头出来的H264流往往有编码延迟不太适合实时预览。分辨率的选择要综合考虑摄像头能力、带宽和处理性能。参考前面算过的带宽账720p下选YUYV可以达到15到20帧选MJPEG能轻松到30帧。如果你的应用需要逐帧处理像素比如做人脸识别、颜色检测那么MJPEG格式解码后和YUYV在像素内容上没有本质区别只是你要额外做一步解压。我的建议是先按摄像头默认格式跑通流程再根据实际需求调整。3.3 缓冲队列机制V4L2采集模式分为两种读模式和流式模式。读模式用的是read系统调用驱动内部帮你完成缓冲和拷贝代码简单但效率低流式模式需要你主动申请缓冲区用mmap映射到用户态然后通过入队出队循环拿帧效率高是工业应用的主流做法。流式模式的核心就是缓冲队列。你在驱动里申请N个缓冲区把缓冲区放入驱动维护的队列驱动从硬件收到一帧数据后填到队列头部缓冲区应用从队列取出已填满的缓冲区处理数据处理完再放回队列供驱动使用。这个环形流水线设计的好处是摄像头始终有可用缓冲不用等应用处理完上一帧才开始下一帧最大限度避免丢帧。缓冲区个数也就是nbuffers一般设4到6个太少容易丢帧太多浪费内存、增加延迟。4. 完整代码实现与详细讲解4.1 基础框架与设备打开下面这套代码我按“能跑、能看懂、能扩展”三个原则来写。它做的事情不复杂打开摄像头设置640×480的YUYV格式申请4个缓冲区启动采集连续采集10帧并把每帧保存成BMP文件。#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include errno.h #include sys/ioctl.h #include sys/mman.h #include linux/videodev2.h #define DEVICE_PATH /dev/video0 #define WIDTH 640 #define HEIGHT 480 #define BUFFER_COUNT 4 struct buffer_info { void *start; size_t length; }; static struct buffer_info buffers[BUFFER_COUNT]; static int fd -1; static int xioctl(int fd, unsigned long request, void *arg) { int ret; do { ret ioctl(fd, request, arg); } while (-1 ret EINTR errno); return ret; }代码里的xioctl是一个简陋的包装目的是自动重试被信号中断的ioctl调用。这是V4L2编程的第一个常见坑ioctl返回EINTR时不能直接当错误处理应该重新调用。当然更完善的做法是用select或poll等待设备可读避免忙等后面编码实战部分会提到。打开设备用open即可fd open(DEVICE_PATH, O_RDWR | O_NONBLOCK); if (fd 0) { perror(open device failed); return 1; }O_NONBLOCK标志很关键它让read和poll在设备没有数据时立即返回而不是阻塞线程方便你在循环里做超时控制或取消处理。4.2 查询设备能力与设置采集格式打开设备后要做两件事查询设备能力确认设备支持视频采集设置采集格式告诉驱动你想要什么类型的数据。struct v4l2_capability cap; memset(cap, 0, sizeof(cap)); if (-1 xioctl(fd, VIDIOC_QUERYCAP, cap)) { perror(VIDIOC_QUERYCAP failed); return 1; } if (!(cap.capabilities V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, device does not support video capture\n); return 1; }设备能力检查通过后接下来设置视频采集格式struct v4l2_format fmt; memset(fmt, 0, sizeof(fmt)); fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width WIDTH; fmt.fmt.pix.height HEIGHT; fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field V4L2_FIELD_NONE; if (-1 xioctl(fd, VIDIOC_S_FMT, fmt)) { perror(VIDIOC_S_FMT failed); return 1; }这里有几个点值得注意。fmt.type必须设成V4L2_BUF_TYPE_VIDEO_CAPTURE如果你用的是V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE那对应的是多平面格式缓冲区设置方式完全不同。pixelformat对应当前像素格式我这里用的是YUYV也就是每个像素占用2字节、每4个Y分量共享一组UV分量的打包格式。field设为V4L2_FIELD_NONE表示逐行扫描USB摄像头基本都是这个模式。设置完格式后建议把fmt重新读一遍if (-1 xioctl(fd, VIDIOC_G_FMT, fmt)) { perror(VIDIOC_G_FMT failed); return 1; } printf(Driver set to %dx%d, format 0x%08x\n, fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat);有些摄像头不支持你请求的精确分辨率驱动可能会自动调整为就近的合法值。如果你不重新读取后面申请的缓冲区大小可能和实际不匹配轻则显示花屏重则申请失败。记住一个经验和硬件打交道请求后一定要确认实际结果。4.3 申请缓冲区与内存映射这一步是V4L2采集的精髓所在。先通过VIDIOC_REQBUFS请求内核分配缓冲区struct v4l2_requestbuffers req; memset(req, 0, sizeof(req)); req.count BUFFER_COUNT; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; if (-1 xioctl(fd, VIDIOC_REQBUFS, req)) { perror(VIDIOC_REQBUFS failed); return 1; } if (req.count 2) { fprintf(stderr, insufficient buffer memory\n); return 1; }注意req.count既是输入也是输出。你请求4个驱动可能只分配2个所以申请完必须检查实际分配的数。memory选择V4L2_MEMORY_MMAP这是最常用、也是效率比较高的一种模式驱动把内核缓冲区直接映射到用户空间省去了内存拷贝。接下来逐个查询缓冲区信息并做映射for (int i 0; i BUFFER_COUNT; i) { struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; if (-1 xioctl(fd, VIDIOC_QUERYBUF, buf)) { perror(VIDIOC_QUERYBUF failed); return 1; } buffers[i].length buf.length; buffers[i].start mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (MAP_FAILED buffers[i].start) { perror(mmap failed); return 1; } }这里查询每个缓冲区得到它的长度和物理偏移然后mmap映射到用户空间。映射标志用MAP_SHARED而不是MAP_PRIVATE因为驱动要直接往这块内存写数据私有映射的内存无法共享。buf.length在映射后保存下来后面分析帧数据长度时会用到。4.4 入队、启动采集与读取帧缓冲区映射完成后把它们全部放入驱动队列然后开始采集for (int i 0; i BUFFER_COUNT; i) { struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; if (-1 xioctl(fd, VIDIOC_QBUF, buf)) { perror(VIDIOC_QBUF failed); return 1; } } enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 xioctl(fd, VIDIOC_STREAMON, type)) { perror(VIDIOC_STREAMON failed); return 1; }这里VIDIOC_QBUF是“把缓冲区还给驱动”的操作驱动拿到空缓冲区后开始往里填数据。所有缓冲区入队后VIDIOC_STREAMON通知驱动“我准备好了开始干活”。从这一刻起摄像头采集数据会持续送到缓冲区。采集循环就是一个典型的出队—处理—入队过程for (int frame 0; frame 10; frame) { struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; if (-1 xioctl(fd, VIDIOC_DQBUF, buf)) { perror(VIDIOC_DQBUF failed); return 1; } // 处理图像数据 process_image(buffers[buf.index].start, buf.bytesused, frame); if (-1 xioctl(fd, VIDIOC_QBUF, buf)) { perror(VIDIOC_QBUF after processing failed); return 1; } }VIDIOC_DQBUF出队一个缓冲区驱动把填满的图像数据交给你buf.bytesused告诉你当前帧的实际字节数buf.index告诉你数据在哪个缓冲区。你处理完数据后必须执行VIDIOC_QBUF把这个缓冲区重新放回驱动队列否则驱动可用缓冲区越来越少很快就没有空缓冲可用了采集就会中断。这个循环是V4L2编程里最核心的节拍务必刻在脑子里。前面open时加了O_NONBLOCK所以VIDIOC_DQBUF在没有数据时会返回EAGAIN错误代码里会直接当成出错退出。实际项目里你应该用select或poll先等待缓冲区就绪再执行出队操作这样能避免CPU空转、也能处理超时场景。4.5 保存为BMP图像光拿到原始YUV数据还不够直观为了验证采集成功我把每一帧保存成BMP文件。YUYV转BMP需要做色彩空间转换下面这个函数把YUYV像素转成RGBstatic void yuyv_to_rgb24(unsigned char *yuyv, unsigned char *rgb, int width, int height) { int size width * height; for (int i 0, j 0; i size; i 2, j 4) { int y0 yuyv[j]; int u yuyv[j 1] - 128; int y1 yuyv[j 2]; int v yuyv[j 3] - 128; int r y0 1.402 * v; int g y0 - 0.344 * u - 0.714 * v; int b y0 1.772 * u; rgb[i * 3 0] r 255 ? 255 : (r 0 ? 0 : r); rgb[i * 3 1] g 255 ? 255 : (g 0 ? 0 : g); rgb[i * 3 2] b 255 ? 255 : (b 0 ? 0 : b); r y1 1.402 * v; g y1 - 0.344 * u - 0.714 * v; b y1 1.772 * u; rgb[(i 1) * 3 0] r 255 ? 255 : (r 0 ? 0 : r); rgb[(i 1) * 3 1] g 255 ? 255 : (g 0 ? 0 : g); rgb[(i 1) * 3 2] b 255 ? 255 : (b 0 ? 0 : b); } }这里用到的转换公式是BT.601标准系数属于行业惯例。实际项目里如果想追求性能可以查表替代浮点运算或者直接用CPU SIMD指令优化但对理解原理来说这个版本足够了。BMP文件的结构比较简单54字节的文件头加上像素数据注意BMP的像素行需要4字节对齐不过640×480正好满足对齐条件省了不少事。static void save_bmp(const char *filename, unsigned char *rgb, int width, int height) { int data_size width * height * 3; int file_size 54 data_size; unsigned char header[54] {0}; header[0] B; header[1] M; header[2] file_size 0xff; header[3] (file_size 8) 0xff; header[4] (file_size 16) 0xff; header[5] (file_size 24) 0xff; header[10] 54; header[14] 40; header[18] width 0xff; header[19] (width 8) 0xff; header[20] (width 16) 0xff; header[21] (width 24) 0xff; header[22] height 0xff; header[23] (height 8) 0xff; header[24] (height 16) 0xff; header[25] (height 24) 0xff; header[26] 1; header[28] 24; FILE *fp fopen(filename, wb); if (!fp) { perror(fopen failed); return; } fwrite(header, 1, 54, fp); fwrite(rgb, 1, data_size, fp); fclose(fp); }最后在main函数里把这些串起来采集10帧每帧保存成一个文件int main(void) { fd open(DEVICE_PATH, O_RDWR | O_NONBLOCK); if (fd 0) { perror(open device failed); return 1; } if (init_device() 0) { cleanup(); return 1; } start_capture(); printf(capture 10 frames...\n); for (int frame 0; frame 10; frame) { struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; while (1) { if (-1 xioctl(fd, VIDIOC_DQBUF, buf)) { if (errno EAGAIN) { usleep(5000); continue; } perror(VIDIOC_DQBUF failed); cleanup(); return 1; } break; } unsigned char *rgb malloc(WIDTH * HEIGHT * 3); yuyv_to_rgb24((unsigned char *)buffers[buf.index].start, rgb, WIDTH, HEIGHT); char filename[64]; snprintf(filename, sizeof(filename), frame_%02d.bmp, frame); save_bmp(filename, rgb, WIDTH, HEIGHT); free(rgb); if (-1 xioctl(fd, VIDIOC_QBUF, buf)) { perror(VIDIOC_QBUF failed); cleanup(); return 1; } } stop_capture(); cleanup(); printf(done.\n); return 0; }实际上我为了代码结构清晰把初始化、开始采集、停止、清理分别封装成了函数。你可以把上面的模块按自己习惯组织核心流程不变查询能力、设置格式、申请缓冲、映射、入队、开始采集、出队处理、入队、结束后停流并解除映射。编译命令很简单gcc -O2 -o v4l2_capture v4l2_capture.c然后以root权限运行摄像头设备节点通常需要root权限sudo ./v4l2_capture运行之后当前目录会多出frame_00.bmp到frame_09.bmp用图片查看器打开如果能看到画面恭喜你整条V4L2链路已经通了。5. 常见问题与排查技巧实录5.1 设备节点找不到或没有video0这个问题处理过不少次大多数情况不是驱动问题而是设备权限或系统未识别。先看lsusb和dmesg确认设备已被USB层枚举。如果lsusb有设备但/dev/video0没出现大概率是UVC驱动没加载手动加载一下sudo modprobe uvcvideo如果模块加载报错检查内核版本和摄像头协议。还有一种比较隐蔽的情况某些摄像头在启动时初始化慢开机后需要等几秒才出现设备节点自动脚本如果立刻访问就会失败。可以在代码里加一个重试逻辑循环判断设备节点是否存在超时再报错。此外权限问题也很常见可以临时用sudo运行程序或者把当前用户加入video组一劳永逸sudo usermod -aG video $USER修改后需要重新登录生效。5.2 mmap失败或VIDIOC_REQBUFS返回ENOMEMVIDIOC_REQBUFS返回内存不足一般是两个原因请求缓冲区个数太多或者系统可用内存紧张。树莓派4B内存够大通常不会因为总量不足而失败更多是摄像头驱动限制比如某些硬件只支持2个缓冲你请求了6个。这时候把req.count改小到2或3如果还是失败那就是驱动或者硬件问题了。mmap失败最常见的原因是映射长度参数不对或者拿到的buf.offset不合法。检查一下你是不是在VIDIOC_QUERYBUF成功后立刻映射之间最好不要穿插其他ioctl调用。还有些老摄像头需要先STREAMON再mmap但UVC设备一般不需要遇到这种情况可以调整一下顺序试试。5.3 画面偏绿或颜色不对画面偏绿是YUYV转RGB时最容易出现的现象。检查两件事第一是否把Y和UV分量顺序搞反YUYV四个字节的顺序是Y0、U、Y1、V不是Y0、V、Y1、U顺序反了画面偏洋红或绿色第二转换公式里的色度系数是否正确。有些摄像头输出的实际格式是UYVY而不是YUYV两者字节顺序差很多你需要在设置格式前先确认摄像头实际支持的格式或者代码里打印一下pixelformat字段看看内核驱动到底给你调成了什么格式。如果是MJPEG格式抓到的数据那就另说了你拿到的是一段JPEG压缩数据必须先解压成YUV或RGB才能显示或保存为普通图片。直接当裸数据写文件会得到一堆乱码。解决办法是用libjpeg或者OpenCV的imdecode让它帮你解压。5.4 采集帧率上不去或画面卡顿帧率达不到标称值先别急着骂摄像头算一笔账640×480的YUYV帧是614KB30帧就是18MB/sUSB 2.0接口实际可用带宽大约在30MB/s上下这个组合还有余量。但如果你用1080p的YUYV每帧接近4MB30帧就是120MB/s远超USB 2.0上限帧率自然掉到7到8帧。解决办法是切到MJPEG格式压缩后帧负载只有几十KB到几百KB30帧毫无压力。除了格式你的应用代码也可能成为瓶颈。比如在采集循环里做大量的文件写入、打印日志、复杂图像处理都会拖慢出队入队速度。把耗时操作挪到单独的线程里或者用环形缓冲把采集和处理解耦是工业项目里的常规方案。另外select和poll配合VIDIOC_DQBUF性能会好一些因为阻塞等待不会空转CPU。代码里用usleep(5000)轮询属于土办法能跑但不够优雅追求精细控制的朋友建议改成poll。6. 扩展方向与进阶指南6.1 用MJPEG格式进一步降低带宽占用把代码里的V4L2_PIX_FMT_YUYV换成V4L2_PIX_FMT_MJPEGbytesused会从60多万字节骤降到几万甚至几千字节。但注意MJPEG是一帧一幅独立JPEG图你需要引入JPEG解码库。最简单的做法是用OpenCV它自带MJPEG解码能力只需要把CAP_PROP_FOURCC设置为MJPG。如果不想用OpenCVlibjpeg-turbo是非常轻量的选择解码速度很快树莓派上跑1080p MJPEG也能达到实时。6.2 结合RTP推流实现网络摄像头采集到MJPEG或H264数据之后可以通过RTP协议封装推给VLC或者网页播放器这就把树莓派变成了一台网络摄像头。需要用到GStreamer或者FFmpegFFmpeg的命令行可以非常简单ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video0 -f rtsp -rtsp_transport tcp rtsp://0.0.0.0:8554/live这条路本质上和你写的V4L2采集程序不冲突FFmpeg内部就是通过V4L2接口拿数据只是它把编码、封装、推流全部做完了。自己写代码的话采集部分完全复用前面这套流程后面加编码器和网络发送逻辑就行。6.3 从单帧采集到实时图像处理这篇文章的代码停在“采集10帧保存图片”实际很多项目要的是连续处理视频流。把for循环改成while(1)每次出队处理完再入队就是一个最简实时处理框架。树莓派4B上跑OpenCV的人脸检测、颜色追踪、运动检测输入源都可以直接用V4L2方式打开。有一点提醒处理慢于帧率时缓冲区队列会被占满新帧进不来表现为画面掉帧解决办法是丢旧帧保新帧或者干脆用多线程排队。我个人在实际操作中的体会是V4L2这套框架最大的价值不在于某个函数怎么写而在于你理解了“设备是资源、缓冲区是流水线”这个模型。把这个模型吃透以后不管换什么摄像头、跑到什么平台都能很快上手。最后再分享一个小技巧调试阶段多利用v4l2-ctl命令行工具它能帮你快速确定是摄像头问题、驱动问题还是自己代码的问题省下大量毫无头绪的排查时间。