windows遠程桌面實現之一 (抓屏技術總覽 MirrorDriver,DXGI,GDI)

                                                                                                        by fanxiushu 2017-06-14     轉載或引用請註明原始作者


要實現遠程桌面功能,首先要做的就是桌面圖片的截取,而且要達到比較流暢的視覺效果,

需要到達每秒20幀以上。

截取桌面圖片,就是定時截取windows桌面的圖片,隨便怎麼都能做到,好像挺簡單的。
通用的做法就是GetDC(GetDesktokWindow() )獲取桌面的DC,
然後使用CreateDIBSection創建一個設備無關位圖以及內存DC,使用BitBlt把桌面DC的翻轉到內存DC,
這樣通過內存DC就能直接獲取到原始RGB數據。
這個就是GDI函數實現的通用做法,能在所有windows平臺實現。
通用歸通用,截取的效率則是有點低,尤其是要達到每秒20幀以上的截取,佔用CPU有點高,
當然配置好的機器倒也看不出多大區別(目前的電腦配置有可能讓人感覺不到GDI的效率問題)。
GDI不能獲取鼠標,需要在截取的圖像中把鼠標畫上去。

windows 8 以上的系統,則實現了一個DXGI,用來截取桌面,它是集成在DirectX之中,成爲DX的一個子功能。
你需要簡單熟悉DirectX技術才能使用DXGI,他通過一堆囉嗦的創建和查詢各種接口,最終獲取到 IDXGIOutputDuplication 接口。
截屏的時候,使用這個接口的AcquireNextFrame 函數獲取當前桌面圖像,
當然這個接口還提供 GetFrameDirtyRects等函數獲取發生了變化的矩形區域。
這應該算是一個比較好的截屏方法,CPU佔用也極低,可惜只支持windows 8 以上的平臺,而win7,winxp等系統不支持。

MirrorDriver 這是個驅動截取方法,MirrorDriver就是顯示鏡像驅動,這個來源於windows2000以上的系統,
屬於XPDM模型的顯示驅動,這是個很老的驅動模型,只支持到WinXP系統,
隨着計算機圖形技術的發展尤其是3D技術的發展,老的XPDM模型已經不再適應最新3D技術。
因此在win7之後,就不再支持 XPDM模型的顯示驅動,而採用WDDM模型的顯示驅動,到了win10, WDDM已經達到了2.0版本。
不知道何原因,微軟估計是在WDDM驅動中沒有提供類似鏡像驅動模型。
然而在高效截屏中需要用到驅動技術,因此才保留了XPDM模型的鏡像驅動跟WDDM顯示驅動兼容。
僅僅只是XPDM鏡像驅動跟WDDM驅動兼容,XPDM顯示驅動不能運行在WIN7以上系統中。
簡單的一句話說就是: MirrorDriver驅動可以安全和方便的運行在WINXP,  WIN7, WIN8, WIN10 等系統中。
因此MirrorDriver的通用性也是跟GDI一樣支持所有windows平臺,但是不支持古老的使用DirectDraw加速的程序,
這些程序在MirrorDriver環境下甚至都無法運行。
MirrorDriver跟DXGI一樣,截取圖像數據以及更新過的矩形區域,佔用的CPU都是極低的。
或者說他們在windows內核中的實現本質上都是一樣的。

然而都挺可惜,以上三種辦法都沒法截取處於屏幕獨佔模式下的遊戲圖像。
也就是對顯卡硬件加速的比如Direct3D等程序不太友好。
DXGI不是太確定,我沒試過win10運行全屏模式遊戲能否使用DXGI截取到圖像。
所謂獨佔模式,就是全屏下的遊戲
(當然有些全屏遊戲可以截取,那它應該屬於那種僞獨佔模式,看起來在全屏中運行,實際上還是屬於某個窗口)。
處理這種截屏,估計只有Hook DirectX(或者OpenGL)中函數庫。
這種HOOk,需要開發一個DLL,把這個DLL注入到具體的程序中,然後掛鉤程序調用的DirectX 渲染函數,從而獲取到圖像數據。
DLL的注入技術倒是不難,難的是我對DirectX或者說是3D圖形技術並不熟練,不清楚哪些程序使用哪些渲染函數,
而且DirectX的版本又多,每個版本估計都得處理一下。

