linux音视频采集技术: v4l2

简介

在 Linux 系统中,视频设备的支持和管理离不开 V4L2(Video for Linux 2)。作为 Linux 内核的一部分,V4L2 提供了一套统一的接口,允许开发者与视频设备(如摄像头、视频采集卡等)进行交互。无论是视频采集、处理,还是编码和显示,V4L2 都提供了强大的支持。本文将简单介绍一下 V4L2 的工作流程以及如何使用它进行视频采集。

参数介绍

v4l2并没有提供单独封装的API接口,而是通过 ioctl 系统调用以及v4l2所提供的特定参数来对设备进行控制和采集。

下面是主要的 ioctl 控制参数:

1.VIDIOC_QUERYCAP :查询设备能力。

可用于查询枚举视频设备,获取设备名、总线名、支持的能力等。并非所有设备都支持,有可能会查询失败。

相关结构定义:

cpp 复制代码
struct v4l2_capability {
    __u8 driver[16];      // 驱动名称
    __u8 card[32];        // 设备名称
    __u8 bus_info[32];    // 设备的总线信息
    __u32 version;        // 驱动版本号
    __u32 capabilities;   // 设备支持的功能
    __u32 device_caps;    // 设备的具体能力
    __u32 reserved[3];    // 保留字段
};

2.VIDIOC_S_FMT :设置视频格式。

不同设备所支持的 pixelformat 不尽相同,所以设置的某些格式可能不会生效,比如我使用的海康摄像头只支持mjpg和yuyv。因此最好先使用 VIDIOC_ENUM_FMT 查询设备设备支持的格式,以确保设置生效。

相关结构定义:

cpp 复制代码
struct v4l2_format {
    enum v4l2_buf_type type; // 缓冲区类型(如视频采集)
    union {
        struct v4l2_pix_format pix; // 视频帧格式
        // 其他格式(如 overlay、视频输出等)
    } fmt;
};

struct v4l2_pix_format {
    __u32 width;          // 视频宽度
    __u32 height;         // 视频高度
    __u32 pixelformat;    // 像素格式(如 V4L2_PIX_FMT_YUYV)
    __u32 field;          // 场序(如逐行扫描、隔行扫描)
    __u32 bytesperline;   // 每行的字节数
    __u32 sizeimage;      // 每帧的总字节数
    // 其他字段
};

3.VIDIOC_REQBUFS :请求分配缓冲区。

memory类型使用MMAP,后续用于mmap内核缓冲区到用户态,避免内存拷贝

相关结构定义:

cpp 复制代码
struct v4l2_requestbuffers {
    __u32 count;          // 请求的缓冲区数量
    enum v4l2_buf_type type; // 缓冲区类型
    enum v4l2_memory memory; // 内存类型(如 MMAP、USERPTR)
    // 其他字段
};

4.VIDIOC_QUERYBUF :查询缓冲区信息。

相关结构定义:

cpp 复制代码
struct v4l2_buffer {
    __u32 index;          // 缓冲区索引
    enum v4l2_buf_type type; // 缓冲区类型
    __u32 bytesused;      // 缓冲区中实际使用的字节数
    __u32 flags;          // 缓冲区标志
    __u32 field;          // 场序
    struct timeval timestamp; // 时间戳
    // 其他字段
};

5.VIDIOC_QBUF :将缓冲区加入队列。

将申请的 v4l2_buffer 实例入队

6.VIDIOC_DQBUF :从队列中取出缓冲区。

弹出 v4l2_buffer 实例,并通过mmap映射的地址读取采集数据

7.VIDIOC_STREAMON:开始视频采集。

8.VIDIOC_STREAMOFF:停止视频采集。

9.VIDIOC_ENUM_FMT :枚举设备支持的像素格式。

用于提前枚举支持的图像采集格式,以便选择最合适的采集方式。

相关结构定义:

cpp 复制代码
struct v4l2_fmtdesc {
    __u32 index;             // 格式索引(从 0 开始)
    enum v4l2_buf_type type; // 缓冲区类型(如视频采集)
    __u32 flags;             // 格式标志
    __u8 description[32];    // 格式描述
    __u32 pixelformat;       // 像素格式(如 V4L2_PIX_FMT_YUYV)
    __u32 reserved[4];       // 保留字段
};

流程

通常使用的采集流程如下:

1.查询设备能力:使用 VIDIOC_QUERYCAP 查询枚举设备是否支持采集。

2.打开设备:使用 open 打开设备节点。

3.查询设备图像能力:使用 VIDIOC_ENUM_FMT 查询设备支持的像素格式是否匹配。

4.设置视频格式:使用 VIDIOC_S_FMT 设置分辨率、像素格式等。

5.请求缓冲区:使用 VIDIOC_REQBUFS 请求分配缓冲区。

6.映射缓冲区:使用 mmap 将缓冲区映射到用户空间。

7.开始采集:使用 VIDIOC_STREAMON 开始视频采集。

8.采集数据:使用 VIDIOC_DQBUF 从队列中取出缓冲区,处理数据后使用 VIDIOC_QBUF 将缓冲区重新加入队列。

9.停止采集:使用 VIDIOC_STREAMOFF 停止视频采集。

10.释放资源:使用 munmap 释放缓冲区,并关闭设备。

代码示例

cpp 复制代码
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <fstream>
#include <vector>
#include <cstring>

#define VIDEO_DEVICE "/dev/video0"
#define WIDTH 640
#define HEIGHT 480
#define FPS 30
#define OUTPUT_FILE "output.yuv"
#define BUFFER_COUNT 4 // 缓冲区数量

