首页 > 技术文章 > 音视频技术开发--V4L2学习(三)

hankgo 2021-10-30 00:24 原文

接上篇《音视频技术开发--V4L2学习(二)》

在上篇中,通过YUYV格式后,已经成功采集播放了UVC摄像头的数据,下面我们来学习下V4L2的采集播放代码。

四、 V4L2 实例代码剖析

我们首先看下UCV数据采集的capturer_mmap.c源码。Linux系统中,视频设备被当作一个设备文件来看待,设备文件存放在 /dev目录下,例如我这边UVC设备为 /dev/video0 。视频采集基本步骤流程如下: 打开视频设备->设置视频设备属性及采集方式->视频数据处理->关闭视频设备。

用户态通过ioctl操作fd句柄方式与V4L2软件层做数据交互,比如在前两篇,我们由于接的UVC支持的像素格式和我们sample的不一样,导致了直接跑sample出现了花屏现象,如果我们可以先获取UVC的像素格式能力,就可以对症配置,通过下面的API,我们可以实现获取对应的UVC的像素格式能力。如下所示,在原有的sample上修改;

[root@localhost V4L2_X264_RTMP]# git diff
diff --git a/capturer_mmap.c b/capturer_mmap.c
index a7cd636..d212ee7 100644
--- a/capturer_mmap.c
+++ b/capturer_mmap.c
@@ -461,6 +461,22 @@ static void enum_standards (int * fd )
        }
 }
 
+//show the available pixel fmt
+static void enum_pixel_fmt(int *fd)
+{
+       struct v4l2_fmtdesc fmt = {0};
+       printf("Available pixelfmt:\n");
+        fmt.index = 0;
+       fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+       while(0 == ioctl(*fd, VIDIOC_ENUM_FMT, &fmt)){
+               fmt.index++;
+               printf("pixelfmt=\"%c%c%c%c\", description=\"%s\"\n",fmt.pixelformat& 0xFF, (fmt.pixelformat >> 8) & 0xFF,
+               (fmt.pixelformat>> 16) & 0xFF, (fmt.pixelformat >> 24) & 0xFF,  fmt.description);
+       }
+
+       return;
+}
+
 //configure the video input
 static void set_input(int * fd, int dev_input)
 {
@@ -556,6 +572,8 @@ int main (int argc, char ** argv)
                                printf("\n");
                                enum_standards(&fd);
                                printf("\n");
+                               enum_pixel_fmt(&fd);
+                               printf("\n");
                                close_device (&fd);
                                exit (EXIT_SUCCESS);
                                //break;

执行以后我们可以看到这个UVC只支持YUYV和MJPEG两种数据采集格式:

 下面我们来看下capturer_mmap.c是如何实现视频帧数据采集的。

第一步、打开UVC的设备文件

*fd = open (dev_name, O_RDWR /* required */ | O_NONBLOCK, 0);

第二步、配置并初始化UVC设备

在该步骤中需要配置设备的图像属性(像素格式、尺寸等)、帧缓存申请等等。

①确认当前摄像头采集能力

    if (-1 == xioctl (*fd, VIDIOC_QUERYCAP, &cap)) 
    {
        if (EINVAL == errno) 
        {
            fprintf (stderr, "%s is no V4L2 device\n", dev_name);
            exit (EXIT_FAILURE);
        } else {
            errno_exit ("VIDIOC_QUERYCAP");
        }
    }

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) 
    {
        fprintf (stderr, "%s is no video capture device\n",dev_name);
        exit (EXIT_FAILURE);
    }

    if (!(cap.capabilities & V4L2_CAP_STREAMING)) 
    {
        fprintf (stderr, "%s does not support streaming i/o\n",dev_name);
        exit (EXIT_FAILURE);
    }

VIDIOC_QUERYCAP 用于查询视频设备的功能,V4L2_CAP_VIDEO_CAPTURE代表支持采集功能,V4L2_CAP_STREAMING代表支持streaming IO的方式读取数据。

关于读取数据的几种方式,参考该文章https://blog.csdn.net/coroutines/article/details/70141086

 ②、配置图像属性,主要为采集的尺寸,像素格式

    //set image properties
    fmt.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width       = width;
    fmt.fmt.pix.height      = height;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;

    if (-1 == xioctl (*fd, VIDIOC_S_FMT, &fmt))
        errno_exit ("\nError: pixel format not supported\n");

③、申请帧缓存buffer

