minicap_工具使用

minicap介绍

minicap是开源项目STF(Smartphone Test Farm)中的一个工具,负责屏幕显示。

stf自己写了一个工具叫minicap用来替代原生的screencap,这个工具是stf框架的依赖工具之一,最近手头上的项目刚好由于帧率卡顿需要优化,刚好来在testerhome社区看到对STF的介绍,WEB 端批量移动设备管理控制工具 STF 的环境搭建和运行

minicap工具是用NDK开发的,属于Android的底层开发,该工具分为两个部分,一个是动态连接库.so文件,一个是minicap可执行文件。但不是通用的,因为CPU架构的不同分为不同的版本文件,STF提供的minicap文件根据CPU 的ABI分为如下4种:

arm64-v8aarmeabi-v7a,x86,x86_64 架构。而minicap.so文件在这个基础上还要分为不同的sdk版本。这些都可以从Github地址:链接地址下载而来

结构树目录.
├── bin
│   ├── arm64-v8a
│   │   ├── minicap
│   │   └── minicap-nopie
│   ├── armeabi-v7a
│   │   ├── minicap
│   │   └── minicap-nopie
│   ├── x86
│   │   ├── minicap
│   │   └── minicap-nopie
│   └── x86_64
│       ├── minicap
│       └── minicap-nopie
└── shared
    ├── android-10
    │   └── armeabi-v7a
    │       └── minicap.so
    ├── android-14
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   └── x86
    │       └── minicap.so
    ├── android-15
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   └── x86
    │       └── minicap.so
    ├── android-16
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   └── x86
    │       └── minicap.so
    ├── android-17
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   └── x86
    │       └── minicap.so
    ├── android-18
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   └── x86
    │       └── minicap.so
    ├── android-19
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   └── x86
    │       └── minicap.so
    ├── android-21
    │   ├── arm64-v8a
    │   │   └── minicap.so
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   ├── x86
    │   │   └── minicap.so
    │   └── x86_64
    │       └── minicap.so
    ├── android-22
    │   ├── arm64-v8a
    │   │   └── minicap.so
    │   ├── armeabi-v7a
    │   │   └── minicap.so
    │   ├── x86
    │   │   └── minicap.so
    │   └── x86_64
    │       └── minicap.so
    ├── android-9
    │   └── armeabi-v7a
    │       └── minicap.so
    └── android-M
        ├── arm64-v8a
        │   └── minicap.so
        ├── armeabi-v7a
        │   └── minicap.so
        ├── x86
        │   └── minicap.so
        └── x86_64
            └── minicap.so

准备对应文件

a、查看CPU架构(adb shell getprop ro.product.cpu.abi)及查看android版本level(adb shell getprop ro.build.version.sdk)

b、根据上面获取的信息,将适合设备的可执行文件和.so文件push到手机的/data/local/tmp目录下,或者在STF框架的源码下找到vendor/minicap文件夹下

c、adb shell进入到目录下chmod 777 minicap

d、测试一下minicap是否可用:(-P后面跟的参数为你屏幕的尺寸)

   adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0 -t

安装运行环境

a、安装nodejs:

  查看版本号:node -v

b、安装运行依赖 ws和express包 

  npm install ws –g

  npm install express -g

启动手机端服务

 就是启动了一个socket服务器

 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0

本地端口转发

a、跟上面的socket服务通信,首先我们要将本地的端口映射到minicap工具上,端口随意:

  adb forward tcp:1717 localabstract:minicap

 b、输入 node app.js 回车启动服务:

  控制台显示  Listening on port 9002 表示启动成功

c、浏览器打开本地 localhost:9002  链接地址,查看

 

获取信息

然后使用命令nc localhost 1717来与minicap通信,然后你会发现好多乱码。官方提供了一个demo来看效果,在minicap项目下的example目录

但是这些信息是有规则的,只是我们无法实际查看。但是我们做的工具需要用java来获得该信息,所以弄懂这些格式是很有必要的,结果分析后得出这些信息分3部分

