嵌入式音视频必备-V4L2架构(采集-格式转换-渲染-H264编码-保存本地)
1 V4L2 架构
1.1 分层示意图
视频讲解(代码领取见视频):嵌入式音视频必备-V4L2采集-格式转换-渲染-H264编码-保存本地
关键组件说明
1. 应用层
- 通过V4L2 API(如 open("/dev/video0") 、 ioctl )与内核交互。
- 典型操作:设置分辨率( VIDIOC_S_FMT )、获取帧数据( VIDIOC_QBUF/VIDIOC_DQBUF )。
2. V4L2核心层
- 内核模块: videodev.ko (提供 /dev/videoX 设备节点)。
- 功能:标准化接口、缓冲管理(DMABUF/MMAP)、事件通知。
3. 驱动层
驱动示例:
- USB摄像头: uvcvideo (通用驱动)。
- MIPI摄像头:厂商自定义驱动(如 ov5640.c )。
实现 struct v4l2_device_ops 中的回调函数(如 s_stream 启停流)。
4. 硬件层
- 传感器:通过I2C配置寄存器(如曝光、增益)。
- 数据传输:MIPI CSI-2/USB UVC协议传输原始图像数据(YUV/RGB)。
数据流示例
1. 应用层通过 ioctl(VIDIOC_REQBUFS) 请求内核分配缓冲区。
2. 驱动层初始化摄像头硬件,通过DMA将图像数据填充到缓冲区。
3. 应用层调用 read() 或 mmap() 获取帧数据(YUV420P/MJPEG格式)。
常见关联文件
- 设备节点: /dev/video0 多个摄像头时: /dev/video1 , /dev/video2 ....
- 内核配置: CONFIG_VIDEO_DEV=y
2 项目架构
2.1 项目框架图
分析当前项目整体的架构图,并说明各个组件之间的关系和需要注意的细节问题。
2.2 关键流程和注意事项
数据流向
摄像头 -> V4L2Capture -> VideoFormatConverter -> SDLDisplay/Encoder -> H264FileWriter
各模块关键点
V4L2Capture
初始化注意事项:
- - 检查设备是否存在
- - 验证设备支持的格式
- - 确保请求的分辨率和格式被设备支持
- - 正确配置内存映射缓冲区数量
性能考虑:
- - 使用足够的缓冲区数量(通常4-8个)
- - 采用MMAP方式而不是read方式
- - 检查实际获取的帧大小
VideoFormatConverter
格式转换注意事项:
- - 确保源格式和目标格式兼容
- - 检查分辨率是否需要缩放
- - 注意YUV格式的内存对齐要求
性能优化:
- - 复用SwsContext避免重复创建
- - 选择合适的缩放算法
SDLDisplay
显示同步:
- - 处理SDL事件避免界面卡死
- - 控制显示帧率
- - 处理窗口大小改变事件
资源管理:
- - 正确释放SDL资源
- - 确保纹理格式匹配
Encoder
编码参数设置:
- - 设置合适的码率和GOP大小
- - 配置正确的时间基准
- - 处理关键帧设置
性能考虑:
- - 选择合适的编码预设
- - 注意编码延迟
H264FileWriter
文件操作:
- - 使用二进制模式打开文件
- - 正确处理文件写入错误
- - 及时刷新文件缓冲区
资源管理:
- - 确保文件正确关闭
- - 处理磁盘空间不足情况
3 V4L2 接口编程
3.1 V4L2 编程基础流程
典型的 V4L2 应用程序开发流程如下:
1. 打开设备文件
2. 查询设备能力
3. 设置视频格式
4. 申请缓冲区
5. 启动视频流
6. 捕获视频帧
7. 处理帧数据
8. 停止视频流
9. 释放资源
3.2 关键数据结构
V4L2 定义了一系列重要的数据结构:
struct v4l2_capability; // 设备能力 struct v4l2_format; // 视频格式 struct v4l2_requestbuffers; // 缓冲区请求 struct v4l2_buffer; // 缓冲区信息 struct v4l2_input; // 输入源 struct v4l2_control; // 控制参数
3.3 详细编程步骤
3.3.1 打开设备
#include <fcntl.h> #include <unistd.h> #include <linux/videodev2.h> // 以读写方式打开V4L2设备 fd_ = open("/dev/video0", O_RDWR); if (fd_ < 0) { std::cerr << "无法打开视频设备: " << device_ << std::endl; return false; }
3.3.2 查询设备能力
// 查询设备能力,检查是否是有效的V4L2设备 struct v4l2_capability cap; if (ioctl(fd_, VIDIOC_QUERYCAP, &cap) < 0) { std::cerr << "无法查询设备能力" << std::endl; Close(); return false; } // 验证设备是否支持视频捕获功能 if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { std::cerr << "设备不支持视频采集" << std::endl; Close(); return false; }
3.3.3 设置视频格式
struct v4l2_format fmt; memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置期望的格式参数 fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420; fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // 应用格式设置 if (ioctl(fd_, VIDIOC_S_FMT, &fmt) < 0) { std::cerr << "无法设置期望的格式: 0x" << std::hex << pixel_format_ << std::dec << std::endl; return false; } // 保存实际的设备参数 width_ = fmt.fmt.pix.width; height_ = fmt.fmt.pix.height; actual_pixel_format_ = fmt.fmt.pix.pixelformat; frame_size_ = fmt.fmt.pix.sizeimage;
3.3.4 申请缓冲区
V4L2 支持多种缓冲模式,最常用的是内存映射(Memory Mapping)模式。
申请缓冲区的意义
在 Linux V4L2 (Video4Linux2) 框架中, V4L2_MEMORY_MMAP 是一种 内存映射(Memory Mapping) 的缓
冲区分配模式,其核心目的是:
- 零拷贝(Zero-Copy):通过将内核驱动的摄像头缓冲区直接映射到用户空间(应用层),避免数据从 内核到用户空间的显式拷贝,减少 CPU 开销。
- 高效访问:应用层可以直接操作映射的内存区域,无需调用 read() 等系统调用逐帧拷贝数据。
数据流框架图(逻辑流程)
关键步骤详解
实际代码
bool V4L2Capture::InitMmap() { struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); req.count = buffer_count_; req.type = buf_type_; req.memory = V4L2_MEMORY_MMAP; // 请求分配缓冲区 if (ioctl(fd_, VIDIOC_REQBUFS, &req) < 0) { std::cerr << "请求缓冲区失败" << std::endl; return false; } // 实际分配到的缓冲区可能少于请求的数量 buffer_count_ = req.count; buffers_ = new Buffer[buffer_count_]; // 映射所有缓冲区 for (unsigned int i = 0; i < buffer_count_; ++i) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = buf_type_; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; // 查询缓冲区信息 if (ioctl(fd_, VIDIOC_QUERYBUF, &buf) < 0) { std::cerr << "查询缓冲区失败: " << i << std::endl; return false; } // 映射缓冲区 buffers_[i].length = buf.length; buffers_[i].start = mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, buf.m.offset); if (buffers_[i].start == MAP_FAILED) { std::cerr << "内存映射失败: " << i << std::endl; return false; } } return true; }
3.3.5 启动视频流
重点命令:
- VIDIOC_QBUF
- VIDIOC_STREAMON
bool V4L2Capture::StartStreaming() { if (is_streaming_) { return true; } // 初始化内存映射 if (!InitMmap()) { return false; } // 将所有缓冲区加入队列 for (unsigned int i = 0; i < buffer_count_; ++i) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = buf_type_; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (ioctl(fd_, VIDIOC_QBUF, &buf) < 0) { std::cerr << "无法将缓冲区加入队列: " << i << std::endl; return false; } } // 开启视频流 if (ioctl(fd_, VIDIOC_STREAMON, &buf_type_) < 0) { std::cerr << "无法启动视频流" << std::endl; return false; } is_streaming_ = true; return true; }
3.3.6 捕获视频帧
重点命令:
- VIDIOC_DQBUF
/** * @brief 读取一帧视频数据 * 从设备读取原始视频数据到提供的缓冲区 * @param buffer 数据缓冲区 * @param buffer_size 缓冲区大小 * @param bytes_read 实际读取的字节数 * @return 成功返回true,失败返回false */ bool V4L2Capture::ReadFrame(uint8_t* buffer, size_t buffer_size, size_t* bytes_read) { if (!is_streaming_) { if (!StartStreaming()) { return false; } } struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = buf_type_; buf.memory = V4L2_MEMORY_MMAP; // 从队列中取出一个已经填充好数据的缓冲区 if (ioctl(fd_, VIDIOC_DQBUF, &buf) < 0) { std::cerr << "无法从队列中取出缓冲区" << std::endl; return false; } // 检查缓冲区大小 if (buffer_size < buf.bytesused) { std::cerr << "目标缓冲区太小: " << buffer_size << " < " << buf.bytesused <<std::endl; return false; } // 复制数据 memcpy(buffer, buffers_[buf.index].start, buf.bytesused); if (bytes_read) { *bytes_read = buf.bytesused; } // 将缓冲区重新加入队列 if (ioctl(fd_, VIDIOC_QBUF, &buf) < 0) { std::cerr << "无法将缓冲区重新加入队列" << std::endl; return false; } return true; }
3.3.7 停止视频流和清理
重点命令:
- VIDIOC_STREAMOFF
void V4L2Capture::StopStreaming() { if (!is_streaming_) { return; } // 停止视频流 if (ioctl(fd_, VIDIOC_STREAMOFF, &buf_type_) < 0) { std::cerr << "停止视频流失败" << std::endl; } is_streaming_ = false; }
3.4 高级功能
3.4.1 控制参数设置
struct v4l2_control ctrl; ctrl.id = V4L2_CID_BRIGHTNESS; ctrl.value = 50; if (ioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) { perror("设置亮度失败"); }
3.4.2 枚举支持的格式
struct v4l2_fmtdesc fmtdesc; fmtdesc.index = 0; fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; printf("支持的格式:\n"); while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) { printf("%d. %s\n", fmtdesc.index, fmtdesc.description); fmtdesc.index++; }
3.4.3 设置帧率
struct v4l2_streamparm parm; parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_G_PARM, &parm) == 0) { parm.parm.capture.timeperframe.numerator = 1; parm.parm.capture.timeperframe.denominator = 30; // 30fps if (ioctl(fd, VIDIOC_S_PARM, &parm) == -1) { perror("设置帧率失败"); } }
3.5 常见问题解决
1. VIDIOC_S_FMT 失败:检查设备是否支持请求的分辨率和格式
2. VIDIOC_REQBUFS 失败:尝试减少缓冲区数量或检查内存限制
3. 帧数据损坏:确认像素格式是否正确解析
4. 性能问题:考虑使用DMA缓冲区或用户指针模式
4 画面实时显示
大致了解即可。
涉及到SDL2开源库,用来渲染摄像头画面,可以使用命令安装
sudo apt-get install libsdl2-dev
具体的显示流程
- 初始化阶段 (SDLDisplay::Init)
- 显示循环 (SDLDisplay::DisplayFrame)
- 事件处理 while (SDL_PollEvent(&event))
- 资源清理 (SDLDisplay::Cleanup)
数据流向详解
1. AVFrame中的YUV数据格式:
frame->data[0] = Y平面数据 (width * height) frame->data[1] = U平面数据 (width/2 * height/2) frame->data[2] = V平面数据 (width/2 * height/2)
2. 内存布局:
Y平面: 连续的width * height字节 U平面: 连续的(width/2) * (height/2)字节 V平面: 连续的(width/2) * (height/2)字节
3. 数据传输路径:
系统内存(AVFrame) → SDL_UpdateYUVTexture → GPU内存(SDL_Texture) → SDL_RenderCopy → 显示缓冲区 → 屏幕
异常问题处理
error while loading shared libraries: libswresample.so.5:
/v4l2_capture_test: error while loading shared libraries: libswresample.so.5: cannot open shared object file: No such file or directory
执行程序时找不到对应的库文件路径,是因为我们的ffmpeg lib只是在项目路径里,可以使用LD_LIBRARY_PATH设置环境变量,在执行程序的终端先使用LD_LIBRARY_PATH设置ffmpeg库文件路径,比如:
export LD_LIBRARY_PATH=/home/lqf/mcms/src/driver/5rd/ffmpegn7.1/lib:$LD_LIBRARY_PATH
注意:需要设置自己的ffmpeg路径,不要直接用我这个路径
#硬件##C++##项目##简历中的项目经历要怎么写##牛客创作赏金赛#