下面是一个更完整的示例程序,它不仅包括了启动和停止视频流的过程,还包含了如何使用 VIDIOC_DQBUF
和 VIDIOC_QBUF
来循环处理图像帧。此外,我还添加了对多平面格式的支持,并且提供了一个简单的机制来显示图像(这里以保存到文件为例)。这个例子假设你有一个可以保存为 PPM 文件的函数 save_image_ppm()
。
完整示例代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <poll.h>
#define DEVICE_NAME "/dev/video0"
#define BUFFER_COUNT 4
struct buffer {
void *start;
size_t length;
};
void save_image_ppm(const char *filename, unsigned char *data, int width, int height) {
// 这里只是一个占位符,用于说明保存图像的方法。
// 实际实现应该根据你的需求来编写。
}
int main() {
int fd, ret;
struct v4l2_requestbuffers reqbufs;
struct v4l2_format fmt;
struct v4l2_buffer buf;
struct v4l2_plane planes[BUFFER_COUNT];
struct pollfd fds[1];
struct buffer *buffers[BUFFER_COUNT];
unsigned int i, j;
// 打开设备
fd = open(DEVICE_NAME, O_RDWR);
if (fd == -1) {
perror("Opening video device");
return errno;
}
// 设置视频格式
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
fmt.fmt.pix_mp.width = 640;
fmt.fmt.pix_mp.height = 480;
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUV420M; // 使用多平面格式
fmt.fmt.pix_mp.num_planes = 3; // YUV420M有三个平面
ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret == -1) {
perror("VIDIOC_S_FMT");
close(fd);
return errno;
}
// 请求缓冲区
memset(&reqbufs, 0, sizeof(reqbufs));
reqbufs.count = BUFFER_COUNT;
reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
reqbufs.memory = V4L2_MEMORY_USERPTR;
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbufs);
if (ret == -1) {
perror("VIDIOC_REQBUFS");
close(fd);
return errno;
}
// 分配用户空间缓冲区
for (i = 0; i < BUFFER_COUNT; ++i) {
buffers[i] = malloc(sizeof(struct buffer));
buffers[i]->length = fmt.fmt.pix_mp.plane_fmt[0].sizeimage +
fmt.fmt.pix_mp.plane_fmt[1].sizeimage +
fmt.fmt.pix_mp.plane_fmt[2].sizeimage;
buffers[i]->start = malloc(buffers[i]->length);
if (!buffers[i]->start) {
perror("malloc");
goto error;
}
}
// 将用户指针加入队列
for (i = 0; i < BUFFER_COUNT; ++i) {
memset(&buf, 0, sizeof(buf));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.m.userptr = (unsigned long)buffers[i]->start;
buf.length = buffers[i]->length;
for (j = 0; j < fmt.fmt.pix_mp.num_planes; j++) {
buf.m.planes[j].bytesused = fmt.fmt.pix_mp.plane_fmt[j].sizeimage;
buf.m.planes[j].length = fmt.fmt.pix_mp.plane_fmt[j].sizeimage;
buf.m.planes[j].data_offset = 0;
buf.m.planes[j].m.userptr = (unsigned long)((char *)buffers[i]->start +
(j > 0 ? fmt.fmt.pix_mp.plane_fmt[0].sizeimage : 0) +
(j > 1 ? fmt.fmt.pix_mp.plane_fmt[1].sizeimage : 0));
}
ret = ioctl(fd, VIDIOC_QBUF, &buf);
if (ret == -1) {
perror("VIDIOC_QBUF");
goto error;
}
}
// 启动流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if (ret == -1) {
perror("VIDIOC_STREAMON");
goto error;
}
printf("Stream started. Press Enter to stop.\n");
// 设置poll结构体
fds[0].fd = fd;
fds[0].events = POLLIN;
while (getchar() != '\n') { // 等待用户输入换行键结束循环
ret = poll(fds, 1, -1); // 阻塞直到有数据可读
if (ret < 0) {
perror("poll");
break;
}
// 取出已填充的缓冲区
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.m.planes = planes;
ret = ioctl(fd, VIDIOC_DQBUF, &buf);
if (ret == -1) {
perror("VIDIOC_DQBUF");
continue;
}
// 此处可以添加处理图像数据的代码
// 例如:保存图像到文件、显示图像等
// 注意,对于多平面格式,需要处理多个平面的数据
// 下面是一个保存图像到PPM文件的例子:
char filename[256];
snprintf(filename, sizeof(filename), "frame_%d.ppm", buf.index);
save_image_ppm(filename, (unsigned char *)buf.m.planes[0].m.userptr,
fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height);
// 重新排队缓冲区以供再次使用
ret = ioctl(fd, VIDIOC_QBUF, &buf);
if (ret == -1) {
perror("VIDIOC_QBUF");
break;
}
}
// 停止流
ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
if (ret == -1) {
perror("VIDIOC_STREAMOFF");
goto error;
}
error:
// 清理资源
for (i = 0; i < BUFFER_COUNT; ++i) {
if (buffers[i]) {
free(buffers[i]->start);
free(buffers[i]);
}
}
close(fd);
return ret == -1 ? errno : 0;
}
关键点解释
-
设置视频格式:在请求缓冲区之前,我们首先设置了期望的视频格式,这里选择了多平面 YUV420 格式(
V4L2_PIX_FMT_YUV420M
),并指定了分辨率。 -
分配用户空间缓冲区:根据视频格式信息,为每个缓冲区分配足够的内存来存储一个完整帧的数据。注意,对于多平面格式,我们需要为所有平面的数据预留空间。
-
将用户指针加入队列:使用
VIDIOC_QBUF
将每个缓冲区的用户指针信息传递给驱动程序,并指定每个平面的具体信息。 -
启动流:调用
VIDIOC_STREAMON
开始视频流传输。 -
轮询机制:使用
poll()
函数等待设备准备好新的图像帧。这有助于避免忙等待,并允许程序响应其他事件。 -
取出和处理图像帧:一旦
poll()
返回,表示有新的图像帧可用,我们就可以使用VIDIOC_DQBUF
获取该帧,并对其进行处理。在这个例子中,我们将图像保存为 PPM 文件,实际应用中可以根据需要进行不同的处理。 -
重新排队缓冲区:处理完图像后,使用
VIDIOC_QBUF
将缓冲区重新放回队列中,以便它可以被再次使用来接收新的图像帧。 -
停止流:当用户按下 Enter 键时,程序会停止视频流,并清理所有分配的资源。
请根据实际情况调整代码中的参数(如设备名称、分辨率、像素格式等)以及图像处理逻辑。上述代码提供了一个完整的框架,你可以在此基础上构建更复杂的功能。