scrcpy源码阅读及在Ubuntu上的实现(一)——了解原理

2023-05-16

那开篇就问问为什么需要研究这个源码吧:

在移动互联网的时代下,手机的功能是日益增加的,要使工作变得更加的高效,那么键盘鼠标其实是必不可少的。在许多软件的架构中,其实并没有提供对应的桌面版本,也不兼容基于x86架构的Android模拟器,按照这样下来,那我们就只能使用投屏工具了。scrcpy就是众多投屏软件中最具特色的一款,作为一款开源的软件,它拥有极佳的性能和丰富的功能,但是这款软件最气的地方在于他不可以输入中文!!看看能不能通过对源码的学习来改善这个问题吧。

环境说明:

  • Ubuntu20.04

  • 源码地址:GitHub - Genymobile/scrcpy: Display and control your Android device

  • scrcpy源码版本v1.23

搭建scrcpy编译开发环境:

  • 下载scrcpy源码

git clone https://github.com/Genymobile/scrcpy
  • 安装meson 
sudo apt install python3-pip
pip3 install meson
  • 安装Java vm
sudo apt install openjdk-11-jdk
export PATH="JAVAHOME/bin:PATH"
  •  安装ninja
sudo apt install ninja-build
  • 安装SDL2 
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

0x01 再次认识scrcpy

scrcpy相对于其他仅依靠adb shell screencap和adb shell input进行设备控制的软件,拥有更加优秀的性能,得益于他的系统架构:

Client——Socket——Server

其中的Server在每次启动scrcpy的时候运行于Android端,使得MediaCodec的API(通过硬件加速解码和编码,为芯片厂商和应用开发者搭建了一个统一接口)对采集到的画面进行编码,并使用多线程,通过socket传输到PC。PC端则使用FFmpegSDL2对画面进行实时解码显示。其中Server使用Java开发,Client使用C开发。

scrcpy的启动阶段

它为什么可以做到执行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。

那为什么这个socket能被pc链接到?

是由于adb提供了端口转发的功能,能转发设备本地的端口到pc端,pc端就能根据这个转发的端口进行链接并收发数据。 

adb forward tcp:5555 localabstract:scrcpy

上面那句话实现的是,将PC上所有的5555端口通信数据将被重定向到手机端UNIX类型localabstract上。

总结其主要步骤如下:

  1. 通过adb push一个scrcpy-server.jar到手机上。(看起来像个zip,实际上人家是apk)

  2. PC端通过adb reverse反向代理手机端口,用来接收手机端发送过来的数据。

  3. 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的代码。

scrcpy-server.jar主要做三件事情:

  1. 开启LocalSocket和PC链接,相应PC端传递过来操作。

  2. 源源不断的将屏幕画面输出到PC,使得Mediacodec编码。PC通过FFmpeg解码播放。

  3. 使用adb来提高scrcpy-server.jar的运行权限。

 

0x02 让我再看看开发者文档

文档所在: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)解码。

初始化

启动时,除了libavSDL初始化外,客户端还必须在设备上推送并启动服务器,并打开两个套接字(一个用于视频流、一个用于控制 )以便他们可以通信。

注意,客户端-服务器角色在应用程序级别表示:

  • 服务器提供视频流并处理来自客户端的请求。

  • 客户端通过服务器控制设备。

但是,在网络级别,角色是相反的:

  • 客户端打开服务器套接字并在启动服务器之前监听端口。

  • 服务器连接到客户端。

这种角色翻转保证了链接不会因为竞争条件而失败,并且避免了轮询。

一旦连接上服务器,服务端会发送设备信息(名称和初始屏幕尺寸)。因此客户端可以在第一帧可用之前初始化窗口和渲染器。

为了最大限度地减少启动时间,SDL在监听来自服务器的连接时进行初始化。

线程

客户端使用4个线程:

  • 主线程:执行SDL事件循环。

  • 流线程:接收视频并用于解码和录制。

  • 控制器线程:向服务器发送控制消息。

  • 接收器线程:(由控制器管理),从服务器接收设备消息。

此外,如果需要,可以启动另一个线程来处理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到手机上:

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]

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

scrcpy源码阅读及在Ubuntu上的实现(一)——了解原理 的相关文章

