├── .travis.yml ├── README.md ├── mocksession_hflex_withmeta ├── scrcpy_client.py └── scrcpy_client_tests.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.5" 5 | - "3.6" 6 | 7 | 8 | before_install: 9 | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo add-apt-repository -y ppa:mc3man/trusty-media; fi 10 | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get update; fi 11 | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get install -y ffmpeg; fi 12 | 13 | # command to install dependencies 14 | install: 15 | - pip install numpy pillow nose pytest 16 | 17 | # command to run tests 18 | script: python /home/travis/build/Allong12/py-scrcpy/scrcpy_client_tests.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-scrcpy 2 | A client implementation of the Android interfacing SCRCPY project, completely in Python! 3 | 4 | [![Appveyor Build status](https://ci.appveyor.com/api/projects/status/o54ontuyjry5vv4n?svg=true)](https://ci.appveyor.com/project/Allong12/py-scrcpy) 5 | 6 | [![Travis CI Build Status](https://travis-ci.org/Allong12/py-scrcpy.svg?branch=master)](https://travis-ci.org/Allong12/py-scrcpy) 7 | 8 | This is only the client, which expects to be placed in the same directory to "SCRCPY" with ffmpeg and adb present in your PATH. 9 | 10 | 11 | Check out the original project here: 12 | https://github.com/Genymobile/scrcpy 13 | -------------------------------------------------------------------------------- /mocksession_hflex_withmeta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlLongley/py-scrcpy/c1845675eba8a504f7cba5bea25a8fac80c2fffd/mocksession_hflex_withmeta -------------------------------------------------------------------------------- /scrcpy_client.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Android viewing/controlling SCRCPY client class written in Python. 3 | Emphasis on near-zero latency, and low overhead for use in 4 | Machine Learning/AI scenarios. 5 | 6 | Check out the SCRCPY project here: 7 | https://github.com/Genymobile/scrcpy/ 8 | 9 | NB: Don't forget to set your path to scrcpy/adb 10 | 11 | Running stand-alone spawns an OpenCV2 window to view data real-time. 12 | ''' 13 | 14 | import socket 15 | import struct 16 | import sys 17 | import os 18 | import subprocess 19 | import io 20 | import time 21 | import numpy as np 22 | import logging 23 | 24 | from threading import Thread 25 | from queue import Queue, Empty 26 | 27 | SVR_maxSize = 600 28 | SVR_bitRate = 999999999 29 | SVR_tunnelForward = "true" 30 | SVR_crop = "9999:9999:0:0" 31 | SVR_sendFrameMeta = "true" 32 | 33 | IP = '127.0.0.1' 34 | PORT = 8080 35 | RECVSIZE = 0x10000 36 | HEADER_SIZE = 12 37 | 38 | SCRCPY_dir = 'C:\\Users\\Al\\Downloads\\scrcpy-win64-v1.5\\scrcpy-win64\\' 39 | FFMPEG_bin = 'ffmpeg' 40 | ADB_bin = os.path.join(SCRCPY_dir,"adb") 41 | #fd = open("savesocksession",'wb') 42 | 43 | logger = logging.getLogger(__name__) 44 | 45 | class SCRCPY_client(): 46 | def __init__(self): 47 | self.bytes_sent = 0 48 | self.bytes_rcvd = 0 49 | self.images_rcvd = 0 50 | self.bytes_to_read = 0 51 | self.FFmpeg_info = [] 52 | self.ACTIVE = True 53 | self.LANDSCAPE = True 54 | self.FFMPEGREADY = False 55 | self.ffoutqueue = Queue() 56 | 57 | 58 | def stdout_thread(self): 59 | logger.info("START STDOUT THREAD") 60 | while self.ACTIVE: 61 | rd = self.ffm.stdout.read(self.bytes_to_read) 62 | if rd: 63 | self.bytes_rcvd += len(rd) 64 | self.images_rcvd += 1 65 | self.ffoutqueue.put(rd) 66 | logger.info("FINISH STDOUT THREAD") 67 | 68 | def stderr_thread(self): 69 | logger.info("START STDERR THREAD") 70 | while self.ACTIVE: 71 | rd = self.ffm.stderr.readline() 72 | if rd: 73 | self.FFmpeg_info.append(rd.decode("utf-8")) 74 | logger.info("FINISH STDERR THREAD") 75 | 76 | def stdin_thread(self): 77 | logger.info("START STDIN THREAD") 78 | 79 | while self.ACTIVE: 80 | if SVR_sendFrameMeta: 81 | header = self.sock.recv(HEADER_SIZE) 82 | #fd.write(header) 83 | pts = int.from_bytes(header[:8], 84 | byteorder='big', signed=False) 85 | frm_len = int.from_bytes(header[8:], 86 | byteorder='big', signed=False) 87 | 88 | 89 | 90 | data = self.sock.recv(frm_len) 91 | #fd.write(data) 92 | self.bytes_sent += len(data) 93 | self.ffm.stdin.write(data) 94 | else: 95 | data = self.sock.recv(RECVSIZE) 96 | self.bytes_sent += len(data) 97 | self.ffm.stdin.write(data) 98 | 99 | logger.info("FINISH STDIN THREAD") 100 | 101 | def get_next_frame(self, most_recent=False): 102 | if self.ffoutqueue.empty(): 103 | return None 104 | 105 | if most_recent: 106 | frames_skipped = -1 107 | while not self.ffoutqueue.empty(): 108 | frm = self.ffoutqueue.get() 109 | frames_skipped +=1 110 | else: 111 | frm = self.ffoutqueue.get() 112 | 113 | frm = np.frombuffer(frm, dtype=np.ubyte) 114 | frm = frm.reshape((self.HEIGHT, self.WIDTH, 3)) 115 | return frm 116 | # PIL.Image.fromarray(np.uint8(rgb_img*255)) 117 | 118 | def connect(self): 119 | logger.info("Connecting") 120 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 121 | self.sock.connect((IP, PORT)) 122 | 123 | DUMMYBYTE = self.sock.recv(1) 124 | #fd.write(DUMMYBYTE) 125 | if not len(DUMMYBYTE): 126 | raise ConnectionError("Did not recieve Dummy Byte!") 127 | else: 128 | logger.info("Connected!") 129 | 130 | # Receive device specs 131 | devname = self.sock.recv(64) 132 | #fd.write(devname) 133 | self.deviceName = devname.decode("utf-8") 134 | 135 | if not len(self.deviceName): 136 | raise ConnectionError("Did not recieve Device Name!") 137 | logger.info("Device Name: "+self.deviceName) 138 | 139 | res = self.sock.recv(4) 140 | #fd.write(res) 141 | self.WIDTH, self.HEIGHT = struct.unpack(">HH", res) 142 | logger.info("WxH: "+str(self.WIDTH)+"x"+str(self.HEIGHT)) 143 | 144 | self.bytes_to_read = self.WIDTH * self.HEIGHT * 3 145 | 146 | return True 147 | 148 | def start_processing(self, connect_attempts=200): 149 | # Set up FFmpeg 150 | ffmpegCmd = [FFMPEG_bin, '-y', 151 | '-r', '20', '-i', 'pipe:0', 152 | '-vcodec', 'rawvideo', 153 | '-pix_fmt', 'rgb24', 154 | '-f', 'image2pipe', 155 | 'pipe:1'] 156 | try: 157 | self.ffm = subprocess.Popen(ffmpegCmd, 158 | stdin=subprocess.PIPE, 159 | stdout=subprocess.PIPE, 160 | stderr=subprocess.PIPE) 161 | except FileNotFoundError: 162 | raise FileNotFoundError("Couldn't find FFmpeg at path FFMPEG_bin: "+ 163 | str(FFMPEG_bin)) 164 | self.ffoutthrd = Thread(target=self.stdout_thread, 165 | args=()) 166 | self.fferrthrd = Thread(target=self.stderr_thread, 167 | args=()) 168 | self.ffinthrd = Thread(target=self.stdin_thread, 169 | args=()) 170 | self.ffoutthrd.daemon = True 171 | self.fferrthrd.daemon = True 172 | self.ffinthrd.daemon = True 173 | 174 | self.fferrthrd.start() 175 | time.sleep(0.25) 176 | self.ffinthrd.start() 177 | time.sleep(0.25) 178 | self.ffoutthrd.start() 179 | 180 | logger.info("Waiting on FFmpeg to detect source") 181 | for i in range(connect_attempts): 182 | if any(["Output #0, image2pipe" in x for x in self.FFmpeg_info]): 183 | logger.info("Ready!") 184 | self.FFMPEGREADY = True 185 | break 186 | time.sleep(1) 187 | logger.info('still waiting on FFmpeg...') 188 | else: 189 | logger.error("FFmpeg error?") 190 | logger.error(''.join(self.FFmpeg_info)) 191 | raise Exception("FFmpeg could not open stream") 192 | return True 193 | 194 | def kill_ffmpeg(self): 195 | self.ffm.terminate() 196 | self.ffm.kill() 197 | #self.ffm.communicate() 198 | def __del__(self): 199 | 200 | self.ACTIVE = False 201 | 202 | self.fferrthrd.join() 203 | self.ffinthrd.join() 204 | self.ffoutthrd.join() 205 | 206 | def connect_and_forward_scrcpy(): 207 | try: 208 | logger.info("Upload JAR...") 209 | adb_push = subprocess.Popen( 210 | [ADB_bin,'push', 211 | os.path.join(SCRCPY_dir,'scrcpy-server.jar'), 212 | '/data/local/tmp/'], 213 | stdout=subprocess.PIPE, 214 | stderr=subprocess.PIPE, 215 | cwd=SCRCPY_dir) 216 | adb_push_comm = ''.join([x.decode("utf-8") for x in adb_push.communicate() if x is not None]) 217 | 218 | if "error" in adb_push_comm: 219 | logger.critical("Is your device/emulator visible to ADB?") 220 | raise Exception(adb_push_comm) 221 | ''' 222 | ADB Shell is Blocking, don't wait up for it 223 | Args for the server are as follows: 224 | maxSize (integer, multiple of 8) 0 225 | bitRate (integer) 226 | tunnelForward (optional, bool) use "adb forward" instead of "adb tunnel" 227 | crop (optional, string) "width:height:x:y" 228 | sendFrameMeta (optional, bool) 229 | 230 | ''' 231 | logger.info("Run JAR") 232 | subprocess.Popen( 233 | [ADB_bin,'shell', 234 | 'CLASSPATH=/data/local/tmp/scrcpy-server.jar', 235 | 'app_process','/','com.genymobile.scrcpy.Server', 236 | str(SVR_maxSize),str(SVR_bitRate), 237 | SVR_tunnelForward, SVR_crop, SVR_sendFrameMeta], 238 | cwd=SCRCPY_dir) 239 | time.sleep(1) 240 | 241 | logger.info("Forward Port") 242 | subprocess.Popen( 243 | [ADB_bin,'forward', 244 | 'tcp:8080','localabstract:scrcpy'], 245 | cwd=SCRCPY_dir).wait() 246 | time.sleep(1) 247 | except FileNotFoundError: 248 | raise FileNotFoundError("Couldn't find ADB at path ADB_bin: "+ 249 | str(ADB_bin)) 250 | return True 251 | 252 | 253 | if __name__ == "__main__": 254 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 255 | 256 | assert connect_and_forward_scrcpy() 257 | 258 | SCRCPY = SCRCPY_client() 259 | SCRCPY.connect() 260 | SCRCPY.start_processing() 261 | 262 | import cv2 263 | try: 264 | while True: 265 | frm = SCRCPY.get_next_frame(most_recent=False) 266 | if isinstance(frm, (np.ndarray, np.generic)): 267 | frm = cv2.cvtColor(frm, cv2.COLOR_RGB2BGR) 268 | cv2.imshow("image", frm) 269 | cv2.waitKey(1000//60) # CAP 60FPS 270 | except KeyboardInterrupt: 271 | from IPython import embed 272 | embed() 273 | finally: 274 | pass 275 | #fd.close() 276 | -------------------------------------------------------------------------------- /scrcpy_client_tests.py: -------------------------------------------------------------------------------- 1 | import scrcpy_client 2 | import io 3 | import sys 4 | import numpy as np 5 | import time 6 | import socket 7 | import unittest 8 | import logging 9 | 10 | SHOWFRAMES = False 11 | 12 | #~136 frames from the game HyperFlex, with PTS meta enabled 13 | MOCKFILE = "mocksession_hflex_withmeta" 14 | 15 | #These were the settings enabled during the saved session 16 | scrcpy_client.SVR_maxSize = 600 17 | scrcpy_client.SVR_bitRate = 999999999 18 | scrcpy_client.SVR_tunnelForward = "true" 19 | scrcpy_client.SVR_crop = "9999:9999:0:0" 20 | scrcpy_client.SVR_sendFrameMeta = "true" 21 | 22 | if SHOWFRAMES: 23 | try: 24 | import cv2 25 | except ImportError: 26 | SHOWFRAMES = False 27 | 28 | class MockSocket(): 29 | ''' 30 | Replay a previously recorded socket session from a file 31 | ''' 32 | def __init__(self, *args): 33 | #print("Starting Mocked Socket",str(args)) 34 | self.filename=MOCKFILE 35 | self.fd = None 36 | 37 | def connect(self, *args): 38 | #print("Connecting Mocked Socket",str(args)) 39 | self.fd = open(self.filename,'rb') 40 | 41 | def recv(self, buffersize,*args): 42 | ret = self.fd.read(buffersize) 43 | 44 | return ret 45 | def __del__(self): 46 | if self.fd: 47 | self.fd.close() 48 | 49 | 50 | class TestClientMockConnect(unittest.TestCase): 51 | def setUp(self): 52 | 53 | self.SCRCPY = scrcpy_client.SCRCPY_client() 54 | # Replace the socket with our mock filebased "socket" 55 | scrcpy_client.socket.socket = MockSocket 56 | 57 | self.assertTrue(self.SCRCPY.connect()) 58 | self.assertTrue(self.SCRCPY.start_processing()) 59 | 60 | #Give FFmpeg a moment to catch up 61 | time.sleep(2.0) 62 | def test_resolution_recieved(self): 63 | self.assertTrue(self.SCRCPY.WIDTH>1) 64 | self.assertTrue(self.SCRCPY.HEIGHT>1) 65 | def test_ffmpeg_running(self): 66 | self.assertIs(self.SCRCPY.ffm.poll(), None) 67 | ''' 68 | def test_ffmpeg_detected_stream(self): 69 | ffinfo = ''.join(self.SCRCPY.FFmpeg_info) 70 | self.assertTrue("Stream #0:0 -> #0:0 (h264 -> rawvideo)" in ffinfo) 71 | ''' 72 | 73 | def test_frames_recieved(self): 74 | 75 | frames_counted = 0 76 | while True: 77 | frm=self.SCRCPY.get_next_frame() 78 | if isinstance(frm, (np.ndarray)): 79 | self.assertEqual(len(frm.shape),3) 80 | self.assertTrue(sum(frm.shape)>3) 81 | frames_counted+=1 82 | if SHOWFRAMES: 83 | frm = cv2.cvtColor(frm, cv2.COLOR_RGB2BGR) 84 | cv2.imshow("image", frm) 85 | cv2.waitKey(1000//60) # CAP 60FPS 86 | else: 87 | break 88 | self.assertTrue(frames_counted>10) 89 | 90 | #print("Recieved:",frames_counted) 91 | 92 | 93 | if __name__ == '__main__': 94 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 95 | unittest.main() 96 | --------------------------------------------------------------------------------