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