// 检查 V4L2 调用的返回值
#define CHECK(x) \
    if ((x) < 0) { \
        std::cerr << "ioctl error at " << __FILE__ << ":" << __LINE__ << " - " << strerror(errno) << std::endl; \
        exit(EXIT_FAILURE); \
    }

// 检查设备是否支持指定格式
bool is_format_supported(int fd, unsigned int pixel_format) {
    struct v4l2_fmtdesc fmt_desc = {};
    fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt_desc.index = 0;

    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
        if (fmt_desc.pixelformat == pixel_format) {
            std::cout << "Device supports format: " << fmt_desc.description << std::endl;
            return true;
        }
        fmt_desc.index++;
    }

    std::cerr << "Device does not support the required format (YUV420)" << std::endl;
    return false;
}

int main() {
    // 打开视频设备
    int fd = open(VIDEO_DEVICE, O_RDWR);
    CHECK(fd);

    // 检查设备是否支持 YUV420P 格式
    if (!is_format_supported(fd, V4L2_PIX_FMT_YUV420)) {
        close(fd);
        return -1;
    }

    // 设置视频格式
    struct v4l2_format fmt = {};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420; // YUV420P 格式
    fmt.fmt.pix.field = V4L2_FIELD_NONE;

    CHECK(ioctl(fd, VIDIOC_S_FMT, &fmt));

    // 检查设备是否实际设置了 YUV420P 格式
    if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_YUV420) {
        std::cerr << "Device does not support YUV420P format" << std::endl;
        close(fd);
        return -1;
    }

    // 设置帧率
    struct v4l2_streamparm streamparm = {};
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    streamparm.parm.capture.timeperframe.numerator = 1;
    streamparm.parm.capture.timeperframe.denominator = FPS;
    CHECK(ioctl(fd, VIDIOC_S_PARM, &streamparm));

    // 请求缓冲区
    struct v4l2_requestbuffers req = {};
    req.count = BUFFER_COUNT; // 4 个缓冲区
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    CHECK(ioctl(fd, VIDIOC_REQBUFS, &req));

    // 映射所有缓冲区
    std::vector<unsigned char *> buffers(BUFFER_COUNT);
    std::vector<size_t> buffer_sizes(BUFFER_COUNT);

    for (unsigned int i = 0; i < BUFFER_COUNT; i++) {
        struct v4l2_buffer buf = {};
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;

        CHECK(ioctl(fd, VIDIOC_QUERYBUF, &buf));

        buffers[i] = (unsigned char *)mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
        if (buffers[i] == MAP_FAILED) {
            std::cerr << "Failed to mmap buffer " << i << std::endl;
            close(fd);
            return -1;
        }

        buffer_sizes[i] = buf.length;

        // 将缓冲区加入队列
        CHECK(ioctl(fd, VIDIOC_QBUF, &buf));
    }

    // 开始采集
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    CHECK(ioctl(fd, VIDIOC_STREAMON, &type));

    // 打开输出文件
    std::ofstream outfile(OUTPUT_FILE, std::ios::binary);
    if (!outfile) {
        std::cerr << "Failed to open output file" << std::endl;
        close(fd);
        return -1;
    }

    // 采集 100 帧数据并保存到文件
    for (int i = 0; i < 100; i++) {
        struct v4l2_buffer buf = {};
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        // 从队列中取出缓冲区
        CHECK(ioctl(fd, VIDIOC_DQBUF, &buf));

        // 将 YUV420P 数据写入文件
        outfile.write((char *)buffers[buf.index], buf.bytesused);

        // 将缓冲区重新加入队列
        CHECK(ioctl(fd, VIDIOC_QBUF, &buf));
    }

    // 停止采集
    CHECK(ioctl(fd, VIDIOC_STREAMOFF, &type));

    // 释放资源
    for (unsigned int i = 0; i < BUFFER_COUNT; i++) {
        munmap(buffers[i], buffer_sizes[i]);
    }
    close(fd);
    outfile.close();

    std::cout << "YUV420P data saved to " << OUTPUT_FILE << std::endl;

    return 0;
}

编译前需要先安装v4l2的开发包:

bash 复制代码
sudo apt install libv4l-dev

也可以同时安装v4l2的工具包,用于信息查询:

bash 复制代码
sudo apt install v4l-utils

注:webrtc在linux下提供了两种采集方式,一种是v4l2,另一种是pipewire,感兴趣的可以看一下它们的实现

相关推荐
Sleepy MargulisItG2 分钟前
【Linux网络编程】应用层协议:HTTP协议
linux·服务器·网络·http
G311354227316 分钟前
Linux 内核设计中的核心思想与架构原则
linux·架构·php
zhuzewennamoamtf26 分钟前
Linux内核platform抽象、数据结构、内核匹配机制
linux·运维·数据结构
好游科技1 小时前
语聊APP新生态!一站式语聊房语音直播APP源码开发搭建
音视频·webrtc·im即时通讯·社交软件·社交语音视频软件
Kira Skyler2 小时前
ELF文件解析 elf.o 文件主要内容.md
linux
逐梦吧!旅行者2 小时前
Linux MySQL 5.7用户管理与用户密码的设置问题
linux·mysql
RisunJan2 小时前
Linux命令-grpck命令(验证和修复组配置文件(`/etc/group` 和 `/etc/gshadow`)完整性的工具)
linux·运维·服务器
loosed3 小时前
Ubuntu mysql8 tar.xz 安装
linux·ubuntu·adb
Xの哲學3 小时前
Linux VxLAN深度解析: 从数据平面到内核实现的全面剖析
linux·服务器·算法·架构·边缘计算
添砖java‘’3 小时前
Linux信号机制详解:从产生到处理
linux·c++·操作系统·信号处理