scrcpy服务端代码分析

Posted li_Jiejun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了scrcpy服务端代码分析相关的知识,希望对你有一定的参考价值。

源码

 Git clone https://github.com/barry-ran/QtScrcpy.git

Server端

入口文件 src/main/java/com/genymobile/scrcpy/Server.java

入口main函数

public static void main(String... args) throws Exception 
       //用来捕捉整个程序的异常
       Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() 
            @Override
            public void uncaughtException(Thread t, Throwable e) 
                Ln.e("Exception on thread " + t, e);
                suggestFix(e);
            
        );

        Options options = createOptions(args);  //解析参数配置,总共有14个参数

        Ln.initLogLevel(options.getLogLevel());

        scrcpy(options);  //下一步
    

下一步scrcpy函数

private static void scrcpy(Options options) throws IOException 
    ……
    final Device device = new Device(options);
    try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward))   //创建socket连接
            ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);  //屏幕编码

            if (options.getControl()) 
                final Controller controller = new Controller(device, connection);

                // asynchronous
                startController(controller);  //指令流监听并做处理,使用的线程,内部死循环监听处理
                startDeviceMessageSender(controller.getSender());  //监听剪切板事件

                device.setClipboardListener(new Device.ClipboardListener() 
                    @Override
                    public void onClipboardTextChanged(String text) 
                        controller.getSender().pushClipboardText(text);
                    
                );
            

            try 
                // synchronous
                screenEncoder.streamScreen(device, connection.getVideoFd());  //视频流编码
             catch (IOException e) 
                // this is expected on close
                Ln.d("Screen streaming stopped");
            
        
    

创建localsocket,视频流和指令流同属一个socket

public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException 
        LocalSocket videoSocket;
        LocalSocket controlSocket;
        if (tunnelForward) 
            LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
            try 
                videoSocket = localServerSocket.accept(); //创建socket并处于阻塞状态
                // send one byte so the client may read() to detect a connection error
                videoSocket.getOutputStream().write(0);  //发送一个字节数据,客户端第一次连上接收到的数据
                try 
                    controlSocket = localServerSocket.accept();
                 catch (IOException | RuntimeException e) 
                    videoSocket.close();
                    throw e;
                
             finally 
                localServerSocket.close();
            
         else 
            videoSocket = connect(SOCKET_NAME);  //视频流socket
            try 
                controlSocket = connect(SOCKET_NAME);  //控制指令socket
             catch (IOException | RuntimeException e) 
                videoSocket.close();
                throw e;
            
        

        DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
        Size videoSize = device.getScreenInfo().getVideoSize();
		//往视频流socket上发送设备名,宽度,高度
        connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
        return connection;
    

开启指令流监听

private static void startController(final Controller controller) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                try 
                    controller.control();  //control()函数
                 catch (IOException e) 
                    // this is expected on close
                    Ln.d("Controller stopped");
                
            
        ).start();
    

control()函数

public void control() throws IOException 
        // on start, power on the device
        if (!device.isScreenOn()) 
            device.injectKeycode(KeyEvent.KEYCODE_POWER);

            // dirty hack
            // After POWER is injected, the device is powered on asynchronously.
            // To turn the device screen off while mirroring, the client will send a message that
            // would be handled before the device is actually powered on, so its effect would
            // be "canceled" once the device is turned back on.
            // Adding this delay prevents to handle the message before the device is actually
            // powered on.
            SystemClock.sleep(500);
        

        //死循环来处理指令流事件
        while (true) 
            handleEvent();
        
    

事件类型处理

private void handleEvent() throws IOException 
        ControlMessage msg = connection.receiveControlMessage();  //接收控制流消息
        switch (msg.getType()) 
            case ControlMessage.TYPE_INJECT_KEYCODE:
                if (device.supportsInputEvents()) 
                    injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
                
                break;
            case ControlMessage.TYPE_INJECT_TEXT:
                if (device.supportsInputEvents()) 
                    injectText(msg.getText());
                
                break;
            case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
                if (device.supportsInputEvents()) 
                    injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
                
                break;
            case ControlMessage.TYPE_INJECT_SCROLL_EVENT:  //以屏幕滚动事件为例
                if (device.supportsInputEvents()) 
                    injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
                
                break;
            case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
                if (device.supportsInputEvents()) 
                    pressBackOrTurnScreenOn();
                
                break;
            case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
                device.expandNotificationPanel();
                break;
            case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
                device.collapsePanels();
                break;
            case ControlMessage.TYPE_GET_CLIPBOARD:
                String clipboardText = device.getClipboardText();
                if (clipboardText != null) 
                    sender.pushClipboardText(clipboardText);  //剪切板事件
                
                break;
            case ControlMessage.TYPE_SET_CLIPBOARD:
                boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
                setClipboard(msg.getText(), paste);
                break;
            case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
                if (device.supportsInputEvents()) 
                    int mode = msg.getAction();
                    boolean setPowerModeOk = device.setScreenPowerMode(mode);
                    if (setPowerModeOk) 
                        Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
                    
                
                break;
            case ControlMessage.TYPE_ROTATE_DEVICE:
                device.rotateDevice();
                break;
            default:
                // do nothing
        
    

