V4l2和HAL-(3)
经过前两篇的铺垫,我们完成了摄像头数据采集前格式及参数的准备、缓冲区的申请和入队,接下来终于要到真正采集视频数据、循环缓冲区的过程:
8.
hal中的调用就比较直接,通过VIDIOC_STREAMON通知设备开始采集视频帧,并且带有失败重试机制
9.
跟着再继续看hal代码,这里以mtk hal为例:
在mtk camera hal 的逻辑中,非YUV格式的视频流需要新开一个decode线程来解码。推测这个线程就是v4l2采集loop的位置。
于是我们找到这个线程的主循环:
位置在 \vendor\mediatek\tv\hardware\interfaces\camera\device\3.4\default\MtkExternalCameraDeviceSession.cpp
函数在 ExternalCameraDeviceSession::CamDecThread::threadLoop()
首先就能看到调用了dequeueV4l2FrameLocked来从队列中取出一个v4l2帧,
记得上一篇我们做了什么吗,我们将buffer全部通过VIDIOC_QBUF的调用进行了入队操作,
那么可以预见的是这个dequeue开头的函数多半就是调用VIDIOC_DQBUF来将已经一个填充过的buffer出队,待后续解码显示等一系列过程的函数。
带着这样的猜测我们进去dequeueV4l2FrameLocked看看:
果然如猜测一样,这里把buffer dequeue出来了(猜对了真爽),还根据之前保存的mV4L2BufferCount变量等等检查了buffer的可用性
(这里要注意,3483行的VIDIOC_QBUF是DQBUF出了问题才会执行,把问题buffer重新入队,别跟我一样没看到if思索半天刚dequeue出来的干嘛又直接入队)
至此,dequeueV4l2FrameLocked函数调用了DQBUF从buffer队列中取出了一个装有数据的buffer
回到threadloop主循环中
这时不难发现,上一篇中提到的map原来放在了这里,通过dequeueV4l2FrameLocked取出的一个buffer被送入V4L2Frame对象的map函数中处理,值得注意的是在dequeueV4l2FrameLocked的最后,实际上是构造了一个V4L2Frame对象来返回,传入了宽、高、格式、buffer index、buffer大小和偏移量,以这些参数构造的V4L2Frame对象会在之后被使用。
接下来看到具体的mmap过程也在map的实现中:
72行,很直接的,调用mmap函数,返回一个addr。当然你作为一个没怎么接触过下层代码的笨蛋怎么会知道mmap发生了什么呢,我们一起来学习一下:
首先函数原型如下:
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
- addr指定了期望映射的起始地址,如果希望操作系统自动为你选择映射的起始地址,则这里传入NULL,可以看到我们的hal也是这样做的,这也意味着写的代码更具可移植性,因为不同的操作系统和不同的系统配置可能有不同的地址空间布局,直接指定一个具体的地址可能在某些环境中并不可用或者导致映射失败。
- port参数制定了一个保护标志,这里的PROT_READ表示映射区域可读,可选的参数还有PROT_NONE,表示页面不可访问;PROT_READ,页面可读;PROT_WRITE,页面可写;PROT_EXEC,页面可执行等。
- flags参数控制了映射对象的特性,MAP_SHARED 意味着映射是共享的:对映射区域的修改会影响到底层文件,且这些改动对所有映射了该文件的其他进程都是可见的。flags 的另一个常见值是 MAP_PRIVATE,创建一个写时复制映射。修改不会反映到底层文件,且其他进程看不到这些改动。
- fd参数则指定了文件描述符,在我们的流程中,这里当然指向了/dev/video这个节点。我们需要通过 mmap 把内核空间中用于存储视频帧的缓冲区映射到用户空间。于是我们可以在用户空间中访问这些视频帧。
- offset:文件中映射区域的起始偏移量,它应该是页面大小的倍数
说到这里,我们还是有一点没说明白——上述的mmap意味着这个调用将内核空间存放视频帧的buffer映射到用户空间,那么具体是哪个参数指定了内核空间那个需要被映射的buffer位置呢?
回到V4L2Frame的构造中我们就可以较为清楚地看到:
其中用于构造的buffer参数是dequeueV4l2FrameLocked通过DQBUF获取的、处于内核空间的、用于描述装载了视频帧信息的对象。
那么也就是说其传入的buffer.m.offset成员是可以用来定位这个buffer位置的!
因此我们终于知道,是 mFd 和 mOffset 这两个参数共同指定了内核空间视频帧的位置。mFd 确定了设备,而 mOffset 确定了特定的视频帧或者视频缓冲区的开始位置。
于是通过mmap的调用我们赋值了一个通用指针addr,它指向了操作系统为我们准备的一块映射区域起始处
并且在77-80行处,通过出参*data指向了这个终于处于用户空间的、可以操作的区域。
至此,我们完成了以下几个事情:
- 通过VIDIOC_DQBUF的从内核空间取出了一个有视频数据的buffer
- 用DQ出来的buffer构造了V4L2Frame对象,在这个对象的map函数中调用mmap将内核空间的视频帧区域映射至用户空间
- 外部出参inData指向了mmap的映射结果,准备后续的使用和解码
10.
接下来,很自然的,经过上一步骤的mmap,我们已经用inData指针指向了处于用户空间的视频帧数据,我们可以直接拿去用来做一些解码显示等等的工作(如下图1393-1397行)
最后,内核空间的buffer已经不再被需要,我们就可以将其再重新enqueue到队列中等待下一次使用了。值得注意的是,我们采集和使用帧数据是一个持续循环的过程,而这个循环的实现方式并非是显式的——而是通过threadLoop的自动循环机制:在继承android::thread类的threadLoop实现中,如果返回值为true则会自动再次循环执行threadLoop,直到同步或手动机制停止这个循环。
这里仍然通过QBUF调用来将buffer重新加入队列。
至此,我们就通过V4L2接口完成了视频帧数据的采集,并通过threadLoop的循环持续这个采集过程,还剩下一些最后处理的过程和细节我们下篇继续。