windows平臺下還有其他一些偏門辦法來截圖,幾乎都沒以上三種辦法好,甚至更糟糕。
windows平臺沒能提供一個完善的抓屏解決方案,需要對各種特殊情況做查漏補缺似的修補。
因此套用別人的一個做法是最好的了:
使用一個高清攝像機對着windows電腦屏幕拍,然後一切都能拍下來,什麼都能解決了,而且還兼容各種操作系統桌面呲牙 。

然而有沒有一個通用的辦法,在所有windows平臺下能截取到所有程序的顯示的圖像呢?
有一個並不值得推薦的辦法就是開發虛擬顯卡驅動程序。
支持win7以上的系統得開發WDDM模型的驅動,如果要支持winxp,還得開發XPDM的虛擬顯卡驅動。
到目前爲止,我還沒研究過WDDM模型驅動,只熟悉MirrorDriver驅動開發。
WDDM模型分爲應用層的顯示驅動部分和內核層的小端口驅動部分,
他們需要實現的回調函數有起碼幾十個,看着都覺得恐怖。
暫時還不想栽倒在WDDM模型研究中,再說研究顯示驅動需要有紮實的圖形技術基礎,等閒下來再說。
有了虛擬顯卡驅動,把它裝到電腦上,然後選擇虛擬顯卡驅動爲主顯卡,
接着電腦所有顯示的數據都進入到我們的虛擬顯卡中,從而就能截取到所有的圖像數據了。
然而電腦顯卡已經不是真實的顯卡,本地顯示器上將是黑屏,我們只能把截獲的圖像數據通過各種通訊線路,
比如USB(採用USB通訊的我們可以簡單稱它爲USB顯卡),網絡等通訊手段還原到遠端設備上顯示出來。
虛擬顯卡這種辦法在我們平常使用的遠程桌面中不大適用,誰也不想自己電腦控制時候,顯示器是黑屏,而且還不能切換回來。

但是這爲雲桌面的構建,提供了一個基礎組件。
雲桌面加了個“雲”字,似乎顯得有點高大上,其實就跟我們平常使用的遠程桌面差不多的功能,
當然被控制端的實現方式跟普通遠程桌面的被控制端有點不大相同。
首先被控端系統被安裝到虛擬機容器中,簡單的說,就是一個物理服務器,
上邊裝類似vmware,hyper-v,virtualbox,kvm等虛擬機軟件,
然後在這些軟件裏再安裝運行幾十個虛擬機系統,
這樣一臺強大的服務器就可以爲用戶提供幾十個虛擬系統,讓用戶選擇自己喜歡的系統愛幹什麼幹什麼。
當然每個系統都有遠程桌面功能,可以直接給用戶提供遠程桌面連接,但是這樣顯得有點原始,而且不利於各種需求的擴展。
因此就需要提供一整套解決方案,包裝起來,讓使用者更方便更容易擴展,
比如一個使用者登錄一個桌面進行操作,其他用戶同樣登錄到這個桌面看這位使用者的操作過程,這有點像遠程辦公或者培訓之類的需求。
類似各種各樣的需求,基本目的是讓使用者在各種終端設備上使用,而且感覺在使用自家的PC電腦一樣。
回到正題,舉個容易理解的例子vmware,我們在使用vmware安裝虛擬機系統比如 windows10,
這個虛擬機系統windows10的桌面都會最終顯示到vmware所在的宿主機上的一個窗口裏邊。這裏邊主要就是虛擬顯卡的功勞。
vmware專門爲虛擬windows系統提供了一個叫“VMware SVGA 3D”的虛擬顯卡(不同的vmware版本,名字可能不一樣)。
安裝在vmware虛擬機裏邊的windows系統的顯示數據,都會朝VMWARE SVGA 3D的顯卡繪畫圖像,
結果這塊虛擬顯卡接收到虛擬機的所有桌面圖像,把圖像數據通過某種手段(比如共享內存)共享給宿主機上的vmware管理軟件。
然後vmware管理軟件就在某個窗口中把圖像還原出來,從而我們就能看到虛擬機裏邊的windows系統的桌面內容了。
而在雲桌面實現中,我們只要把虛擬顯卡共享給宿主機的圖像數據截獲下來
(每種虛擬機都提供了專門的API函數提供每個虛擬機圖像的截獲,
而且能安全和毫無遺漏的獲取,不會像windows平臺自身的桌面截獲那麼讓人不放心)
再通過某種壓縮算法,一般採用H264壓縮(或者改進後的壓縮算法),
通過網絡(可以用公開的網絡協議,或者私有協議,這個隨你的興趣)傳遞給客戶端。
因爲H264壓縮質量和壓縮率都很高,如果服務器機器足夠強大,
基本上擁有10M帶寬的客戶端就可以流暢的觀看高清視頻和玩遊戲,視頻和遊戲都流暢了,其他辦公操作自然就不在話下。
因此雲桌面能實現,另一個核心組件就是先進的圖像的壓縮算法。
只有把基本和核心的桌面顯示解決流暢了,纔能有接下來的音頻重定向,鼠標鍵盤重定向,USB設備重定向等內容。

