Windows遠程桌面實現之十 - 把xdisp_virt項目移植到iOS,macOS,linux平臺(一)

by fanxiushu 2019-12-06 轉載或引用請註明原始作者。

xdisp_virt項目到目前爲止,持續了兩年多時間,幾乎都是在windows平臺下的實現各種功能,
因爲持續時間比較長,能想到的功能都給添加到xdisp_virt中了,
尤其在windows截屏這部分,爲了更好的截取windows桌面屏幕數據,能想到的都想辦法實現了。
爲了支持全屏3D遊戲,添加了DXHOOK動態庫,爲了更好的控制鼠標鍵盤,添加了虛擬HID鼠標鍵盤驅動。
爲了實現擴展顯示器效果,也想法實現了Indirect Display驅動。
在對圖像編碼壓縮方面,也是變着法的利用各種編碼算法,什麼H264,H264硬編碼,H265,MPEG1/2/4, JPEG,VP8/9,
幾乎是現有的通用各種編碼算法都給實現了一個遍。
網絡通訊方面也是,不單可以原生客戶端可以鏈接進控制,還能在WEB頁面上進行遠程控制,通訊也添加了 (SSL) https 加密傳輸。
還專門開發了中轉服務器解決通過公網訪問處於局域網的機器。
各種複雜配置也直接簡單的在WEB頁面上進行配置,不再開發專門的配置界面程序。
而且還實現了 RTMP,RTSP推流,把桌面圖像推流到 直播服務器。
不單截屏桌面數據,還能處理攝像頭數據,還能處理電腦內部聲音,以及麥克風聲音,也能多路音頻混音。
想想這兩年的”戰績“確實有點多。。。
可是還算不夠,最近心血來潮,想把xdisp_virt移植到 iOS,macOS和CentOS平臺。
其實CentOS(linux)平臺的桌面使用者不多,移植到linux沒多大必要,
但是 iOS和macOS使用者還是比較多,尤其是iOS手機。
他們都屬於UNIX類系統,只要移植到一個系統,很容易就能在別的系統上編譯運行,因此這裏也就統一解決了。
這裏單單沒有Android系統,不是我不想移植,只是因爲手上暫時沒有Android系統的設備,沒法弄。

有興趣可以直接到GITHUB上下載xdisp_virt試玩。
https://github.com/fanxiushu/xdisp_virt
(程序並未開放源代碼,這可能會讓人不爽。)

在 文章 “ Windows遠程桌面實現之六(新版本框架更新,以及網頁HTML5音頻採集通訊)”
https://blog.csdn.net/fanxiushu/article/details/81905680
就曾簡單介紹過xdisp_virt的網絡通訊基本框架。
當時只考慮的是windows平臺,採用的最高效的完成端口來作爲底層基礎通訊框架。
這次要移植,首先要解決的就是這個基礎通訊框架。
程序所有通訊都是通過這個底層框架來完成的,比如程序內部集成的簡單Web服務器,鏈接到中轉服務器,
在此框架上實現的HTTPS加密傳輸等等,都是這個底層通訊框架的功勞。
(RTSP,RTMP 除外,這個是ffmpeg內部的通訊協議)
這個框架當時開發的時候,完全採用的是回調函數方式的異步通信的,什麼意思呢?
當接受到指定長度數據包的時候,對應的回調函數會被調用,在這個回調函數中處理接收到的數據。
同樣的,函數發送數據包也是立即返回,到數據包真正發送成功或者失敗,對應的回調函數會被調用。
這種函數方式,在windows平臺下的完成端口模型,是比較容易實現的
(當然,也並不是那麼容易,這個得自己多去實踐,)。
現在要移植到UNIX類平臺下,linux有epoll,但是macos、ios對應的是kqueue,不是epoll,
我可不想在linux實現一套,又在macos,ios中另外實現一套,因此選擇大家都通用的 select/poll 通訊模型,包括windows也能用。
根據select/poll模型的特點, 在 recv接收到數據時候,調用回調函數是容易實現的,但是 send發送數據卻比較麻煩了。
windows平臺完成端口模型中,調用WSASend異步發送數據,函數立即返回,數據發送完成後調用GetStatus函數就能獲取到完成通知。
而select中,首先select來確定某個socket是否可以發送數據,
確定可以發送之後再調用 send發送數據,並且通常設置socket爲非阻塞,防止send時候阻塞。
這與完成端口模型完全就是兩個世界,而現在必須讓select函數模擬這種send async方式:
send_async函數發送數據,並且立即返回 -> 數據發送完成,callback回調函數被調用。
其實這種方式也是可以模擬實現的,給每個socket套接字關聯一個發送隊列,send_async的時候,把數據包投遞到這個發送隊列。
然後採用某種通知機制通知socket有數據包需要發送,這時候send_async立即返回。
現在關鍵是如何通知這個socket,讓它立即開發送數據。
根據select模型, select 的線程中往往有許多socket在 select中等待中,每個socket通常是隻加到select的讀事件中,
只有當有數據需要寫的時候,才把socket添加到寫事件中,否則每個socket添加select寫事件,會造成select立即返回,白白浪費CPU。
假設添加到select中的所有socket 套接字都沒有數據接收到,但是上面的send_async投遞了一個數據包給某個socket。
因爲select沒有返回,這個數據包不能及時發送出去。這個時候,就必須採用某個辦法,在send_async投遞數據包之後,讓select立即甦醒。
這樣才能讓這個數據包能立即被髮送出去。
在這裏也沒找到更好更通用的辦法,於是就給每個select線程創建一個UDP套接字,並且綁定到127.0.0.1,
select也把這個UDP套接字添加到讀事件中, send_async在把數據包投遞到發送隊列之後,給這個UDP發送一個數據包。
這樣 select就能立即甦醒。 這也是沒辦法中想到的一種辦法。
不過總算是成功模擬了 異步發送行爲。

