Windows远程桌面实现之十 - 移植xdisp_virt之Linux(Utunbu,CentOS等)屏幕截屏,键鼠控制,声音 摄像头采集(四)

17 篇文章 1 订阅
13 篇文章 6 订阅

                                                               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图:

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值