勢(shì))
01-采集后端選型DXGI與GDI的7倍差距和300倍靜息優(yōu)勢(shì)作者黒漂技術(shù)佬遠(yuǎn)程桌面這套東西說白了就三件事把這邊屏幕拍下來、壓縮了傳過去、那邊再畫出來??粗狡綗o奇可光是第一步怎么把屏幕拍下來在 Windows 上就藏著一條分水嶺。這一步選錯(cuò)后面編碼再怎么優(yōu)化都救不回來——因?yàn)椴杉旧硎钦麠l鏈路里最硬的一筆固定開銷。本篇我們就專門聊采集后端為什么項(xiàng)目最終選了 DXGI 桌面復(fù)制當(dāng)主力又為什么必須留著 GDI 當(dāng)備胎以及那個(gè)看起來很玄乎的靜止時(shí)快 300 倍到底是怎么來的。一、先弄清楚屏幕采集到底在采什么新手容易誤以為遠(yuǎn)程桌面是每隔幾毫秒截一次全屏圖。這話不算錯(cuò)但漏了個(gè)關(guān)鍵細(xì)節(jié)Windows 根本不提供把當(dāng)前桌面畫到一張圖里這種現(xiàn)成接口。你得自己繞到顯卡/系統(tǒng)的底層去拿。而繞的路有兩條走的完全是兩套機(jī)制DXGI Desktop Duplication(桌面復(fù)制)這是 Windows 8 之后系統(tǒng)基于顯卡驅(qū)動(dòng)直接給你的能力。你可以理解成系統(tǒng)自己也在記著桌面的每一幀變化你只要來領(lǐng)就行。GDI 抓取走的是更古老、更通用的圖形設(shè)備接口。相當(dāng)于你用一套兼容性最好但最笨的辦法讓系統(tǒng)把桌面重新畫一遍給你。項(xiàng)目里這兩位分別對(duì)應(yīng)dxcam(封裝了 DXGI)和mss(封裝了 GDI)。名字不重要重要的是它們背后的原理差異。二、DXGI 桌面復(fù)制不是截圖是領(lǐng)變化DXGI 桌面復(fù)制的思路很聰明。Windows 的桌面合成器(DWM)本身每時(shí)每刻都在把各個(gè)窗口合成為最終畫面它天然就知道屏幕上一幀和上一幀哪里變了。桌面復(fù)制 API 就是把這個(gè)能力開放給你你拿走當(dāng)前這一幀而且系統(tǒng)很明確地告訴你這一幀有沒有新內(nèi)容。它的好處是不經(jīng)過 CPU 重繪。直接從顯卡的合成結(jié)果里拿幾乎零拷貝。它知道沒變化。當(dāng)桌面靜止時(shí)系統(tǒng)壓根沒有新東西產(chǎn)生于是這個(gè) API 會(huì)直接告訴你哥們兒沒新幀別等了。這兩點(diǎn)決定了它的性能特征變的時(shí)候快不變的時(shí)候更快。我們后面用數(shù)據(jù)說話。三、GDI 抓取最穩(wěn)的老實(shí)人GDI 那一套是上世紀(jì)就有的老接口幾乎所有 Windows 程序都能用兼容性拉滿。它的工作方式樸素得多你申請(qǐng)一張圖系統(tǒng)用 GDI 把桌面畫到這張圖上返回給你。問題也正出在這畫字上它每次都要走一遍合成不管屏幕變沒變。它不知道也沒興趣知道有沒有變化你問它要它就畫一張給你。所以 GDI 的開銷是恒定的——你每秒問 60 次它就老老實(shí)實(shí)畫 60 次哪怕屏幕上啥都沒動(dòng)。四、實(shí)測(cè)對(duì)照7 倍與 300 倍是怎么來的項(xiàng)目在 Windows 11 上做過一輪實(shí)打?qū)嵉牟杉阅茯?yàn)證(雙顯示器、主屏 2560×1440)。結(jié)果非常刺眼后端有新幀時(shí)(畫面在動(dòng))畫面靜止時(shí)dxcam(DXGI)? 首選4.65 ms0.11 msmss(GDI) 回落33 ms33 ms(無論變沒變都要付)把這兩個(gè)數(shù)擺一塊畫面在動(dòng)時(shí):4.65 ms 對(duì) 33 ms,dxcam 快約7 倍。畫面靜止時(shí):0.11 ms 對(duì) 33 ms差距拉到約300 倍。一倍是快七倍是爽三百倍……那已經(jīng)不是同一個(gè)量級(jí)的物種了。五、為什么靜止時(shí)差距能拉到 300 倍關(guān)鍵就在前面說的那句話DXGI知道沒變化。項(xiàng)目源碼里dxcam 的抓幀函數(shù)長(zhǎng)這樣(示意):defgrab(self,force:boolFalse)-Optional[np.ndarray]:# new_frame_onlynot force:# forceFalse 時(shí)只領(lǐng)新產(chǎn)生的幀# 畫面沒變化 - 系統(tǒng)壓根沒新幀 - 直接返回 Nonereturnself._cam.grab(new_frame_onlynotforce)也就是說當(dāng)你盯著屏幕發(fā)呆、文檔半天沒動(dòng)時(shí)dxcam 去系統(tǒng)領(lǐng)幀系統(tǒng)說沒有新東西它立刻返回None——整個(gè)過程的代價(jià)只有 0.11 ms因?yàn)楦緵]做實(shí)質(zhì)性的拷貝和搬運(yùn)。而 mss 不管這些你grab()一次它就老老實(shí)實(shí)把桌面重畫一遍塞給你雷打不動(dòng) 33 ms。為什么靜止對(duì)遠(yuǎn)程桌面至關(guān)重要因?yàn)檎鎸?shí)辦公場(chǎng)景里屏幕絕大部分時(shí)間是靜止的。你看文檔、想思路、讀代碼可能好幾秒畫面都不帶動(dòng)一下。在這種大部分靜止、偶爾更新的負(fù)載下DXGI 的 300 倍優(yōu)勢(shì)會(huì)被真實(shí)流量放大成肉眼可見的體驗(yàn)差異采集線程幾乎不占 CPU整臺(tái)被控機(jī)安安靜靜只有你真的動(dòng)了一下鼠標(biāo)、敲了一個(gè)字它才醒來工作。六、DXGI 的兩個(gè)硬限制逼出 mss 回落既然 DXGI 這么香能不能只用它、把 GDI 刪了不能。因?yàn)?DXGI 桌面復(fù)制有兩個(gè)系統(tǒng)級(jí)的硬限制項(xiàng)目源碼的注釋里寫得很直白每個(gè)顯示器只允許有一個(gè)復(fù)制器。也就是說同一塊屏幕誰先占了桌面復(fù)制這個(gè)能力別人就用不了。如果你開著 OBS、錄屏軟件、某些截圖工具甚至 Xbox Game Bar它們可能已經(jīng)在用這個(gè)復(fù)制器了你的程序再去初始化就會(huì)失敗。RDP 遠(yuǎn)程會(huì)話下不支持。如果你的被控機(jī)本身是通過 Windows 遠(yuǎn)程桌面(RDP)連上去的在那種會(huì)話里桌面復(fù)制 API 直接不可用。這兩個(gè)限制意味著DXGI 不是總能成功初始化。你沒法賭用戶的環(huán)境永遠(yuǎn)干凈所以項(xiàng)目必須把 mss(GDI) 當(dāng)備胎留著——dxcam 初始化失敗自動(dòng)回落 mss功能不受影響只是采集慢約 7 倍。這正是源碼里make_capturer的邏輯backend取auto時(shí)先試 dxcam拋異常就 log 一句dxcam 不可用回落 mss然后建 mss 采集器。用戶幾乎無感頂多看到啟動(dòng)日志里多一行提示。七、拒絕訪問(0x80070005)到底是什么意思dxcam 初始化失敗時(shí)最常見的錯(cuò)誤碼是_E_ACCESSDENIED -2147024891也就是十六進(jìn)制的0x80070005——經(jīng)典的拒絕訪問。光拋一個(gè)數(shù)字給用戶等于沒說。項(xiàng)目源碼專門寫了個(gè)_dxgi_hint函數(shù)把這個(gè)冰冷的 HRESULT 翻譯成人話原因通常是該顯示器已被別的程序占用桌面復(fù)制器(Windows 限制每個(gè)顯示器同時(shí)只能有一個(gè))。常見占用者OBS、錄屏/截圖工具、其他遠(yuǎn)程桌面客戶端、Xbox Game Bar。另一個(gè)可能會(huì)話處于鎖定狀態(tài)或顯示器已關(guān)閉。排查建議關(guān)掉上述程序后重試也可以先跑自檢看是否恢復(fù)。最關(guān)鍵的一句不影響使用會(huì)自動(dòng)回落到 mss/GDI只是采集慢約 7 倍。這點(diǎn)很關(guān)鍵。新手看到拒絕訪問四個(gè)字容易慌以為程序掛了。其實(shí)它只是換了個(gè)更慢但穩(wěn)的采集方式繼續(xù)干活。把原因和后果講清楚焦慮就消了一大半。八、processor_backend“numpy”繞開 opencv 的小心思dxcam 創(chuàng)建時(shí)有一行很容易被忽略的參數(shù)self._camdxcam.create(output_idxoutput_idx,output_colorRGB,processor_backendnumpy,# 關(guān)鍵:繞開對(duì) opencv(cv2) 的依賴)processor_backend控制 dxcam 內(nèi)部怎么處理拿到的畫面幀。默認(rèn)情況下它可能依賴 opencv(cv2) 來做后處理。但項(xiàng)目用的是numpy后端——直接拿 numpy 數(shù)組完全不碰 opencv。為什么要繞開因?yàn)樯僖粋€(gè)依賴就少一堆麻煩打包體積更小依賴樹更干凈。不會(huì)出現(xiàn)運(yùn)行時(shí)找不到 cv2這類只在打包后才暴露的靈異問題(這個(gè)項(xiàng)目在 PyInstaller 打包時(shí)確實(shí)踩過 dxcam 編譯擴(kuò)展的坑后面工程化那篇會(huì)細(xì)講)。對(duì)采集功能本身沒影響輸出同樣是規(guī)整的 RGB numpy 數(shù)組后面差分編碼照常進(jìn)行。這是一個(gè)提前規(guī)避風(fēng)險(xiǎn)的典型取舍。九、force 抓幀新 Viewer 進(jìn)房必須有首屏最后說一個(gè)容易被忽略、但少了它會(huì)出大問題的設(shè)計(jì)抓幀函數(shù)支持forceTrue。rgbcap.grab(forcewant_keyframe)回顧一下畫面完全靜止時(shí)dxcam 永遠(yuǎn)返回None。這在前面的300 倍優(yōu)勢(shì)里是好事。但它會(huì)帶來一個(gè)副作用——設(shè)想這樣一個(gè)場(chǎng)景你的 Viewer(控制端)剛連上來想立刻看到公司電腦現(xiàn)在的畫面??善丝坦灸桥_(tái)電腦桌面是靜止的。如果按沒新幀就返回 None的默認(rèn)邏輯dxcam 會(huì)一直返回None,Viewer 永遠(yuǎn)等不到第一幀屏幕一片黑直到你動(dòng)一下鼠標(biāo)為止。這顯然不能接受。新連上的 Viewer 必須立刻看到當(dāng)前畫面(首屏)。所以源碼的套路是當(dāng)需要關(guān)鍵幀(首幀、或 Viewer 主動(dòng)請(qǐng)求、或尺寸變化)時(shí)把force設(shè)成True。此時(shí) dxcam 的new_frame_only被設(shè)為False它會(huì)回退到上次緩存的那一幀返回給你——哪怕畫面沒變化也能拿到一張當(dāng)前畫面發(fā)出去當(dāng)作首屏關(guān)鍵幀。一句話總結(jié)這個(gè)機(jī)制靜止時(shí) dxcam 不主動(dòng)給幀是為了省但該給的時(shí)候(新連接進(jìn)場(chǎng))必須強(qiáng)制給一張否則對(duì)方看不到東西。這個(gè)force開關(guān)就是省和必須給之間那個(gè)閥門。十、采集在整條鏈路里到底占多少前面給的 4.65 ms / 33 ms 是抓一幀的純開銷有人會(huì)問整條鏈路一幀才 29.7 ms采集占這點(diǎn)是不是無關(guān)緊要恰恰相反把這兩個(gè)數(shù)擺進(jìn)整幀預(yù)算里看結(jié)論很有意思DXGI 路徑抓幀 4.65 ms占 29.7 ms 整幀預(yù)算的約16%。剩下 25 ms 留給差分、atlas、JPEG綽綽有余20 fps 輕松達(dá)標(biāo)。GDI 回落路徑抓幀 33 ms已經(jīng)超過整幀 29.7 ms 的預(yù)算了——也就是說一旦回落到 mss光抓幀就把幀率天花板按死在 30 fps 以下再怎么優(yōu)化編碼都補(bǔ)不回來。這正是采集是整條鏈路最硬的固定開銷那句話的含義它慢后面全慢它快后面才有騰挪空間。所以把采集后端選對(duì)不是錦上添花是決定整條鏈路能不能跑起來的地基。也解釋了為什么項(xiàng)目寧愿為 DXGI 的兼容性坑(占用沖突、RDP 不支持)專門寫回落邏輯也不干脆只用 GDI 圖省事——用 GDI 的話這項(xiàng)目的幀率目標(biāo)從根上就達(dá)不到。還有一個(gè)容易混的點(diǎn)靜止時(shí)的 0.11 ms 不算進(jìn)變動(dòng)幀預(yù)算它發(fā)生在沒新幀的空輪詢里靠LOOP_SLEEP_IDLE 0.002這種小步進(jìn)輪詢控制既不空轉(zhuǎn)燒 CPU又不拖慢真正的變動(dòng)幀。所以 DXGI 的 300 倍優(yōu)勢(shì)省的是安靜時(shí)的每一秒而不是變動(dòng)時(shí)的那一幀。十一、小結(jié)采集后端這道選擇題結(jié)論其實(shí)很樸素主力用 DXGI(dxcam)變動(dòng)時(shí)快 7 倍、靜止時(shí)快 300 倍辦公場(chǎng)景幾乎零負(fù)擔(dān)。備胎留 GDI(mss)因?yàn)?DXGI 有每顯示器單復(fù)制器和RDP 不支持兩個(gè)硬限制環(huán)境不干凈就得回落功能不能斷但回落后抓幀 33 ms 會(huì)吃光幀率預(yù)算所以它是兜底而非常態(tài)。錯(cuò)誤要翻譯成人話:0x80070005不是災(zāi)難告訴用戶只是慢點(diǎn)即可。少依賴是福:processor_backendnumpy繞開 opencv。首屏不能等:force抓幀保證新連上的 Viewer 立刻有畫面看。采集是地基它占整幀預(yù)算 16%(DXGI)或超 100%(GDI)選錯(cuò)后端后面再怎么優(yōu)化都救不回幀率。