 GPU 加速的實時視頻傳輸)
1. avox 到底想解決什么問題第一次看到 avox 這個名字加上 C、Vulkan、WebRTC、GPU 這幾個關(guān)鍵詞我腦子里第一反應(yīng)是又一個把視頻鏈路從頭到尾自己擼一遍的項目。市面上做視頻采集、編碼、傳輸、渲染的輪子不少但大多數(shù)要么是某個大廠 SDK 的封裝要么是 Python 腳本拼湊出來的玩具真正用 C 從底層圖形 API 一路打通到網(wǎng)絡(luò)傳輸?shù)拈_源項目其實并不多。avox 的定位我理解就是填補這塊空白——用 Vulkan 做 GPU 側(cè)的圖像處理與渲染用 WebRTC 做低延遲的實時傳輸中間用 C 把整條管線串起來。這件事的價值在哪舉個實際場景。你在做一個遠程桌面、云游戲、或者多路攝像頭的實時監(jiān)控系統(tǒng)傳統(tǒng)做法是采集端用某個庫拿到幀CPU 做格式轉(zhuǎn)換和縮放再交給編碼器編碼完通過 socket 發(fā)出去。這條鏈路里 CPU 是瓶頸尤其是多路 1080p 甚至 4K 的時候CPU 占用率直接拉滿幀率還上不去。avox 的思路是把格式轉(zhuǎn)換、縮放、甚至部分濾鏡處理全部丟到 GPU 上用 Vulkan 的 compute shader 或者圖形管線來做CPU 只負責調(diào)度和網(wǎng)絡(luò)收發(fā)。這樣單機跑十幾路高清流才有可能。適合誰來研究這個項目我覺得有三類人。第一類是做音視頻底層開發(fā)的想看看別人怎么把 Vulkan 和 WebRTC 縫合在一起第二類是做 GPU 通用計算的想了解 compute shader 在圖像處理上的實際工程用法第三類是想學(xué) C 工程架構(gòu)的因為這種跨模塊項目對代碼組織能力要求很高。如果你只是想在網(wǎng)頁上播個視頻那用現(xiàn)成的 WebRTC 庫就夠了沒必要碰 avox 這種偏底層的方案。需要提前說明的是avox 目前公開的信息非常有限項目正文和關(guān)鍵詞都是空的所以下面很多內(nèi)容是我基于這類項目的常見工程實踐做的合理推演。我會明確標注哪些是推測、哪些是通用做法你參考的時候心里有數(shù)。2. Vulkan 在這類項目里到底承擔什么角色2.1 為什么不用 OpenGL 而選 Vulkan很多人會問圖像處理用 OpenGL 不是更簡單嗎確實OpenGL 上手快幾行代碼就能畫個三角形。但 avox 這種項目選 Vulkan核心原因是控制力和多線程能力。OpenGL 的狀態(tài)機是全局的多線程下很容易出問題而 Vulkan 的命令緩沖可以每個線程獨立錄制天然適合多路視頻流并行處理。另外 Vulkan 的顯存管理是顯式的你可以精確控制每一幀圖像放在哪塊內(nèi)存、什么時候上傳、什么時候釋放這對實時視頻這種對延遲敏感的場景非常關(guān)鍵。還有一個現(xiàn)實因素Vulkan 在移動端和桌面端都能跑Android、Windows、Linux 一套代碼基本通吃。WebRTC 本身也是跨平臺的兩者結(jié)合能讓 avox 的適用范圍更廣。OpenGL 在移動端雖然也能用但新設(shè)備對 Vulkan 的支持越來越好長遠看 Vulkan 是更穩(wěn)妥的選擇。2.2 圖像格式轉(zhuǎn)換為什么適合放到 GPU視頻鏈路里最耗 CPU 的操作之一就是顏色空間轉(zhuǎn)換比如攝像頭出來的 YUV420 要轉(zhuǎn)成 RGB 才能顯示或進一步處理。YUV420 的特點是亮度分量 Y 是全分辨率的色度分量 U、V 是降采樣的一個 1920x1080 的幀Y 有 200 多萬個采樣點U、V 各只有 50 多萬。CPU 做這個轉(zhuǎn)換要逐像素算數(shù)據(jù)量大、分支多很難向量化到極致。放到 GPU 上就完全不一樣了。一個 compute shader每個線程處理一個像素幾千個線程并行跑轉(zhuǎn)換速度是 CPU 的幾十倍。而且轉(zhuǎn)換完的數(shù)據(jù)直接留在顯存里下一步渲染或者編碼可以直接用省掉了顯存和內(nèi)存之間的來回拷貝。avox 如果要做多路視頻這個優(yōu)化是必須的。2.3 Vulkan 圖像處理的典型管線一個典型的 Vulkan 圖像處理管線大概是這樣先把采集到的幀上傳到一個 staging buffer然后拷貝到 device local 的 image 里接著用 compute shader 做格式轉(zhuǎn)換或縮放結(jié)果寫到另一張 image最后這張 image 要么被渲染管線采樣顯示要么被讀回到 CPU 送去編碼。每一步都涉及內(nèi)存屏障和管線屏障寫錯了就會出現(xiàn)花屏或者數(shù)據(jù)競爭。這里有個容易踩的坑圖像布局轉(zhuǎn)換。Vulkan 里 image 有不同的 layout比如 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL、VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL用錯 layout 輕則性能下降重則直接報驗證層錯誤。我建議開發(fā)階段一定要開 Vulkan validation layer它會把這類問題直接指出來比你自己調(diào)試快得多。3. WebRTC 與 GPU 管線的銜接細節(jié)3.1 WebRTC 在 avox 里的位置WebRTC 負責的是傳輸層它把 GPU 處理完的幀編碼、打包、通過 UDP 發(fā)出去對端收到后再解碼、渲染。avox 用 WebRTC 而不是自己寫 socket 協(xié)議主要是看中它成熟的擁塞控制、丟包重傳、抖動緩沖這些機制。自己實現(xiàn)一套可靠的實時傳輸協(xié)議工作量巨大且容易出問題WebRTC 經(jīng)過這么多年打磨這些細節(jié)都處理得比較好了。但 WebRTC 和 Vulkan 之間有個銜接問題WebRTC 的編碼器通常期望拿到 CPU 內(nèi)存里的幀數(shù)據(jù)而 Vulkan 處理完的幀在顯存里。如果每次都把顯存讀回內(nèi)存那 GPU 加速的意義就打了折扣。所以 avox 這類項目一般會做兩件事一是盡量讓編碼器也支持 GPU 輸入比如用硬件編碼器直接讀顯存二是如果必須回讀就用異步的方式用 fence 或者 event 來同步避免 CPU 空等。3.2 視頻數(shù)據(jù)流的完整路徑把整條鏈路串起來看一幀數(shù)據(jù)從進入到離開大概經(jīng)過這些步驟采集端拿到原始幀通常是 YUV 格式在 CPU 內(nèi)存里上傳到 GPU用 staging buffer 轉(zhuǎn)到 device local imagecompute shader 做格式轉(zhuǎn)換、縮放、可能的降噪或銳化結(jié)果幀要么直接喂給硬件編碼器要么回讀到內(nèi)存給軟件編碼器編碼后的碼流交給 WebRTC 的 RTP 打包模塊通過網(wǎng)絡(luò)發(fā)送對端接收后解碼解碼幀再上傳 GPU用 Vulkan 渲染顯示這條鏈路里第 2 到第 4 步是 avox 用 Vulkan 重點優(yōu)化的部分第 5 到第 6 步是 WebRTC 的領(lǐng)域。兩邊的交界處就是幀數(shù)據(jù)的傳遞這里的設(shè)計好壞直接決定整體延遲。3.3 延遲優(yōu)化的幾個關(guān)鍵點實時視頻最怕延遲。我實測下來幾個地方最容易積攢延遲一是上傳和回讀的同步等待如果用 blocking 的方式CPU 會一直卡在那里二是編碼器的緩沖隊列如果隊列太長幀就會堆積三是 WebRTC 的抖動緩沖設(shè)得太大延遲高設(shè)得太小又容易卡頓。avox 如果要做低延遲我建議上傳和回讀都用異步方式用多個 buffer 輪轉(zhuǎn)讓 GPU 和 CPU 真正并行起來。編碼器那邊盡量用低延遲配置比如關(guān)閉 B 幀、減小 GOP。WebRTC 的 jitter buffer 要根據(jù)網(wǎng)絡(luò)情況動態(tài)調(diào)整不能一刀切。4. C 工程架構(gòu)上的取舍4.1 模塊劃分與接口設(shè)計avox 這種項目代碼組織不好很容易變成一團亂麻。我一般會按職責分成幾個模塊采集模塊、GPU 處理模塊、編碼模塊、傳輸模塊、渲染模塊。每個模塊對外暴露一個干凈的接口內(nèi)部實現(xiàn)隨便換。比如 GPU 處理模塊對外可能就是processFrame(input, output)這樣一個函數(shù)至于里面用 compute shader 還是圖形管線調(diào)用方不關(guān)心。接口設(shè)計上有個經(jīng)驗盡量用不透明句柄或者抽象基類不要把 Vulkan 的 VkImage、VkBuffer 這些類型暴露到模塊外面。否則一旦你想換渲染后端或者想在某個平臺上用別的 API改動量會非常大。C 里可以用 pimpl 慣用法把實現(xiàn)細節(jié)藏起來頭文件只留必要的聲明。4.2 資源管理的坑Vulkan 的資源管理是出了名的繁瑣。每個 VkImage、VkBuffer、VkPipeline 都要手動創(chuàng)建和銷毀還要處理內(nèi)存分配。avox 如果跑多路視頻資源數(shù)量會很多管理不好就會泄漏或者碎片化。我的做法是封裝一層資源池。比如 image 按分辨率和格式分類用的時候從池里取用完還回去而不是每次都創(chuàng)建銷毀。內(nèi)存分配用 VMAVulkan Memory Allocator這個庫它能把顯存分配管理得很好比手寫 VkDeviceMemory 分配省心得多。另外所有 GPU 資源最好用一個 RAII 包裝類管起來析構(gòu)的時候自動釋放避免忘記。4.3 多線程與同步多路視頻天然適合多線程。每路視頻一個線程各自錄制命令緩沖、提交到不同的隊列。但 Vulkan 的隊列提交和同步需要小心多個線程同時往一個隊列提交會需要加鎖影響性能。通常的做法是每路視頻用獨立的隊列或者用一個提交線程統(tǒng)一管理。CPU 和 GPU 之間的同步用 fenceGPU 內(nèi)部各階段之間用 barrier。這里有個常見錯誤barrier 的 srcStageMask 和 dstStageMask 設(shè)得太寬導(dǎo)致不必要的等待。比如你只是從 transfer 階段轉(zhuǎn)到 compute 階段就只寫這兩個階段不要圖省事寫 ALL_COMMANDS那樣會拖慢整體速度。5. 實際開發(fā)中容易踩的坑5.1 Vulkan 驗證層報錯看不懂剛開始寫 Vulkan 的人最頭疼的就是驗證層報錯。一堆 VUID 編號英文描述又長看半天不知道哪錯了。我的經(jīng)驗是先看報錯里提到的對象類型和操作比如是 image layout 問題還是 descriptor 問題然后對照 Vulkan 規(guī)范里對應(yīng)的章節(jié)看。常用的幾個錯誤image layout 不匹配、descriptor set 沒更新、command buffer 在錄制狀態(tài)就被提交。多踩幾次就有感覺了。5.2 WebRTC 編譯與集成WebRTC 的編譯是出了名的麻煩源碼大、依賴多、工具鏈復(fù)雜。如果 avox 要集成 WebRTC建議直接用官方提供的預(yù)編譯庫或者用 gn 構(gòu)建系統(tǒng)按需裁剪。不要試圖把整個 WebRTC 源碼拖進來編譯那樣一次編譯可能幾個小時開發(fā)效率極低。另外 WebRTC 的 API 在不同版本間變化較大鎖定一個穩(wěn)定版本很重要。5.3 GPU 崩潰與設(shè)備丟失GPU 編程繞不開設(shè)備丟失的問題。驅(qū)動崩潰、顯存不足、超時都可能導(dǎo)致 VK_ERROR_DEVICE_LOST。一旦設(shè)備丟失所有 GPU 資源都失效了必須重建。avox 如果做長時間運行的服務(wù)必須處理這種情況檢測到設(shè)備丟失后銷毀所有 Vulkan 對象重新初始化然后恢復(fù)視頻流。這個過程要做好狀態(tài)保存否則用戶會看到畫面中斷。5.4 性能調(diào)優(yōu)的誤區(qū)很多人一上來就追求極致性能把能開的優(yōu)化全開了結(jié)果代碼復(fù)雜度飆升bug 一堆。我的建議是先跑通再優(yōu)化。用 RenderDoc 或者 Nsight 抓幀分析看瓶頸到底在哪。有時候你以為瓶頸在 compute shader實際是上傳帶寬不夠或者同步等待太多。數(shù)據(jù)說話別憑感覺優(yōu)化。6. 從 avox 能學(xué)到什么avox 這類項目的價值不只是它本身能做什么更在于它展示了一種把底層圖形 API 和實時傳輸結(jié)合起來的工程思路。Vulkan 的顯式控制、WebRTC 的成熟傳輸、C 的性能三者結(jié)合能解決很多傳統(tǒng)方案搞不定的場景。如果你在做云游戲、遠程桌面、實時監(jiān)控這類對延遲和并發(fā)有要求的系統(tǒng)這套思路值得借鑒。我在實際折騰類似項目時的體會是最難的不是某個 API 怎么用而是模塊之間的邊界怎么劃、數(shù)據(jù)怎么流轉(zhuǎn)、錯誤怎么處理。這些沒有標準答案只能根據(jù)具體需求權(quán)衡。avox 如果開源得比較完整它的架構(gòu)設(shè)計本身就是很好的學(xué)習材料。后續(xù)如果項目有更多文檔和示例我打算再深入看看它的命令緩沖管理和幀同步策略那部分是最能體現(xiàn)作者功力的地方。