minicap java_minicap

本文介绍了minicap工具,它是STF框架的一部分,用于替代Android原生的screencap以实现快速截图。minicap分为动态链接库.so文件和可执行文件,适用于不同CPU架构和Android版本。通过adb命令获取设备的CPU版本和系统版本,将minicap文件推送到设备并启动。使用端口转发和Java解析minicap输出的二进制数据,实现高效截取和处理屏幕画面。
摘要由CSDN通过智能技术生成

从WEB 端批量移动设备管理控制工具 STF 的环境搭建和运行文章了解到STF这个工具,然后试用了一下。最近在做一个测试工具,发现Android原生的截图工具截图非常缓慢,然后想起了stf工具中截图非常快,甚至连执行monkey的动作都能在web端查看,这就很爽了,所以在github上提了一个Issue,询问这个是如何实现的,很快得到答复,stf自己写了一个工具叫minicap用来替代原生的screencap,这个工具是stf框架的依赖工具。

minicap使用

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

.

├── 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

从上面可以看出,minicap可执行文件分为4种,分别针对arm64-v8a、armeabi-v7a,x86,x86_64 架构。而minicap.so文件在这个基础上还要分为不同的sdk版本。

获取设备的CPU版本和系统版本

CPU版本

adb shell getprop ro.product.cpu.abi | tr -d '\r'

58deMacBook-Pro:minicap wuxian$ adb shell getprop ro.product.cpu.abi | tr -d '\r'

armeabi-v7a

1

2

系统版本

adb shell getprop ro.build.version.sdk | tr -d '\r'

58deMacBook-Pro:minicap wuxian$ adb shell getprop ro.build.version.sdk | tr -d '\r'

22

1

2

将文件push到手机

根据上面获取的信息,将适合设备的可执行文件和.so文件push到手机的/data/local/tmp目录下,如果你不想自己build这些文件可以去STF框架的源码下找到vendor/minicap文件夹下找到这些文件,我上面的tree信息就是我在stf根目录vendor/minicap下打印的,所以我们将这两个文件导入到我手机的/data/local/tmp目录下:

shell@shamu:/data/local/tmp $ ls -l

-rw-rw-r-- shell shell 1053609 2015-08-07 19:19 1.png

-rwxr-xr-x shell shell 1062992 2015-08-03 12:02 busybox

-rwxr-xr-x shell shell 358336 2015-08-03 12:02 busybox1

drwxrwxrwx shell shell 2015-07-21 15:16 dalvik-cache

-rw-r--r-- shell shell 193 2015-08-13 19:44 krperm.txt

-rwxrwxrwx shell shell 370424 2015-08-07 18:16 minicap

-rw-rw-rw- shell shell 13492 2015-08-07 18:26 minicap.so

-rw------- shell shell 11192 2015-08-06 10:46 ui.xml

-rw------- shell shell 2501 2015-08-07 10:36 uidump.xml

启动工具

首先我们测试一下我们的minicap工具是否可用,命令如下(其中-P后面跟的参数为你屏幕的尺寸,你可以修改成你自己设备的尺寸):

adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0 -t

最后输出OK就表明minicap可用:

58deMacBook-Pro:minicap wuxian$ adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0 -t

PID: 7105

INFO: Using projection 1440x2560@1440x2560/0

INFO: (external/MY_minicap/src/minicap_22.cpp:240) Creating SurfaceComposerClient

INFO: (external/MY_minicap/src/minicap_22.cpp:243) Performing SurfaceComposerClient init check

INFO: (external/MY_minicap/src/minicap_22.cpp:250) Creating virtual display

INFO: (external/MY_minicap/src/minicap_22.cpp:256) Creating buffer queue

INFO: (external/MY_minicap/src/minicap_22.cpp:261) Creating CPU consumer

INFO: (external/MY_minicap/src/minicap_22.cpp:265) Creating frame waiter

INFO: (external/MY_minicap/src/minicap_22.cpp:269) Publishing virtual display

INFO: (jni/minicap/JpgEncoder.cpp:64) Allocating 11061252 bytes for JPG encoder

INFO: (external/MY_minicap/src/minicap_22.cpp:284) Destroying virtual display

OK

然后我们启动minicap工具,命令如下(就比上面的检测工具少了个-t):

adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0

本地端口转发

上面其实是启动了一个socket服务器,我们需要跟该socket服务通信,首先我们要将本地的端口映射到minicap工具上,端口自己随意:

adb forward tcp:1717 localabstract:minicap

获取信息

然后使用命令nc localhost 1717来与minicap通信,然后你会发现好多乱码。

91b7fa93093033024e3084e75d05d6ef.png

效果

上面的乱码我们也看不懂,官方提供了一个demo来看效果,在minicap项目下的example目录,我们来启动该例子:

58deMacBook-Pro:example wuxian$ PORT=9002 node app.js

Listening on port 9002

1

2

然后我们在浏览器下输入localhost:9002就可以看到如下效果了:

2b68f3d62bbe3fb89ef802d83393e74b.gif

minicap传输的信息解析

我们在上面的nc localhost 1717 那一步可以看出来,minicap工具会不断的向命令行下输出乱码信息,但是这些信息是有规则的,只是我们无法实际查看。但是我们做的工具需要用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 dataQueue = new ConcurrentLinkedQueue();

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] &

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值