├── adb.py ├── capclient.py ├── gui.py ├── screencap-streamer.py ├── start-mirror.py ├── threadqueue.py └── touchclient.py /adb.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | ADBBIN = 'adb.exe' 4 | 5 | def run_adb(arguments, clean=False, as_str=False, print_out=False, out_file=None): 6 | if type(arguments) == str: 7 | arguments = arguments.split(' ') 8 | result = subprocess.run([ADBBIN] + arguments, stdout=subprocess.PIPE) 9 | stdout = result.stdout 10 | if clean: 11 | stdout = stdout.replace(b'\r\n', b'\n') 12 | if as_str: 13 | stdout = stdout.decode("utf-8") 14 | if print_out: 15 | print(stdout) 16 | if out_file: 17 | mode = 'w' if as_str else 'wb' 18 | with open(out_file, mode) as file: 19 | file.write(stdout) 20 | return stdout 21 | 22 | -------------------------------------------------------------------------------- /capclient.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | import errno 4 | from subprocess import Popen 5 | from threadqueue import ThreadedInOutQueue 6 | from time import sleep 7 | from adb import ADBBIN 8 | 9 | BANNER = 0 10 | BANNER_SIZE = 24 11 | HEAD = 1 12 | HEAD_SIZE = 4 13 | DATA = 2 14 | 15 | class CapClient(ThreadedInOutQueue): 16 | def __init__(self, parent): 17 | ThreadedInOutQueue.__init__(self) 18 | disp_max = max(parent.size) 19 | dev_max = max(parent.orig) 20 | args = "-P %ux%u@%ux%u/0 -S -Q 80" % (dev_max, dev_max, disp_max, disp_max) 21 | cmd = [ADBBIN, "shell", "LD_LIBRARY_PATH=%s %s/minicap %s" % (parent.path, parent.path, args)] 22 | self.server = Popen(cmd) 23 | 24 | def cut_data(self, size): 25 | tmp = self.data[:size] 26 | self.data = self.data[size:] 27 | return tmp 28 | 29 | def run(self): 30 | sleep(1) 31 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | self.socket.connect(("localhost", 1313)) 33 | self.socket.setblocking(0) 34 | self.running = True 35 | self.state = BANNER 36 | self.data = b"" 37 | 38 | while self.running: 39 | for msg in self.internal_read(): 40 | cmd = msg[0] 41 | if cmd == "end": 42 | self.running = False 43 | try: 44 | data = self.socket.recv(1024 * 1024) 45 | self.data += data 46 | except socket.error as e: 47 | err = e.args[0] 48 | if err == errno.EAGAIN or err == errno.EWOULDBLOCK: 49 | pass 50 | if self.state == BANNER and len(self.data) >= BANNER_SIZE: 51 | banner_data = self.cut_data(BANNER_SIZE) 52 | self.banner = struct.unpack("= HEAD_SIZE: 56 | head_data = self.cut_data(HEAD_SIZE) 57 | self.data_size, = struct.unpack("= self.data_size: 60 | img_data = self.cut_data(self.data_size) 61 | self.internal_write(["data", img_data]) 62 | self.state = HEAD 63 | 64 | self.socket.close() 65 | self.server.kill() 66 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | import numpy as np 4 | import threading 5 | from time import time 6 | from io import BytesIO, StringIO 7 | from capclient import CapClient 8 | from touchclient import TouchClient 9 | from PIL import Image 10 | 11 | class Main(): 12 | def __init__(self): 13 | assert len(sys.argv) == 4 14 | self.size = list(map(int, sys.argv[1].split("x"))) 15 | orig = list(map(int, sys.argv[2].split("x"))) 16 | self.orig = orig[0], orig[1] 17 | self.path = sys.argv[3] 18 | 19 | self.cap = CapClient(self) 20 | self.cap.start() 21 | self.touch = TouchClient(self) 22 | self.touch.start() 23 | 24 | self.mouse_down = False 25 | self.mouse_pos = (0, 0) 26 | 27 | self.scale = self.orig[0] / float(self.size[0]) 28 | self.ratio = self.orig[1] / float(self.orig[0]) 29 | print('Scale, ratio:', self.scale, self.ratio) 30 | 31 | self.sizep = self.size[0], int(self.orig[1] / self.scale) 32 | self.sizel = int(self.orig[1] / self.scale), self.size[0] 33 | print('Raw image size l/p:', self.sizel, self.sizep) 34 | 35 | self.rotation = 0 36 | self.calc_scale() 37 | 38 | pygame.init() 39 | pygame.font.init() 40 | self.screen = pygame.display.set_mode(self.size) 41 | 42 | def calc_scale(self): 43 | self.landscape = self.rotation in [90, 270] 44 | max_w = self.size[0] 45 | if self.landscape: 46 | x = 0 47 | w = max_w 48 | h = w / self.ratio 49 | y = (self.size[1] - h) / 2 50 | else: 51 | y = 0 52 | h = self.size[1] 53 | w = h / self.ratio 54 | x = (self.size[0] - w) / 2 55 | 56 | self.proj = list(map(int, [x, y, w, h])) 57 | print('Projection:', self.proj) 58 | self.frame_update = True 59 | 60 | def blit_center(self, dst, src, rect): 61 | x = rect[0] - int((src.get_width() / 2) - (rect[2] / 2)) 62 | y = rect[1] - int((src.get_height() / 2) - (rect[3] / 2)) 63 | dst.blit(src, (x, y)) 64 | 65 | def exit(self): 66 | self.running = False 67 | self.cap.write(["end"]) 68 | self.touch.write(["end"]) 69 | 70 | def save_image(self, img, fn): 71 | img.save(fn,"PNG") 72 | 73 | def events(self): 74 | for event in pygame.event.get(): 75 | if event.type == pygame.QUIT: 76 | self.exit() 77 | 78 | if hasattr(event, "pos"): 79 | ix, iy = event.pos 80 | fx = min(max(0, (ix - self.proj[0]) / float(self.proj[2])), 1) 81 | fy = min(max(0, (iy - self.proj[1]) / float(self.proj[3])), 1) 82 | if self.rotation == 0: 83 | x = fx 84 | y = fy 85 | if self.rotation == 90: 86 | x = 1.0 - fy 87 | y = fx 88 | if self.rotation == 180: 89 | x = 1.0 - fx 90 | y = 1.0 - fy 91 | if self.rotation == 270: 92 | x = fy 93 | y = 1.0 - fx 94 | pygame.display.set_caption(str(x) + ' - ' + str(y)) 95 | 96 | if hasattr(event, "button"): 97 | if event.button is not 1: 98 | continue 99 | if event.type == pygame.MOUSEBUTTONDOWN: 100 | self.touch.write(["down", x, y]) 101 | self.mouse_down = True 102 | if event.type == pygame.MOUSEBUTTONUP: 103 | self.touch.write(["up"]) 104 | self.mouse_down = False 105 | 106 | if event.type == pygame.MOUSEMOTION: 107 | if self.mouse_down: 108 | self.touch.write(["move", x, y]) 109 | self.mouse_pos = (x, y) 110 | 111 | 112 | def run(self): 113 | self.running = True 114 | self.screen_update = True 115 | self.frame_update = False 116 | self.frame_cache = pygame.Surface(self.size) 117 | last_frame = None 118 | 119 | while self.running: 120 | self.events() 121 | 122 | msgs = self.cap.read() 123 | msgl = len(msgs) 124 | if msgl: 125 | msg = msgs[msgl - 1] 126 | cmd = msg[0] 127 | if cmd == "data": 128 | last_frame = pygame.image.load(BytesIO(msg[1])) 129 | self.frame_update = True 130 | 131 | if self.frame_update: 132 | self.frame_update = False 133 | if last_frame is not None: 134 | if self.landscape: 135 | a = last_frame.subsurface(pygame.Rect((0,0), self.sizel)) 136 | else: 137 | a = last_frame.subsurface(pygame.Rect((0,0), self.sizep)) 138 | aw, ah = a.get_size() 139 | if aw != self.proj[2] or ah != self.proj[3]: 140 | self.frame_cache = pygame.transform.smoothscale(a, (self.proj[2], self.proj[3])) 141 | else: 142 | self.frame_cache = a.copy() 143 | self.screen_update = True 144 | 145 | if self.screen_update: 146 | self.screen.fill((0, 0, 0)) 147 | self.screen_update = False 148 | self.screen.blit(self.frame_cache, (self.proj[0], self.proj[1])) 149 | pygame.display.update() 150 | 151 | 152 | 153 | a = Main() 154 | a.run() 155 | -------------------------------------------------------------------------------- /screencap-streamer.py: -------------------------------------------------------------------------------- 1 | from adb import * 2 | import tkinter as tk 3 | from time import time 4 | from PIL import ImageTk, Image 5 | from io import BytesIO 6 | 7 | window = tk.Tk() 8 | window.title("Image") 9 | window.geometry("360x660") 10 | window.configure(background='grey') 11 | 12 | panel = tk.Label(window) 13 | panel.pack(side="bottom", fill="both", expand="yes") 14 | 15 | previous_time = time() 16 | frames_drawn = 0 17 | while True: 18 | data = run_adb('exec-out screencap -p', clean=False) 19 | im = Image.open(BytesIO(data)) 20 | im.thumbnail((im.size[0] * .33, im.size[1] * .33), Image.ANTIALIAS) 21 | img = ImageTk.PhotoImage(im) 22 | panel.configure(image=img) 23 | panel.image = img 24 | window.update_idletasks() 25 | window.update() 26 | frames_drawn += 1 27 | if time() > previous_time + 10: 28 | print('FPS:', frames_drawn / (time() - previous_time)) 29 | previous_time = time() 30 | frames_drawn = 0 -------------------------------------------------------------------------------- /start-mirror.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import re 3 | from time import time 4 | from PIL import ImageTk, Image 5 | from io import BytesIO 6 | from os.path import isfile 7 | from os import remove, mkdir 8 | from shutil import copyfile, rmtree, copy 9 | from adb import * 10 | 11 | 12 | if __name__ == '__main__': 13 | abi = run_adb('shell getprop ro.product.cpu.abi', as_str=True).strip() 14 | sdk = run_adb('shell getprop ro.build.version.sdk', as_str=True).strip() 15 | rel = run_adb('shell getprop ro.build.version.release', as_str=True).strip() 16 | dev_size = run_adb('shell wm size', as_str=True).strip() 17 | mtch = re.search(r"(\d+x\d+)", dev_size) 18 | dev_size = mtch.group(1) 19 | 20 | print('Device info:', abi, sdk, rel, dev_size) 21 | 22 | dev_dir = '/data/local/tmp/adbmirror/' 23 | 24 | minicap = 'bin/minicap/{}/minicap'.format(abi) 25 | minitouch = 'bin/minitouch/{}/minitouch'.format(abi) 26 | minicap_shared = 'bin/minicap-shared/android-{}/{}/minicap.so'.format(rel, abi) 27 | if not isfile(minicap_shared): 28 | minicap_shared = 'bin/minicap-shared/android-{}/{}/minicap.so'.format(sdk, abi) 29 | 30 | print('Now pushing files') 31 | run_adb('push {} {}'.format(minicap, dev_dir), as_str=True, print_out=True) 32 | run_adb('push {} {}'.format(minitouch, dev_dir), as_str=True, print_out=True) 33 | run_adb('push {} {}'.format(minicap_shared, dev_dir), as_str=True, print_out=True) 34 | 35 | run_adb('shell chmod 0777 {}*'.format(dev_dir), as_str=True, print_out=True) 36 | 37 | run_adb('forward tcp:1313 localabstract:minicap', as_str=True, print_out=True) 38 | run_adb('forward tcp:1111 localabstract:minitouch', as_str=True, print_out=True) 39 | 40 | print('Now ready to start GUI, press ENTER when done for cleanup') 41 | print('Example command:') 42 | print('python gui.py {} {} {}'.format('540x960', dev_size, dev_dir)) 43 | input() 44 | 45 | run_adb('shell killall minicap', as_str=True, print_out=True) 46 | run_adb('shell killall minitouch', as_str=True, print_out=True) 47 | run_adb('shell rm {}*'.format(dev_dir), as_str=True, print_out=True) 48 | -------------------------------------------------------------------------------- /threadqueue.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from multiprocessing import Queue 3 | 4 | class ThreadedInOutQueue(threading.Thread): 5 | def __init__(self): 6 | threading.Thread.__init__(self) 7 | self.q_in = Queue() 8 | self.q_out = Queue() 9 | 10 | def read(self): 11 | msgs = [] 12 | try: 13 | while True: 14 | msgs.append(self.q_out.get(False)) 15 | finally: 16 | return msgs 17 | return msgs 18 | 19 | def write(self, data): 20 | self.q_in.put(data) 21 | 22 | def internal_read(self): 23 | msgs = [] 24 | try: 25 | while True: 26 | msgs.append(self.q_in.get(False)) 27 | finally: 28 | return msgs 29 | 30 | def internal_write(self, data): 31 | self.q_out.put(data) 32 | 33 | -------------------------------------------------------------------------------- /touchclient.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import errno 3 | from subprocess import Popen 4 | from threadqueue import ThreadedInOutQueue 5 | from time import sleep 6 | from adb import ADBBIN 7 | 8 | class TouchClient(ThreadedInOutQueue): 9 | def __init__(self, parent): 10 | ThreadedInOutQueue.__init__(self) 11 | cmd = [ADBBIN, "shell", " %s/minitouch" % (parent.path)] 12 | self.server = Popen(cmd) 13 | # Sensible defaults for my device, can be overridden later on 14 | self.pressure = 0 15 | self.max_x = 1079 16 | self.max_y = 1919 17 | 18 | def cut_data(self, size): 19 | tmp = self.data[:size] 20 | self.data = self.data[size:] 21 | return tmp 22 | 23 | def send(self, data): 24 | data = bytes(data, 'UTF-8') + b"\nc\n"; 25 | self.socket.sendall(data) 26 | 27 | def run(self): 28 | sleep(1) 29 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 30 | self.socket.connect(("localhost", 1111)) 31 | self.socket.setblocking(0) 32 | self.running = True 33 | self.data = b"" 34 | 35 | while self.running: 36 | for msg in self.internal_read(): 37 | cmd = msg[0] 38 | if cmd == "end": 39 | self.running = False 40 | if cmd == "down": 41 | x = int(msg[1] * self.max_x) 42 | y = int(msg[2] * self.max_y) 43 | self.send("d 0 %u %u %u" % (x, y, self.pressure)) 44 | if cmd == "up": 45 | self.send("u 0") 46 | if cmd == "move": 47 | x = int(msg[1] * self.max_x) 48 | y = int(msg[2] * self.max_y) 49 | self.send("m 0 %u %u %u" % (x, y, self.pressure)) 50 | 51 | try: 52 | data = self.socket.recv(1024) 53 | self.data += data 54 | except socket.error as e: 55 | err = e.args[0] 56 | if err == errno.EAGAIN or err == errno.EWOULDBLOCK: 57 | pass 58 | 59 | while b'\n' in self.data: 60 | data = self.cut_data(self.data.find(b'\n') + 1) 61 | data = data.split() 62 | print('Data received:', data) 63 | if data[0] is b'v': 64 | self.version = int(data[1]) 65 | if data[0] is b'$': 66 | self.pid = int(data[1]) 67 | if data[0] is b'^': 68 | self.max_x = int(data[2]) 69 | self.max_y = int(data[3]) 70 | self.pressure = int(data[4]) 71 | 72 | self.socket.close() 73 | self.server.kill() 74 | --------------------------------------------------------------------------------