Banner模块(第一部分)

这一部分的信息只在连接后,只发送一次,是一些汇总信息,一般为24个16进制字符,每一个字符都表示不同的信息:

位置信息
0 版本
1 该Banner信息的长度,方便循环使用
2,3,4,5 相加得到进程id号
6,7,8,9 累加得到设备真实宽度
10,11,12,13 累加得到设备真实高度
14,15,16,17 累加得到设备的虚拟宽度
18,19,20,21 累加得到设备的虚拟高度
22 设备的方向
23 设备信息获取策略

携带图片大小信息和图片二进制信息模块(第二部分)

得到上面的Banner部分处理完成后,以后不会再发送Banner信息,后续只会发送图片相关的信息。那么接下来就接受图片信息了,第一个过来的图片信息的前4个字符不是图片的二进制信息,而是携带着图片大小的信息,我们需要累加得到图片大小。这一部分的信息除去前四个字符,其他信息也是图片的实际二进制信息,比如我们接受到的信息长度为n,那么4~(n-4)部分是图片的信息,需要保存下来。

只携带图片二进制信息模块(第三部分)

每一个变化的界面都会有上面的[携带图片大小信息和图片二进制信息模块],当得到大小后,或许发送过来的数据都是要组装成图片的二进制信息,知道当前屏幕的数据发送完成。 
有2种方式可以看出来图片组装完成了:

  • 又遇到第二部分
  • 设定大小的数据已经装满了

java的实现:

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.imageio.ImageIO;

import org.apache.log4j.Logger;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.CollectingOutputReceiver;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.TimeoutException;
import com.wuba.utils.DirStructureUtil;
import com.wuba.utils.TimeUtil;

/**
 * @date 2015年8月12日 上午11:02:53
 */
public class MiniCapUtil {
    private Logger LOG = Logger.getLogger(MiniCapUtil.class);
    // CPU架构的种类
    public static final String ABIS_ARM64_V8A = "arm64-v8a";
    public static final String ABIS_ARMEABI_V7A = "armeabi-v7a";
    public static final String ABIS_X86 = "x86";
    public static final String ABIS_X86_64 = "x86_64";

    private Queue<byte[]> dataQueue = new ConcurrentLinkedQueue<byte[]>();

    private Banner banner = new Banner();
    private static final int PORT = 1717;
    private IDevice device;
    private String REMOTE_PATH = "/data/local/tmp";
    private String ABI_COMMAND = "ro.product.cpu.abi";
    private String SDK_COMMAND = "ro.build.version.sdk";
    private String MINICAP_BIN = "minicap";
    private String MINICAP_SO = "minicap.so";
    private String MINICAP_CHMOD_COMMAND = "chmod 777 %s/%s";
    private String MINICAP_WM_SIZE_COMMAND = "wm size";
    private String MINICAP_START_COMMAND = "LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P %s@%s/0";
    private boolean isRunning = false;

    public MiniCapUtil(IDevice device) {
        this.device = device;
        init();
    }