自然我們在自己的普通遠程桌面中利用H264壓縮圖像,保證效果也很好,而且還能超過VNC之類的遠程控制軟件,
能提供流暢的遠程高清視頻觀看,可惜我無法獲取全屏模式遊戲圖像數據,無法知道全屏遊戲遠程之後是啥感覺。
說的有點遠,回到我們的windows截屏上來。
雖說GDI截屏佔用CPU有點高,但是大部分場合下使用它,已經足夠了。
在我的電腦上,CPU是 I5-3320M,購買於5年前,顯卡是集成的,
WIN7 64位平臺,以每秒25幀固定頻率使用GDI函數抓屏,CPU佔用率在 2%-4%之間波動。
5年前電腦的CPU佔用都不高,可想而知現在的電腦CPU估計更不是問題。
然後再以 x264開源庫編碼成 H264(當然先使用小塊掃描辦法確定屏幕圖像數據有沒有更新,沒更新則不壓縮),
進行一般的操作,就是打開文檔,看看文章,寫些文檔什麼的,整個從抓屏到壓縮,CPU使用率在 10%左右,
如果再打開視頻遠程端流暢的觀看視頻,抓屏和x264壓縮的CPU佔用大概飈升到20-30%甚至更高。
1600X900,全屏下遠程視頻大概 500KB/s 字節不到的網絡速度,根據不同的H264壓縮參數這個速度會有所不同。

GDI抓屏函數使用僞代碼如下:

   HDC hdc = GetDC(NULL); //屏幕DC
   HDC memdc = CreateCompatibleDC(hdc);//兼容內存DC
   BITMAPINFOHEADER bi;
   ....//設置位圖參數 
   byte* rgb_buffer; //CreateDIBSection成功後,rgb_buffer指向存儲圖像RGB原始數據的指針,可以從這個內存區直接讀取圖像數據;
   HBITMAP hbmp = CreateDIBSection(hdc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, (void**)&rgb_buffer, NULL, 0);
   SelectObject(memdc,hbmp);// 把位圖和memdc關聯起來
至此,初始化操作就完成了。

定時截屏:
   HDC hdc = GetDC(NULL);
    BitBlt( memdc, 0, 0, ScreenX, ScreenY, hdc, 0, 0, SRCCOPY | CAPTUREBLT); // 屏幕DC翻轉到memdc
    ReleaseDC(NULL, hdc);
然後就直接從 rgb_buffer指向的內存區讀取圖像數據。

使用起來是非常的方便和簡潔,光從這個截屏方法來說是非常值得稱讚的,可以跟某些linux函數的簡潔性媲美了。
這裏需要注意一點是  CAPTUREBLT 參數,要成功截取 layered windows, 必須設置這個參數,否則將有一大部分透明窗口截取不到,
但是使用這參數,頻繁截屏時候(比如每秒25幀)鼠標閃爍的比較厲害,基本沒有好的解決鼠標閃爍的辦法。

win8, win10之類的系統DXGI截屏僞代碼如下:

   ID3D11Device*             d11dev;       ///D3D設備對象
   ID3D11DeviceContext*      d11ctx;   ///對象上下文
   IDXGIOutputDuplication*   dxgi_dup;///這個就是我們需要的採集屏幕數據的接口

   D3D11CreateDevice(.... ..., &d11dev, ... &d11ctx...); //調用D3D11CreateDevice 創建設備對象
   IDXGIDevice* dxdev = 0;
   QueryInterface(__uuidof(IDXGIDevice), (void**)&dxdev);
   IDXGIAdapter* DxgiAdapter = 0;
   dxdev->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&DxgiAdapter));
   IDXGIOutput* DxgiOutput = 0;
   DxgiAdapter->EnumOutputs(nOutput, &DxgiOutput);
   IDXGIOutput1* DxgiOutput1 = 0;
   DxgiOutput->QueryInterface(__uuidof(IDXGIOutput1), reinterpret_cast<void**>(&DxgiOutput1));
   DxgiOutput1->DuplicateOutput(d11dev, &dxgi_dup);    ///經過上邊囉嗦的步驟,終於獲取到我們需要的IDXGIOutputDuplication接口
