嵌入式音视频必备-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++##项目##简历中的项目经历要怎么写##牛客创作赏金赛#
全部评论

相关推荐

评论
点赞
7
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务