minicap java_AndroidControl/Minicap.java at master · imharryzhu/AndroidControl · GitHub

/*

*

* MIT License

*

* Copyright (c) 2017 朱辉 https://blog.yeetor.com

*

* Permission is hereby granted, free of charge, to any person obtaining a copy

* of this software and associated documentation files (the "Software"), to deal

* in the Software without restriction, including without limitation the rights

* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

* copies of the Software, and to permit persons to whom the Software is

* furnished to do so, subject to the following conditions:

*

* The above copyright notice and this permission notice shall be included in all

* copies or substantial portions of the Software.

*

* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

* SOFTWARE.

*

*/

package com.yeetor.minicap;

import com.android.ddmlib.*;

import com.yeetor.adb.AdbDevice;

import com.yeetor.adb.AdbForward;

import com.yeetor.adb.AdbServer;

import com.yeetor.util.Constant;

import com.yeetor.util.Util;

import org.apache.commons.lang3.StringUtils;

import org.apache.log4j.Logger;

import sun.misc.BASE64Decoder;

import java.io.*;

import java.net.Socket;

import java.util.*;

import java.util.List;

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.concurrent.TimeUnit;

import static com.yeetor.adb.AdbDevice.SCREEN_SIZE;

/**

* Created by harry on 2017/4/17.

*/

public class Minicap extends ScreencapBase {

private static Logger logger = Logger.getLogger(Minicap.class);

private static final String MINICAP_BIN = "minicap";

private static final String MINICAP_SO = "minicap.so";

private static final String REMOTE_PATH = "/data/local/tmp";

private AdbDevice device;

// 物理屏幕宽高

private Size deviceSize;

private boolean running = false;

// 启动minicap的线程

private Thread minicapThread, minicapInitialThread, dataReaderThread, imageParserThread;

private AdbForward forward;

// listener

private List listenerList = new ArrayList();

private BlockingQueue dataQueue = new LinkedBlockingQueue();

private Banner banner;

private Socket minicapSocket;

public static void installMinicap(AdbDevice device) throws MinicapInstallException {

if (device == null) {

throw new MinicapInstallException("device can't be null");

}

if (isMinicapInstalled(device)) {

return;

}

String sdk = device.getProperty(Constant.PROP_SDK);

String abi = device.getProperty(Constant.PROP_ABI);

if (StringUtils.isEmpty(sdk) || StringUtils.isEmpty(abi)) {

throw new MinicapInstallException("cant not get device info. please check device is connected");

}

sdk = sdk.trim();

abi = abi.trim();

// minicap

File minicap_bin = Constant.getMinicap(abi);

if (minicap_bin == null || !minicap_bin.exists()) {

throw new MinicapInstallException("File: " + minicap_bin.getAbsolutePath() + " not exists!");

}

try {

AdbServer.server().executePushFile(device.getIDevice(), minicap_bin.getAbsolutePath(), REMOTE_PATH + "/" + MINICAP_BIN);

} catch (Exception e) {

throw new MinicapInstallException(e.getMessage());

}

AdbServer.executeShellCommand(device.getIDevice(), "chmod 777 " + REMOTE_PATH + "/" + MINICAP_BIN);

// minicap.so

File minicap_so = Constant.getMinicapSo(abi, sdk);

if (minicap_so == null || !minicap_so.exists()) {

throw new MinicapInstallException("File: " + minicap_so.getAbsolutePath() + " not exists!");

}

try {

AdbServer.server().executePushFile(device.getIDevice(), minicap_so.getAbsolutePath(), REMOTE_PATH + "/" + MINICAP_SO);

} catch (Exception e) {

throw new MinicapInstallException(e.getMessage());

}

}

public Minicap(AdbDevice device) {

this.device = device;

try {

installMinicap(device);

} catch (MinicapInstallException e) {

e.printStackTrace();

}

// 获取屏幕大小

String[] ss = device.findPropertyCahe(SCREEN_SIZE).split("x");

int w = Integer.parseInt(ss[0]);

int h = Integer.parseInt(ss[1]);

deviceSize = new Size(w, h);

}

public Minicap(String serialNumber) {

this(AdbServer.server().getDevice(serialNumber));

}

public Minicap() {

this(AdbServer.server().getFirstDevice());

}

public void addEventListener(MinicapListener listener) {

if (listener != null) {

this.listenerList.add(listener);

}

}

public List getEventListener() {

return listenerList;

}

/*

Usage: /data/local/tmp/minicap [-h] [-n ]

-d : Display ID. (0)

-n : Change the name of the abtract unix domain socket. (minicap)

-P : Display projection (x@x/{0|90|180|270}).

-Q : JPEG quality (0-100).

-s: Take a screenshot and output it to stdout. Needs -P.

-S: Skip frames when they cannot be consumed quickly enough.

-t: Attempt to get the capture method running, then exit.

-i: Get display information in JSON format. May segfault.

*/

public String getMinicapCommand(int ow, int oh, int dw, int dh, int rotate, boolean shipFrame, String name, String[] args) {

ArrayList commands = new ArrayList();

commands.add(String.format("LD_LIBRARY_PATH=%s", REMOTE_PATH));

commands.add(REMOTE_PATH + "/" + MINICAP_BIN);

commands.add("-P");

commands.add(String.format("%dx%d@%dx%d/%d", ow, oh, dw, dh, rotate));

commands.add("-n");

commands.add(name);

if (shipFrame)

commands.add("-S");

if (args != null) {

for (String s : args) {

commands.add(s);

}

}

String command = StringUtils.join(commands, " ");

return command;

}

public AdbForward createForward() {

forward = generateForwardInfo();

try {

device.getIDevice().createForward(forward.getPort(), forward.getLocalabstract(), IDevice.DeviceUnixSocketNamespace.ABSTRACT);

} catch (Exception e) {

e.printStackTrace();

System.out.println("create forward failed");

}

return forward;

}

private void removeForward() {

if (forward == null || !forward.isForward()) {

return;

}

try {

device.getIDevice().removeForward(forward.getPort(), forward.getLocalabstract(), IDevice.DeviceUnixSocketNamespace.ABSTRACT);

forward = null;

} catch (Exception e) {

e.printStackTrace();

}

}

/**

* 屏幕截图

*

* 由于个平台的换行符导致二进制流输出有问题,二进制数据将先base64编码后传输

*

* @return

*/

public byte[] takeScreenShot() {

String command = getMinicapCommand(deviceSize.w, deviceSize.h, deviceSize.w, deviceSize.h, 0, false, "minicap", new String[] {"-s -b"});

logger.info(String.format("device(%s) takeScreenShot", device.getSerialNumber()));

BinaryOutputReceiver receiver = new BinaryOutputReceiver();

try {

device.getIDevice().executeShellCommand(command, receiver, 0);

} catch (Exception e) {

logger.warn("device execute command error!");

e.printStackTrace();

}

// remove text output

byte[] bytes = receiver.getOutput();

do {

String dataStr = new String(bytes);

int jpgStart = dataStr.indexOf("/9j/");

if (jpgStart >= 0) {

dataStr = dataStr.substring(jpgStart) ; // jpg特征码base64

} else {

break;

}

try {

bytes = new BASE64Decoder().decodeBuffer(dataStr);

} catch (IOException e) {

logger.warn("base64 decode error!");

e.printStackTrace();

break;

}

if (bytes[0] != -1 && bytes[1] != -40) {

logger.warn("not a jpg file!");

break;

}

return bytes;

} while(false);

return new byte[0];

}

public void start(int ow, int oh, int dw, int dh, int rotate, boolean shipFrame, String[] args) {

AdbForward forward = createForward();

String command = getMinicapCommand(ow, oh, dw, dh ,rotate, shipFrame, forward.getLocalabstract(), args);

logger.info("start minicap:" + command);

minicapThread = startMinicapThread(command);

minicapInitialThread = startInitialThread("127.0.0.1", forward.getPort());

}

public void start(final float scale, final int rotate) {

start(deviceSize.w, deviceSize.h, (int)(deviceSize.w * scale), (int)(deviceSize.h * scale), rotate,true, null);

}

public void reStart(final float scale, final int rotate) {

running = false;

if (minicapThread != null) {

minicapThread.stop();

}

if (dataReaderThread != null) {

try {

dataReaderThread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

if (imageParserThread != null) {

try {

imageParserThread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

start(scale, rotate);

}

public void kill() {

onClose();

running = false;

if (minicapThread != null) {

minicapThread.stop();

}

// 关闭socket

if (minicapSocket != null && minicapSocket.isConnected()) {

try {

minicapSocket.close();

} catch (IOException e) {

}

minicapSocket = null;

}

if (dataReaderThread != null) {

try {

dataReaderThread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

if (imageParserThread != null) {

try {

imageParserThread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

/**

* 启动线程开启 minicap

* @param shellCommand

* @return 线程

*/

private Thread startMinicapThread(final String shellCommand) {

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

try {

device.getIDevice().executeShellCommand(shellCommand, new IShellOutputReceiver() {

@Override

public void addOutput(byte[] bytes, int offset, int len) {

System.out.println("startMinicapThread" + new String(bytes, offset, len));

}

@Override

public void flush() {}

@Override

public boolean isCancelled() {

return false;

}

}, 0);

} catch (Exception e) {

e.printStackTrace();

}

}

});

thread.start();

return thread;

}

private Thread startInitialThread(final String host, final int port) {

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

try {

byte[] bytes = null;

int tryTime = 50;

while (true) {

// 连接minicap启动的服务

minicapSocket = new Socket(host, port);

InputStream inputStream = minicapSocket.getInputStream();

bytes = new byte[4096];

int n = inputStream.read(bytes);

if (n == -1) {

Thread.sleep(10);

minicapSocket.close();

} else {

// bytes内包含有信息,需要给Dataparser处理

dataQueue.add(Arrays.copyOfRange(bytes, 0, n));

running = true;

onStartup(true);

// 启动 DataReader ImageParser

dataReaderThread = startDataReaderThread(minicapSocket);

imageParserThread = startImageParserThread();

break;

}

tryTime--;

if (tryTime == 0) {

onStartup(false);

break;

}

}

} catch (Exception ex) {

ex.printStackTrace();

}

}

});

thread.start();

return thread;

}

/**

* 生成forward信息

*/

private AdbForward generateForwardInfo() {

AdbForward[] forwards = AdbServer.server().getForwardList();

// serial_cap_number

int maxNumber = 0;

if (forwards.length > 0) {

for (AdbForward forward : forwards) {

if (forward.getSerialNumber().equals(device.getIDevice().getSerialNumber())) {

String l = forward.getLocalabstract();

String[] s = l.split("_");

if (s.length == 3) {

int n = Integer.parseInt(s[2]);

if (n > maxNumber) maxNumber = n;

}

}

}

}

maxNumber += 1;

String forwardStr = String.format("%s_cap_%d", device.getIDevice().getSerialNumber(), maxNumber);

int freePort = Util.getFreePort();

AdbForward forward = new AdbForward(device.getIDevice().getSerialNumber(), freePort, forwardStr);

return forward;

}

private Thread startDataReaderThread(Socket minicapSocket) {

Thread thread = new Thread(new DataReader(minicapSocket));

thread.start();

return thread;

}

private Thread startImageParserThread() {

Thread thread = new Thread(new ImageParser());

thread.start();

return thread;

}

private void onStartup(boolean success) {

for (MinicapListener listener : listenerList) {

listener.onStartup(this, success);

}

}

private void onClose() {

for (MinicapListener listener : listenerList) {

listener.onClose(this);

}

removeForward();

}

private void onBanner(Banner banner) {

for (MinicapListener listener : listenerList) {

listener.onBanner(this, banner);

}

}

private void onJPG(byte[] data) {

for (MinicapListener listener : listenerList) {

listener.onJPG(this, data);

}

}

/**

* 判断该手机是否已经安装minicap

* @return

*/

public static boolean isMinicapInstalled(AdbDevice device) {

if (device == null || device.getIDevice() == null) {

return false;

}

String s = AdbServer.executeShellCommand(device.getIDevice(), String.format("LD_LIBRARY_PATH=%s %s/%s -i", REMOTE_PATH, REMOTE_PATH, MINICAP_BIN));

// TODO: 这里简单处理了一下

return s.startsWith("{");

}

private class DataReader implements Runnable {

static final int BUFF_SIZ = 4096;

Socket socket = null;

InputStream inputStream = null;

long ts = 0;

DataReader(Socket minicapSocket) {

this.socket = minicapSocket;

try {

this.inputStream = minicapSocket.getInputStream();

} catch (IOException e) {

e.printStackTrace();

onClose();

}

}

@Override

public void run() {

try {

readData();

} catch (IOException e) {

logger.warn("minicap lost connection: " + e.getMessage());

onClose();

}

}

public void readData() throws IOException {

DataInputStream stream = new DataInputStream(inputStream);

while (running) {

byte[] buffer = new byte[BUFF_SIZ];

ts = System.currentTimeMillis();

int len = stream.read(buffer);

if (len == -1) {

return;

}

if (len == BUFF_SIZ) {

dataQueue.add(buffer);

} else {

dataQueue.add(Util.subArray(buffer, 0, len));

}

}

}

}

private class ImageParser implements Runnable {

int readn = 0; // 已读大小

int bannerLen = 2; // banner信息大小

int readFrameBytes = 0;

int frameBodyLength = 0;

byte[] frameBody = new byte[0];

long t = 0;

@Override

public void run() {

while (running) {

try {

banner = new Banner();

readData();

} catch (InterruptedException e) {

System.out.println(e.getMessage());

onClose();

}

}

}

void readData() throws InterruptedException {

byte[] buffer = dataQueue.poll(5000, TimeUnit.MILLISECONDS);

if (buffer == null) { // TODO 使用阻塞队列就不用判断了

return;

}

int length = buffer.length;

for (int cursor = 0; cursor < length;) {

int ch = buffer[cursor] & 0xff;

if (readn < bannerLen) {

cursor = parserBanner(cursor, ch);

} else if(readFrameBytes < 4) { // frame length

frameBodyLength += (ch << (readFrameBytes * 8));

cursor += 1;

readFrameBytes += 1;

if (readFrameBytes == 4) {

t = System.currentTimeMillis();

}

} else {

if (length - cursor >= frameBodyLength) {

byte[] subByte = Arrays.copyOfRange(buffer, cursor,

cursor + frameBodyLength);

frameBody = Util.mergeArray(frameBody, subByte);

if ((frameBody[0] != -1) || frameBody[1] != -40) {

System.out.println("Frame body does not start with JPG header");

return;

}

byte[] finalBytes = Arrays.copyOfRange(frameBody, 0, frameBody.length);

onJPG(finalBytes);

cursor += frameBodyLength;

frameBodyLength = 0;

readFrameBytes = 0;

frameBody = new byte[0];

long timeused = (System.currentTimeMillis() - t);

timeused = timeused == 0 ? 1 : timeused;

String log = String.format("jpg: %d timeused: %dms fps: %d", finalBytes.length, (int)timeused, 1000 / timeused);

} else {

byte[] subByte = Arrays.copyOfRange(buffer, cursor, length);

frameBody = Util.mergeArray(frameBody, subByte);

frameBodyLength -= (length - cursor);

readFrameBytes += (length - cursor);

cursor = length;

}

}

}

}

// banner

int pid = 0;

int realWidth = 0;

int realHeight = 0;

int virtualWidth = 0;

int virtualHeight = 0;

int orientation = 0;

int quirks = 0;

int parserBanner(int cursor, int ch) {

switch (cursor) {

case 0:

banner.setVersion(ch);

break;

case 1:

bannerLen = ch;

banner.setLength(bannerLen);

break;

case 2:

case 3:

case 4:

case 5: {

pid += (ch << ((readn - 2) * 8));

if (cursor == 5)

banner.setPid(pid);

break;

}

case 6:

case 7:

case 8:

case 9:

{

realWidth += (ch << ((readn - 6) * 8));

if (cursor == 9)

banner.setReadWidth(realWidth);

break;

}

case 10:

case 11:

case 12:

case 13:

realHeight += (ch << ((readn - 10) * 8));

if (cursor == 13)

banner.setReadHeight(realHeight);

break;

case 14:

case 15:

case 16:

case 17:

virtualWidth += (ch << ((readn - 14) * 8));

if (cursor == 17)

banner.setVirtualWidth(virtualWidth);

break;

case 18:

case 19:

case 20:

case 21:

virtualHeight += (ch << ((readn - 18) * 8));

if (cursor == 21)

banner.setVirtualHeight(virtualHeight);

break;

case 22:

orientation = ch * 90;

banner.setOrientation(orientation);

break;

case 23:

quirks = ch;

banner.setQuirks(quirks);

onBanner(banner);

break;

}

++readn;

++cursor;

return cursor;

}

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值