【译】Introducing scrcpy

我开发了一个应用程序来显示和控制连接在USB上的Android设备。 它不需要任何root访问权限。 它适用于GNU / Linux,Windows和Mac OS。

它侧重于:

  • 亮度 (原生,仅显示设备屏幕)
  • 表演 (30~60fps)
  • 质量 (1920×1080或以上)
  • 低延迟 (70~100ms)
  • 启动时间短 (显示第一张图像约1秒)
  • 非侵入性 (设备上没有安装任何东西)

就像我之前的项目gnirehtet一样 , Genymobile接受了开源: scrcpy 。

您可以构建,安装和运行它。

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 

在Android上运行Java main

捕获设备屏幕需要一些权限,这些权限授予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框架。

例如,让我们使用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是不够的,所以它使用反射 。

就像一个APK

如果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 

在scrcpy中

为了简化构建系统,我决定使用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/tmpshell可读写的,但不是/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个事件的数组初始化事件:

  1. KEYCODE_SHIFT_LEFT
  2. KEYCODE_SLASH
  3. 发布KEYCODE_SLASH
  4. 发布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_SetWindowIconSDL_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/

你可能感兴趣的:(Android)