接收控制流消息

public ControlMessage receiveControlMessage() throws IOException 
        ControlMessage msg = reader.next();
        while (msg == null) 
            reader.readFrom(controlInputStream);  //向控制流的输入通道内读取数据
            msg = reader.next();  //循环读取
        
        return msg;
    

以屏幕滚动事件为例

private boolean injectScroll(Position position, int hScroll, int vScroll) 
        long now = SystemClock.uptimeMillis();
        Point point = device.getPhysicalPoint(position);
        if (point == null) 
            // ignore event
            return false;
        

        MotionEvent.PointerProperties props = pointerProperties[0];
        props.id = 0;

        MotionEvent.PointerCoords coords = pointerCoords[0];
        coords.x = point.getX();
        coords.y = point.getY();
        coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
        coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);

        //事件类型为ACTION_SCROLL
        MotionEvent event = MotionEvent
                .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
                        InputDevice.SOURCE_TOUCHSCREEN, 0);
        return device.injectEvent(event);  //事件注入机制
    

事件注入机制

public boolean injectEvent(InputEvent inputEvent, int mode) 
        if (!supportsInputEvents()) 
            throw new AssertionError("Could not inject input event if !supportsInputEvents()");
        

        if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) 
            return false;
        

        return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
    

 使用了安卓内核的事件注入机制;
 到此,控制流server端分析完

private Method getInjectInputEventMethod() throws NoSuchMethodException 
        if (injectInputEventMethod == null) 
            injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
        
        return injectInputEventMethod;
    

监听剪切板事件

private static void startDeviceMessageSender(final DeviceMessageSender sender) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                try 
                    sender.loop();
                 catch (IOException | InterruptedException e) 
                    // this is expected on close
                    Ln.d("Device message sender stopped");
                
            
        ).start();
    

循环监听

public void loop() throws IOException, InterruptedException 
        while (true) 
            String text;
            synchronized (this) 
                while (clipboardText == null) 
                    wait();
                
                text = clipboardText;
                clipboardText = null;
            
            DeviceMessage event = DeviceMessage.createClipboard(text);  //安卓剪切板事件
            connection.sendDeviceMessage(event);
        
    

视频流相关逻辑
视频流编码

public void streamScreen(Device device, FileDescriptor fd) throws IOException 
        Workarounds.prepareMainLooper();

        try 
            internalStreamScreen(device, fd);
         catch (NullPointerException e) 
            // Retry with workarounds enabled:
            // <https://github.com/Genymobile/scrcpy/issues/365>
            // <https://github.com/Genymobile/scrcpy/issues/940>
            Ln.d("Applying workarounds to avoid NullPointerException");
            Workarounds.fillAppInfo();
            internalStreamScreen(device, fd);
        
    

    private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException 
        MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
        device.setRotationListener(this);
        boolean alive;
        try 
            do 
                MediaCodec codec = createCodec();  //创建编码器-h264 MediaFormat.MIMETYPE_VIDEO_AVC
                IBinder display = createDisplay();
                ScreenInfo screenInfo = device.getScreenInfo();
                Rect contentRect = screenInfo.getContentRect();
                // include the locked video orientation
                Rect videoRect = screenInfo.getVideoSize().toRect();
                // does not include the locked video orientation
                Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
                int videoRotation = screenInfo.getVideoRotation();
                int layerStack = device.getLayerStack();

                setSize(format, videoRect.width(), videoRect.height());
                configure(codec, format);  //配置编码器,包括码流,fps,I帧时间间隔等
                Surface surface = codec.createInputSurface();  //创建输入缓冲区
                setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
                codec.start();
                try 
                    alive = encode(codec, fd);  //屏幕编码
                    // do not call stop() on exception, it would trigger an IllegalStateException
                    codec.stop();
                 finally 
                    destroyDisplay(display);
                    codec.release();
                    surface.release();
                
             while (alive);
         finally 
            device.setRotationListener(null);
        
    

屏幕编码

private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException 
        boolean eof = false;
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();  //编码缓冲区

        while (!consumeRotationChange() && !eof) 
            int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);  //获取输出区的缓冲的索引
            eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
            try 
                if (consumeRotationChange()) 
                    // must restart encoding with new size
                    break;
                
                if (outputBufferId >= 0) 
                    ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);  //根据索引获取数据

                    if (sendFrameMeta) 
                        writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());  //先将帧元数据信息发送
                    

                    IO.writeFully(fd, codecBuffer);  //对外发送编码后的帧数据
                
             finally 
                if (outputBufferId >= 0) 
                    codec.releaseOutputBuffer(outputBufferId, false);  //根据索引释放缓冲区
                
            
        

        return !eof;
    

以上是关于scrcpy服务端代码分析的主要内容,如果未能解决你的问题,请参考以下文章

移动端超级实用工具Scrcpy操作分享 (下)

解放双手,在PC端进行Android真机调试

Scrcpy使用入门

scrcpy 安卓投屏

manjaro下使用scrcpy安卓设备投屏

scrcpy——Android投屏神器