在實現了跨平臺的異步底層通訊框架之後,應該可以大刀闊斧的移植代碼了。
可是開始之前,還有個麻煩要解決,那就是各種開源庫的編譯問題。
xdisp_virt使用了非常多的開源庫,大致算下來,有十多種。大致:
ffmpeg, fdk-aac,x264, openh264, x265,turbojpeg,libyuv,unqlite,openssl,libvpx,jbig2,lzma,uuid
這些開源庫在 linux平臺和macOS平臺都比較好編譯,
通常都是 直接運行 configure, 然後make就可以了,使用cmake也差不多類似,按照每個開源庫的編譯說明,
這樣直接使用系統自帶的gcc編譯就可以了。
比較麻煩的是 iOS的交叉編譯。
但是熟悉嵌入式開發環境,以及經常編譯這些嵌入式系統的人來說,也不算是個大麻煩。
編譯這些開源庫到iOS系統,只要不是非必要,不要使用Xcode開發環境來編譯,而是直接採用命令行方式。
macOS系統中,C,C++,ObjC的編譯器早就已經替換成 clang,
我們在終端敲入gcc也能運行,其實macos系把gcc作爲 clang的一個軟連接而已。
編譯iOS的開源庫,其實也是使用同樣的clang,只是編譯的時候,使用的編譯參數不同而已。
這個與linux以及對應的linux嵌入式平臺不同(Android也可以理解成其中一個linux嵌入式系統),
這些嵌入式系統往往需要下載自己的編譯程序。
畢竟 macOS和iOS都是Apple自己的產品,完全可以把這些功能集成到一起。

現在我們就來處理這些參數問題。
網上也有現成的編譯ffmpeg,x264這些庫的編譯腳本,如果只是其中一兩個,使用腳本到無所謂,這裏的開源庫太多。
如果每個都去找腳本編譯,那得累暈,再說有些開源庫還沒編譯腳本下載,而且編譯腳本往往把編譯搞複雜了。
clang兩個參數非常重要, -arch 直到編譯成什麼CPU芯片,因爲現在iPhone手機基本都是arm64的,所以直接指定 -arch arm64 就可以。
還有一個 就是 -isysroot 這個是 編譯的iOS 環境的SDK庫的路徑。 這個路徑通常固定爲:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS{版本號}.sdk
基本上,只要指定這兩個參數,使用clang編譯的程序或者dylib庫,就能在iOS上運行。
編譯一些開源庫,還得設置一些環境變量,最常用的就是 CC,CPP,CXXCPP,CFLAGS, CXXFLAGS,CPPFLAGS
這些環境變量的具體意思可以查閱網上的介紹。
在macOS系統中,打開terminal終端,
比如導出CC環境變量:
export CC="xcrun -sdk iphoneos clang"     //導出 C編譯器
export CPP = "xcrun -sdk iphoneos clang -E" 導出 C預編譯器
export CFLAGS="-arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS{版本號}.sdk     其他編譯參數"  // 導出 C編譯參數
.....
這樣,預備工作就做好了,然後就是查看每個開源庫對應的編譯說明,這裏以ffmpeg爲例:
./configure \
--enable-static \
--enable-cross-compile \
--cc="xcrun -sdk iphoneos clang" \
--arch="arm64" \
--target-os=darwin
...其他參數

