/*

*

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

}

}

}

minicap java_AndroidControl/Minicap.java at master · imharryzhu/AndroidControl · GitHub相关推荐

  1. java并发框架支持锁包括,tip/面试题_并发与多线程.md at master · 171437912/tip · GitHub...

    01. java用()机制实现了进程之间的同步执行 A. 监视器 B. 虚拟机 C. 多个CPU D. 异步调用 正解: A 解析: 监视器机制即锁机制 02. 线程安全的map在JDK 1.5及其更 ...

  2. java j集合_JNotes/Java-集合篇(2)集合之List.md at master · harryjudy2240/JNotes · GitHub...

    ArrayList 存储机制 基于动态数组实现,默认长度 10 :因为数组特性,其读取速度快,增删效率慢 因为写入或读取都是通过数组索引直接操作,所以,允许为 null 值, 且可以重复 当数组大小不 ...

  3. linux at java,Linux-Tutorial/Java-bin.md at master · linsanityHuang/Linux-Tutorial · GitHub

    Java bin 目录下的工具 JVM 内存结构 运行时数据区(JVM 规范) VM 栈(JVM 虚拟机栈) 是线程私有的,它的生命周期和线程相同.它描述的是 Java 方法执行的内存模式. Java ...

  4. c await和java_blog/java/test/awaitility.zh.md at master · c-rainstorm/blog · GitHub

    ```java AtomicInteger atomic = new AtomicInteger(0); // Do some async stuff that eventually updates ...

  5. java cookbook 3_CookBook/Java核心/3-Java反射.md at master · zhgdbut/CookBook · GitHub

    #Java核心(三)反射 Java反射给我们提供了在运行时检查甚至修改应用行为的机制. 反射是java高级的核心技术,所有有经验的程序员都应该理解. 通过反射机制,我们可以在运行时检视 类.接口.枚举 ...

  6. java编程规范每行代码窄字符,wiki/0xFE_编程规范.md at master · islibra/wiki · GitHub

    0xFE_编程规范 使用UTF-8编码 使用空格缩进 命名 清晰表达意图, 少用缩写(行业通用除外, 如: request=req, response=resp, message=msg), 不应使用 ...

  7. java p=x,Java-Tutorial/20、javac和javap.md at master · allenchenx/Java-Tutorial · GitHub

    目录 title: 夯实Java基础系列20:从IDE的实现原理聊起,谈谈那些年我们用过的Java命令 date: 2019-9-20 15:56:26 # 文章生成时间,一般不改 categorie ...

  8. java 正则表达式 table_JavaEdge/Java/Java中正则表达式.md at master · VegTableBird/JavaEdge · GitHub...

    主要用到的是这两个类 - java.util.regex.Pattern - java.util.regex.Matcher. Pattern对应正则表达式,一个Pattern与一个String对象关 ...

  9. think in java第6_think-in-java/6.5 protected.md at master · quanke/think-in-java · GitHub

    6.5 protected 现在我们已理解了继承的概念,protected这个关键字最后终于有了意义.在理想情况下,private成员随时都是"私有"的,任何人不得访问.但在实际应 ...

最新文章

  1. TensorFlow优化器及用法
  2. php校友录毕业论文,基于WEB的同学校友录的设计(PHP,MySQL)(附答辩记录)
  3. Nature 子刊:加州大学Banfield组揭示CPR细菌和DPANN古菌多样性及与低温TEM下宿主互作关系...
  4. java socket datagramsocket_用DatagramSocket写的个渣渣聊天后台,求拍砖
  5. Android中Application类用法
  6. Zoom Host可以真正看到您的所有私人消息吗?
  7. Python源码深度解析—对象的行为和多态性
  8. [设计素材]你也在找CTBiaoSongSJ吗?我这里有耶!
  9. ubuntu20.04 NVIDIA显卡驱动安装教程(Y9000p)
  10. 第3章 词性标注(pos、N-gram、NER等标注器)
  11. java 去掉图片水印文字_Java实现图片水印工具类
  12. tomcat 设置缓存大小
  13. 天马G6二期、腾龙光谷数据中心 落户武汉东湖高新区
  14. ATECC508A芯片开发笔记(十一):NXP 平台移植ATECCx08 CryptoAuthLib库(I2C)
  15. 使用Dubbo实现简单的RPC调用(Spring配置文件版)
  16. python项目对接钉钉SDK
  17. 东风破 苏轼 喜欢的词,方文山的歌词好象就是这样借过来的。
  18. 开放手机联盟(OHA)
  19. android平板投屏,Mac、ipad投屏安卓平板
  20. 序列向量和时间向量matlab,MATLAB日期和时间处理

热门文章

  1. android 华为pad 自动对焦,有料更有“生产力”,华为平板M6体验,补足安卓生态短板...
  2. OpenOcean 是世界上第一个领先的完整聚合器
  3. 百度搜索“一起自学吧”做大数据专家,不做第一,只做唯一
  4. 在网页上播放多种后缀的视频文件的临时解决方案
  5. php中strchr的语法,如何使用php strchr函数
  6. 职称计算机execl试题,职称计算机考试2017年Excel全真模拟试题
  7. android如何确保应用进程不被杀死,使得一个android应用不会被进程杀死
  8. 关于辛普森积分法的研究
  9. Windows10 ls命令
  10. 【笔记】华莱士(Wallis)公式