【投屏】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.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. 时序图

照例附上时序图,不同颜色代表不同的线程。
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,然后通过事件注入的方式实现了转发。

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

猜你喜欢

转载自blog.csdn.net/ZivXu/article/details/129095894
今日推荐