那开篇就问问为什么需要研究这个源码吧:
环境说明:
搭建scrcpy编译开发环境:
如果上一步安装后会出现这样的错:
那么指令改为:
0x01 再次认识scrcpyscrcpy相对于其他仅依靠adb shell screencap和adb shell input进行设备控制的软件,拥有更加优秀的性能,得益于他的系统架构: Client——Socket——Server 其中的Server在每次启动scrcpy的时候运行于Android端,使得MediaCodec的API(通过硬件加速解码和编码,为芯片厂商和应用开发者搭建了一个统一接口)对采集到的画面进行编码,并使用多线程,通过socket传输到PC。PC端则使用FFmpeg和SDL2对画面进行实时解码显示。其中Server使用Java开发,Client使用C开发。 scrcpy的启动阶段:它为什么可以做到执行scrcpy命令,在较短的时间内就立马获取到了安卓设备的屏幕的?而且他还不需要向设备申请任何的获取屏幕权限,并且还可以对设备进行较低延迟的控制。回到正常的使用adb访问屏幕,当我们需要PC端调试安卓设备时,我们需要输入:
就可以直接截取手机屏幕,去掉这个-p这个开关,更改成>,就可以直接截图并重定向到电脑本地,包括使用screenrecorder命令对手机进行录屏。 以上的操作明明都是会用到截取手机屏幕权限的,但是scrcpy是如何做到没有向用户申请就能获取到屏幕?
在push这个jar时,安卓设备的app_process会直接启动这个jar。这样就会输出一些参数给Server类的main函数进行接受,main函数接受到参数后会开启两个socket等待客户端来链接本设备,一个是视频流的socket,一个是设备控制的socket。 那为什么这个socket能被pc链接到?是由于adb提供了端口转发的功能,能转发设备本地的端口到pc端,pc端就能根据这个转发的端口进行链接并收发数据。
上面那句话实现的是,将PC上所有的5555端口通信数据将被重定向到手机端UNIX类型localabstract上。 总结其主要步骤如下:
scrcpy-server.jar主要做三件事情:
0x02 让我再看看开发者文档文档所在:scrcpy/DEVELOP.md at master · Genymobile/scrcpy · GitHub,下面就是关于文档的一些翻译: 这个应用使用两部分组成:
一旦客户端和服务器相连接,服务器首先发送设备信息(设备名称和初始化屏幕发送尺寸),然后就可以开始发送设备屏幕的原始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文件夹中,那么可以运行:
路径/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个线程:
此外,如果需要,可以启动另一个线程来处理APK安装或文件推送请求(通过在主窗口上拖放)或在控制台中定期打印帧率。 流客户端会在单独的线程中从套接字(连接到设备上的服务器)接收视频流。 如果存在解码器(即未设置 --no-display),则它使用 libav解码来自套接字的H.264流,并在新帧可用时通知主线程。 内存中同时有两帧:
当新的解码帧可用时,解码器交换解码和渲染帧(具有适当同步)。因此,当主线程渲染最后一帧时,它立即开始解码新帧。 控制器控制器负责向设备发送控制消息。它在一个单独的线程中运行,以避免在主线程上进行I/O。 在主线程上接收到SDL事件上,输入管理器创建适当的控制消息。它负责将SDL事件转换为Android事件(使用convert)。他将控制消息推送到由控制器持有的队列,在它自己的线程上,控制器从队列中获取消息,将其序列化并发送给客户端。 用户界面和事件循环初始化、输入事件和渲染都在主线程中管理。 事件在事件循环中处理,它更新屏幕或委托给输入管理器。 0x03 动手试试
使用预先构建的服务器,不需要依赖系统以及架构,也不需要Android SDK。 下载scrcpy-server-v1.23jar:https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-server-v1.23 将其push到手机上:
之后执行反向代理手机端口,用来接收手机端发送过来的数据:
使用app_process运行scrcpy-server.jar的代码
在scrcpy中运行:
最后再执行
|
|