我的编程空间,编程开发者的网络收藏夹
学习永远不晚

【投屏】Scrcpy源码分析四(最终章 - Server篇)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

【投屏】Scrcpy源码分析四(最终章 - Server篇)

Scrcpy源码分析系列
【投屏】Scrcpy源码分析一(编译篇)
【投屏】Scrcpy源码分析二(Client篇-连接阶段)
【投屏】Scrcpy源码分析三(Client篇-投屏阶段)
【投屏】Scrcpy源码分析四(最终章 - Server篇)

在前两篇我们探究了Scrcpy Client的连接和投屏逻辑,本篇我们就要继续探究Server端的逻辑了。

1. 入口函数

我们先来回忆下,还记得Server端是怎么运行起来的么?

答:由Client端执行adb push把Server程序上传到设备侧,然后执行app_process将Server端程序运行起来的。完整的命令是adb -s serial shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25 [PARAMS]

app_process的好处一个是方便我们在安卓侧运行一个纯java程序(是dalvik的字节码,不是jvm字节码),二个是提权,使程序拥有root权限或者shell同等权限。

因为Client指定的类是com.genymobile.scrcpy.Server,所以Server的入口方法就是Server.java类的main()方法,其关键代码是:

// Server.javapublic static void main(String... args) {// 解析参数Options options = createOptions(args);// scrcpy方法scrcpy(options);}private static void scrcpy(Options options) {// 调用DesktopConnection的open函数DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte);// 控制逻辑Controller controller = new Controller(device, connection,);startController(controller);// 投屏逻辑ScreenEncoder screenEncoder = new ScreenEncoder();screenEncoder.streamScreen(device, connection.getVideoFd());}

我们看到,入口函数里主要的逻辑有:

  1. createOptions - 解析参数。

  2. DesktopConnection.open - 连接PC端(第二篇有提到,所以业务上安卓设备是Server,PC是Client,但网络层面安卓设备的Client, PC是Server):

    // DeskopConnection.javaprivate static final String SOCKET_NAME = "scrcpy";public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) {videoSocket = connect(SOCKET_NAME);controlSocket = connect(SOCKET_NAME);return new DesktopConnection(videoSocket, controlSocket);}private static LocalSocket connect(String abstractName) {LocalSocket localSocket = new LocalSocket();localSocket.connect(new LocalSocketAddress(abstractName));return localSocket;}

    因为PC端通过adb用localabstract:scrcpy开启了端口映射,所以这里通过LocalServerSocketLocalSocket指定Unix Socket Name就可以连接上PC了,这里的Unix Socket Name是"scrcpy",必须和adb指定的保持一致。配合PC侧的逻辑,这里需要连接两次,可以得到videoSocket和controlSocket,同时因为这两个是基于Unix Domain Socket的LocalSocket,所以可以直接拿到其对应的文件描述符FileDescription,后续可以直接通过读写文件描述符进行网络数据传输。对这部分不了解的同学可以回顾下第二篇文章Client端这部分的逻辑描述。

  3. startController - 事件控制相关逻辑,基于controlSocket。

  4. streamScreen - 投屏相关逻辑,基于videoSocket。

看到这里,我们应该知道了,在Sever程序起来后就会去连接PC端,拿到两个Socket。
在这里插入图片描述

下面我们继续看下投屏和控制逻辑。

2. 投屏逻辑

投屏逻辑的入口是streamScreen方法:

// ScreenEncoder.javapublic void streamScreen(Device device, FileDescriptor fd) {internalStreamScreen(device, fd);}private void internalStreamScreen(Device device, FileDescriptor fd) {// MediaCodec录屏的模板代码MediaFormat format = createFormat(bitRate, maxFps, codecOptions);MediaCodec codec = createCodec(encoderName);IBinder display = createDisplay();surface = codec.createInputSurface();    setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);    codec.start();    // 编码    encode(codec, fd);}private boolean encode(MediaCodec codec, FileDescriptor fd) {while (!consumeRotationChange() && !eof) {int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);// 写fd即发送给PC侧IO.writeFully(fd, codecBuffer);}}

我们看到,投屏这部分其实就是利用录屏和利用MediaCodec硬编码。这部分偏模板代码,基本就是设置MediaCodec的参数,通过硬编码拿到H264的packet数据,然后通过IO.writeFully对fd进行写操作将数据发出。

大致的流程图下:
在这里插入图片描述

3. 控制逻辑

控制逻辑的入口是startController方法:

private static Thread startController(final Controller controller) { Thread thread = new Thread(new Runnable() {            @Override            public void run() {                controller.control();            }        });        thread.start();}public void control() {while (true) {    handleEvent();    }}private void handleEvent() {// 从controlSocket的inputStream读数据ControlMessage msg = connection.receiveControlMessage();switch (msg.getType()) {    case ControlMessage.TYPE_INJECT_KEYCODE:    injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());       case ControlMessage.TYPE_INJECT_TOUCH_EVENT:       injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());      // ...    }}

我们看到控制部分是开启子线程,不断地从controlSocket中PC传来的读控制事件数据,然后根据事件类型的不同做不同的处理。这里我们看到键盘事件或鼠标事件最终都是调用到```injectXXX``方法。其实我们也能猜到,这里肯定是将PC传来的事件转成Android的事件,然后分发事件。那么Scrcpy是怎么实现这个步骤的呢?

3.1 事件注入

我们先来看下injectKeyCode方法:

// Controller.javaprivate boolean injectKeycode(int action, int keycode, int repeat, int metaState) {// 调用Device的injectKeycode方法device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);}// Device.javapublic static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {long now = SystemClock.uptimeMillis();// 构建一个KeyEventKeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,        InputDevice.SOURCE_KEYBOARD);return injectEvent(event, displayId, injectMode);}public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {    InputManager.setDisplayId(inputEvent, displayId)    return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode);}

injectKeyCode的调用链中构建了一个KeyEvent,然后调用到了最后这两个方法:

  • InputManager.setDisplayId() - 通过反射调用InputEventsetDisplayMethod方法,为事件指定目标Display:

    // InputManager.javapublic static boolean setDisplayId(InputEvent inputEvent, int displayId) {Method method = getSetDisplayIdMethod();method.invoke(inputEvent, displayId);return true;}private static Method getSetDisplayIdMethod() throws NoSuchMethodException {    if (setDisplayIdMethod == null) {        setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class);    }    return setDisplayIdMethod;}
  • ServiceManager.getInputManager().injectInputEvent() - 通过反射的方式获取到系统中InputManager的实例,并用工程里的InputManager类包装一下:

    // ServiceManager.javapublic static InputManager getInputManager() {    if (inputManager == null) {        try {        // 反射调用系统InputManager的getInstance方法            Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance");            android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null);            // 将系统的InputManager实例传入工程自己的InputManager类,包装一下            inputManager = new InputManager(im);        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {            throw new AssertionError(e);        }    }    return inputManager;}

    然后通过反射调用系统InputManagerinjectInputEvent方法,进行事件注入处理,即通过系统InputManagerService将事件发到了目标Display上:

    private Method getInjectInputEventMethod() throws NoSuchMethodException {    injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);    return injectInputEventMethod;}public boolean injectInputEvent(InputEvent inputEvent, int mode) {    Method method = getInjectInputEventMethod();    return (boolean) method.invoke(manager, inputEvent, mode);}

injectTouch方法大同小异,注入的是MotionEvent。但Android中MotionEventKeyEvent都是继承于InputEvent,所以最终都是走的injectInputEvent将事件发送到目标Display上。

所以我们的流程图可以填充完整了:
在这里插入图片描述

至此,Server端的连接、投屏、和控制逻辑就已经分析完了。

4. 时序图

照例附上时序图,不同颜色代表不同的线程。
Scrcpy-server

5. 小结

本篇我们探究了Scrcpy Server端的逻辑,相较Client端而言,Server端的逻辑比较清晰简单。涉及的点有Android录屏、LocalSocket、MediaCode硬编码、事件注入。

到此,关于Scrcpy软件我们就全部分析完了,我们从项目结构开始,研究了其编译系统Meson,然后到Client端(PC端)的建立连接和投屏过程,最后到Server端(Android端)的连接、投屏和控制过程。主线流程还是比较清晰的。

其实最让我个人感到收获的有三个地方:

  1. ADB端口映射,这种方式为PC和手机的相互访问提供了便利,结合Unix Domain Socket,大大拓展了使用场景,应用非常广泛。
  2. SDL,笔者之前对SDL了解不深,只知道他可以用来做多媒体相关的界面。但Scrcpy中广泛地运用了SDL的库函数,比较同步、事件机制等和多媒体不太相关的功能。可以说是一套强大的工具库。所以目前笔者已经果断地将SDL加入了自己的后续学习清单。
  3. Android事件注入,Client端的事件注入机制主要是用了InputEvent的私有API,setDisplayinjectInputEvent。这种方式可以实现自己构建KeyEventMotionEvent后发到指定的屏上。刚巧笔者最近有在做的一个项目是有关多屏的,其中有个需要攻克的技术难点,就是其实要用户在真实物理屏上的触摸事件转发到一个我们自己创建的VirtualDisplay上。于是就借鉴上了Scrcpy中关于事件注入的方法,将Event事件的Display设置成VirtualDisplay的ID,然后通过事件注入的方式实现了转发。

所以,没事多研究成功的开源软件还是有好处的~

来源地址:https://blog.csdn.net/ZivXu/article/details/129095894

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

【投屏】Scrcpy源码分析四(最终章 - Server篇)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录