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推流的摄像头图像。
图像很清晰吧,而且还是晚上,采光也很差。

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