ARTICLE DETAIL

资讯详情

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

从V4L2到H.264:嵌入式Linux视频采集编码实践

从V4L2到H.264:嵌入式Linux视频采集编码实践 简介这是一份面向嵌入式开发者的视频采集编码示例工程解决从Linux V4L2接口获取原始视频数据并编码为H.264格式的问题。资源基于C语言编写核心代码覆盖V4L2设备打开、帧缓冲读取、x264编码器调用等关键环节适合正在学习嵌入式Linux视频流处理、或需要开发监控与图传项目的开发者参考。包内共11个文件以C源文件、头文件为主搭配Makefile编译脚本、JSON工程配置及README说明文档整体打包约817KB结构清晰便于按模块阅读。目前已有283人学习内容包括v4l2_device与x264_encoder两个模块的源码实现并配有配置文件与编译脚本可对照代码理解V4L2视频采集、原始帧预处理、色彩空间转换以及H.264编码参数设置的全部流程。项目体量适中能够帮助读者快速搭建采集编码流程并在此基础上向实际嵌入式硬件平台移植扩展。1. V4L2采集到H.264编码一个能跑的嵌入式参考实现假设你手头是一块ARM Linux开发板需要把USB摄像头拍到的画面实时变成H.264码流又不愿意为这一点活把GStreamer整个拉进来。这个raw2H.264-master包给的就是一条最短链路V4L2打开/dev/video0拿到YUYV或NV12原始帧转成I420后交给x264编码器最终输出Annex-B格式的H.264码流。它没有用libavcodec那种大而全的抽象而是用C语言直接操作ioctl更适合用来理解采集和编码之间到底怎么衔接。适合嵌入式Linux、驱动、流媒体入门者拆开读。2. V4L2设备采集链路从打开摄像头到拿到原始帧2.1 V4L2核心ioctl与缓冲区管理V4L2Video4Linux2是Linux内核给视频设备提供的统一接口它把摄像头抽象成字符设备所有控制都通过ioctl完成。项目里的v4l2_device.c对设备打开、能力查询、格式设置、缓冲区申请做了一层封装最终暴露给上层的是一个能持续返回帧指针的接口。我一般会先查询设备能力确认驱动支持VIDIOC_STREAMON和MMAP模式代码大致是这个样子#include linux/videodev2.h #include fcntl.h #include sys/ioctl.h int v4l2_open_device(const char *path) { int fd open(path, O_RDWR | O_NONBLOCK); if (fd 0) return -1; struct v4l2_capability cap; if (ioctl(fd, VIDIOC_QUERYCAP, cap) 0) { close(fd); return -1; } if (!(cap.capabilities V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, not a capture device\n); close(fd); return -1; } return fd; }这段代码做了三层判断open失败说明设备节点不存在或权限不够VIDIOC_QUERYCAP失败说明驱动没有正确响应V4L2_CAP_VIDEO_CAPTURE标志位不存在说明当前设备不是采集设备。项目里在v4l2_device.h中暴露的v4l2_device_open基本就是这个逻辑只是多了一个设备名参数和一个错误码记录。缓冲区管理上V4L2支持MMAP、USERPTR和DMABUF三种内存方式。这个示例默认走MMAP因为它在用户态只需要维护映射地址省去手动分配对齐内存的麻烦。申请缓冲区调用VIDIOC_REQBUFS指定V4L2_MEMORY_MMAP和缓冲区数量然后通过VIDIOC_QUERYBUF拿到偏移量并mmap到用户空间。这里有个容易忽略的点REQBUFS的数量不是越多越好嵌入式平台往往只有3个左右可用请求10个反而可能让驱动拒绝常见做法是请求4个然后逐个检查。2.2 像素格式与缓冲区映射参数怎么设采集参数集中在struct v4l2_format里面最关键的几个字段是width、height、pixelformat和field。项目里的config.h一定预留了这些宏比如CAMERA_WIDTH、CAMERA_HEIGHT和V4L2_PIX_FMT_YUYV。struct v4l2_format fmt {0}; fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width 640; fmt.fmt.pix.height 480; fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); return -1; }这里必须注意VIDIOC_S_FMT是设置和返回实际格式驱动可能会因为不支持而改写宽高或像素格式。所以设置完要读回fmt.fmt.pix对比width和pixelformat是否和期望一致否则后续编码会拿到完全不对的尺寸。field设成V4L2_FIELD_NONE表示逐行扫描隔行设备需要额外做去隔行但这个项目针对的是USB摄像头和CSI摄像头基本都可以按逐行处理。接下来是VIDIOC_REQBUFS和mmap。一个常见的参数组合是4个缓冲区每个缓冲区大小用fmt.fmt.pix.sizeimage协商出来。要注意sizeimage在某些驱动里并不是严格的width*height*bpp比如YUV422格式下可能是width*height*2加上对齐填充。因此项目里应该用这个字段来分配而不是自己按公式硬算。下面是一个常见的RAW帧格式对照表判断摄像头输出和x264输入的差距格式排列方式每像素位数x264直接输入YUYVY0 U0 Y1 V0 交替16不支持需转换NV12Y平面 UV交错平面12需转换为I420I420/YU12Y平面 U平面 V平面12直接支持x264内部常用X264_CSP_I420也就是YUV420 planar三个平面分别连续存放。如果V4L2给出的是NV12就需要做一个UV interleaved到planar的拷贝这个转换在v4l2_device.c或main.c里会有单独的小函数。转换时别忽略y_stride和uv_stride摄像头行对齐可能不是16像素直接memcpy整行会把花屏问题带进编码器。2.3 采集循环与丢帧处理采集开始后最外层的循环就是不断从驱动拿帧再还回去。标准流程是先VIDIOC_STREAMON然后对每个空闲缓冲区调用VIDIOC_QBUF入队再阻塞在poll上等待数据就绪最后VIDIOC_DQBUF出队拿到一帧。while (running) { fd_set fds; FD_ZERO(fds); FD_SET(fd, fds); struct timeval tv {2, 0}; int ret select(fd 1, fds, NULL, NULL, tv); if (ret 0) continue; struct v4l2_buffer buf {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, buf) 0) { if (errno EAGAIN) continue; break; } process_frame(buffers[buf.index].start, buf.bytesused); ioctl(fd, VIDIOC_QBUF, buf); }select的2秒超时是防止驱动异常时死等。DQBUF拿到buf.index后立刻用该索引去查之前mmap的地址然后交给后续编码线程。处理完必须马上VIDIOC_QBUF还给驱动否则缓冲区耗尽摄像头会自动丢帧。有些驱动没有把bytesused对齐到行编码时如果按整帧大小读可能读到上一帧的残留数据这一步需要结合v4l2_pix_format的bytesperline做边界处理。丢帧不只是发生在缓冲区不足的时候。如果编码线程平均耗时超过帧间隔采集线程却始终保持全速运行用户态队列就会溢出。我在类似项目里的做法是当DQBUF返回成功但当前帧时间戳和上一帧间隔小于预期时不做编码直接将该缓冲区重新入队。这样编码器永远拿到的是节奏稳定的帧而不是突然来一帧突然丢一帧。3. x264编码器接入把YU12帧变成H.264码流3.1 x264_param_default_preset的参数权衡x264库是H.264编码的事实标准项目里的x264_encoder.c本质上做的是“图像帧进、NAL包出”的胶水工作。初始化编码器的第一步是准备好x264_param_t调用x264_param_default_preset来填充参数。x264_param_t param; x264_param_default_preset(param, veryfast, zerolatency); param.i_width 640; param.i_height 480; param.i_fps_num 30; param.i_fps_den 1; param.i_csp X264_CSP_I420; param.rc.i_rc_method X264_RC_ABR; param.rc.i_bitrate 1000; param.i_keyint_max 30; param.i_threads 1;veryfast预设意味着用极少的计算换吞吐量对嵌入式CPU比较友好zerolatency取消帧重排编码器不会为了双向预测等未来帧这对实时传输至关重要。i_threads设为1是为了避免帧级并行带来的延迟和内存开销如果主板有多个核心并且不是硬实时场景也可以设成2。参数表里i_fps_num/fps_den必须和V4L2实际帧率一致否则码率控制会按错误的时间基计算。i_csp必须是X264_CSP_I420所以第2章采集到的YUV数据要先做格式转换。i_keyint_max控制IDR帧间隔30代表1秒一个关键帧方便客户端快速开始解码但会牺牲一点码率。i_bitrate是码率控制的目标值单位kbps在编码循环中需要最后再调用一次x264_encoder_headers来获取SPS/PPS。还有一个隐藏参数容易被忽略param.b_repeat_headers。建议设为1让每个关键帧前面都带上SPS/PPS。这样播放器从文件中间开始播也能解码对于RTSP拉流这种场景尤其重要。核心参数汇总如下参数推荐值说明i_threads1减少并行延迟i_keyint_max301秒关键帧rc.i_methodABR简单码率控制b_repeat_headers1关键帧附带SPS/PPS3.2 图像帧封装与编码器交互x264接受的不是裸的内存指针而是x264_picture_t。这个结构描述了图像格式、平面指针和时间戳。需要把第2章拿到帧数据填充进去x264_picture_t pic_in, pic_out; x264_picture_alloc(pic_in, X264_CSP_I420, 640, 480); pic_in.img.i_plane 3; pic_in.img.plane[0] y_plane; pic_in.img.plane[1] u_plane; pic_in.img.plane[2] v_plane; pic_in.img.i_stride[0] width; pic_in.img.i_stride[1] width / 2; pic_in.img.i_stride[2] width / 2; pic_in.i_pts frame_count;如果V4L2输出的是NV12或YUYV就不能直接塞进pic_in.img.plane。项目一般在main.c里完成转换转换时要注意i_stride数组它表示每个平面每一行占用的字节数。很多摄像头bytesperline大于width因为硬件要求行对齐比如宽640的NV12帧bytesperline可能是672。这种情况下把整个帧按width做转换会出斜纹或绿色边缘。编码调用很简单x264_nal_t *nals; int i_nals 0; int frame_size x264_encoder_encode(encoder, nals, i_nals, pic_in, pic_out); if (frame_size 0) { for (int i 0; i i_nals; i) { fwrite(nals[i].p_payload, 1, nals[i].i_payload, out_fp); } }x264_encoder_encode一次会输出一个或多个NAL单元其中可能有SPS、PPS、IDR或普通P帧。项目里是把所有NAL直接写进文件或socket这里要注意i_payload不等于frame_sizeframe_size是编码后总大小真正的数据分布在nals数组里必须逐个写。3.3 编码输出的封装与调试x264默认输出的是Annex-B格式也就是每个NAL前面有00 00 00 01起始码可以直接存储为.h264文件也可以通过live555或FFmpeg封装。如果要转成.mp4一般把裸流交给ffmpeg -i raw.h264 -c copy out.mp4不需要重新编码保留原始码流。调试时我习惯先把码流写到文件再用ffprobe验帧ffprobe -v error -select_streams v:0 -show_packets output.h264正常输出里可以看到flagsK的包对应关键帧。如果发现第一个关键帧迟迟不出现检查i_keyint_max是否设置成功如果PTS乱跳检查递增的frame_count是否被重置如果文件大小远大于目标码率检查i_bitrate的单位——x264用的是kbps不是bps。项目里的README.md一般会写明输出路径和验证命令这个包的README只有简短说明所以我通常自己用上面的方法确认编码器没有空转。4. 从Makefile到主板编译、部署与常见坑4.1 交叉编译与Makefile调整压缩包根目录的Makefile直接控制整个项目的构建里面默认的CC是gcc如果目标机器是ARM开发板就需要改成交叉编译器。项目里sources很干净只有main.c、v4l2_device.c、x264_encoder.c头文件也齐全没有复杂的生成步骤。一个适用于arm-linux-gnueabihf的Makefile片段CC : arm-linux-gnueabihf-gcc CFLAGS : -O2 -Wall -I. -I$(X264_ROOT)/include LDFLAGS : -L$(X264_ROOT)/lib -lx264 -lpthread -lm TARGET : raw_to_h264 OBJS : main.o v4l2_device.o x264_encoder.o $(TARGET): $(OBJS) $(CC) -o $ $(OBJS) $(LDFLAGS) clean: rm -f $(OBJS) $(TARGET)这里有几个点要说明。-lx264要求目标板上有对应的libx264库文件优先选择静态库libx264.a这样部署时不用带着.so到处拷贝。如果x264源码本身也需要编译用./configure --hostarm-linux-gnueabihf --enable-static --disable-opencl --disable-avs生成Makefile注意--disable-avs不是必须的看清configure帮助就好。编译时如果报undefined reference to x264_encoder_open大概率是链接库顺序错误把-lx264放到目标文件后面就行。Makefile里的CFLAGS不建议用-g -O0因为帧处理循环会非常慢。用-O2既能保持代码可读又能让x264编码路径跑满。.vscode/c_cpp_properties.json在项目里是给IDE解析头文件用的不影响构建但如果你用不同交叉编译链需要同步修改里面的compilerPath否则排查花屏问题时看到的宏定义可能对不上。4.2 运行时的V4L2权限与驱动问题拿到编译好的raw_to_h264之后最常见的运行错误是open /dev/video0: Permission denied。开发板在rootfs下通常不会自动加udev规则需要手动把当前用户加入video组或者直接在/etc/udev/rules.d/放一条规则。sudo usermod -a -G video $USER newgrp video如果设备节点存在但打开后返回Invalid argument要用v4l2-ctl确认摄像头支持的分辨率和格式。例如v4l2-ctl -d /dev/video0 --list-formats-ext这个命令会列出设备全部格式、每种格式下支持的分辨率。如果项目config.h写的是V4L2_PIX_FMT_YUYV但摄像头只支持NV12VIDIOC_S_FMT会因为协商失败返回EINVAL。解决方式是先读回驱动实际支持的格式把config.h改成对应的宏。还有一个隐蔽问题某些CSI摄像头驱动需要先设置media-ctl链路才能采集这属于平台相关只能参考芯片SDK。运行时的mmap失败多数和内存碎片有关。V4L2请求连续内存失败时可以改用USERPTR模式自己在用户态分配对齐内存并将指针传给驱动。但该项目没有实现USERPTR分支所以如果遇到这种情况优先检查ulimit -l限制和内核预留CMA大小。4.3 编码花屏、延迟和内存泄漏定位花屏是所有编码项目最头疼的问题。先分清是“采集端原图就花”还是“编码后花”。把第2章的process_frame里直接fwrite一帧YUV到文件用ffplay -f rawvideo -pixel_format yuyv422 -video_size 640x480 frame.yuv看原始图。如果原图正常那就是格式转换或stride问题。x264编码时i_stride必须和bytesperline一致很多作者会用width代替导致每行数据错位。解决方法是把v4l2_pix_format的bytesperline打印出来然后在填充x264_picture_t时传给i_stride。下面是一张快速定位表按出现频率排序现象最常见原因先检查哪里画面斜纹stride与bytesperline不一致打印bytesperline偏色/绿边像素格式转换错YUYV与NV12搞混间歇性卡顿DQBUF后未及时QBUF检查环形队列整体延迟高bframes或lookahead未关确认zerolatency延迟方面如果从采集到输出码流之间有200ms以上的延迟先看x264_param_t里的i_bframeszerolatency预设下应该是0。再看rc.i_lookahead这个值控制编码器向前看多少帧在实时场景建议设0。最后用strace跟踪调用耗时如果VIDIOC_DQBUF阻塞时间很长说明驱动层积压帧此时编码线程可能已经跑不动了需要降低分辨率或码率。内存泄漏排查看两个地方x264_picture_alloc分配的图像缓冲是否在循环外只分配一次循环结束后调用x264_picture_cleanV4L2缓冲区在程序退出时是否执行了munmap和VIDIOC_REQBUFS(0)。另外DQBUF每成功的帧如果没有配对QBUF驱动侧缓冲区池会慢慢耗尽表现为编码器帧率抖降然后select超时退出。用valgrind --leak-checkfull ./raw_to_h264跑几十秒如果看到definitely lost增长优先级最高。5. 进阶把裸码流推进RTSP推流与时间戳对齐5.1 时间戳来源与对齐V4L2的struct v4l2_buffer自带timestamp字段类型是struct timeval但它在不同驱动下可能是CLOCK_MONOTONIC或CLOCK_REALTIME。直接把它当作编码PTS会出问题因为x264只认单一时间基。我习惯在采集线程里用clock_gettime(CLOCK_MONOTONIC)维护一个计数器每采集到一帧就把当前纳秒值换算成90kHz单位作为pic_in.i_pts换算公式是纳秒乘以9再除以100000得到的整数就是RTP标准时间戳。这样一路走下来推流端的音画同步才有基准。5.2 接上live555或自写RTSP推流项目本身没有推流功能但编码器输出的是标准Annex-B码流接推流框架很容易。用live555做RTSP Server时需要把x264的NAL交给H264VideoRTPSink关键是把SPS/PPS从码流里剥离出来单独成帧。常见做法是解析00 00 00 01之后的NAL typetype为7和8的分别保存然后在continuePlaying里通过getNextFrame函数逐个发送。if ((nal_type 0x1F) 7) { memcpy(sps, nal_data, nal_size); sps_len nal_size; }如果不做SPS/PPS分离直接把整个H.264文件丢给VideoOpenFile播放端很可能只出一个黑屏。另外记得为每个编码帧设置fDurationInMicroseconds 33000对应30fpsRTP打包器才知道时间戳间隔。5.3 验证编码器性能的小工具验证不一定要等推流。在裸码流阶段可以用ffprobe确认帧类型和PTSffprobe -v trace -i output.h264 21 | grep -E keyframe|pict_type要测实际编码吞吐用time跑1分钟对比帧计数time ./raw_to_h264 -c 1800记录real时间和编码成功帧数除以real就是平均吞吐。如果吞吐低于目标帧率优先降低i_threads和lookahead再考虑换编码预设。最后还有一个技巧在编码器输出端加一个环形缓冲采集线程与编码线程解耦这样V4L2的帧率抖动不会直接传导到推流端。缓冲区大小设3帧就够太大反而增加延迟。本文还有配套的精品资源点击获取
返回列表