至此初始化大致完成。

定時截屏:
    IDXGIResource* DesktopResource = 0;
    DXGI_OUTDUPL_FRAME_INFO FrameInfo;
   
    ///這個比較奇特,當我們調用AcquireNextFrame獲取一幀圖像,之後肯定要進行壓縮傳輸等處理,這中間會花去不少時間
    ///這段時間電腦屏幕肯定有變化,爲了保證AcquireNextFrame之後的獲取的圖像數據不變,
    ///因此我們不能調用 AcquireNextFrame之後就調用ReleaseFrame釋放,而是在下次AcquireNextFrame之前再ReleaseFrame釋放。

    dxgi_dup->ReleaseFrame(); //先釋放,然後再獲取,保證釋放和獲取間隔最短

    dxgi_dup->AcquireNextFrame( 0, &FrameInfo, &DesktopResource);  ///截取屏幕數據,但是還不能直接訪問原始數據,
   
    ID3D11Texture2D* image2d = 0;
    DesktopResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&image2d);    //獲取紋理2D
    ID3D11Texture2D* dxgi_text2的;                  //創建一個新的2D紋理對象,用於把 image2d的數據copy進去
    d11dev->CreateTexture2D(&frameDescriptor, NULL, &dxgi_text2d);
    d11ctx->CopyResource( dxgi_text2d, image2d); ///獲取整個幀的數據
    IDXGISurface* dxgi_surf;          // 獲取這個dxgi_text2d的表面
    dxgi_text2d->QueryInterface(__uuidof(IDXGISurface), (void**)&dxgi_surf); ///

    DXGI_MAPPED_RECT mappedRect;
    dxgi_surf->Map(&mappedRect, DXGI_MAP_READ);  /////映射鎖定表面,從而獲取表面的數據地址
    這個時候 mappedRect.pBits 指向的內存就是原始的圖像數據,因爲DXGI固定爲 32位深度色,
    因此 mappedRect.pBits 指向的就是 BGRA 元數組。
      
    通過判斷 FrameInfo裏邊的 TotalMetadataBufferSize 確定圖像有沒發生局部變化,
    然後調用 GetFrameDirtyRects等函數獲取髒矩形區域。

    然而我在實際使用中,發現不太好用,估計是沒用對,
    在win8上使用 GetFrameDirtyRects 正常,到win10 使用GetFrameDirtyRects獲取的髒矩形區域,遠端顯示的結果是亂七八糟的。
    因此最終還是隻獲取整個一幀屏幕數據之後,
    採用前後緩存對比辦法獲取變化的矩形區域,用小塊128X8矩形塊對比掃描的辦法獲取髒矩形區域。
 
下篇繼續講解MirrorDriver驅動開發,
當初爲了興趣研究了MirrorDriver驅動,結果爲了測試MirrorDriver驅動,需要實現一個簡單的遠程桌面,
當實現遠程桌面之後,發現使用H264編碼壓縮圖像,能達到一個非常理想的效果,比起經常使用的VNC更流暢,還能流暢看視頻。
因此決定繼續遠程桌面開發,把windows屏幕的抓取辦法擴展到 DXGI, GDI,MirrorDriver三種辦法,互補長短。
記得這也是大學剛畢業時候,非常熱衷的事情,記得當時使用 GDI函數抓屏的辦法實現了一個粗淺的遠程桌面控制程序,
當時電腦配置不夠,當時的熱情高漲歸熱情高漲,畢竟技術水平還不夠,因此傳輸的圖像效果很差而且很佔網絡帶寬。


下篇相關文章地址:

http://blog.csdn.net/fanxiushu/article/details/76039801

抓屏代碼下載地址:

http://download.csdn.net/detail/fanxiushu/9910360


敬請關注稍後公佈到CSDN上的DXGI,GID,Mirrordriver抓屏代碼, 最後來張圖片,
這個是在windows抓取到屏幕數據之後,
簡單的壓縮成jpeg圖片,通過 HTTP協議頭的multipart/x-mixed-replace ,實現連續不斷的傳遞JPEG圖片,
然後在手機的瀏覽器中打開連接就看到遠程桌面效果了。
這是最快最簡單實現直播流效果了,當然也很簡陋佔用帶寬也高,是MJPG攝像頭網絡傳輸的一種方式。
稍後公佈的接口代碼中簡單的實現了這部分功能。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章