V4L2学习
代码地址v4l2grab/v4l2grab.c at master · twam/v4l2grab (github.com)
V4L2文档地址 https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/
函数详解
打开/关闭设备
// 打开file设备,oflag决定可读可写,返回fd;
fd = v4l2_open(const char *file, int oflag, ...)
// 关闭设备,成功返回0;
int v4l2_close(int fd)
IO_METHOD_READ:直接从设备读一条数据到buffer中,然后解码;
/**
* fd open()返回的文件描述符;
* buf 填充的缓冲区;
* count 读取的最大字节数
* 描述:阻塞状态会数据可用时才读,使用select()/poll()提醒,非阻塞会直接返回错误;使用单个缓冲区,正在读数据时,设备停止捕获信息
* 返回值:成功就返回读取的字节数,如果此数字小于请求字节数或一帧所需的数据量不是错误
出错返回-1,并设置相应变量,下一次读取将从新帧的开头开始
* 错误码:
EAGAIN:非阻塞时无数据可读;
EBADF:fd不是有效的文件描述符或未打开,或进程已具有打开的最大文件数
EBUSY:驱动程序不支持多个读取流,且设备已在使用中
EFAULT:buf引用无法访问的内存区域
EINTR:在读取任何数据之前,呼叫被信号中断
EIO:IO错误,硬件问题或无法与远程设备通信
EINVAL:驱动程序不支持read()函数
*/
ssize_t v4l2_read(int fd, void *buf, size_t count)
IO_METHOD_MMAP:使用DQBUF,从设备buffer读多条数据数据后直接解码成图片并存储;
IO_METHOD_USERPTR:也是用的DQBUF,但是有多个buffer,用户决定需要哪个buffer数据,然后解码;
图像裁剪
/**
* 注意第二个参数VIDIOC_CROPCAP,表示函数作用
* 应用程序使用此函数查询裁剪限制、图像的像素方面以及计算比例因子;将v4l2_cropcap结构的类型字段设置为相应的缓冲区类型,驱动程序填充v4l2_cropcap,且v4l2_cropcap结果是静态除非更改视频格式。
* 成功返回0,错误返回-1;
* 错误码:
EINVAL:v4l2_cropcap结构无效;
ENODATA:裁剪不被支持;
*/
int ioctl(int fd, VIDIOC_CROPCAP, struct v4l2_cropcap *argp)
/**
* 第二个参数VIDIOC_S_CROP,配套struct v4l2_crop
* 改变裁剪矩阵,应用程序初始化类型以及v4l2_rect子结构c;当参数不合适时,应用程序可以循环修改裁剪或图像参数直至得到满意的参数;
* 驱动程序首先根据硬件限制调整所请求的尺寸然后将其调整到最接近的水平和垂直偏移量、宽度和高度值,驱动程序必须将裁剪矩阵的四边形垂直偏移到frame lines module two,字段顺序就不能混淆;然后驱动程序调整图片大小到最接近的值同时保持当前的水平和垂直因子;
* VIDIOC_S_CROP仅写,查询需要通过VIDIOC_G_CROP,VIDIOC_G_FMT, VIDIOC_S_FMT,VIDIOC_TRY_FMT.
* 成功返回0,错误返回-1;
* 错误码ENODATA:不支持裁剪;
*/
int ioctl(int fd, VIDIOC_S_CROP, const struct v4l2_crop *argp)
/**
* 查询裁剪矩阵尺寸和位置,应用程序将v4l2_crop结构的类型字段设置为相应的缓冲区类型,并使用指向该结构的指针调用VIDIOC_G_CROP,驱动程序填充结构的剩余部分。
*/
int ioctl(int fd, VIDIOC_G_CROP, struct v4l2_crop *argp)
/**
* 用于应用程序和驱动程序之间,查询协商当前数据格式
* VIDIOC_G_FMT,get视频格式
* VIDIOC_S_FMT,set视频格式,根据硬件能力检查和调整参数;
* VIDIOC_TRY_FMT等效于VIDIOC_S_FMT但它不会更改驱动程序状态,随时调用,永不返回,无需禁用IO或可能耗时的硬件设备(一般不用);
*/
int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *argp)
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *argp)
int ioctl(int fd, VIDIOC_TRY_FMT, struct v4l2_format *argp)
视频流
/**
* 开启或关闭IO捕获输出数据流(例如MMAP、USEPTR、DMABUF);
* 成功返回0,失败返回-1;如果在队列已开启情况下再一次ON不会有什么影响,已关闭情况下再一次OFF,队列缓冲区会返回到它们的起始状态;
* 错误码:
EINVAL:buffer类型不支持或buffers没有分配空间或没有入队;
EPIPE:驱动程序实现pad-level配置,管道配置无效;
ENOLINK:驱动程序时间Media Controller接口,管道链接配置无效。
*/
int ioctl(int fd, VIDIOC_STREAMON, const int *argp)
int ioctl(int fd, VIDIOC_STREAMOFF, const int *argp)
/**
* 检查调整视频流参数,应用程序可以要求不同的视频帧间隔,设备需要支持不同视频帧;改变视频帧间隔不会改变视频格式;可以用来确定驱动程序在读写模式下内部使用的缓冲区数量;
* 成功返回0,错误返回-1;
*/
int ioctl(int fd, VIDIOC_G_PARM, v4l2_streamparm *argp)
int ioctl(int fd, VIDIOC_S_PARM, v4l2_streamparm *argp)
内存映射
将设备数据读取到内存,内存buffer映射到应用程序地址空间
/**
* start 将设备的输出buffer映射到应用程序地址空间start位置;指定MAP_FIXED,start必须是页面大小的倍数,当指定的地址无法使用时,mmap失败(一般不用),应用程序一般在这里使用NULL指针;
* length 映射的内存空间长度,必须与v4l2_buffer(single-planar)中长度一致或v4l2_plane(multi-planar)长度一致;
* prot 内存保护,允许读写PROT_READ | PROT_WRITE;
* flags 指定映射对象的类型、映射选项以及对页的映射副本所做的修改是否对进程私有,还是要与其他引用共享;指定MAP_FIXED,指定地址必须可使用,否则会报错;
* offset 设备内存中的buffer偏移量,必须与v4l2_buffer(single-planar)中长度一致或v4l2_plane(multi-planar)长度一致
*
* 将设备buffer起始于offset,长度为length的内存映射到应用程序地址空间start;
* 成功则返回映射buffer指针,失败返回-1;
* 错误码:
EBADF:fd无效;
EACCESS:fd不可读可写;
EINVAL:length、offset越界;flags、prot不支持;设备buffer还没有分配;
ENOMEM:内存不足(虚存或物理内存);
*/
void v4l2_mmap(void *start, size_t length, int prot, int flags, int fd, int64_t offset);
/**
* start是mmap()返回的mapped buffer地址;
* length 是mapped buffer长度,与驱动程序v4l2_buffer/v4l2_plane结构中一致;
* 取消之前的buffer映射并释放buffer
* 成功返回0,错误返回-1;
* 错误码EINVAL:start或length错误,或没有映射的buffer;
*/
int v4l2_munmap(void *_start, size_t length);
应用程序与驱动程序交换缓冲区
/**
* fd open()返回的文件描述符;
* VIDIOC_DQBUF,request
* argp 填充的缓冲区;
* 描述:与驱动程序交换缓冲区,应用程序调用ioctl在驱动程序的传入队列中对空或填充buffer进行排队;程序设置结构v4l2_buffer类型、内存和保留字段,使用v4l2_buffer结构指针调用VIDIOC_DQBUF时,驱动程序填充所有剩余字段或返回错误代码;
对缓冲区应用程序排队,设置v4l2_buffer设置成v4l2_format,有效索引号从0到VIDIOC_QUERYBUF-1;
内存映射缓冲区程序排队,
用户指针缓冲区排队,
DMABUF缓冲区应用程序排队,
* 成功就返回0,出错返回-1并设置错误码;
* 错误码:
EAGAIN:非阻塞时无数据可读;
EINVAL:buffer结构不支持,或索引越界,或尚未分配缓冲区,或useptr或长度无效,或设置了V4L2_BUF_FLAG_REQUEST_FD标志但给定的request_fd无效,或m.fd是无效的DMABUF文件描述符;
EIO:VIDEO_DQBUF出现内部错误(这个问题可能导致驱动程序取消buffer队列甚至停止捕获,因此一般不用这个,改设置V4L2_BUF_FLAG_ERROR然后返回0;
EPIPE:在mem2mem编解码器捕获空队列;V4L2_BUF_FLAG_LAST缓冲区已经退出队列且预计不会有新的缓冲区可用;
EBADR:设置V4L2_BUF_FLAG_REQUEST_FD标志,但设备不支持给定缓冲区类型请求,或者没有设置,但设备要求设置;
EBUSY:缓冲区通过请求排队,但应用程序尝试直接排队,反之亦然(不允许混合使用两个API;
*/
int ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *argp)
// 退出buf部分
int ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *argp)
设备缓冲区先映射到内存缓冲区,再映射到应用程序地址空间
/**
* 初始化MMAP、USEPTR IO或DMA buffer IO;
* MMAP内存映射缓冲区buffer位于设备内存中,在映射到应用程序的地址空间之前,ioctl负责分配
* USEPTR用户缓冲区由应用程序自己分配,ioctl用于将驱动程序切换到用户指针IO模式并设置一些内部结构;
* DMA缓冲区由应用程序通过设备驱动程序分配,ioctl将驱动程序配置为DMABUF IO模式;
* 成功返回0,失败返回-1;
* 错误码EINVAL:buffer类型或IO方式不支持;
*/
int ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *argp)
/**
* 在设备内存中的buffer由REQBUFS分配后,应用程序可以在任意时刻通过QUERYBUF查询buffer状态;
* 成功返回0,错误返回-1;
* 错误码EINVAL:buffer类型不支持,或索引越界;
*/
int ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *argp)
/**
* 所有的V4L2设备都支持VIDIOC_QUERYCAP,用来识别与此规范兼容的内核设备,并获取有关驱动和硬件功能的信息;v4l2_capability结构体由驱动填充;
* 成功返回0,错误返回-1;
*/
int ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *argp)
基本流程
使用v4l2设备基本流程:打开设备–初始化设备–捕获数据–处理视频帧–停止捕获–还原设备–关闭设备;
// open and initialize device
deviceOpen();
deviceInit();
// start capturing
captureStart();
// process frames
mainLoop();
// stop capturing
captureStop();
// close device
deviceUninit();
deviceClose();
打开设备
// stat获取文件属性,S_ISCHR判断是否是设备;
// 使用v4l2_open打开设备,返回设备描述符fd;
static void deviceOpen(void)
{
struct stat st;
// stat file
if (-1 == stat(deviceName, &st)) {
fprintf(stderr, "Cannot identify '%s': %d, %s\n", deviceName, errno, strerror(errno));
exit(EXIT_FAILURE);
}
// check if its device
if (!S_ISCHR(st.st_mode)) {
fprintf(stderr, "%s is no device\n", deviceName);
exit(EXIT_FAILURE);
}
// 核心代码open device
fd = v4l2_open(deviceName, O_RDWR /* required */ | O_NONBLOCK, 0);
// check if opening was successfull
if (-1 == fd) {
fprintf(stderr, "Cannot open '%s': %d, %s\n", deviceName, errno, strerror(errno));
exit(EXIT_FAILURE);
}
}
初始化设备
1、首先VIDIOC_QUERYCAP得到设备能力,判断设备是否有录像功能;
2、根据内存映射不同模式,IO_READ判断V4L2_CAP_READWRITE,IO_METHOD_MMAP、IO_METHOD_USERPTR判断V4L2_CAP_STREAMING;
3、VIDIOC_CROPCAP设置裁剪cropcap,根据cropcap得到crop.c,VIDIOC_S_CROP设置crop;
4、根据输入参数设置format,VIDIOC_S_FMT补足剩下部分;
5、VIDIOC_S_PARM设置视频间隔帧;
6、根据内存映射不同模式,初始化设备buffer;IO_READ提前分配buffer空间;IO_MMAP,VIDIOC_REQBUFS在设备分配buffer,新建2个buffer,并与应用程序做内存映射;IO_USERPTR,VIDIOC_REQBUFS同上,新建4个buffer;
// 查看当前设备能力
int V4l2Camera::VerifyCapabilities() {
int fd = camera_inf.fd_cim;
struct v4l2_capability cap;
enum io_method io = camera_ctl.io_method;
// 获取当前设备能力,存储在cap中
if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &cap)) {
if (EINVAL == errno)
spdlog::error("{} is not V4L2 device\n", camera_inf.video);
else
spdlog::error("Query capability failed!\n");
return -1;
}
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
spdlog::error("{} is not capture device\n", camera_inf.video);
return -1;
}
// 判断设备是否具备输出数据方式
switch (io) {
case IO_METHOD_READ:
if (!(cap.capabilities & V4L2_CAP_READWRITE)) {
spdlog::error("{} doesn't support read i/o\n", camera_inf.video);
return -1;
}
break;
case IO_METHOD_MMAP:
case IO_METHOD_USERPTR:
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
spdlog::error("{} doesn't support streaming i/o\n", camera_inf.video);
return -1;
}
break;
}
}
// 初始化设备
int V4l2Camera::InitDevice() {
unsigned int frmsize;
enum io_method io = camera_ctl.io_method;
/* select video input, video standard and tune here. */
// SetFormat返回图像大小frmsize
if (SetFormat(&frmsize) < 0) return -1;
camera_inf.param.frmsize = frmsize;
switch (io) {
// 初始化数据读取方式
case IO_METHOD_READ:
if (InitRead(frmsize) < 0) return -1;
break;
case IO_METHOD_MMAP:
if (InitMmap() < 0) return -1;
break;
case IO_METHOD_USERPTR:
if (InitUserp(frmsize) < 0) return -1;
break;
}
// spdlog::info("device initialized");
return 0;
}
*这里其实还有初始化编码
int V4l2Camera::EncodecInit() {
if (jpeg_enc_init(&camera_inf) < 0) {
jpeg_enc_stop(&camera_inf);
spdlog::error("jpeg_enc_init failed\n");
return -1;
}
// spdlog::info("jpeg initialized");
return 0;
}
开始捕获数据
1、根据内存映射不同模式,IO_READ不需要提前测试;IOMMAP打开视频流VIDIOC_STREAMON;IO_USERPTR设置用户指针,VIDIOC_STREAMON打开用户指针;
int V4l2Camera::StartCapturing() {
unsigned int i;
enum v4l2_buf_type type;
enum io_method io = camera_ctl.io_method;
int fd = camera_inf.fd_cim;
switch (io) {
case IO_METHOD_READ:
/* nothing to do */
break;
case IO_METHOD_MMAP:
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;
// 关键代码,设置buffer
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) {
LogErrno("ioctl VIDIOC_QBUF error");
return -1;
}
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// 开始捕获数据
if (-1 == xioctl(fd, VIDIOC_STREAMON, &type)) {
LogErrno("ioctl VIDIOC_STREAMON error");
return -1;
}
break;
case IO_METHOD_USERPTR:
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
CLEAR(buf);
// 设置参数,并指定指针
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.index = i;
buf.m.userptr = (unsigned long)buffers[i].start;
buf.length = buffers[i].length;
// 设置buffer
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) {
LogErrno("ioctl VIDIOC_QBUF error");
return -1;
}
}
// 开始捕获
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMON, &type)) {
LogErrno("ioctl VIDIOC_STREAMON error");
return -1;
}
break;
}
// spdlog::info("start capture");
return 0;
}
处理视频帧
不同用户处理视频帧方式不同,最后得到的图片格式不同。
1、根据poll/select触发得到事件,读数据,根据不同内存映射方式读:
IO_READ:直接从设备读到应用程序buffers中,然后解码成图片;
IO_MMAP:执行VIDIOC_DQBUF存储在 buf中,解码图片,再VIDIOC_QBUF查询;
IO_USERPTR:VIDIOC_DQBUF存储再buf中,设定用户指针,解码图片,VIDIOC_QBUF查询;
2、解码图片,新分配空间存储p中读取的数据,并将格式从YUV420转成YUV444,并将数据写入filename文件中(filename是参数);
停止捕获
根据不同内存映射方式,IO_READ不需要做其他操作;IO_MMAP、IO_USERPTR需要关闭视频流VIDIOC_STREAMOFF;
void V4l2Camera::StopCapturing() {
enum v4l2_buf_type type;
enum io_method io = camera_ctl.io_method;
int fd = camera_inf.fd_cim;
switch (io) {
// READ取数据只需要停止就行
case IO_METHOD_READ:
/* nothing to do */
break;
// 以MMAP或者指针取数据,需要关闭数据流
case IO_METHOD_MMAP:
case IO_METHOD_USERPTR:
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (fd >= 0) {
// 关闭数据流
if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &type))
LogErrno("ioctl VIDIOC_STREAMOFF error");
}
break;
}
}
还原设备
IO_READ:释放buffers[0].start空间;
IO_MMAP:取消应用和设备之间的映射,根据start、length取消;
IO_USERPTR:释放buffers[i].start;
// 释放IO设备
void V4l2Camera::ReleaseDevice() {
// 释放缓冲区buffer
ReleaseBuffers();
if (camera_inf.encodec_mode == 1) {
// 停止数据编码
if (camera_inf.stop_jpeg == 0) jpeg_enc_stop(&camera_inf);
int fd_helix = camera_inf.fd_helix;
if (fd_helix >= 0) close(fd_helix);
}
int fd = camera_inf.fd_cim;
// 关闭设备的文件描述符
if (fd >= 0) close(fd);
}
void V4l2Camera::ReleaseBuffers() {
unsigned int i;
enum io_method io = camera_ctl.io_method;
switch (io) {
// 释放buffer
case IO_METHOD_READ:
free(buffers[0].start);
break;
// mmap需要释放length长度
case IO_METHOD_MMAP:
for (i = 0; i < n_buffers; ++i)
if (-1 == munmap(buffers[i].start, buffers[i].length))
LogErrno("munmap");
break;
// userptr需要释放所有的起始地址
case IO_METHOD_USERPTR:
for (i = 0; i < n_buffers; ++i) free(buffers[i].start);
break;
}
free(buffers);
}
关闭设备
调用v4l2_close;
V4L2硬件编码
首先将编码格式传给设备,并测试设备是否有该编码功能
// struct setformat是编码格式
int try_format(int fd, struct setformat *setformat) {
struct v4l2_format format;
int rc;
setup_format(&format, setformat);
rc = ioctl(fd, VIDIOC_TRY_FMT, &format);
if (rc < 0) {
fprintf(stderr, "Unable to try format for type %d: %s\n", setformat->type,
strerror(errno));
return -1;
}
return 0;
}
int helix_set_format(int fd, struct setformat *setformat) {
int rc;
struct v4l2_format vfmt;
if (try_format(fd, setformat))
return -1;
setup_format(&vfmt, setformat);
// 将vfmt编码传给设备
rc = ioctl(fd, VIDIOC_S_FMT, &vfmt);
if (rc < 0) {
fprintf(stderr, "Unable to set format for type %d: %s\n", setformat->type,
strerror(errno));
return -1;
}
return 0;
}
设备将数据输出到内核空间的缓冲区,然后再copy到应用程序空间缓冲区。