嵌入式音视频必备-V4L2架构(采集-格式转换-渲染-H264编码-保存本地)
典型操作:设置分辨率( VIDIOC_S_FMT )、获取帧数据( VIDIOC_QBUF/VIDIOC_DQBUF )。:通过将内核驱动的摄像头缓冲区直接映射到用户空间(应用层),避免数据从 内核到用户空间的显式拷贝,减少 CPU 开销。设备节点: /dev/video0 多个摄像头时: /dev/video1 , /dev/video2 ....通过V4L2 API(如 open("/dev/
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路径,不要直接用我这个路径
更多推荐
所有评论(0)