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