    /**
     * 将minicap的二进制和.so文件push到/data/local/tmp文件夹下,启动minicap服务
     */
    private void init() {

        String abi = device.getProperty(ABI_COMMAND);
        String sdk = device.getProperty(SDK_COMMAND);
        File minicapBinFile = new File(DirStructureUtil.getMinicapBin(), abi
                + File.separator + MINICAP_BIN);
        File minicapSoFile = new File(DirStructureUtil.getMinicapSo(),
                "android-" + sdk + File.separator + abi + File.separator
                        + MINICAP_SO);
        try {
            // 将minicap的可执行文件和.so文件一起push到设备中
            device.pushFile(minicapBinFile.getAbsolutePath(), REMOTE_PATH
                    + File.separator + MINICAP_BIN);
            device.pushFile(minicapSoFile.getAbsolutePath(), REMOTE_PATH
                    + File.separator + MINICAP_SO);
            executeShellCommand(String.format(MINICAP_CHMOD_COMMAND,
                    REMOTE_PATH, MINICAP_BIN));
            // 端口转发
            device.createForward(PORT, "minicap",
                    DeviceUnixSocketNamespace.ABSTRACT);
            // 获取设备屏幕的尺寸
            String output = executeShellCommand(MINICAP_WM_SIZE_COMMAND);
            String size = output.split(":")[1].trim();
            final String startCommand = String.format(MINICAP_START_COMMAND,
                    size, size);
            // 启动minicap服务
            new Thread(new Runnable() {
                @Override
                public void run() {
                    LOG.info("minicap服务器启动");
                    executeShellCommand(startCommand);
                }
            }).start();

        } catch (SyncException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (AdbCommandRejectedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (TimeoutException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private String executeShellCommand(String command) {
        CollectingOutputReceiver output = new CollectingOutputReceiver();
        try {
            device.executeShellCommand(command, output, 0);
        } catch (TimeoutException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (AdbCommandRejectedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ShellCommandUnresponsiveException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return output.getOutput();
    }

    public void startScreenListener() {
        isRunning = true;
        new Thread(new ImageConverter()).start();
        new Thread(new ImageBinaryFrameCollector()).start();
    }

    public void stopScreenListener() {
        isRunning = false;
    }

    private synchronized void createImageFromByte(byte[] binaryData) {
        InputStream in = new ByteArrayInputStream(binaryData);
        try {
            BufferedImage bufferedImage = ImageIO.read(in);
            ImageIO.write(bufferedImage, "jpg", new File("screen.jpg"));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    // java合并两个byte数组
    private static byte[] byteMerger(byte[] byte_1, byte[] byte_2) {
        byte[] byte_3 = new byte[byte_1.length + byte_2.length];
        System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);
        System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);
        return byte_3;
    }

    private static byte[] subByteArray(byte[] byte1, int start, int end) {
        byte[] byte2 = new byte[end - start];
        System.arraycopy(byte1, start, byte2, 0, end - start);
        return byte2;
    }

    class ImageBinaryFrameCollector implements Runnable {
        private Socket socket;

        @Override
        public void run() {
            LOG.debug("图片二进制数据收集器已经开启");
            // TODO Auto-generated method stub
            InputStream stream = null;
            DataInputStream input = null;
            try {

                socket = new Socket("localhost", PORT);
                stream = socket.getInputStream();
                input = new DataInputStream(stream);
                while (isRunning) {
                    byte[] buffer;
                    int len = 0;
                    while (len == 0) {
                        len = input.available();
                    }
                    buffer = new byte[len];
                    input.read(buffer);
                    dataQueue.add(buffer);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (socket != null && socket.isConnected()) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

            LOG.debug("图片二进制数据收集器已关闭");
        }
    }

    class ImageConverter implements Runnable {
        private int readBannerBytes = 0;
        private int bannerLength = 2;
        private int readFrameBytes = 0;
        private int frameBodyLength = 0;
        private byte[] frameBody = new byte[0];

        @Override
        public void run() {
            LOG.debug("图片生成器已经开启");
            long start = System.currentTimeMillis();
            while (isRunning) {
                byte[] binaryData = dataQueue.poll();
                if (binaryData == null)
                    continue;
                int len = binaryData.length;
                for (int cursor = 0; cursor < len;) {
                    int byte10 = binaryData[cursor] & 0xff;
                    if (readBannerBytes < bannerLength) {
                        cursor = parserBanner(cursor, byte10);
                    } else if (readFrameBytes < 4) {
                        // 第二次的缓冲区中前4位数字和为frame的缓冲区大小
                        frameBodyLength += (byte10 << (readFrameBytes * 8)) >>> 0;
                        cursor += 1;
                        readFrameBytes += 1;
                    } else {
                        if (len - cursor >= frameBodyLength) {
                            byte[] subByte = subByteArray(binaryData, cursor,
                                    cursor + frameBodyLength);
                            frameBody = byteMerger(frameBody, subByte);
                            if ((frameBody[0] != -1) || frameBody[1] != -40) {
                                LOG.error(String
                                        .format("Frame body does not start with JPG header"));
                                return;
                            }
                            byte[] finalBytes = subByteArray(frameBody, 0,
                                    frameBody.length);
                            // 转化成bufferImage
                            createImageFromByte(finalBytes);
                            long current = System.currentTimeMillis();
                            LOG.info("图片已生成,耗时: "
                                    + TimeUtil.formatElapsedTime(current
                                            - start));
                            start = current;
                            cursor += frameBodyLength;
                            frameBodyLength = 0;
                            readFrameBytes = 0;
                            frameBody = new byte[0];
                        } else {
                            byte[] subByte = subByteArray(binaryData, cursor,
                                    len);
                            frameBody = byteMerger(frameBody, subByte);
                            frameBodyLength -= (len - cursor);
                            readFrameBytes += (len - cursor);
                            cursor = len;
                        }
                    }
                }
            }
            LOG.debug("图片生成器已关闭");
        }

        private int parserBanner(int cursor, int byte10) {
            switch (readBannerBytes) {
            case 0:
                // version
                banner.setVersion(byte10);
                break;
            case 1:
                // length
                bannerLength = byte10;
                banner.setLength(byte10);
                break;
            case 2:
            case 3:
            case 4:
            case 5:
                // pid
                int pid = banner.getPid();
                pid += (byte10 << ((readBannerBytes - 2) * 8)) >>> 0;
                banner.setPid(pid);
                break;
            case 6:
            case 7:
            case 8:
            case 9:
                // real width
                int realWidth = banner.getReadWidth();
                realWidth += (byte10 << ((readBannerBytes - 6) * 8)) >>> 0;
                banner.setReadWidth(realWidth);
                break;
            case 10:
            case 11:
            case 12:
            case 13:
                // real height
                int realHeight = banner.getReadHeight();
                realHeight += (byte10 << ((readBannerBytes - 10) * 8)) >>> 0;
                banner.setReadHeight(realHeight);
                break;
            case 14:
            case 15:
            case 16:
            case 17:
                // virtual width
                int virtualWidth = banner.getVirtualWidth();
                virtualWidth += (byte10 << ((readBannerBytes - 14) * 8)) >>> 0;
                banner.setVirtualWidth(virtualWidth);

                break;
            case 18:
            case 19:
            case 20:
            case 21:
                // virtual height
                int virtualHeight = banner.getVirtualHeight();
                virtualHeight += (byte10 << ((readBannerBytes - 18) * 8)) >>> 0;
                banner.setVirtualHeight(virtualHeight);
                break;
            case 22:
                // orientation
                banner.setOrientation(byte10 * 90);
                break;
            case 23:
                // quirks
                banner.setQuirks(byte10);
                break;
            }

            cursor += 1;
            readBannerBytes += 1;

            if (readBannerBytes == bannerLength) {
                LOG.debug(banner.toString());
            }
            return cursor;
        }
    }
}

总结

1.在实际过程由于minicap发送信息的速度很快,如果不及时处理,会造成某一次获取的数据是将minicap多次发送的数据一起处理了,这就会造成错误。所以上面的代码是将生成BufferImage的操作放到了线程中,但是最好是将获取socket数据部分和解析数据部分独立开来,获取socket数据将获取到的数据立即放到队列中,然后立马得到下一次数据的获取,数据解析部分在独立线程中来获取队列中的信息来解析。这样就能避免上面提到的问题。 
2.目前不支持下面三款机器和模拟器

  • Xiaomi “HM NOTE 1W” (Redmi Note 1W), 
  • Huawei “G750-U10” (Honor 3X) 
  • Lenovo “B6000-F” (Yoga Tablet 8).

3.我们实测的速度(针对N6)原生为5秒左右,minicap在1秒内。

 

微信搜索【水勺子】关注我,获取更多详细信息

posted @ 2017-04-17 19:22  咸的  阅读(9550)  评论(1编辑  收藏  举报