by fanxiushu 2019-12-30 转载或引用请注明原始作者。
前几章介绍 xdisp_virt移植的时候,分别阐述了xdisp_virt移植整个工程,iOS平台相关的各种数据采集,macOS平台相关的各种数据采集。
这篇文章阐述Linux平台下的桌面图像数据采集,鼠标键盘控制,摄像头采集,声音采集等内容。
内容也是较多,不大可能很细致的阐述每一个部分,但其实也并不复杂,
我们只需要使用里边简单的一些系统函数就可以采集相关的数据了, 而且比起windows中的函数的使用,总是显得很简单。
Linux的桌面环境使用率并不高(其实也是难用,还不如直接使用命令行环境),
但说起嵌入式方面,linux是大展拳脚的地方,服务器方面表现也较强势。
(一)先来看看声音的采集:
在linux中,使用ALSA框架, linux中的ALSA的地位等同于windows中的WASAPI,macOS中的CoreAudio,都是属于底层框架。
ALSA包括驱动和应用层部分,驱动支持ALSA声卡开发规范,应用层提供一些标准API函数供我们使用声卡。
这里只关注ALSA的应用层接口部分。
使用前,需要包含 #include <alsa/asoundlib.h> 头文件,链接时候需要添加 -lasound,
有些linux系统默认没有安装 ALSA 开发环境,可以自行去网上下载开发包,比如CentOS可以执行 yum install alsa-lib-devel来安装。
首先,我们需要查询系统中所有声卡设备,因为这里只关心录音,所有其实查询的是 麦克风录音设备。
使用 snd_card_next 函数查询声卡,对每个声卡调用snd_ctl_open 打开,使用snd_ctl_card_info查询信息,
然后使用 snd_ctl_pcm_next_device 查询这个声卡中对应的具体设备(比如哪些mic录音设备,有哪些播放设备等)。
查询的代码片段如下:
int card = -1;
if (snd_card_next(&card) < 0 || card < 0) {
return -1;
}
do {
///
char name[32];
sprintf(name, "hw:%d", card);
if ((err = snd_ctl_open(&handle, name, 0)) < 0) {
printf("control open (%i): %s", card, snd_strerror(err));
continue;
}
if ((err = snd_ctl_card_info(handle, info)) < 0) {
printf("control hardware info (%i): %s", card, snd_strerror(err));
snd_ctl_close(handle);
continue;
}
dev = -1;
while (true) {
if (snd_ctl_pcm_next_device(handle, &dev) < 0)
printf("snd_ctl_pcm_next_device");
if (dev < 0)
break;
snd_pcm_info_set_device(pcminfo, dev);
snd_pcm_info_set_subdevice(pcminfo, 0);
snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE); // microphone devices,查询录音设备
if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
if (err != -ENOENT)
printf("control digital audio info (%i): %s", card, snd_strerror(err));
continue;
}
。。。处理已经查询到的设备
char dev_name[32];
sprintf(dev_name, "hw:%d,%d", card, dev); ///这个名字传递给 snd_pcm_open函数,用于打开具体的声卡设备。
}
snd_ctl_close(handle);
} while (snd_card_next(&card) >= 0 && card >= 0);
之后调用 snd_pcm_open 打开设备,
err = snd_pcm_open(&handle, dev_name, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); // open capture
///调用 snd_pcm_hw_params_set_format 设置采集格式,
err = 0; int rate = 44100;
do {
err = snd_pcm_hw_params_any(handle, params); if (err < 0)break;
err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); if (err < 0)break;
err = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); if (err < 0)break;
err = snd_pcm_hw_params_set_channels(handle, params, 2); if (err < 0)break;
err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0); if (err < 0)break;
} while (false);
err = snd_pcm_hw_params(handle, params);
循环调用 snd_pcm_readi 就可以采集到具体的PCM声音数据了。关闭设备调用 snd_pcm_close。
因为xdisp_virt需要采集电脑内部声音,在windows中我们使用WASAPI,
在macOS中很不幸,没办法,但是通过开发虚拟声卡能把电脑内部声音转换到虚拟麦克风设备上。
而在linux中,可以添加一个虚拟Loopback虚拟声卡设备来采集,也就是帮我们做好了这个现成的虚拟设备。
对此并没仔细研究,有兴趣可自行去添加Loopback设备。
(二)摄像头采集:
在linux中摄像头等视频数据的采集,使用 v4l2 规范,使用到的系统函数没有新鲜的,全是 open 和 ioctl 来处理所有相关数据,
尤其是大量使用ioctl 函数来跟摄像头驱动进行数据交互 。
需要包含的头文件 #include <linux/videodev2.h>,
首先同样是列举系统中的所有摄像头设备,不像ALSA声音框架,摄像头没有提供专门的函数来列举,
因此我们从 /dev 目录查找设备,一般dev目录下名字有video,v4l-subdev等名字的应该具备摄像头功能,
然后列举到这个目录下的设备名字之后,open打开,比如 open( "/dev/video0,"O_RDWR);
调用 ioctl 查询 v4l2_capability ,就能确定是否camera设备了。类似如下。
struct v4l2_capability vcap; ioctl(fd, VIDIOC_QUERYCAP, &vcap); 然后判断是否能成功查询到。
查找到满足我们要求的camera设备之后,调用open打开这个设备, 然后就是清一色使用ioctl函数获取和设置各种参数。
比如使用 VIDIOC_G_FMT和VIDIOC_S_FMT命令可以获取和设置当前 摄像头的像素格式,宽高等参数。
使用VIDIOC_S_PARM 可以设置帧率等,配置参数较多,可以查阅相关说明,
或者网上查阅相关代码,V4L2采集摄像头的代码一大堆,估计是比较通用,因为不光是linux桌面平台,
在嵌入式linux中采集摄像头或声卡数据,都是比较常见的需求。
接下来我们需要采集摄像头数据,要采集数据,需要知道对应的buffer 。
V4L2框架的camera驱动帮我们提供了buffer,我们要做的就是把这个内核buffer映射到我们的应用层空间中,
使用 VIDIOC_QUERYBUF 命令来请求对应的buffer,然后调用 mmap把内核buffer映射到程序空间,
准备好之后,使用VIDIOC_QBUF把查询到的buffer送入内核采集队列,这样内核驱动就会填充这些buffer,
我们直接访问mmap映射的内存,就采集到驱动发送上来的摄像头图像数据了。大致伪代码如下:
struct v4l2_requestbuffers req;
req.count = 10;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(handle, VIDIOC_REQBUFS, &req) ; //查询驱动提供的buffer,
///映射内存
for (int i = 0; i < req.count; ++i) {
struct v4l2_buffer buffer ;
memset(&buffer, 0, sizeof(buffer));
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
buffer.index = i;
ioctl(handle, VIDIOC_QUERYBUF, &buffer);
memory[i] = mmap(NULL, buffer.length, PROT_READ | PROT_WRITE,
MAP_SHARED, handle, buffer.m.offset);
。。。
///把查询的buffer入队到驱动中,这样驱动就会使用这个buffer采集数据。
ioctl(handle, VIDIOC_QBUF, &buffer);
}
然后循环调用 VIDIOC_DQBUF 命令截取到已经采集完成的buffer,我们处理完这个数据之后,接着调用 VIDIOC_QBUF再次入队。
while(1){
for(int i=0;i<req.count;++i){
struct v4l2_buffer buffer;
memset(&buffer, 0, sizeof(buffer));
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
buffer.index = i;
ioctl(handle, VIDIOC_DQBUF, &buffer); //需要判断返回值,如果成功,就表面采集到数据了,
///之后我们直接使用上面映射的memory[i], 来处理接收到的数据。
。。。。。
ioctl(handle, VIDIOC_QBUF, &buffer);//处理完成之后,继续把buffer入队。
}
}
至此camera采集就完成了,
需要注意两点:
1, 创建和循环采集必须在一个线程里,我之前初始化摄像头在一个线程里,
然后循环采集是另外开启一个线程,结果程序直接崩溃,原因不清楚,也许是摄像头的问题,也许是系统的问题。
2, 停止摄像头不能重复调用VIDIOC_STREAMOFF ,否则系统直接崩溃,估计不是驱动的问题,就是系统的问题。
其实使用V4L2开发摄像头比较繁琐,而且一不留神很容易造成崩溃,这似乎与linux一项简洁的风格不符。
(三),屏幕数据采集。
linux的桌面,是比较奇特的,linux内核不处理跟桌面图形相关的,linux的桌面显示只是一个单纯的应用层程序,
这个与windows和macOS把图形集成到系统中是不一样的。具体使用的是X11协议,是一种 Server/Client 的架构。
正是基于此,我们可以在远端直接显示X11桌面,但是实际估计慢的要命,本来图像数据量就很庞大,占用的资源也多。
还来个 C/S 通讯,是比较够呛,也就只能简单的使用。
本来使用XGetImage函数就能截取到桌面图像数据,但是这个函数每次都会创建XImage 图像数据缓存空间,效率比较差。
因此实际在频繁截屏中,经常使用的是 XShmGetImage 函数,它通过共享内存的方式,直接从X11的Server端截取图像数据。
我们使用 XOpenDisplay 打开X11 桌面之后,获取到默认的Root的Window,
然后调用 XShmCreateImage 创建 XImage。
之后调用 shmget 获取共享内存ID, 再调用shmat 映射这个共享内存。
这样准备好了之后,调用XShmAttach , 这样就初始化完成了,大致代码如下:
int screen_stream::create()
{
display = XOpenDisplay(NULL);
if (!display) {
printf("**** XOpenDisplay error.\n");
return -1;
}
root = DefaultRootWindow(display);
Status st = XGetWindowAttributes(display, root, &window_attributes);
if (st == 0) {// error
///
printf("XGetWindowAttributes error.\n");
return -1;
}
/// screen size change notify
XSelectInput(display, root, StructureNotifyMask); // notify
screen = window_attributes.screen;
int width = screen->width;
int height = screen->height;
///
bitcount = 32;// DefaultDepthOfScreen(screen); ///
ximg = XShmCreateImage(display, DefaultVisualOfScreen(screen),
bitcount , ZPixmap, NULL, &shminfo, width, height);
if (!ximg) {
printf("XShmCreateImage error.\n");
return -1;
}
printf("---ximg->bytes_per_line=%d\n", ximg->bytes_per_line);
/
shminfo.shmid = shmget(IPC_PRIVATE, ximg->bytes_per_line * ximg->height, IPC_CREAT | 0777); //create shmem
if (shminfo.shmid < 0) {
printf("*** shmget err=%d\n", errno);
return -1;
}
shminfo.shmaddr = ximg->data = (char*)shmat(shminfo.shmid, 0, 0); // map shmem
if (!ximg->data) {
printf("*** shmat err=%d\n", errno );
return -1;
}
shminfo.readOnly = False;
///
st = XShmAttach(display, &shminfo);
if (st == 0) {/// error
printf("*** XShmAttach error\n");
return -1;
}
is_attach = true;
//
///
return 0;
}
int screen_stream::capture() {
/
Bool f = XShmGetImage(display, root, ximg, 0, 0, 0x00ffffff);
if (!f) {
printf("*** XShmGetImage error\n");
return -1;
}
return 0;
}
之后创建一个线程,循环调用 XShmGetImage 函数, 也就是上面代码中的capture函数,
调用了之后, ximg->data里边存储的就是当前的桌面图像数据了,够简单的吧。
这个很像windows平台下的GDI截屏,但是调用方式比起GDI简单多。
同windows平台中GDI截屏一样的,截取到的桌面图像数据是没有鼠标的,需要另外调用其他函数来截取鼠标的数据。
使用 XFixesGetCursorImage 函数就能截取到鼠标数据了, 函数返回 XFixesCursorImage 结构,
里边包含鼠标的图像数据,鼠标的位置等信息。
然后再把鼠标图像数据画到上面 XShmGetImage 截取到的整个桌面图像中即可。
(四),鼠标键盘模拟控制:
这里的鼠标键盘控制,也是使用X11中的函数,主要是为了统一,因为linux提供了更底层的操作鼠标键盘的函数,
其实也就是直接 打开 /dev目录下的 inputXX 等设备文件 来直接读写键盘鼠标数据。这个跟上面的V4L2操作方式类似。
X11中模拟鼠标键盘也不难,
使用 XTestFakeButtonEvent 函数模拟鼠标的点击,包括左键,右键,中间键,滚轮水平上下左右滚动等。
使用 XTestFakeMotionEvent 函数模拟 鼠标的移动操作。
类似如下代码,就能模拟一个鼠标的全部操作(移动,左右键,滚轮上下滚动,):
if ( flags & MF_MOVE ) {// mouse move
XTestFakeMotionEvent(ev->display, -1, x, y, CurrentTime); //
}
if (flags & MF_LDOWN) { // left down
XTestFakeButtonEvent(ev->display, 1, true, CurrentTime);
}
if (flags & MF_LUP) {// left up
XTestFakeButtonEvent(ev->display, 1, false, CurrentTime);
}
if (flags & MF_RDOWN) { // right down
XTestFakeButtonEvent(ev->display, 3, true, CurrentTime);
}
if (flags & MF_RUP) { // right up
XTestFakeButtonEvent(ev->display, 3, false, CurrentTime);
}
if (flags & MF_WHEEL) {// mouse wheel
int id = 4;
if (wheel < 0)id = 5;
XTestFakeButtonEvent(ev->display, id, true, CurrentTime);
XTestFakeButtonEvent(ev->display, id, false, CurrentTime);
}
XFlush(ev->display);//刷新,让操作立即生效。
键盘的模拟则更加简单,使用 XTestFakeKeyEvent 函数就可以。
但是,跟macOS系统一样,又是使用自己的一套键盘码。
因此任然需要把PS2虚拟键盘码转成它的键盘码,又是一个无聊的对照键码表的转换,这里也不再赘述。
至此,移植到xdisp_virt到UNIX类平台就算全部完成了。有兴趣可关注:
https://github.com/fanxiushu/xdisp_virt
等整理好xdisp_virt,会把macOS系统和linux系统下的xdisp_virt发布到GITHUB上。
下图是Ubuntu系统中采集图,可预先预览一下:
下图是CentOS8系统的xdisp_virt图: