草庐IT

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

都都的大头 2024-06-29 原文

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.java
public 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.java
    private 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.java
public 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.java
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
	// 调用Device的injectKeycode方法
	device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
}

// Device.java
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
	long now = SystemClock.uptimeMillis();
	// 构建一个KeyEvent
	KeyEvent 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.java
    public 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.java
    public 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. 时序图

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

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,然后通过事件注入的方式实现了转发。

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

有关【投屏】Scrcpy源码分析四(最终章 - Server篇)的更多相关文章

  1. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  2. ruby-on-rails - rails : uninitialized constant just happen on production server - 2

    我有一个放在lib/network中的类:moduleNetworkApiclassNetworkProxyendend然后在另一个类中,我引用了这个类:network_proxy=::NetworkApi::NetworkProxy.new(params)一切都在我的开发环境中正常运行,但是当我部署到服务器时,我在上面一行收到错误消息:NameError:uninitializedconstantNetworkApi::NetworkProxy我不知道为什么会出现这个奇怪的错误。请告诉我为什么。 最佳答案 请注意Rails5dis

  3. 建模分析 | 平面2R机器人(二连杆)运动学与动力学建模(附Matlab仿真) - 2

    目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标

  4. 网站日志分析软件--让网站日志分析工作变得更简单 - 2

    网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.

  5. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  6. (附源码)vue3.0+.NET6实现聊天室(实时聊天SignalR) - 2

    参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍  介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。  内容有:    ①:Hub模型的方法介绍    ②:服务器端代码介绍    ③:前端vue3安装并调用后端方法    ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke()  去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on

  7. ABB-IRB-1200运动学分析MATLAB RVC工具分析+Simulink-Adams联合仿真 - 2

    一、机器人介绍        此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接

  8. 关于Qt程序打包后运行库依赖的常见问题分析及解决方法 - 2

    目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'

  9. ruby-on-rails - Rails 和 MQTT : Subscribe to topic in background at server startup? - 2

    我想在服务器启动时在我的Rails应用程序中订阅一个mqtt主题,并保持订阅始终处于事件状态和运行状态。我正在使用这个mqttgem进行mqtt通信:https://github.com/njh/ruby-mqtt这是我现在拥有的:在application.rb中:config.after_initializedomqttSub=BackgroundMQTT.newmqttSub.runend后台MQTT类:classMQTTSubscriberdefrunThread.newdoMQTT::Client.connect(:host=>'localhost',:port=>1883,)

  10. ruby-on-rails - 如何使用 ruby​​-prof 和 JMeter 分析 Rails - 2

    我想使用ruby​​-prof和JMeter分析Rails应用程序。我对分析特定Controller/操作/或模型方法的建议方法不感兴趣,我想分析完整堆栈,从上到下。所以我运行这样的东西:RAILS_ENV=productionruby-prof-fprof.outscript/server>/dev/null然后我在上面运行我的JMeter测试计划。然而,问题是使用CTRL+C或SIGKILL中断它也会在ruby​​-prof可以写入任何输出之前杀死它。如何在不中断ruby​​-prof的情况下停止mongrel服务器? 最佳答案

随机推荐