资讯详情

V4L2采集原始数据编码H.264:从裸帧到视频流的关键技术

📅 2026/9/16 15:16:10 | 华诺云谱 👁 阅读
V4L2采集原始数据编码H.264:从裸帧到视频流的关键技术
简介从V4L2接口采集原始视频数据并编码为H.264格式是嵌入式Linux开发中常见且实用的任务。这个C语言工程特别适合嵌入式开发者和视频流应用学习者详细演示了V4L2设备操作、原始帧获取以及基于x264库的H.264编码流程。压缩包共11个文件包括3个C源文件、3个头文件、2个JSON配置、1个Makefile及README说明结构紧凑便于在真实设备或模拟环境上阅读、修改与交叉编译。目前已累计283人学习该资源。项目完整展示了从摄像头初始化、缓冲队列管理、原始图像读取到编码参数设置、输出码流的关键代码并提供了可直接参考的Makefile构建脚本。学习后可快速掌握嵌入式视频采集与压缩的落地方法适用于监控终端、无人机图传、车载记录仪等实时视频传输场景的入门与二次开发。1. V4L2 采集原始数据再编码 H.264这条链路比想象中更值得手工打通很多摄像头开发者在拿到一个 v4l2 设备节点后第一件事是打开cheese或ffplay看一眼画面。但真正要交付的往往不是“能预览”而是“能拿到裸帧并重新编码”。V4L2 采集出来的数据默认是 YUYV、NV12 这类未压缩的像素数据一帧 1080p 的 YUYV 要占约 4MB 带宽直接存储或传输都不现实所以必须接到 H.264 编码器后面。这个压缩包标题恰好把整条链路点明了先通过 v4l2 驱动框架抓原始数据再编码为 H.264 视频数据。整个过程不依赖神秘的黑盒只需要把VIDIOC_S_FMT、mmap、VIDIOC_DQBUF这几个 ioctl 调对再把帧交给软编码器或 V4L2 自带的 M2M 编码设备。适合正在做嵌入式摄像头、USB 摄像头采集或视频网关的工程师。2. V4L2 采集原始数据的核心路径从打开设备到拿到一帧2.1 先弄清原始数据是什么格式像素格式和 strideV4L2 的设备节点通常对应/dev/video0但同一个物理摄像头可以暴露多个节点其中 video0 通常是 raw capture 节点video1 可能是 metadata 或 JPEG 节点。在开始采集之前必须通过VIDIOC_ENUM_FMT查清楚设备驱动支持哪些像素格式。常见的输出格式有格式标识四字码每像素位数典型用途YUYVYUYV16免转换直接送 USB 摄像头但占用带宽大NV12NV1212H.264 编码器最喜欢的输入格式MJPEGMJPG可变摄像头内部压缩不是真正意义上的原始数据YUV420YU1212平面格式软件编码时常用一个反直觉的点是很多号称输出“裸数据”的摄像头实际输出的是 MJPEG这不算原始数据。如果要做 H.264 编码NV12 是最直接的输入因为无论是 libx264 还是硬件编码器原生支持的都是 NV12 而不是 YUYV。如果你的摄像头只给 YUYV那就需要在编码前做一次颜色空间转换这一步后续会提到。另一个容易踩坑的是 stride行跨距。V4L2 驱动为了内存对齐bytesperline不一定等于width * 2对 YUYV 而言。比如 1920 宽的图像实际每行可能多出 32 字节的 padding。如果忽略struct v4l2_pix_format里的bytesperline直接把 buffer 当紧凑数据送编码器会产生斜纹或绿边。2.2 设置采集格式的 ioctlVIDIOC_S_FMT 的参数细节我用一个最小 C 片段演示如何设置采集格式为 NV12。这里不讨论完整的错误处理重点看参数含义。#include linux/videodev2.h #include sys/ioctl.h #include fcntl.h #include string.h int v4l2_set_format(int fd, unsigned int w, unsigned int h) { struct v4l2_format fmt; memset(fmt, 0, sizeof(fmt)); fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width w; fmt.fmt.pix.height h; fmt.fmt.pix.pixelformat V4L2_PIX_FMT_NV12; fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); return -1; } // 注意设置后要用驱动回填的值不能再用自己传入的 w/h printf(driver set %ux%u, bytesperline%u, sizeimage%u\n, fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.bytesperline, fmt.fmt.pix.sizeimage); return 0; }这个函数有两个关键点。第一VIDIOC_S_FMT不保证满足你的所有请求比如驱动可能把 NV12 换成 YUYV或者把 width 对齐到某个宏块边界。所以在调用后必须重新读取fmt.fmt.pix用驱动回填的bytesperline和sizeimage来计算 buffer 大小而不是直接用w * h * 1.5。第二field设置为V4L2_FIELD_NONE表示输入是逐行扫描。如果摄像头是隔行设备这里会返回V4L2_FIELD_INTERLACED后续编码时还要做去隔行处理但这不在标题范围内。2.3 申请和管理缓冲区mmap 与 queue/dequeue 的最小闭环设置完格式后需要向驱动申请 buffer。V4L2 支持三种 I/O 方式read/write、mmap、userptr。对摄像头采集来说mmap是最通用的方式驱动把 buffer 映射到用户空间我们通过VIDIOC_QBUF和VIDIOC_DQBUF循环取帧。struct v4l2_requestbuffers req; memset(req, 0, sizeof(req)); req.count 4; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, req); struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type req.type; buf.memory V4L2_MEMORY_MMAP; buf.index 0; ioctl(fd, VIDIOC_QUERYBUF, buf); void *buffer mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); // 启动采集 enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, type); // 取帧循环 for (int i 0; i req.count; i) { // 把所有 buffer 先放入队列 memset(buf, 0, sizeof(buf)); buf.type req.type; buf.memory V4L2_MEMORY_MMAP; buf.index i; ioctl(fd, VIDIOC_QBUF, buf); } while (1) { ioctl(fd, VIDIOC_DQBUF, buf); // 取出已填充的帧 // 此时 buffer 里的数据就是一帧 NV12 process_frame(fd, buf, buffer buf.m.offset); }req.count 4是一个在实践中验证过的最小值。低于 3 个 buffer 时驱动内部流水线可能无法保证帧率尤其在 ARM 平台上。buf.m.offset是物理页地址偏移mmap 的 length 必须是buf.length而不是整个 buffer 大小。取帧后buffer buf.m.offset才是这一帧的起始地址因为驱动可能把多个 buffer 打包映射。还有一点很容易漏VIDIOC_QBUF不只是启动时要做每次处理完一帧后必须把该 index 重新放回队列否则 4 个 buffer 很快耗尽。2.4 帧率达到预期前先看这三个计数器如果你发现采集帧率只有标称的一半不要急着优化编码器。V4L2 驱动实现了帧丢失计数器和缓冲区状态可以通过VIDIOC_G_PARM查询帧率也可以通过读取/sys/class/video4linux/video0/下的frame_drops等节点来判断。用v4l2-ctl是最直接的方式v4l2-ctl -d /dev/video0 --get-parm v4l2-ctl -d /dev/video0 --list-formats-ext v4l2-ctl -d /dev/video0 --stream-mmap --stream-count100 --stream-to/dev/null--stream-mmap会启动 mmap 采集并统计实际帧率--stream-to/dev/null避免磁盘写入影响性能。如果这里的帧率正常说明问题在编码环节如果帧率已经偏低那就要检查时钟频率、USB 带宽或者 sensor 配置。压缩包里的代码如果在某个板子上跑不满第一步就得测这个。3. 编码为 H.264 的两条路线软件 x264 与 V4L2 M2M 硬件编码器3.1 为什么优先看硬件编码V4L2 驱动框架里的 codec 设备V4L2 不只是 capture它还定义了一整套video codec设备类型。很多 SoC 的编码器会以/dev/video10、/dev/video11这样的节点暴露出来设备类型是V4L2_BUF_TYPE_VIDEO_OUTPUT输入原始帧配合V4L2_BUF_TYPE_VIDEO_CAPTURE输出 H.264 码流。这就是 V4L2 M2Mmemory to memory设备。优先用硬件编码器原因不只是 CPU 占用低。在cef arm64 h.264这类场景里Chromium Embedded Framework 通常会调用系统的硬件视频解码/编码能力如果你的采集链路用软件 x264 占满 CPU主页面渲染就会掉帧。而 V4L2 硬件编码器通常能处理 1080p60 的同时保持 CPU 占用在个位数。判断一个设备是不是编码器可以看它的 capabilitiesv4l2-ctl -d /dev/video10 --info输出中如果有V4L2_CAP_VIDEO_M2M这就是一个 memory-to-memory codec 设备。v4l2-ctl --list-devices可以列出所有节点的拓扑通常video0是 sensor 采集video10是编码器 inputvideo11是编码器 output。3.2 用 FFmpeg/libx264 编码采集帧直接送入编码器如果没有硬件编码器或者只是想快速验证采集数据是否正确用 FFmpeg 的 libx264 是最短路径。从命令行的角度一条命令就可以完成采集和编码ffmpeg -f v4l2 -input_format nv12 -video_size 1920x1080 -framerate 30 -i /dev/video0 -c:v libx264 -preset veryfast -b:v 4M -f mp4 output.mp4参数说明-input_format nv12让 ffmpeg 请求 NV12 格式避免默认使用 YUYV-preset veryfast牺牲压缩率换取编码速度适合实时场景-b:v 4M限制码率。但在我们自己写的 C 程序里更常用的是直接调用 libavcodec 的 API这样能够复用从 v4l2 拿到的 buffer减少一次 memcpy。参考结构如下AVCodecContext *ctx avcodec_alloc_context3(avcodec_find_encoder(AV_CODEC_ID_H264)); ctx-width 1920; ctx-height 1080; ctx-pix_fmt AV_PIX_FMT_NV12; ctx-time_base (AVRational){1, 30}; ctx-framerate (AVRational){30, 1}; avcodec_open2(ctx, NULL, NULL); AVFrame *frame av_frame_alloc(); frame-buf[0] av_buffer_ref(v4l2_buffer_ref); // 复用 mmap buffer frame-data[0] v4l2_buffer_base; frame-data[1] v4l2_buffer_base ctx-width * ctx-height; frame-data[2] NULL; // NV12 是半平面只需要 data[0] 和 data[1] frame-linesize[0] v4l2_bytesperline; frame-linesize[1] v4l2_bytesperline;注意pix_fmt必须和 V4L2 协商到的pixelformat一致。当 V4L2 返回 NV12 时linesize[1]通常也是bytesperline因为 NV12 的 UV 平面每行采样减半但每个样本 2 字节所以 UV 平面的 stride 等于 Y 平面的 stride。而 YUYV 转 NV12 时需要逐行做 4:2:0 采样这里不展开但要知道这个转换本身就有开销。3.3 使用 V4L2 M2M 设备编码setup 和 streamon硬件编码器虽然传输量大但接口并不复杂。相对于采集节点M2M 编码设备有两个 buffer 队列一个用于投递原始帧一个用于接收码流。// 编码器 input 端设置为 OUTPUT buffer type struct v4l2_format out_fmt; memset(out_fmt, 0, sizeof(out_fmt)); out_fmt.type V4L2_BUF_TYPE_VIDEO_OUTPUT; out_fmt.fmt.pix.width 1920; out_fmt.fmt.pix.height 1080; out_fmt.fmt.pix.pixelformat V4L2_PIX_FMT_NV12; ioctl(enc_fd, VIDIOC_S_FMT, out_fmt); // 编码器 output 端设置为 CAPTURE buffer type请求 H.264 struct v4l2_format cap_fmt; memset(cap_fmt, 0, sizeof(cap_fmt)); cap_fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; cap_fmt.fmt.pix.pixelformat V4L2_PIX_FMT_H264; ioctl(enc_fd, VIDIOC_S_FMT, cap_fmt);这里一个常见的坑VIDIOC_S_FMT在 M2M 设备上需要成对设置先设置 OUTPUT 再设置 CAPTURE且 CAPTURE 的 width/height 一般不能大于 OUTPUT。有些驱动在设置 OUTPUT 后返回EINVAL原因是驱动只支持裁剪到特定分辨率范围必须先用VIDIOC_S_FMT查询驱动的sizeimage和bytesperline。之后申请 buffer 的流程和采集端一致只是type分别用V4L2_BUF_TYPE_VIDEO_OUTPUT和V4L2_BUF_TYPE_VIDEO_CAPTURE。投递原始帧时要把v4l2_buffer.flags里的V4L2_BUF_FLAG_KEYFRAME置位来请求 IDR 帧这个在码流 seek 时很有用。3.4 H.264/HEVC/MPEG4 在编码器选择上的差异做技术选型时压缩包标题锁定了 H.264但实际硬件编码器往往同时支持 H.264、HEVC、MPEG4。它们不是简单换一个pixelformat就完事编码格式压缩率硬件开销典型码率1080p30V4L2 四字码H.264中低4-8 MbpsH264HEVC高高2-4 MbpsHEVCMPEG4低极低8-12 MbpsMP4V如果你的目标是把码流存到本地MPEG4 完全够用但浪费空间如果要推到流媒体服务器HEVC 的兼容性在某些终端上仍然是问题。H.264 依然是最稳的中间值。V4L2 编码器在两个队列的格式设置上CAPTURE 端四字码变成V4L2_PIX_FMT_HEVC即可但驱动是否支持还要看VIDIOC_ENUM_FMT。我一般会在选择硬件编码器前先跑一个快速验证用v4l2-ctl --stream-mmap --stream-out-mmap抓一小段测试码流然后ffprobe看真实输出的 profile 和 level避免实际使用时才发现驱动只支持 Baseline。4. 一个最小 C 程序把采集与 H.264 编码串起来4.1 数据流和线程模型单线程轮询还是双线程队列在让采集和编码跑起来之前先决定架构。最简单的做法是单线程循环从 v4l2 capture 拿到一帧立刻送给编码器然后等待编码输出。这在帧率不高低于 30fps时是可行的。但如果编码器是 M2M 设备它也需要等待编码完成再 dqueue 码流两个 ioctl 发生在两个不同 fd 上单线程会导致编码期间采集 buffer 无法及时归还最终帧率被拖到编码速度的档次。更合理的结构是双线程加一个 FIFO。采集线程负责DQBUF - push frame - QBUF编码线程从队列取帧并执行OUTPUT_QBUF - CAPTURE_DQBUF。线程间同步用sem_t或者简单的互斥锁 条件变量即可。队列长度设置在 3-5 之间超过 5 个未编码帧就丢新帧避免内存膨胀。这里不贴完整代码重点看下面一节的格式转换。4.2 关键代码把 YUYV 转换成 NV12 再喂给编码器假设采集端摄像头只支持 YUYV而硬件编码器输入要求 NV12那么每个采集帧都需要一个转换步骤。这个转换用整数计算就可以保持合理速度void yuyv_to_nv12(const uint8_t *yuyv, uint8_t *nv12, int w, int h) { int uv_offset w * h; for (int y 0; y h; y 2) { for (int x 0; x w; x 2) { int src_idx (y * w x) * 2; int y1 yuyv[src_idx]; int y2 yuyv[src_idx 2]; int u yuyv[src_idx 1]; int v yuyv[src_idx 3]; nv12[y * w x] y1; nv12[y * w x 1] y2; nv12[(y 1) * w x] yuyv[((y 1) * w x) * 2]; nv12[(y 1) * w x 1] yuyv[((y 1) * w x) * 2 2]; int uv_idx uv_offset (y / 2) * w x; nv12[uv_idx] u; nv12[uv_idx 1] v; } } }这个函数利用了 YUYV 的半采样特性每个 2x2 像素块只保存第一个像素的 U 和 V。代码里没有做滤波对于大多数摄像头场景肉眼基本看不出区别。如果编码器要求bytesperline对齐需要在转换循环里带上 stride 参数上面的函数默认紧凑排列只适合bytesperline w * 2的情况。很多压缩包里的代码会直接假设紧凑在 USB 摄像头上是没问题的但在某些 SoC 的 ISP 输出上会导致绿线。4.3 时间戳和帧率控制不要丢掉原始 buffer 的 timestampv4l2 的struct v4l2_buffer里有一个timestamp字段是内核在帧完成时打上的。这个值来自于CLOCK_MONOTONIC语义是单调递增的墙上时间。送到编码器时AVFrame 的pts应该由这个 timestamp 换算而来而不是自己用gettimeofday打点。// 假设采集到的是单调时钟单位是纳秒 int64_t pts_ns buf.timestamp.tv_sec * 1000000000LL buf.timestamp.tv_usec * 1000LL; frame-pts pts_ns / (1000000000 / 30); // 转换为 30fps 的时间尺度如果在编码前做格式转换buf.timestamp仍然来自采集帧转码过程耗时不会改变这一个时刻。但如果使用硬件编码器 M2M 的 OUTPUT 队列驱动会忽略用户给的pts直接使用自己的 monotonic 时间这时候需要在v4l2_buffer.flags里设置V4L2_BUF_FLAG_TIMESTAMP_COPY确保编码器输出的码流 PTS 和采集端一致。这个微妙的小点容易导致你最后生成的视频在播放器里拖动时跳跃。4.4 压缩包内的典型模块划分和编译脚本从工程组织角度一个可移植的程序通常会分成这几个文件v4l2_capture.c负责采集encoder.c抽象编码器接口main.c做线程编排。Makefile里如果没有指定平台交叉编译时只要改前缀CROSS_COMPILE ? aarch64-linux-gnu- CC $(CROSS_COMPILE)gcc CFLAGS -O2 -Wall -Iinclude LDFLAGS -lavcodec -lavutil -lpthread OBJS main.o v4l2_capture.o encoder.o all: v4l2_h264 v4l2_h264: $(OBJS) $(CC) $^ -o $ $(LDFLAGS) clean: rm -f *.o v4l2_h264这里的-lavcodec -lavutil表明用的是 FFmpeg 软件编码路径如果是硬件编码器则不需要链接这两个库。注意到main.c里应该留一个宏开关用#ifdef HW_ENCODER来分别调用wait_for_m2m_event或avcodec_send_frame这样一个源代码包就能适配板卡和 x86 开发环境。5. 验证 H.264 码流并处理对齐、丢帧和 PTS 问题5.1 用 ffprobe 验证编码输出编码器配置与诊断拿到输出的 H.264 文件后不要只看能播放。ffprobe 会告诉你更多ffprobe -v error -show_streams output.mp4重点看width、height、profile、level和r_frame_rate。如果 profile 是 Baseline说明编码器默认没有开启 B 帧压缩率会差一截如果 level 是 4.0 以下而分辨率是 1080p60播放器可能会因为 level 限制拒绝流畅播放。更实用的验证是检查码流的 sps/pps 是否完整H.264 的 SPS/PPS 通常放在每个 IDR 帧之前如果丢掉了很多播放器在 seek 后无法解码。用 FFmpeg 转存时可以通过-bsf:v h264_mp4toannexb转换封装看看 bs 是否正常。5.2 行对齐stride不对时的花屏和绿边花屏是所有视频采集开发者的噩梦但原因往往只有几种。第一是bytesperline大于width * 2而你直接按紧凑方式把 buffer 送给了编码器会导致每条扫描线都偏移画面变成斜向切片。第二是编码器要求width对齐到 16而采集端给的是 1920通常没问题但如果是 1280x720 这种非 16 倍数分辨率V4L2 可能实际采集的是 1280x720但 stride 对齐到 1296编码器必须知道这个信息。在 FFmpeg 的AVFrame里设置linesize[0]为实际的bytesperline而不是width就能避免这一类问题。对于 V4L2 M2M 编码器检查VIDIOC_S_FMT返回的bytesperline是否在设置后发生改变。5.3 PTS 不连续导致的播放卡顿和实时性取舍最后考虑实时传输场景。如果采集帧率是 30fps编码器处理速度跟不上就会出现 deque 到太晚导致上一帧还没编码完的情况。这时我认为应该直接丢新帧而不是把每一帧都排队。因为 H.264 编码器状态下连续丢帧产生的 GOP 仍然可以被解码器正确重组而如果所有帧都堆积最终会导致采集 buffer 空等待整个链路延迟增加数秒。判断丢帧的阈值是队列深度每当待编码队列超过 3 帧就丢弃新采集帧并递增一个 counter。这个计数器可以暴露成日志在运行一段时间后如果 counter 增长速度超过每秒 2 次就要考虑降低码率或者切换更快的编码器。另一个技巧是用 V4L2 采集端的VIDIOC_G_PARM把帧率从 30 降为 25很多 sensor 硬件内部可以直接做 5:6 抽帧比 CPU 端丢帧更平滑。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。