diff --git a/module/device/method/adb.py b/module/device/method/adb.py index 282447ce2..48324fd5d 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -128,7 +128,7 @@ class Adb(Connection): if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') diff --git a/module/device/method/ascreencap.py b/module/device/method/ascreencap.py index 10ac1110c..c93321cfe 100644 --- a/module/device/method/ascreencap.py +++ b/module/device/method/ascreencap.py @@ -165,11 +165,11 @@ class AScreenCap(Connection): # ValueError: cannot reshape array of size 0 into shape (720,1280,4) raise ImageTruncated(str(e)) - image = cv2.flip(image, 0) + cv2.flip(image, 0, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.flip') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 39baeb4e5..1a5e71f62 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -6,11 +6,11 @@ import numpy as np import requests from adbutils.errors import AdbError -from module.base.decorator import Config, cached_property, del_cached_property +from module.base.decorator import cached_property, del_cached_property from module.base.timer import Timer -from module.device.method.uiautomator_2 import Uiautomator2, ProcessInfo -from module.device.method.utils import (retry_sleep, RETRY_TRIES, handle_adb_error, - ImageTruncated, PackageNotInstalled) +from module.device.method.uiautomator_2 import ProcessInfo, Uiautomator2 +from module.device.method.utils import ( + ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, retry_sleep) from module.exception import RequestHumanTakeover from module.logger import logger @@ -91,7 +91,7 @@ def retry(func): class DroidCast(Uiautomator2): """ DroidCast, another screenshot method, https://github.com/rayworks/DroidCast - DroidCast_raw, a modified version of DroidCast sending raw bitmap https://github.com/Torther/DroidCastS + DroidCast_raw, a modified version of DroidCast sending raw bitmap and png, https://github.com/Torther/DroidCastS """ _droidcast_port: int = 0 @@ -103,92 +103,73 @@ class DroidCast(Uiautomator2): self._droidcast_port = self.adb_forward('tcp:53516') return session - def droidcast_url(self, url='/screenshot?format=png'): - """ - Check APIs from source code: - https://github.com/rayworks/DroidCast/blob/master/app/src/main/java/com/rayworks/droidcast/Main.java - - Available APIs: - - /screenshot - To get JPG screenshots. - - /screenshot?format=png - To get PNG screenshots. - - /screenshot?format=webp - To get WEBP screenshots. - - /src - Websocket to get JPG screenshots. - - Note that /screenshot?format=jpg is unavailable. - """ + """ + Check APIs from source code: + https://github.com/Torther/DroidCast_raw/blob/DroidCast_raw/app/src/main/java/ink/mol/droidcast_raw/KtMain.kt + Available APIs: + - /screenshot + To get a RGB565 bitmap + - /preview + To get PNG screenshots. + """ + def droidcast_url(self, url='/preview'): + return f'http://127.0.0.1:{self._droidcast_port}{url}' + + def droidcast_raw_url(self, url='/screenshot'): return f'http://127.0.0.1:{self._droidcast_port}{url}' - @Config.when(DROIDCAST_VERSION='DroidCast') def droidcast_init(self): - logger.hr('Droidcast init') + logger.hr('DroidCast init') self.droidcast_stop() logger.info('Pushing DroidCast apk') self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE) logger.info('Starting DroidCast apk') - # CLASSPATH=/data/local/tmp/DroidCast.apk app_process / com.rayworks.droidcast.Main > /dev/null + # DroidCast_raw-release-1.0.apk + # CLASSPATH=/data/local/tmp/DroidCast_raw.apk app_process / ink.mol.droidcast_raw.Main > /dev/null + # adb shell CLASSPATH=/data/local/tmp/DroidCast_raw.apk app_process / ink.mol.droidcast_raw.Main resp = self.u2_shell_background([ - 'CLASSPATH=/data/local/tmp/DroidCast.apk', + 'CLASSPATH=/data/local/tmp/DroidCast_raw.apk', 'app_process', '/', - 'com.rayworks.droidcast.Main', + 'ink.mol.droidcast_raw.Main', '>', '/dev/null' ]) logger.info(resp) - del_cached_property(self, 'droidcast_session') _ = self.droidcast_session - logger.attr('DroidCast', self.droidcast_url()) - self.droidcast_wait_startup() - @Config.when(DROIDCAST_VERSION='DroidCast_raw') - def droidcast_init(self): - logger.hr('Droidcast init') - self.resolution_check_uiautomator2() - self.droidcast_stop() - - logger.info('Pushing DroidCast apk') - self.adb_push(self.config.DROIDCAST_RAW_FILEPATH_LOCAL, self.config.DROIDCAST_RAW_FILEPATH_REMOTE) - - logger.info('Starting DroidCast apk') - # DroidCastS-release-1.1.5.apk - # CLASSPATH=/data/local/tmp/DroidCastS-release-1.1.5.apk app_process / com.torther.droidcasts.Main > /dev/null - resp = self.u2_shell_background([ - 'CLASSPATH=/data/local/tmp/DroidCastS.apk', - 'app_process', - '/', - 'com.torther.droidcasts.Main', - '>', - '/dev/null' - ]) - logger.info(resp) - - del_cached_property(self, 'droidcast_session') - _ = self.droidcast_session - logger.attr('DroidCast', self.droidcast_url()) - self.droidcast_wait_startup() + if self.config.DROIDCAST_VERSION == 'DroidCast': + logger.attr('DroidCast', self.droidcast_url()) + self.droidcast_wait_startup() + elif self.config.DROIDCAST_VERSION == 'DroidCast_raw': + logger.attr('DroidCast_raw', self.droidcast_raw_url()) + self.droidcast_wait_startup() + else: + logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}') @retry def screenshot_droidcast(self): self.config.DROIDCAST_VERSION = 'DroidCast' - image = self.droidcast_session.get(self.droidcast_url(), timeout=3).content + resp = self.droidcast_session.get(self.droidcast_url(), timeout=3) + if resp.status_code == 404: + raise DroidCastVersionIncompatible('DroidCast server does not have /preview') + image = resp.content image = np.frombuffer(image, np.uint8) if image is None: raise ImageTruncated('Empty image after reading from buffer') if image.shape == (1843200,): raise DroidCastVersionIncompatible('Requesting screenshots from `DroidCast` but server is `DroidCast_raw`') + if image.size < 500: + logger.warning(f'Unexpected screenshot: {resp.content}') image = cv2.imdecode(image, cv2.IMREAD_COLOR) if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') @@ -197,12 +178,14 @@ class DroidCast(Uiautomator2): @retry def screenshot_droidcast_raw(self): self.config.DROIDCAST_VERSION = 'DroidCast_raw' - image = self.droidcast_session.get(self.droidcast_url(), timeout=3).content + image = self.droidcast_session.get(self.droidcast_raw_url(), timeout=3).content # DroidCast_raw returns a RGB565 bitmap try: arr = np.frombuffer(image, dtype=np.uint16).reshape((720, 1280)) except ValueError as e: + if len(image) < 500: + logger.warning(f'Unexpected screenshot: {image}') # Try to load as `DroidCast` image = np.frombuffer(image, np.uint8) if image is not None: @@ -228,14 +211,25 @@ class DroidCast(Uiautomator2): # image = cv2.merge([r, g, b]) # The same as the code above but costs about 5ms instead of 10ms. - r = cv2.multiply(arr & 0b1111100000000000, 0.00390625).astype(np.uint8) - g = cv2.multiply(arr & 0b0000011111100000, 0.125).astype(np.uint8) - b = cv2.multiply(arr & 0b0000000000011111, 8).astype(np.uint8) - r = cv2.add(r, cv2.multiply(r, 0.03125)) - g = cv2.add(g, cv2.multiply(g, 0.015625)) - b = cv2.add(b, cv2.multiply(b, 0.03125)) - image = cv2.merge([r, g, b]) + r = cv2.bitwise_and(arr, 0b1111100000000000) + cv2.multiply(r, 0.00390625, dst=r) + r = np.uint8(r) + m = cv2.multiply(r, 0.03125) + cv2.add(r, m, dst=r) + g = cv2.bitwise_and(arr, 0b0000011111100000) + cv2.multiply(g, 0.125, dst=g) + g = np.uint8(g) + m = cv2.multiply(g, 0.015625) + cv2.add(g, m, dst=g) + + b = cv2.bitwise_and(arr, 0b0000000000011111) + cv2.multiply(b, 8, dst=b) + b = np.uint8(b) + m = cv2.multiply(b, 0.03125) + cv2.add(b, m, dst=b) + + image = cv2.merge([r, g, b]) return image def droidcast_wait_startup(self): @@ -262,13 +256,12 @@ class DroidCast(Uiautomator2): def droidcast_uninstall(self): """ - Stop all DroidCast processes and remove DroidCast APK. - DroidCast has't been installed but a JAVA class call, uninstall is a file delete. + Stop DroidCast processes and remove DroidCast APK. + DroidCast hasn't been installed but a JAVA class call, uninstall is a file delete. """ self.droidcast_stop() logger.info('Removing DroidCast') self.adb_shell(["rm", self.config.DROIDCAST_FILEPATH_REMOTE]) - self.adb_shell(["rm", self.config.DROIDCAST_RAW_FILEPATH_REMOTE]) def _iter_droidcast_proc(self) -> t.Iterable[ProcessInfo]: """ @@ -280,10 +273,12 @@ class DroidCast(Uiautomator2): yield proc if 'com.torther.droidcasts.Main' in proc.cmdline: yield proc + if 'ink.mol.droidcast_raw.Main' in proc.cmdline: + yield proc def droidcast_stop(self): """ - Stop all DroidCast processes. + Stop DroidCast processes. """ logger.info('Stopping DroidCast') for proc in self._iter_droidcast_proc(): diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index dea534a31..69520f0a2 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -122,7 +122,7 @@ class Uiautomator2(Connection): if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor')