STF STF 框架之 minicap 工具

易寒 · August 14, 2015 · Last by DJL箫氏 replied at March 04, 2019 · 10082 hits
本帖已被设为精华帖!

stf
minicap

minicap 介绍

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-v8aarmeabi-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

系统版本

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

将文件 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通信,然后你会发现好多乱码。

这里写图片描述

效果

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

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

然后我们在浏览器下输入localhost:9002就可以看到如下效果了:
这里写图片描述

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 的实现

/**
 * 
 */
package com.wuba.utils.screenshot;

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.SocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import javax.imageio.ImageIO;

import org.apache.log4j.Logger;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace;
import com.android.ddmlib.TimeoutException;
import com.sun.org.apache.bcel.internal.generic.NEW;
import com.wuba.utils.TimeUtil;

/**
 * @date 2015812 上午11:02:53
 */
public class MiniCapUtil implements ScreenSubject{

    private Stack<Byte[]> stack = new Stack<Byte[]>();

    private Logger LOG = Logger.getLogger(MiniCapUtil.class);
    private Banner banner;
    private static final int PORT = 1717;
    private Socket socket;
    private int readBannerBytes = 0;
    private int bannerLength = 2;
    private int readFrameBytes = 0;
    private int frameBodyLength = 0;
    private byte[] frameBody = new byte[0];
    private IDevice device;
    private int total;
    private boolean debug = false;

    private List<AndroidScreenObserver> observers = new ArrayList<AndroidScreenObserver>();

    private byte[] finalBytes = null;

    private BufferedImage bufferedImage;

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

    }

    private void init() {
        banner = new Banner();
        try {
            this.device.createForward(PORT, "minicap",
                    DeviceUnixSocketNamespace.ABSTRACT);
        } catch (TimeoutException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (AdbCommandRejectedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

    public void takeBufferedImageByMinicap() {
        InputStream stream = null;
        DataInputStream input = null;
        try {
            socket = new Socket("localhost", PORT);
            while (true) {
                stream = socket.getInputStream();
                input = new DataInputStream(stream);
                byte[] buffer;
                int len = 0;
                while (len == 0) {
                    len = input.available();
                }
                buffer = new byte[len];
                input.read(buffer);
                LOG.info("length=" + buffer.length);
                if (debug) {
                    continue;
                }
                byte[] currentBuffer = subByteArray(buffer, 0, buffer.length);
                for (int cursor = 0; cursor < len;) {
                    int byte10 = buffer[cursor] & 0xff;
                    if (readBannerBytes < bannerLength) {
                        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.info(banner.toString());
                        }
                    } else if (readFrameBytes < 4) {
                        // 第二次的缓冲区中前4位数字和为frame的缓冲区大小
                        frameBodyLength += (byte10 << (readFrameBytes * 8)) >>> 0;
                        cursor += 1;
                        readFrameBytes += 1;
                        total = frameBodyLength;

                    } else {
                        LOG.info("图片大小 : " + total);
                        // LOG.info("frame body部分");
                        // LOG.info(String.format("设想图片的大小 : %d", total));
                        if (len - cursor >= frameBodyLength) {
                            byte[] subByte = subByteArray(currentBuffer,
                                    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;
                            }
                            LOG.info(String.format("实际图片的大小 : %d",
                                    frameBody.length));
                            if (finalBytes == null) {
                                finalBytes = subByteArray(frameBody, 0,
                                        frameBody.length);
                                new Thread(new Runnable() {

                                    @Override
                                    public void run() {
                                        // TODO Auto-generated method stub
                                        try {
                                            createImageFromByte();
                                        } catch (IOException e) {
                                            // TODO Auto-generated catch block
                                            e.printStackTrace();
                                        }
                                    }
                                }).start();
                            }
                            cursor += frameBodyLength;
                            frameBodyLength = 0;
                            readFrameBytes = 0;
                            frameBody = new byte[0];
                        } else {
                            // LOG.debug(String.format("body(len=%d)", len
                            // - cursor));
                            byte[] subByte = subByteArray(currentBuffer,
                                    cursor, len);
                            frameBody = byteMerger(frameBody, subByte);
                            frameBodyLength -= (len - cursor);
                            readFrameBytes += (len - cursor);
                            cursor = len;
                        }
                    }
                }
            }
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        finally {
            if (socket != null && socket.isConnected()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }

    private synchronized void createImageFromByte() throws IOException {
        if (finalBytes.length == 0) {
            LOG.info("frameBody大小为0");
        }
        InputStream in = new ByteArrayInputStream(finalBytes);
        BufferedImage bufferedImage = ImageIO.read(in);
        notifyObservers(bufferedImage);
        // String filePath = String.format("0.jpg");
        // LOG.info(filePath);
        // ImageIO.write(bufferedImage, "jpg", new File(filePath));
        finalBytes = null;
    }

    // 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;
    }

    private String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv + " ");
        }
        return stringBuilder.toString();

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.wuba.utils.screenshot.AndroidScreenSubject#registerObserver(com.wuba
     * .utils.screenshot.AndroidScreenObserver)
     */
    @Override
    public void registerObserver(AndroidScreenObserver o) {
        // TODO Auto-generated method stub
        observers.add(o);

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.wuba.utils.screenshot.AndroidScreenSubject#removeObserver(com.wuba
     * .utils.screenshot.AndroidScreenObserver)
     */
    @Override
    public void removeObserver(AndroidScreenObserver o) {
        // TODO Auto-generated method stub
        int index = observers.indexOf(o);
        if (index != -1) {
            observers.remove(o);
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.wuba.utils.screenshot.AndroidScreenSubject#notifyObservers()
     */
    @Override
    public void notifyObservers(BufferedImage image) {
        // TODO Auto-generated method stub
        for (AndroidScreenObserver observer : observers) {
            observer.frameImageChange(image);
        }
    }

}

总结

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 秒内。

源码

用 java 写了一个小 demo,有兴趣可以下载体验下
minicap_java

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 29 条回复 时间 点赞

碉堡了!

好强大!

简直了

按照 LZ 的文章步骤不能成功。

很多手机直接 push 对应的 minicap 和 minicap.so 文件后,
执行 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 480x854@480x854/0 -t 命令会出现错误。

错误信息 “/system/bin/sh: /data/local/tmp/minicap: can't execute: Permission denied”

建议按照https://github.com/openstf/minicap 中的描述进行。

总结一下直接的步骤:
git clone https://github.com/openstf/minicap.git
cd minicap
git submodule init
git submodule update
ndk-build

注意 ndk 的版本,android-ndk-r10e-linux-x86_64.bin

由于直 git 的 minicap 项目依赖 libjpeg-turbo 模块 ,所以需要进行
git submodule init
git submodule update

注意:直接下载项目和 git clone 项目是不一样的,所以要用 git clone 。否则 submodule 的操作不会成功。

还需要注意一点:

#!/usr/bin/env bash
# Fail on error, verbose output
set -exo pipefail
# Build project
ndk-build 1>&2
# Figure out which ABI and SDK the device has
abi=$(adb shell getprop ro.product.cpu.abi | tr -d '\r')
sdk=$(adb shell getprop ro.build.version.sdk | tr -d '\r')
rel=$(adb shell getprop ro.build.version.release | tr -d '\r')
# PIE is only supported since SDK 16
if (($sdk >= 16)); then
bin=minicap
else
bin=minicap-nopie
fi
args=
if [ "$1" = "autosize" ]; then
set +o pipefail
size=$(adb shell dumpsys window | grep -Eo 'init=\d+x\d+' | head -1 | cut -d= -f 2)
if [ "$size" = "" ]; then
w=$(adb shell dumpsys window | grep -Eo 'DisplayWidth=\d+' | head -1 | cut -d= -f 2)
h=$(adb shell dumpsys window | grep -Eo 'DisplayHeight=\d+' | head -1 | cut -d= -f 2)
size="${w}x${h}"
fi
args="-P $size@$size/0"
set -o pipefail
shift
fi
# Create a directory for our resources
dir=/data/local/tmp/minicap-devel
adb shell "mkdir $dir 2>/dev/null"
# Upload the binary
adb push libs/$abi/$bin $dir
# Upload the shared library
if [ -e jni/minicap-shared/aosp/libs/android-$rel/$abi/minicap.so ]; then
adb push jni/minicap-shared/aosp/libs/android-$rel/$abi/minicap.so $dir
else
adb push jni/minicap-shared/aosp/libs/android-$sdk/$abi/minicap.so $dir
fi
# Run!
adb shell LD_LIBRARY_PATH=$dir $dir/$bin $args "$@"
# Clean up
adb shell rm -r $dir

在一些电脑上面

size=$(adb shell dumpsys window | grep -Eo 'init=\d+x\d+' | head -1 | cut -d= -f 2)

需要修改成

size=$(adb shell dumpsys window | grep -Eo 'init=[0-9]+x[0-9]+' | head -1 | cut -d= -f 2)

不然无法获取大 height 和 width

真碉堡!

在 github 看得一头雾水,搜到了楼主这篇好文,太赞了!!
补充一下:
运行 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1800@1080x1800/0 -t
提示 Permission denied 的,试下 adb shell chmod 0755 /data/local/tmp/minicap
另外,minicap 和 minicap.so,最好还是去官方 github 上直接下载自己手机对应的 https://github.com/openstf/stf/tree/master/vendor/minicap

58deMacBook-Pro:example wuxian$ PORT=9002 node app.js
Listening on port 9002
这步骤不懂····PORT 放前面么。 还有我 node app.js 就会报错的说
Error: Cannot find module 'ws'
at Function.Module._resolveFilename (module.js:336:15)
at Function.Module._load (module.js:278:25)
at Module.require (module.js:365:17)
at require (module.js:384:17)
at Object. (/Users/lemon/work/git/minicap/example/app.js:1:85)
at Module._compile (module.js:460:26)
at Object.Module._extensions..js (module.js:478:10)
at Module.load (module.js:355:32)
at Function.Module._load (module.js:310:12)
at Function.Module.runMain (module.js:501:10)

汪汪 [Topic was deleted] 中提及了此贴 29 Jun 20:34

为什么我最后页面上只显示一个红框

#8 楼 @ws328686288 你需要先 npm install

@doctorq
手机 cpu :armeabi-v7a
SDK: 23
Android 手机设备已 root
同时 adb remount
执行 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/ minicap.so -P 1280x720@1280x720/0 -t
报错: /system/bin/sh: /data/local/tmp/minicap.so: can't execute: Permission denied
请问 minicap.so 为什么没有访问权限呢?

@jianjianjianbing 我是从 github 上直接下载的,权限问题没有解决,请指教

minicap-nopie 的作用是:

if (($sdk >= 16)); then
bin=minicap
else
bin=minicap-nopie
fi

所以 sdk level>= 16 push minicamp, 否则 pushminicap-nopie

上面代码来自于:github/minicap/run.sh

#12 楼 @victors 基础问题 chmod 777 minicap; chmod 777 minicap.so

亲 有没有 C++ 或 MFC 的 图片解析 代码。。我转不过来。。。解不出图片。。。帮帮忙
@DoctorQ 博主大大 或 网友 谁 帮个忙

#15 楼 @lfjking @victors @kilmer 谁有 时间 帮个忙 昨天 图片 解了一天 没成。。 崩溃了

#5 楼 @kilmer 我用 MFC 解 图片 出来 图片 颜色 不对会是 什么原因呢?

这个没有 stf 配置出来的顺滑,是什么原因呢

汪汪 STF 框架之 minitouch 工具 中提及了此贴 06 Dec 15:53
易寒 关于自动化中截图操作讨论 中提及了此贴 20 Dec 12:27
wangst 关于自动化中截图操作讨论 中提及了此贴 20 Dec 16:34

你好请问下,读取到的 16 进制乱码,用 python 如何处理。

携带图片大小信息和图片二进制信息模块 (第二部分),只携带图片二进制信息模块 (第三部分) // 我觉得这个标题会让人觉得 minicap 会发送三部分数据,而实际只有两种形式,第一种 Banner,第二种:包含图片描述信息和图片内容的二进制块。发送完 Banner 后,就是不断的 图片描述 + 内容 的二进制块

codefarmer 回复

你好大哥 你解析出来了吗 我现在也在做这方面 转不过来呀

shuta STF 实时显示设备截图功能源码分析 中提及了此贴 27 May 11:01

多谢分享,特意注册个号来回复下。
我在 windows 上用 Android studio 直接打开,能正常运行。我的手机是 4.4 的。
遇到的问题有 push 库到手机上时,代码需要改下:

问题一:
要把原来代码的 File.separator 改成"/",否则 push 成了 tmp\minicap
修改前:
// 将 minicap 的可执行文件和.so 文件一起 push 到设备中
device.pushFile(minicapBinFile.getAbsolutePath(), REMOTE_PATH
+ File.separator + MINICAP_BIN);
device.pushFile(minicapSoFile.getAbsolutePath(), REMOTE_PATH
+ File.separator + MINICAP_SO);
修改后:
// 将 minicap 的可执行文件和.so 文件一起 push 到设备中
device.pushFile(minicapBinFile.getAbsolutePath(), REMOTE_PATH
+ "/" + MINICAP_BIN);
device.pushFile(minicapSoFile.getAbsolutePath(), REMOTE_PATH
+ "/" + MINICAP_SO);

问题二:
getADBPath 里面的 adb 路径需要写死。
修改前:
if(adbPath != null){
adbPath += File.separator + adbPlatformTools;
}else {
return null;
}
adbPath += File.separator + "adb";
修改后:
if(adbPath != null) {
adbPath += File.separator + adbPlatformTools;
} else {
adbPath = "D:\Software\android_tools\android-sdk\platform-tools";
}
}
adbPath += File.separator + "adb.exe";

git submodule init 失败的可以试试先 git submodule sync

陈恒捷 Appium IOS 测试速度优化策略 中提及了此贴 07 Aug 13:11
water iOS-minicap + WDA 实现 ios 远程真机测试 中提及了此贴 26 Sep 09:22

楼主你好,我也再看这个工具,这个工具是截图后传到 pc 上,如果我不需要传入到 pc 上,只保留到 sdcard 上,有什么办法么?

楼主,你好;
从 github 上下载的 minicap 没有你所说的这个文件夹 vendor/minicap,要怎么办?
你那有编译好的吗?

真厉害 对这个文章相见恨晚,学习了

PID: 7105
INFO: Using projection 1080x1920@1080x1920/0

请教,执行完 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0 -t 后,只显示这两行,并没有出现 OK 字样,是出了什么问题,需要清理什么杀掉进程吗 windows 和 shell 下面都没有这个 pid

adb forward 出来的端口只能本地访问,大家一般是怎么解决的,修改重编译 adb 还是本地写代码转发,还是怎样?

rayncc 回复

为啥我运行是一个红框:

lb 回复

选在 stf github 上面比较老的版本 比如 v1.2.0 就行了

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up