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,感兴趣的可以看一下它们的实现

相关推荐
花糖纸木14 分钟前
【Linux】深入理解文件系统(超详细)
linux·运维·服务器
深度Linux1 小时前
Linux性能优化策略:让你的系统运行如飞
linux·运维·性能优化
Damon小智2 小时前
Linux系统中解决端口占用问题
linux·运维·服务器·进程·端口占用
encoding-console2 小时前
centos挂载的基本步骤
linux·运维·centos·挂载
大哥喝阔落3 小时前
linux相关conda操作
linux·运维·conda
豆是浪个3 小时前
Linux(Centos 7.6)命令详解:pwd
linux·运维·服务器
来吧~3 小时前
vue3使用vue3-video-play播放m3u8视频
前端·javascript·vue.js·音视频
水水阿水水5 小时前
第一章:C++是C语言的扩充(一)
linux·c语言·数据结构·c++·算法
-Harvey8 小时前
ubuntu为Docker配置代理
linux·ubuntu·docker
thehunters8 小时前
win10 ubuntu 使用Android ndk 问题:clang-14: Exec format error
android·linux·ubuntu