例子通过stream IO的方式读取采集的帧数据。Memory Map方式访问帧缓存,帧内存通过V4L2驱动申请,如下代码所示,通过API向驱动申请了4块帧缓存。UVC设备采集到的视频数据存到这4块buffer中,通过Map方式,将内存Map到用户空间,之后通过指针直接读取数据。使用这种方式只有指向这段内存的用户空间指针在各个处理环节中传递,不会发生真实的数据拷贝。

    //向驱动申请4块帧缓存
    req.count               = 4;
    req.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory              = V4L2_MEMORY_MMAP;
    if (-1 == xioctl (*fd, VIDIOC_REQBUFS, &req)) 
    {
        if (EINVAL == errno) 
        {
            fprintf (stderr, "%s does not support "
                                "memory mapping\n", dev_name);
            exit (EXIT_FAILURE);
        } else {
            errno_exit ("VIDIOC_REQBUFS");
        }
    }

    //mmap驱动申请的缓存,使用户态可以直接访问
    for (*n_buffers = 0; *n_buffers < req.count; ++*n_buffers) 
    {
        struct v4l2_buffer buf;

        CLEAR (buf);

        buf.type        = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory      = V4L2_MEMORY_MMAP;
        buf.index       = *n_buffers;

        if (-1 == xioctl (*fd, VIDIOC_QUERYBUF, &buf))
            errno_exit ("VIDIOC_QUERYBUF");

        buffers[*n_buffers].length = buf.length;
        buffers[*n_buffers].start = mmap (NULL /* start anywhere */,
                            buf.length,
                            PROT_READ | PROT_WRITE /* required */,
                            MAP_SHARED /* recommended */,
                            *fd, buf.m.offset);

        if (MAP_FAILED == buffers[*n_buffers].start)
            errno_exit ("mmap");
    }

第三步、启动视频数据采集

首先将第三步申请的内存插入视频缓存链表中去,通过传递buffer的index即可。V4L2驱动会根据Index找到对应的buffer。

    for (i = 0; i < *n_buffers; ++i) 
    {
        struct v4l2_buffer buf;

        CLEAR (buf);

        buf.type        = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory      = V4L2_MEMORY_MMAP;
        buf.index       = i;

        if (-1 == xioctl (*fd, VIDIOC_QBUF, &buf))
            errno_exit ("VIDIOC_QBUF");
    }

之后开启采集:

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    //start the capture from the device
    if (-1 == xioctl (*fd, VIDIOC_STREAMON, &type))
        errno_exit ("VIDIOC_STREAMON");

第四步、获取采集视频帧

在前面三步,已经正确配置了UVC设备,并申请了足够的帧缓存,启动采集;现在我们可以向设备拿视频帧了。

首先通过select监听设备句柄,一旦发现有数据可读,则查询V4L2的帧缓存链表,取出帧缓存的idex号,这个idex就是第三步中插入的buffer。通过该idex找到对应的buffer,然后使用mmap的虚拟地址读取帧数据,至此,成功采集到了UVC的YUYV视频帧。

        //监听UVC设备句柄
        FD_ZERO (&fds);
        FD_SET (*fd, &fds);

        /* Select Timeout */
        tv.tv_sec = 2;
        tv.tv_usec = 0;

        //the classic select function, who allows to wait up to 2 seconds, 
        //until we have captured data,
        r = select (*fd + 1, &fds, NULL, NULL, &tv);

监听成功后,从视频队列中获取buffer idex,进而读取对应的buffer。

    //从缓存队列中获取视频帧
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    if (-1 == xioctl (*fd, VIDIOC_DQBUF, &buf)) 
    {
        switch (errno) 
        {
            case EAGAIN:
                return 0;

            case EIO://EIO ignored

            default:
                errno_exit ("VIDIOC_DQBUF");
        }
    }
            
    assert (buf.index < *n_buffers);

    //YUYV
    Bpf = width*height*2;

    //读取保存视频帧
    ret = write(file_fd, buffers[buf.index].start, Bpf);
    
    //将帧buffer放回队列,用于传递下一帧
    if (-1 == xioctl (*fd, VIDIOC_QBUF, &buf))
        errno_exit ("VIDIOC_QBUF");

 

以上为sample的功能主要实现,省略了影响不大的代码。更多V4L2的API使用详见API文档:

贴上官方的API使用文档:https://linuxtv.org/downloads/v4l-dvb-apis/

另外附上修改的sample:https://github.com/HankShao/V4L2_X264_RTMP.git

 

推荐阅读