那开篇就问问为什么需要研究这个源码吧:
在移动互联网的时代下,手机的功能是日益增加的,要使工作变得更加的高效,那么键盘鼠标其实是必不可少的。在许多软件的架构中,其实并没有提供对应的桌面版本,也不兼容基于x86架构的Android模拟器,按照这样下来,那我们就只能使用投屏工具了。scrcpy就是众多投屏软件中最具特色的一款,作为一款开源的软件,它拥有极佳的性能和丰富的功能,但是这款软件最气的地方在于他不可以输入中文!!看看能不能通过对源码的学习来改善这个问题吧。
Ubuntu20.04
源码地址:GitHub - Genymobile/scrcpy: Display and control your Android device
scrcpy源码版本v1.23
下载scrcpy源码
git clone https://github.com/Genymobile/scrcpy
sudo apt install python3-pip
pip3 install meson
sudo apt install openjdk-11-jdk
export PATH="JAVAHOME/bin:PATH"
sudo apt install ninja-build
sudo apt-get install libsdl2-2.0
sudo apt-get install libsdl2-dev
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev
sudo apt install ffmpeg libsdl2-2.0-0 adb
sudo snap install gradle
如果上一步安装后会出现这样的错:
error: This revision of snap "gradle" was published using classic confinement and thus may perform arbitrary system changes outside of the security sandbox that snaps are usually confined to, which may put your system at risk.
If you understand and want to proceed repeat the command including
那么指令改为:
sudo snap install gradle --classic
scrcpy相对于其他仅依靠adb shell screencap和adb shell input进行设备控制的软件,拥有更加优秀的性能,得益于他的系统架构:
Client——Socket——Server
其中的Server在每次启动scrcpy的时候运行于Android端,使得MediaCodec的API(通过硬件加速解码和编码,为芯片厂商和应用开发者搭建了一个统一接口)对采集到的画面进行编码,并使用多线程,通过socket传输到PC。PC端则使用FFmpeg和SDL2对画面进行实时解码显示。其中Server使用Java开发,Client使用C开发。
它为什么可以做到执行scrcpy命令,在较短的时间内就立马获取到了安卓设备的屏幕的?而且他还不需要向设备申请任何的获取屏幕权限,并且还可以对设备进行较低延迟的控制。回到正常的使用adb访问屏幕,当我们需要PC端调试安卓设备时,我们需要输入:
adb shell /system/bin/screencap -p
就可以直接截取手机屏幕,去掉这个-p这个开关,更改成>,就可以直接截图并重定向到电脑本地,包括使用screenrecorder命令对手机进行录屏。
以上的操作明明都是会用到截取手机屏幕权限的,但是scrcpy是如何做到没有向用户申请就能获取到屏幕?
在scrcpy启动时,将自身sdk中的一个jar上传到了安卓设备上,这个jar并不是java的.class文件,是class Java字节码通过dx工具转换成了dex文件,所以这个jar解压后就有一个dex,这个是安卓上的字节码,是可以直接运行的。
在push这个jar时,安卓设备的app_process会直接启动这个jar。这样就会输出一些参数给Server类的main函数进行接受,main函数接受到参数后会开启两个socket等待客户端来链接本设备,一个是视频流的socket,一个是设备控制的socket。
是由于adb提供了端口转发的功能,能转发设备本地的端口到pc端,pc端就能根据这个转发的端口进行链接并收发数据。
adb forward tcp:5555 localabstract:scrcpy
上面那句话实现的是,将PC上所有的5555端口通信数据将被重定向到手机端UNIX类型localabstract上。
总结其主要步骤如下:
通过adb push一个scrcpy-server.jar到手机上。(看起来像个zip,实际上人家是apk)
PC端通过adb reverse反向代理手机端口,用来接收手机端发送过来的数据。
adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process /com.genymobile.scrcpy.Server com.genymobile.scrcpy.Server 0 8000000 false - false
使用app_process运行scrcpy-server.jar的代码。
开启LocalSocket和PC链接,相应PC端传递过来操作。
源源不断的将屏幕画面输出到PC,使得Mediacodec编码。PC通过FFmpeg解码播放。
使用adb来提高scrcpy-server.jar的运行权限。
文档所在:scrcpy/DEVELOP.md at master · Genymobile/scrcpy · GitHub,下面就是关于文档的一些翻译:
这个应用使用两部分组成:
服务端(scrcpy-server):将会在设备中(指手机等移动设备)运行。
客户端(scrcpy binary):将会在主机电脑上运行。负责将服务器推送(adb push)到设备上并开始执行。负责捕获相关的键盘和鼠标事件,并将其传输到服务器,服务器将他们注入设备。
一旦客户端和服务器相连接,服务器首先发送设备信息(设备名称和初始化屏幕发送尺寸),然后就可以开始发送设备屏幕的原始H.264视频流。客户端解码视频帧,并且在没有缓冲的情况下尽快显示它们,以最大限度减少延迟。而且客户端并不知道设备旋转(由服务器处理),它只知道视频帧的尺寸。
关于权限:捕获屏幕要求一些授予给shell的权限。
该服务端是一个Java应用(通过public static void main(String...args)方法),经由Android框架编译后由shell运行在Android设备中。
为了运行这个Java项目,必须对类进行dexed(通常是classes.dex)。如果“my.package.MainClass”是主类,编译成classes.dex,推送到设备中的/data/local/tmp文件夹中,那么可以运行:
adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / my.package.MainClass
路径/data/local/tmp是推送Server的一个很好的候选位置,因为它可以被shell读写,但不是全局可写的,所以恶意应用程序可能不会在Client执行之前替换Server。
比起原始的dex文件,app_process接受包含classes.dex(例如APK)的jar。为了简化并且使用gradle构建系统的优点,Server构建为(无签名的)APK(重命名为scrcpy-server)。
该Server使用了三个线程:
主线程:将视频编码并流式传输到客户端。
控制器线程:用来监听来自客户端的控制消息(通常是键盘和鼠标事件)
接收器线程(由控制器管理):向客户端发送设备信息(目前,它仅用于发送设备剪贴板内容)。
由于视频编码通常是硬件,因此在两个不同的线程中进行编码和流式传输没有任何好处,因此在两个不同的线程中进行编码和流式传输是没有任何好处的。
编码由ScreenEncoder管理。
视频由MeadiaCodec API进行编码。编解码器从与显示器关联的表面获取其输入,并将生成的H.264流写入提供的输出流(连接到客户端的套接字)。
在设备旋转时,编码器、表面和显示器被重新初始化,并产生新的视频流。
只有当表面发生变化时才会产生新的框架。这样也可以让他避免发送不必要的帧,但是也是有缺点的:
如果设备屏幕没有变化,那它不会在启动时发送任何帧,快速运动更改后,最后一帧可能质量较差。这两个问题都由标志KEY_REPEAT_PREVIOUS_FRAME_AFTER来解决。
控制器从客户端接收控制信息(在单独的线程中运行)。有几种类型的输入事件:
键码
文本
鼠标移动、点击
鼠标滚动
其他命令
其中一些需要向系统注入输入事件。为此,他们使用隐藏方法InputManager.injectInputEvent。
客户端依赖于SDL,它为UI、输入事件、线程等提供跨平台API。
视频流由libav(FFmpeg)解码。
启动时,除了libav和SDL初始化外,客户端还必须在设备上推送并启动服务器,并打开两个套接字(一个用于视频流、一个用于控制 )以便他们可以通信。
注意,客户端-服务器角色在应用程序级别表示:
服务器提供视频流并处理来自客户端的请求。
客户端通过服务器控制设备。
但是,在网络级别,角色是相反的:
客户端打开服务器套接字并在启动服务器之前监听端口。
服务器连接到客户端。
这种角色翻转保证了链接不会因为竞争条件而失败,并且避免了轮询。
一旦连接上服务器,服务端会发送设备信息(名称和初始屏幕尺寸)。因此客户端可以在第一帧可用之前初始化窗口和渲染器。
为了最大限度地减少启动时间,SDL在监听来自服务器的连接时进行初始化。
客户端使用4个线程:
主线程:执行SDL事件循环。
流线程:接收视频并用于解码和录制。
控制器线程:向服务器发送控制消息。
接收器线程:(由控制器管理),从服务器接收设备消息。
此外,如果需要,可以启动另一个线程来处理APK安装或文件推送请求(通过在主窗口上拖放)或在控制台中定期打印帧率。
客户端会在单独的线程中从套接字(连接到设备上的服务器)接收视频流。
如果存在解码器(即未设置 --no-display),则它使用
libav解码来自套接字的H.264流,并在新帧可用时通知主线程。
内存中同时有两帧:
解码帧:由解码器从解码器线程写入。
渲染帧:在主线程的纹理中渲染。
当新的解码帧可用时,解码器交换解码和渲染帧(具有适当同步)。因此,当主线程渲染最后一帧时,它立即开始解码新帧。
控制器负责向设备发送控制消息。它在一个单独的线程中运行,以避免在主线程上进行I/O。
在主线程上接收到SDL事件上,输入管理器创建适当的控制消息。它负责将SDL事件转换为Android事件(使用convert)。他将控制消息推送到由控制器持有的队列,在它自己的线程上,控制器从队列中获取消息,将其序列化并发送给客户端。
初始化、输入事件和渲染都在主线程中管理。
事件在事件循环中处理,它更新屏幕或委托给输入管理器。
使用预先构建的服务器
使用预先构建的服务器,不需要依赖系统以及架构,也不需要Android SDK。
下载scrcpy-server-v1.23jar:https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-server-v1.23
将其push到手机上:
adb push scrcpy-server-v1.23 /data/local/tmp
之后执行反向代理手机端口,用来接收手机端发送过来的数据:
adb reverse
使用app_process运行scrcpy-server.jar的代码
adb shell CLASSPATH=/data/local/tmp/scrcpy-server-v1.23 app_process /com.genymobile.scrcpy.Server com.genymobile.scrcpy.Server 0 8000000 false - false
在scrcpy中运行:
meson x --buildtype=release --strip -Db_lto=true -Dprebuilt_server=/path/to/scrcpy-server
ninja -Cx
最后再执行
./run x[options]