我开发了一个应用程序来显示和控制连接在USB上的Android设备。 它不需要任何root访问权限。 它适用于GNU / Linux,Windows和Mac OS。
它侧重于:
就像我之前的项目gnirehtet一样 , Genymobile接受了开源: scrcpy 。
您可以构建,安装和运行它。
应用程序在设备上执行服务器。 客户端和服务器通过adb隧道上的套接字进行通信。
服务器流式传输设备屏幕的H.264视频。 客户端解码视频帧并显示它们。
客户端捕获输入(键盘和鼠标)事件,将它们发送到服务器,服务器将它们注入设备。
文档提供了更多详细信息。
在这里,我将详细介绍应用程序可能感兴趣的应用程序的几个技术方面。
编码,传输和解码视频流需要时间。 为了减少延迟,我们必须避免任何额外的延迟。
例如,让我们使用screenrecord
流式传输屏幕并使用VLC播放:
adb exec-out screenrecord --output-format=h264 - | vlc - --demux h264
最初,它可以工作,但很快就会延迟并且帧被破坏。 原因是VLC将PTS与帧相关联,并缓冲流以在某个目标时间播放帧。
因此,它有时会在stderr上打印出这样的错误:
ES_OUT_SET_(GROUP_)PCR is called too late (pts_delay increased to 300 ms)
就在我开始这个项目之前,与WebRTC一起工作的同事Philippe建议我“手动”解码(使用FFmpeg )并渲染帧,以避免任何额外的延迟。 这使我免于浪费时间,这是正确的解决方案。
解码视频流以使用FFmpeg检索单个帧非常简单 。
如果由于任何原因,渲染被延迟,则丢弃解码的帧,以便scrcpy始终显示最后一个解码的帧。
请注意,可以使用配置标志更改此行为:
mesonconf x -Dskip_frames=false
捕获设备屏幕需要一些权限,这些权限授予shell
。
通过从adb shell
调用app_process
,可以在Android上执行Java代码作为adb shell
。
这是一个简单的Java应用程序:
public class HelloWorld { public static void main ( String ... args ) { System . out . println ( "Hello, world!" ); } }
让我们编译并解释它:
javac -source 1.7 -target 1.7 HelloWorld.java "$ANDROID_HOME"/build-tools/27.0.2/dx \ --dex --output classes.dex HelloWorld.class
然后,我们将classes.dex
推送到Android设备:
adb push classes.dex /data/local/tmp/
并执行它:
$ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld Hello, world!
应用程序可以在运行时访问Android框架。
例如,让我们使用android.os.SystemClock
:
import android.os.SystemClock ; public class HelloWorld { public static void main ( String ... args ) { System . out . print ( "Hello," ); SystemClock . sleep ( 1000 ); System . out . println ( " world!" ); } }
我们将我们的类与android.jar
:
javac -source 1.7 -target 1.7 \ -cp "$ANDROID_HOME"/platforms/android-27/android.jar HelloWorld.java
然后像以前一样运行它。
请注意,scrcpy还需要从框架中访问隐藏的方法 。 在这种情况下,链接android.jar
是不够的,所以它使用反射 。
如果classes.dex
嵌入在zip / jar中,则执行也有效:
jar cvf hello.jar classes.dex adb push hello.jar /data/local/tmp/ adb shell CLASSPATH=/data/local/tmp/hello.jar app_process / HelloWorld
你知道一个包含classes.dex
的zip的例子吗? 一个APK !
因此,它适用于任何已安装的APK,其中包含一个带有main方法的类:
$ adb install myapp.apk … $ adb shell pm path my.app.package package:/data/app/my.app.package-1/base.apk $ adb shell CLASSPATH=/data/app/my.app.package-1/base.apk \ app_process / HelloWorld
为了简化构建系统,我决定使用gradle将服务器构建为APK,即使它不是真正的Android应用程序: gradle提供运行测试,检查样式等的任务。
以这种方式调用,服务器被授权捕获设备屏幕。
用户无需在设备上安装任何内容:在启动时,客户端负责在设备上执行服务器。
我们看到我们可以从APK执行服务器的主要方法:
/data/local/tmp
。哪一个选择?
$ time adb install server.apk … real 0m0,963s … $ time adb push server.apk /data/local/tmp/ … real 0m0,022s …
所以我决定推。
请注意, /data/local/tmp
是shell
可读写的,但不是/data/local/tmp
可写的,因此恶意应用程序可能无法在客户端执行之前替换服务器。
如果你执行了Hello,那么世界! 在上一节中,您可能已经注意到运行app_process
需要一些时间: Hello, World!
在一些延迟之前(0.5到1秒之间)不打印。
在客户端中,初始化SDL也需要一些时间。
因此,这些初始化步骤已经并行化 。
使用后,我们要从设备中删除服务器( /data/local/tmp/scrcpy-server.jar
)。
我们可以在退出时将其删除,但之后,它将在设备断开连接时保留。
相反,一旦app_process
打开服务器, scrcpy unlink s( rm
)就可以了。 因此,文件仅存在不到1秒(甚至在显示屏幕之前它也被删除)。
当最后一个关联的打开文件描述符关闭时(最迟,当app_process
死亡时),实际上删除了文件本身(不是它的名字)。
处理从键盘接收的输入比我想象的更复杂。
有两种“键盘”事件:
键事件提供 扫描码 (键盘上键的物理位置)和键码 (取决于键盘布局)。 scrcpy只使用密钥 代码 (它不需要物理密钥的位置)。
但是,关键事件不足以处理文本输入 :
有时可能需要多次按键才能产生角色。 有时一次按键可以产生多个字符。
即使是简单的字符也可能无法通过键事件轻松处理,因为它们取决于布局。 例如,在法语键盘上输入.
(点)生成Shift
+ ;
。
因此, scrcpy仅针对一组有限的密钥将密钥事件转发给设备。 其余的由文本输入事件处理。
在Android方面,我们可能不直接注入文本(注入由相关构造函数创建的KeyEvent
不起作用)。 相反,我们可以使用getEvents(char[])
检索为char[]
生成的KeyEvent
列表。
例如:
char [] chars = { '?' }; KeyEvent [] events = charMap . getEvents ( chars );
在这里,使用4个事件的数组初始化事件:
KEYCODE_SHIFT_LEFT
KEYCODE_SLASH
KEYCODE_SLASH
KEYCODE_SHIFT_LEFT
正确地注入这些事件会产生char '?'
。
不幸的是,以前的方法仅适用于ASCII字符:
char [] chars = { 'é' }; KeyEvent [] events = charMap . getEvents ( chars ); // events is null!!!
我首先想到没有办法从那里注入这样的事件,直到我与Philippe讨论(是的,和之前一样),谁知道解决方案:当我们使用组合变音死键字符分解字符时它起作用。
具体而言,我们注入"\u0301e"
而不是注入"é"
"\u0301e"
:
char [] chars = { '\u0301' , 'e' }; KeyEvent [] events = charMap . getEvents ( chars ); // now, there are events
因此,为了支持重音字符, scrcpy尝试使用KeyComposition
分解字符。
编辑:重音字符不适用于虚拟键盘Gboard(默认的谷歌键盘),但使用默认(AOSP)键盘和SwiftKey。
应用程序窗口可能有一个图标,用于标题栏(对于某些桌面环境)和/或桌面任务栏中。
必须通过SDL_SetWindowIcon
从SDL_Surface
设置窗口图标。 使用图标内容创建表面取决于开发人员。 例如,我们可以决定从PNG文件加载图标,或者直接从内存中的原始像素加载图标。
相反,另一位同事Aurélien建议我使用XPM图像格式,这也是一个有效的C源代码: icon.xpm
。
请注意,图像不是icon_xpm
声明的变量icon_xpm
的内容:它是整个文件! 因此, icon.xpm
既可以在Gimp中直接打开,也可以包含在C源代码中:
#include "icon.xpm"
作为一个好处,我们直接“识别”源代码中的图标,我们可以轻松地对其进行修补:在调试模式下, 图标颜色会发生变化。
开发这个项目是一个令人敬畏和激励的经验。 我学到了很多东西(之前从未使用过SDL或者libav / FFmpeg )。
由此产生的应用程序比我最初预期的更好,我很高兴能够开源它。
讨论reddit和黑客新闻 。
https://blog.rom1v.com/2018/03/introducing-scrcpy/