随机推荐

  • 支持nvidia GPU 的硬件编解码的ffmpeg编译记要

    支持nvidia GPU 的硬件编解码的ffmpeg编译记要 中间目录 xff1a out 1 x264 下载x264 stable zip unzip x264 stable zip cd x264 stable configure en
  • 软件工程学习笔记——第三周:结构化分析方法-1

    结构化分析方法的概念 结构化分析模型 结构化分析过程
  • GBK编码表

    说明 xff1a 比如第一个 34 顿号 34 的编码就是A1A2 GBK 汉字内码扩展规范 编码表 二 全国信息技术标准化技术委员会 汉字内码扩展规范 GBK Chinese Internal Code Specification 1 0
  • SSH 使用过程中,出现的问题以及解决办法

    1 ssh登陆提示 server unexpectedly closed network connection 在使用ssh登入Linux時 xff0c 卻發生了serverunexpectedly closed network conne
  • CentOS7安装vncserver

    1 关闭防火墙和selinux systemctl stop firewalld service setenforce 0 2 安装图形支持 yum groups install 34 GNOME Desktop 34 或yum group
  • Echarts tooltip加上单位并带着图例颜色

    模仿腾讯疫情地图 xff0c Y轴有个百分比 xff0c 也就是Y轴有单位 xff0c 使用JS代码如下 xff1a tooltip trigger 39 axis 39 formatter function params var relV
  • xv6调试

    窗口1作为xv6的运行窗口 make CPUS 61 1 qemu gdb 窗口2作为gdb调试窗口 gdb multiarch kernel kernel 进入gdb后执行 set confirm off set architecture
  • mit6.s081-21-Lab1/ Xv6 and Unix utilities

    sleep Implement the UNIX program sleep for xv6 your sleep should pause for a user specified number of ticks A tick is a
  • Python+scrcpy+pyminitouch实现自动化(四)——实现语音识别自动打卡机器人

    首先要去网上下载一个想要实现自动化的软件 xff0c 下载对应的apk后拖拉到虚拟器的页面即可实现自动下载 以上是对于AS打开的模拟器进行的下载安装 xff0c 由于我找不到关于x86的企业微信 xff0c 所以我就换了逍遥模拟器 xff0
  • CMU15445 lab1 - BUFFER POOL

    本文为本人完成15445 2020fall B 43 树版本 时的一些记录 xff0c 仅作为备忘录使用 TASK 1 LRU REPLACEMENT POLICY 本任务为实现一个LRU页面置换策略 xff0c 建立一个关于面向磁盘的数据
  • 医院信息管理系统(Python与MySQL数据库的连接与相关增删改查操作)

    题目意义 医院信息管理是一项琐碎 复杂而又十分细致的工作 xff0c 这关系到医院体系能否运行起来这一关乎国民健康水平的重大问题 我们只有利用好了医院中每个医生 护士的各项资源 xff0c 才能使得医院系统能够有序而条理的进行 xff0c
  • 慢速协议-Slow Protocol-LACP

    慢速协议有三种 xff0c 包括802 3ah OAM LACP协议和Marker协议 慢速协议的特点 xff1a 1 xff0c 每秒钟传输的报文不超过10帧 xff1b 2 xff0c 报文不携带vlan tag xff1b 3 xff
  • fork() && fork() || fork()

    include lt unistd h gt include lt stdio h gt int main fork fork amp amp fork fork fork sleep 100 return 0 问题是不算main这个进程自
  • list_entry()详解

    Linux内核中 xff0c 获取节点地址的函数list entry 非常常用 xff0c 由于其定义有点晦涩 xff0c 先解析如下 xff1a list entry的宏定义 xff1a define list entry ptr typ
  • Linux 内核 hlist 详解

    在Linux内核中 xff0c hlist xff08 哈希链表 xff09 使用非常广泛 本文将对其数据结构和核心函数进行分析 和hlist相关的数据结构有两个 xff1a hlist head 和 hlist node hash桶的头结
  • 判断手机号码合法性

    问题描述 xff1a 我国大陆运营商的手机号码标准格式为 xff1a 国家码 43 手机号码 xff0c 例如 xff1a 8613912345678 特点如下 xff1a 1 长度13位 xff1b 2 以86的国家码打头 xff1b 3
  • linux c捕获信号

    linux c捕获信号 在程序中为了实现优雅退出 xff0c 需要对信号进行处理 xff0c 本文主要记录一下两个方面 xff1a 如何捕获SIGINT SIGTERM SIGQUIT等信号 xff0c 并进行处理 如何知道是哪个进程给自己
  • go语言获取发送信号的进程pid

    背景 今天在发布一个程序之前 xff0c 给qa提测的时候 xff0c qa反馈程序运行10几分钟之后 xff0c 退出了 排查过程 在程序中加日志 xff0c 发现程序捕获到了一个SIGTERM信号 xff0c 然后做了一些退出前的清理工
  • ubuntu-E:Encountered a section with no Package: header的解决办法

    刚才打开ubuntu xff0c 我的版本是12 04 正想使用sudo apt get install build essential 时 xff0c 出现了如下错误 xff1a E Encountered a section with
  • scrcpy源码阅读及在Ubuntu上的实现(一)——了解原理

    那开篇就问问为什么需要研究这个源码吧 xff1a 在移动互联网的时代下 xff0c 手机的功能是日益增加的 xff0c 要使工作变得更加的高效 xff0c 那么键盘鼠标其实是必不可少的 在许多软件的架构中 xff0c 其实并没有提供对应的桌