一款可以将安卓手机无缝投屏到电脑的项目-Scrcpy

基础

官方地址:https://github.com/Genymobile/scrcpy

官方描述: This application provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access. It works on GNU/Linux, Windows and MacOS.

译文: 一款通过Usb(TCP/IP)无需root权限的将屏幕显示在GNU/Linux, Windows and MacOS上并能控制手机的软件。

使用

Linux\Windows未能验证。

Mac

  • 安装JDK和ADB
  • 使用homebrew安装
    brew update
    brew install scrcpy
    
    ####运行
    连接手机、打开终端
    scrcpy
    

主要快捷键

Home:ctrl + h 
BACK:ctrl + b 
POWER:ctrl + p
volume_up:ctrl + up(向上箭头) 
volume_down:ctrl + down(向下箭头)

是不是超级厉害!!!

基础原理分析

  1. 此项目是c/s架构其中:手机设备是server,电脑显示画面是client
  2. 控制:通信client通过socket发送adb命令给server即手机处理
  3. 显示:server截取屏幕帧发送给client即电脑,解码显示(SDL2项目)

服务端(手机)

当电脑端运行命令后你可能会想,手机端作为server并没有运行任何程序呀,他为什么能接受命令,并且将屏幕投送到电脑呢? 其实这个问题也困扰我很久,一直没明白,自从简单阅读代码后才渐渐清晰起来。

虽然我们没有直接启动任何程序,但是当运行scrcpy时候它已经通过adb命令将java程序push到了手机。 具体如下(样例):

  1. hello world
    public class HelloWorld {
     public static void main(String... args) {
         System.out.println("Hello, world!");
     }
    }
    
  2. java转换成dex
    javac -source 1.7 -target 1.7 HelloWorld.java
    "$ANDROID_HOME"/build-tools/27.0.2/dx \
     --dex --output classes.dex HelloWorld.class
    
  3. push dex到安卓设备
    adb push classes.dex /data/local/tmp/
    
  4. 执行
    $ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld
    Hello, world!
    

安卓竟然可以不安卓apk就能运行,涨知识了,涨知识了。具体请Google,本人也不清楚哇。

代码分析

主入口位置:/scrcpy/server/src/main/java/com/genymobile/scrcpy/Server.java

public static void main(String... args) throws Exception {
       // 略
        // 如果已经存在jar先删掉,用最新的
        unlinkSelf();
        // 初始化配置项
        Options options = createOptions(args);
        scrcpy(options);
    }

接下来我们分析scrcpy()函数。

private static void scrcpy(Options options) throws IOException {
        final Device device = new Device(options);
        boolean tunnelForward = options.isTunnelForward();
        // 创建LocalServerSocket
        try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
            ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());

            // asynchronous
            // 异步接受clent发送过来的指令,主要是控制指令,比如按返回键 home建
            // 反射injectInputEvent执行,将事件发送给手机;// 重要知识点
            startEventController(device, connection);

            try {
                // synchronous
                // 编码屏幕数据
                screenEncoder.streamScreen(device, connection.getFd());
            } catch (IOException e) {
                // this is expected on close
                Ln.d("Screen streaming stopped");
            }
        }
    }
  1. 创建local socket sever,并发送手机信息
  2. 接受控制指令,通过反射injectInputEvent,输入到手机
  3. 编码屏幕数据 其中1和2比较简单,3用到的技术相对比较多。 我们一个一个来看。
  4. 创建socket
    public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
         LocalSocket socket;
         if (tunnelForward) {
             // new LocalServerSocket() 并accept()等待接收数据
             socket = listenAndAccept(SOCKET_NAME);
             // send one byte so the client may read() to detect a connection error
             // 发送一条数据看看连接是否成功
             socket.getOutputStream().write(0);
         } else {
             socket = connect(SOCKET_NAME);
         }
         // 获取到输入输出流,打开文件句柄描述符
         // 获取屏幕数据,并发送给client
         DesktopConnection connection = new DesktopConnection(socket);
         Size videoSize = device.getScreenInfo().getVideoSize();
         connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
         return connection;
     }
    
  5. 接受控制指令,通过反射injectInputEvent,输入到手机 即: startEventController(device, connection);
  • 开启一个异步线程
  • new EventController(device, connection).control(); control()方法即switch back键 home键,等待输入
  1. 编码屏幕数据 编码屏幕数据主要通过MediaFormat,MediaCodec,Surface获取屏幕图像,获取缓冲区,最后通过socket发送出去
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;
    }

获取屏幕数据也是通过大量反射,比如android.hardware.display.IDisplayManager,android.view.IWindowManager等获取到屏幕数据 到此服务短分析完成。

Client,即电脑显示的画面

项目:/scrcpy/app/src/main.c Client的代码分析起来有些复杂,全是c/c++代码,在这里我们简单接受一下大致流程,有兴趣的自行查阅。

  1. main(),输出帮助信息,解析命令参数等
  2. av_register_all()注册视频解码程序 // ffmpeg
  3. scrcpy() 创建socket收发数据,解码(SDL2库)显示画面,接收输入(鼠标点击)

总结

Scrcpy项目对于不喜欢一会看手机一会看电脑的同学非常有用,而且项目用到的技术点也很值得学习。但看项目的架构并没有太突出的点。 重复下流程:

  1. 此项目是c/s架构其中:手机设备是server,电脑显示画面是client
  2. 控制:通信client通过socket发送adb命令给server即手机处理
  3. 显示:server截取屏幕帧发送给client即电脑,解码显示(SDL2项目)
声明:原创文章,版权所有,转载请注明出处,https://litets.com。