视频设备
一个视频捕获接口中,可以有多路视频输入源,经过视频格式转换,也可以有多个视频输出通道。每一个通道可能支持不同的输出格式,如用于直接显示的 RGB 格式,或者用于压缩存储或传输的 YUV 或 JPEG 等格式。用户在使用一个具体的视频设备时,通常不会过多地关心视频输入源的信息(这些信息通常由驱动处理),更多的是关心视频输出的信息,如输出图像的格式、大小、占用的内存大小等。SylixOS 中对视频设备的所有定义位于 <system/device/video/video.h> 头文件中,其所有的数据结构和控制命令正是基于以上所说的视频接口特点而设计的。
设备描述
SylixOS 使用以下结构来描述一个具体的视频设备:
typedef struct video_dev_desc {
CHAR driver[32];
CHAR card[32];
CHAR bus[32];
UINT32 version; /* video 驱动版本 */
#define VIDEO_DRV_VERSION 1
UINT32 capabilities; /* 具有的能力 */
#define VIDEO_CAP_CAPTURE 1 /* 视屏捕捉能力 */
#define VIDEO_CAP_READWRITE 2 /* read/write 系统调用支持 */
UINT32 sources; /* 视频源个数 */
UINT32 channels; /* 总采集通道数 */
……
} video_dev_desc;
下面是 video_dev_desc 结构成员含义:
- driver 为视频设备所使用的驱动名称。
- card 为对应的视频接口卡(视频处理器件)的名称。
- bus 为视频接口卡总线描述信息。
- version 代表视频设备驱动遵循的视频框架版本。
- capabilities 描述了一个视频设备所具有的功能,当前定义的功能有视频捕获功能和支持 read/write 系统调用的功能。
- sources 为一个视频设备总的视频输入源数量。
- channels 为视频采集通道数量,也就是视频输出通道的数量。
- reserve 为保留字节,兼容后续扩展定义。
设备通道描述
根据 video_dev_desc,我们可以获得一个视频设备的整体信息,应用程序最关心的还是每一个视频输出通道的信息。SylixOS 通过以下结构描述一个视频输出通道:
typedef struct video_channel_desc {
UINT32 channel; /* 指定的视频采集通道 */
CHAR description[32]; /* 说明 */
UINT32 xsize_max; /* 最大尺寸 */
UINT32 ysize_max;
UINT32 queue_max; /* 最大支持存储序列数 */
UINT32 formats; /* 支持的视频采集格式个数 */
UINT32 capabilities; /* 具有的能力 */
#define VIDEO_CHAN_ONESHOT 1 /* 仅采集一帧 */
……
} video_channel_desc;
下面是 video_channel_desc 结构成员含义:
- channel 为通道号。
- description 为格式描述字符串。
- xsize_max 和 ysize_max:分别表示该通道支持的输出图像最大宽度和最大高度,单位为像素。
- queue_max:最大支持存储序列数。这里的序列指的就是一个图像帧序列。
- formats:支持的视频格式数量。
- capabilities:为通道的功能,当前仅定义了允许通道每一次仅采集一帧数据。
设备通道图像格式描述
typedef struct video_format_desc {
UINT32 channel; /* 指定的视频采集通道 */
UINT32 index; /* 指定的序列编号 */
CHAR description[32]; /* 说明 */
UINT32 format; /* 帧格式 video_pixel_format */
UINT32 order; /* MSB or LSB video_order_t */
UINT32 reserve[8];
} video_format_desc;
上面讲到,通道描述符里用 formats 成员给出了该通道支持的视频格式数量,video_format_desc 用以描述一个具体的视频格式。
下面是 video_format_desc 结构成员含义:
- channel 为该格式对应的通道号,用户设置该值获取指定通道的支持的格式。
- index 是相对 formats 而言的,取值应为 0~formats,表示获取第几种格式描述信息。
- description 为该格式的描述字符串。
- order 为一个像素数据的存储方式是大端还是小端,其值为 video_order_t 类型。
typedef enum {
VIDEO_LSB_CRCB = 0, /* 低位在前 */
VIDEO_MSB_CRCB = 1 /* 高位在前 */
} video_order_t;
format 为具体的视频格式标志,其值为 video_pixel_format 枚举类型,定义如下:
typedef enum {
VIDEO_PIXEL_FORMAT_RESERVE = 0,
/*
* RGB
*/
VIDEO_PIXEL_FORMAT_RGBA_8888 = 1,
VIDEO_PIXEL_FORMAT_RGBX_8888 = 2,
VIDEO_PIXEL_FORMAT_RGB_888 = 3,
VIDEO_PIXEL_FORMAT_RGB_565 = 4,
VIDEO_PIXEL_FORMAT_BGRA_8888 = 5,
VIDEO_PIXEL_FORMAT_RGBA_5551 = 6,
VIDEO_PIXEL_FORMAT_RGBA_4444 = 7,
/*
* 0x8 ~ 0xF range reserve
*/
VIDEO_PIXEL_FORMAT_YCbCr_422_SP = 0x10, /* NV16 */
VIDEO_PIXEL_FORMAT_YCrCb_420_SP = 0x11, /* NV21 */
VIDEO_PIXEL_FORMAT_YCbCr_422_P = 0x12, /* IYUV */
VIDEO_PIXEL_FORMAT_YCbCr_420_P = 0x13, /* YUV9 */
VIDEO_PIXEL_FORMAT_YCbCr_422_I = 0x14, /* YUY2 */
/*
* 0x15 reserve
*/
VIDEO_PIXEL_FORMAT_CbYCrY_422_I = 0x16,
/*
* 0x17 0x18 ~ 0x1F range reserve
*/
VIDEO_PIXEL_FORMAT_YCbCr_420_SP_TILED = 0x20, /* NV12 tiled */
VIDEO_PIXEL_FORMAT_YCbCr_420_SP = 0x21, /* NV12 */
VIDEO_PIXEL_FORMAT_YCrCb_420_SP_TILED = 0x22, /* NV21 tiled */
VIDEO_PIXEL_FORMAT_YCrCb_422_SP = 0x23, /* NV61 */
VIDEO_PIXEL_FORMAT_YCrCb_422_P = 0x24 /* YV12 */
} video_pixel_format;
当前 SylixOS 仅针对 YUV 和 RGB 图像格式进行了定义,并未定义其他压缩格式如 JPEG、BMP 等,即主要处理视频捕获相关的设备。从上面可以看出视频格式非常多,关于 YUV 和 RGB 更多的知识本节不作更多的介绍,读者可通过其他途径了解。
设备通道设置
在获取设备描述符、通道描述符和每个通道的格式描述符信息后,用户可根据实际的需求设置通道的相关参数。通道控制的结构定义如下:
typedef struct video_channel_ctl {
UINT32 channel; /* 视频通道号 */
UINT32 xsize; /* 采集输出的尺寸 */
UINT32 ysize;
UINT32 x_off; /* 相对 size_max 采集起始偏移量 */
UINT32 y_off;
UINT32 x_cut; /* 相对 size_max 采集结束偏移量 */
UINT32 y_cut;
UINT32 queue; /* 采集序列数 */
UINT32 source; /* 指定的视频输入源 */
UINT32 format; /* 帧格式 video_pixel_format */
UINT32 order; /* MSB or LSB video_order_t */
……
} video_channel_ctl;
下面是 video_channel_ctl 结构成员含义:
- xsize 和 ysize 用于指定最终输出的图像大小。
- x_off、y_off、x_cut、y_cut 为图像帧剪裁处理。意味着我们可以只提取原始图像帧的部分区域,通过 xsize 和 ysize 将部分区域进行缩放处理。当然,这些功能都需要具体的视频设备驱动支持。如果视频设备不支持裁剪和缩放,则应用程序只能获得原始的视频图像数据。
- queue 表示需要视频设备采集多少帧数据。该值不能超过视频设备支持的最大帧数(上面所讲的通道描述符定义的 queue_max)。
- source 指定该通道的输入源,不超过设备支持的最大视频源数量。
- format 和 order 应为通道支持的视频格式描述符中的其中一种。
设备缓冲区设置
视频采集是一个连续的过程,在上层应用处理采集完的一帧数据的同时,采集过程继续进行。这就需要有一个可容纳数帧视频数据的缓冲区,用户始终处理的是存有有效帧数据的缓冲,视频设备总是将采集好的数据放入空闲的帧缓冲,两者之间互不冲突。实际上,绝大多数的视频采集设备,在硬件上都支持多帧缓冲的设置,即每一个视频输出通道都有一个或多个帧缓冲队列,该队列又称为 ping-pong 缓冲区,视频采集卡循环地将视频数据放在 ping-pong 缓冲区中。例如一个通道有 4 个 queue 缓冲区, 则视频采集卡会从 1 循环到 4 并不停的一帧一帧重复这个过程。在前面的设备通道描述符中,queue_max 即代表一个视频通道所能缓存的有效帧数据的最大数量。不同于其他系统,SylixOS 定义的帧缓冲区为一片物理地址连续的可容纳数帧数据的内存,这片内存可由应用分配供驱动使用,也可由驱动内部自动分配。
除了缓冲区的帧数量这一参数外,还有其他参数需要用户关心。前面讲到使用通道控制结构 video_channel_ctl 设置通道参数,包括图像的大小、裁剪区域和格式等。实际上,在设置完通道参数后,驱动程序便能根据自身的硬件情况,得到每一帧数据的缓冲区大小和总的缓冲区大小。用户使用缓冲区计算请求结构,获得驱动程序给出的缓冲区详细参数:
typedef struct video_buf_cal {
UINT32 channel; /* 视频通道号 */
size_t align; /* 最小内存对齐要求 */
size_t size; /* 该通道缓冲内存总大小 */
size_t size_per_fq; /* 队列中每一帧图像内存大小 */
size_t size_per_line; /* 一帧图像中每一行内存大小 */
……
} video_buf_cal;
align 为整个缓冲区内存对齐值。多数情况下,视频设备内部使用 DMA 传输视频数据,对使用的内存都有地址对齐需求。
size 为总的内存大小。size_per_fq 为一帧数据的大小,size_per_line 为一帧图像中每一行内存大小。通常情况下,一帧数据的大小等于每个像素的字节数与帧的长和宽的乘积。但是,不同的硬件,对帧数据的每一行可能有数据对齐要求,因此会存在行末填充无效数据以满足该对齐条件的情况。用户根据图像的实际大小配合 size_per_fq 和 size_per_line,便可知道如何处理帧数据。
在获得缓冲区的具体参数后,通过缓冲区控制结构,设置具体的缓冲区:
typedef struct video_buf_ctl {
UINT32 channel; /* 视频通道号 */
PVOID mem; /* 帧缓冲区 (物理内存地址) */
size_t size; /* 缓冲区大小 */
UINT32 mtype; /* 帧缓存类型 video_mem_t */
……;
} video_buf_ctl;
前面说到,帧缓冲区内存可由用户分配并提供给驱动使用,mem 则指向用户分配的内存,注意需满足由 video_buf_cal 规定的参数需求,如总的内存大小,对齐值等。size 表示实际的内存大小,不小于 videoo_buf_cal 规定的大小。mtype 表示帧缓存类型,其值为枚举类型 video_mem_t,定义如下:
typedef enum {
VIDEO_MEMORY_AUTO = 0, /* 自动分配帧缓冲 */
VIDEO_MEMORY_USER = 1 /* 用户分配帧缓冲 */
} video_mem_t;
如果用户自己分配缓冲区,则需要将 mtype 设置为 VIDEO_MEMORY_USER,反之则设置为 VIDEO_MEMORY_AUTO,mem 设置为 LW_NULL 即可。用户自行分配物理内存,在某些时候可为应用带来更好的性能和效率。如果驱动自行分配内存,则应用程序只能通过 mmap 内存映射访问这片内存,会消耗虚拟内存页面空间。如果系统中存在一种需要将图像数据进行再处理的硬件,它也使用 DMA 访问图像内存,则我们可以将用户分配的内存同时应用于该硬件和视频接口,让两者直接进行内存交互,实现零拷贝。
视频捕获控制
在完成以上几步之后,就可以启动视频设备开始视频捕获了。控制视频捕获的结构如下:
typedef struct video_cap_ctl {
UINT32 channel; /* 视频通道号 */
#define VIDEO_CAP_ALLCHANNEL 0xffffffff
UINT32 on; /* on / off */
UINT32 flags;
#define VIDEO_CAP_ONESHOT 1 /* 仅采集一帧 */
……;
} video_cap_ctl;
下面是 video_cap_ctl 结构成员含义:
- channel 规定启动捕获的通道,如果设置了所有的通道,则可以通过 VIDEO_CAP_ALLCHANNEL 一次启动多个通道。当然,如果只设置了部分通道,就需要分别启动各个通道。
- on 表示启动或停止捕获,0 为停止,非零为启动。
- flags 为捕获标志,当前仅定义了 VIDEO_CAP_ONESHOT,与通道描述符里的 capabilities 对应。
在启动视频捕获后,为了正确地处理捕获数据,我们还需要通过获取当前的捕获状态,使用以下结构获取:
typedef struct video_cap_stat {
UINT32 channel; /* 视频通道号 */
UINT32 on; /* on / off */
UINT32 qindex_vaild; /* 最近一帧有效画面的队列号 */
UINT32 qindex_cur; /* 正在采集的队列号 */
#define VIDEO_CAP_QINVAL 0xffffffff
……
} video_cap_stat;
下面是 video_cap_stat 结构成员含义:
- on 表示当前的捕获状态,0 为停止,非零为启动。
- qindex_valid 表示最近一帧有效画面的队列号,应用程序应该使用该帧数据。
- qindex_cur 表示正在采集的队列号,应用程序不应该使用该帧数据。
如果队列号为 VIDEO_VAP_QINVAL,则表示无效队列号,说明内部还没有任何有效的帧数据,应用程序应该继续查询捕获状态。
前面说到,SylixOS 中,视频捕获的所有帧缓冲区为一个连续的物理内存空间,应用程序根据每帧数据的大小以及帧索引可访问指定的帧数据。
视频设备操作命令汇总
与其他系统一样,SylixOS 中的视频设备为一个标准的 I/O 设备,前面所讲的所有对该设备的操作均通过 ioctl 系统命令完成,下表列出了所有操作命令。
命令字 | 参数(为该类型指针) | 说明 |
---|---|---|
VIDIOC_DEVDESC | video_dev_desc | 获取设备描述符 |
VIDIOC_CHANDESC | video_channel_desc | 获取设备指定的通道描述符 |
VIDIOC_FORMATDESC | video_format_desc | 获取通道支持的格式描述符 |
VIDIOC_GCHANCTL | video_channel_ctl | 获取当前通道的参数 |
VIDIOC_SCHANCTL | video_channel_ctl | 设置当前通道的参数 |
VIDIOC_MAPCAL | video_buf_cal | 获取帧缓冲区参数 |
VIDIOC_MAPPREPAIR | video_buf_ctl | 设置帧缓冲区(内存预分配) |
VIDIOC_CAPSTAT | video_cap_stat | 获取捕获状态 |
VIDIOC_GCAPCTL | video_cap_ctl | 获取当前的捕获参数 |
VIDIOC_SCAPCTL | video_cap_ctl | 设置当前的捕获参数 |
视频设备应用实例
下面我们通过一个例子展示如何获得一个视频设备的具体信息,如下程序所示:
#include <SylixOS.h>
#include <video.h>
int main (int argc, char *argv[])
{
int fd;
int i, j;
video_dev_desc dev;
video_channel_desc channel;
video_format_desc format;
fd = open("/dev/video0", O_RDWR);
ioctl(fd, VIDIOC_DEVDESC, &dev);
for (i = 0; i < dev.channels; i++) {
channel.channel = i;
ioctl(fd, VIDIOC_CHANDESC, &channel);
for (j = 0; j < channel.formats; j++) {
format.channel = i;
format.index = j;
ioctl(fd, VIDIOC_FORMATDESC, &format);
}
}
return 0;
}
上面的程序中,首先获取设备描述符,得到设备支持的视频输出通道数量,然后针对每个通道,获取其通道描述符,得到每一个通道支持的视频格式数量,从而得到该视频设备每一个通道各自支持的所有视频格式。
下面考虑一个具体的应用场景:我们需要将捕获的视频通过 LCD 实时显示,我们需要了解 LCD 设备(也就是帧缓冲设备 FrameBuffer)的显示参数和视频数据的格式参数,进行适当的软件处理后放入 FrameBuffer 显示。限于篇幅,下面的程序以伪代码实现,并且作了很多简化处理。
#include <SylixOS.h>
#include <video.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int main (int argc, char *argv[])
{
int fd;
int fb_fd;
video_channel_ctl channel;
video_buf_cal cal;
video_buf_ctl buf;
video_cap_ctl cap;
video_cap_stat sta;
void *pcapmem;
void *pfbmem;
void *pframe;
fd_set fdset;
/*
* 打开视频设备并获得必要信息
*/
fd = open("/dev/video0", O_RDWR);
...
/*
* 设置需要的输出图像参数
* 同时获得帧内存参数
*/
channel.channel = 0;
channel.xsize = 640;
channel.ysize = 480;
channel.x_off = 0;
channel.y_off = 0;
channel.queue = 1;
channel.source = 0;
channel.format = VIDEO_PIXEL_FORMAT_RGBX_8888;
channel.order = VIDEO_LSB_CRCB;
ioctl(fd, VIDIOC_SCHANCTL, &channel);
ioctl(fd, VIDIOC_MAPCAL, &cal);
/*
* 准备内存数据
*/
buf.channel = 0;
buf.mem = NULL;
buf.size = cal.size;
buf.mtype = VIDEO_MEMORY_AUTO;
ioctl(fd, VIDIOC_MAPPREPAIR, &buf);
/*
* 映射帧内存
*/
pcapmem = mmap(NULL, buf.size, PROT_READ, MAP_SHARED, fd, 0);
cap.channel = 0;
cap.on = 1;
cap.flags = 0;
ioctl(fd, VIDIOC_SCAPCTL, &cap);
/*
* 打开FrameBuffer 设备
* 映射FrameBuffer 内存
*/
fb_fd = open(...);
pfbmem = mmap(..., fb_fd, 0);
for (;;) {
FD_ZERO(&fdset);
FD_SET(fd, &fdset);
/*
* 等待设备可读.
* 每一次有效帧数据完成, 驱动都会唤醒阻塞在此的线程
*/
select(fd + 1, &fdset, NULL, NULL, NULL);
if (FD_ISSET(fd, &fdset)) {
ioctl(fd, VIDIOC_CAPSTAT, &sta);
pframe = (char *)pcapmem + cal.size_per_fq * sta.qindex_vaild;
...
memcpy(pfbmem, pframe, cal.size_per_fq);
}
}
munmap(pcapmem, buf.size);
close(fd);
munmap(pfbmem, ...);
close(fb_fd);
return (0);
}
上面的例子中,我们假设已经知道了显示设备的格式参数为 RGB32 格式,并且宽高分别为 640 和 480 像素,并设置了相同的视频输出格式。在启动视频捕获后,我们通过 select 系统调用等待每一帧数据的到来。通过 VIDIOC_CAPSTAT 命令,得到当前的有效帧索引。
注意,在获得当前需要处理的帧数据时,上面的程序作了很多简化处理,并没有考虑行无效数据的填充问题,而是假设所有的数据都是有效像素数据。
我们通过每帧的大小和当前有效帧索引得到当前帧缓冲地址,并随后将其直接拷贝到显示缓冲区。这里又假设捕获的视频数据格式与显示缓冲区格式需求完全相同,否则在拷贝之前,应该作适当的变换处理。
实际上,视频设备的操作要比其他设备相对繁琐一些,因为其涉及更多的参数控制。一个能够广泛适应多种软硬件平台的视频应用程序的实现也并不简单。例如上面的情况,如果视频格式不支持 RGB 格式,则我们还需要将其转换为 RGB 格式才能正确显示。此例仅起到抛砖引玉的作用,读者可通过其他途径深入了解。