Windows遠程桌面實現之十 - 移植xdisp_virt之Linux(Utunbu,CentOS等)屏幕截屏,鍵鼠控制,聲音 攝像頭採集(四)

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

發佈了76 篇原創文章 · 獲贊 101 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章