這樣生成之後,然後直接 make,就能編譯出iOS的ffmpeg靜態庫。其他開源庫的編譯這裏不再羅嗦,都差不多。
是不是感覺比起 腳本方便多了,如果需要在iPhone模擬器中運行,對應的 arch 爲 x86_64, 再把arm64和x86 用lipo打包在一起。
因爲我直接使用真機調試開發,所以懶得再去折騰 x86_64的模擬器編譯。

編譯完全部的開源庫,就可以開始移植xdisp_virt項目了,
xdisp_virt項目裏邊的源代碼較多,還好當初開發的時候,雖然只想到了windows平臺,但是使用的C、C++代碼幾乎都是跨平臺的,
很少使用windows的自己框架,比如MFC,ATL在xdisp_virt項目裏找不到影子。
即使跟平臺相關的桌面圖像採集,攝像頭採集,音頻採集。最終都實現成一個一個的標準C接口函數。

比如音頻採集就簡單濃縮成如下三個函數:

void* audio_capture_create(int is_capture_self, int audio_capture_index, audio_format_t* fmt, int* p_sleep_msec);
//創建音頻採集接口, 其中audio_capture_index指定使用哪個音頻設備,規定 0 表示採集電腦內部聲音,從1開始是採集話筒聲音。

void audio_capture_destroy(void* handle); //銷燬採集接口

int audio_capture_capture(void* handle, unsigned char* buffer, int length); // 獲取音頻數據,

簡單的說,在移植的時候,在macos,iOS,或者linux等平臺,只要先寫上面三個佔位函數,有關音頻編碼,傳輸等都能編譯。
之後把全部代碼編譯成功之後,再來具體實現跟這些平臺相關的採集函數。

採用這種思路,很快就把macOS和linux系統平臺下跟跟數據採集不相關的代碼全部編譯成功,留下20多個佔位函數,
需要實現桌面採集,鼠標鍵盤模擬,攝像頭採集,聲音採集等這些跟具體系統相關的函數。
而且爲了驗證整個xdisp_virt工程除是否正常運行,在這些採集函數中隨機模擬一些數據,看看運行起來是否正常。
目前基本都正常,還差實現這些具體的採集函數了。

linux和macOS平臺都可以直接在命令行中編譯,即使以後要編譯跟系統相關的採集函數,我想也能在命令行中編譯。
現在iOS需要創建Xcode工程,否則沒法推送到iPhone上去調試運行。
爲了儘量簡單,在iOS編譯中,我把已經編譯的xdisp_virt導出一個xdisp_virt.dylib動態庫,
並且跟windows,macos,linux一樣的做法,
所有的開源庫都靜態編譯進dylib中,這樣,其實只要在iOS的Xcode工程中集中實現 對應的採集函數就可以了。

xdisp_virt.dylib導出
void* xdisp_virt_start(const char* conf_path, struct unix_capture_funcs_addr* addr); 函數,

struct unix_capture_funcs_addr結構就是全部的採集函數地址,
struct unix_capture_funcs_addr
{
    ///
    int(*fn_get_unix_screen_size)(int* cx, int *cy, int* bitcount);
    unsigned char* (*fn_get_unix_screen_data)(int cx, int cy, int bitcount);
    int(*fn_get_cursor_pos)(int* x, int* y);
   
    ////audio capture
    void* (*fn_audio_capture_create)(int is_capture_self, int audio_capture_index, struct audio_format_t* fmt, int* p_sleep_msec);
    void(*fn_audio_capture_destroy)(void* handle);
    int(*fn_audio_capture_capture)(void* handle, unsigned char* buffer, int length);

    。。。。。
};

在macOS和iOS系統中,系統的採集函數基本都是 obj-c 的,而我們的xdisp_virt項目基本都是  C/C++的,
爲了 又要使用c++,又要使用obj-c,直接使用 mm 後綴的文件名,實現 c,c++,objc混編,這也是感覺挺好玩的。
在Android中,要使用c++,還得實現java對應的jni接口,麻煩的要命。

文章未完待續,以後注意講述如何在macos,ios,linux系統中實現跟採集相關的內容。

下圖是移植xdisp_virt之後,首先在iOS系統實現的攝像頭採集效果,
其實當時心血來潮,一個重要原因是發現iphone11Pro的浴霸攝像頭拍照太清晰了,而電腦中的攝像頭差的太遠了。
本來是打算單獨實現iPhone實現攝像頭採集,後來想還是打算乾脆移植 整個xdisp_virt工程好了。

圖中,chrome瀏覽器顯示的網頁方式展現的攝像頭圖像,VLC播放器播放的是xdisp_virt 的 RTMP推流的攝像頭圖像。
圖像很清晰吧,而且還是晚上,採光也很差。

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