From 27b9fcad43e10d238e7e0ef5da7a1e4ad4550102 Mon Sep 17 00:00:00 2001 From: Deko Date: Sat, 30 Nov 2024 15:44:31 +0100 Subject: [PATCH 1/7] bump config version --- alune/resources/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alune/resources/config.yaml b/alune/resources/config.yaml index 212bd96..9e6ae4f 100644 --- a/alune/resources/config.yaml +++ b/alune/resources/config.yaml @@ -44,6 +44,6 @@ adb_port: 5555 # Changing these below values manually can potentially break the bot, so don't! # Version of the YAML. -version: 7 +version: 8 # Version of the TFT set. set: 13 From b39f7178038eb69b327584136334ff54f700ec69 Mon Sep 17 00:00:00 2001 From: Deko Date: Tue, 3 Dec 2024 23:21:11 +0100 Subject: [PATCH 2/7] implement screen record --- alune/adb.py | 122 +++++++++++++++++++++++++++++++++++++++++++++------ main.py | 6 +++ 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/alune/adb.py b/alune/adb.py index d2adfaa..629d0bd 100644 --- a/alune/adb.py +++ b/alune/adb.py @@ -2,6 +2,8 @@ Module for all ADB (Android Debug Bridge) related methods. """ +import asyncio +import atexit import os.path import random @@ -9,6 +11,8 @@ from adb_shell.adb_device_async import AdbDeviceTcpAsync from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pythonrsa import PythonRSASigner +from adb_shell.exceptions import TcpTimeoutException +import av import cv2 from loguru import logger import numpy @@ -22,7 +26,9 @@ from alune.screen import ImageSearchResult -class ADB: +# The amount of attributes is fine in my opinion. +# We could split off screen recording into its own class, but I don't see the need to. +class ADB: # pylint-disable: too-many-instance-attributes """ Class to hold the connection to an ADB connection via TCP. USB connection is possible, but not supported at the moment. @@ -38,6 +44,11 @@ def __init__(self): self._rsa_signer = None self._device = None + self._video_codec = av.codec.CodecContext.create("h264", "r") + self._is_screen_recording = False + self._should_stop_screen_recording = False + self._latest_frame = None + async def load(self, port: int): """ Load the RSA signer and attempt to connect to a device via ADB TCP. @@ -97,6 +108,23 @@ async def scan_localhost_devices(self) -> int | None: logger.warning("No local device was found. Make sure ADB is enabled in your emulator's settings.") return None + def mark_screen_record_for_close(self): + """ + Tells the screen recording to close itself when possible. + """ + if self._is_screen_recording: + self._should_stop_screen_recording = True + + def _create_screen_record_task(self): + """ + Create the screen recording task. Will not start recording if there's already a recording. + """ + if self._is_screen_recording: + return + + asyncio.create_task(self.__screen_record()) + atexit.register(self.mark_screen_record_for_close) + async def _connect_to_device(self, port: int, retry_with_scan: bool = True): """ Connect to the device via TCP. @@ -107,6 +135,7 @@ async def _connect_to_device(self, port: int, retry_with_scan: bool = True): connection = await device.connect(rsa_keys=[self._rsa_signer], auth_timeout_s=1) if connection: self._device = device + self._create_screen_record_task() return except OSError: self._device = None @@ -136,7 +165,7 @@ async def get_screen_size(self) -> str: Returns: A string containing 'WIDTHxHEIGHT'. """ - shell_output = await self._device.shell("wm size | awk 'END{print $3}'") + shell_output = await self._device.exec_out("wm size | awk 'END{print $3}'") return shell_output.replace("\n", "") async def get_screen_density(self) -> str: @@ -146,20 +175,20 @@ async def get_screen_density(self) -> str: Returns: A string containing the pixel density. """ - shell_output = await self._device.shell("wm density | awk 'END{print $3}'") + shell_output = await self._device.exec_out("wm density | awk 'END{print $3}'") return shell_output.replace("\n", "") async def set_screen_size(self): """ Set the screen size to 1280x720. """ - await self._device.shell("wm size 1280x720") + await self._device.exec_out("wm size 1280x720") async def set_screen_density(self): """ Set the screen pixel density to 240. """ - await self._device.shell("wm density 240") + await self._device.exec_out("wm density 240") async def get_memory(self) -> int: """ @@ -168,13 +197,23 @@ async def get_memory(self) -> int: Returns: The memory of the device in kB. """ - shell_output = await self._device.shell("grep MemTotal /proc/meminfo | awk '{print $2}'") + shell_output = await self._device.exec_out("grep MemTotal /proc/meminfo | awk '{print $2}'") return int(shell_output) async def get_screen(self) -> ndarray | None: """ Gets a ndarray which contains the values of the gray-scaled pixels - currently on the screen. + currently on the screen. Uses buffered frames from screen recording, available instantly. + + Returns: + The ndarray containing the gray-scaled pixels. Is None until the first screen record frame is processed. + """ + return self._latest_frame + + async def get_screen_capture(self) -> ndarray | None: + """ + Gets a ndarray which contains the values of the gray-scaled pixels + currently on the screen. Uses screencap, so will take some processing time. Returns: The ndarray containing the gray-scaled pixels. @@ -237,7 +276,7 @@ async def click(self, x: int, y: int): """ # input tap x y comes with the downtime of tapping too fast for the game sometimes, # so we swipe on the same coordinate to simulate a longer press with a random duration. - await self._device.shell(f"input swipe {x} {y} {x} {y} {self._random.randint(60, 120)}") + await self._device.exec_out(f"input swipe {x} {y} {x} {y} {self._random.randint(60, 120)}") async def is_tft_installed(self) -> bool: """ @@ -246,7 +285,7 @@ async def is_tft_installed(self) -> bool: Returns: Whether the TFT package is in the list of the installed packages. """ - shell_output = await self._device.shell(f"pm list packages | grep {self.tft_package_name}") + shell_output = await self._device.exec_out(f"pm list packages | grep {self.tft_package_name}") if not shell_output: return False @@ -273,14 +312,14 @@ async def is_tft_active(self) -> bool: Returns: Whether TFT is the currently active window. """ - shell_output = await self._device.shell("dumpsys window | grep -E 'mCurrentFocus' | awk '{print $3}'") + shell_output = await self._device.exec_out("dumpsys window | grep -E 'mCurrentFocus' | awk '{print $3}'") return shell_output.split("/")[0].replace("\n", "") == self.tft_package_name async def start_tft_app(self): """ Start TFT using the activity manager (am). """ - await self._device.shell(f"am start -n {self.tft_package_name}/{self._tft_activity_name}") + await self._device.exec_out(f"am start -n {self.tft_package_name}/{self._tft_activity_name}") async def get_tft_version(self) -> str: """ @@ -289,7 +328,7 @@ async def get_tft_version(self) -> str: Returns: The versionName of the tft package. """ - return await self._device.shell( + return await self._device.exec_out( f"dumpsys package {self.tft_package_name} | grep versionName | sed s/[[:space:]]*versionName=//g" ) @@ -297,4 +336,61 @@ async def go_back(self): """ Send a back key press event to the device. """ - await self._device.shell("input keyevent 4") + await self._device.exec_out("input keyevent 4") + + async def __convert_frame_to_cv2(self, frame_bytes: bytes): + """ + Convert frame bytes to a CV2 compatible gray image. + + Args: + frame_bytes: Byte output of the screen record session. + """ + packets = self._video_codec.parse(frame_bytes) + if not packets: + return + + try: + frames = self._video_codec.decode(packets[0]) + except av.error.InvalidDataError: + return + if not frames: + return + + self._latest_frame = frames[0].to_ndarray(format="gray8").copy() # Change to bgr24 if color is ever needed. + + async def __write_frame_data(self): + """ + Start a streaming shell that outputs screenrecord frame bytes and store it as a cv2 compatible image. + """ + # output-format h264 > H264 is the only format that outputs to console which we can work with. + # bit-rate 16M > 16_000_000 Mbps, could probably be lowered or made configurable, but works well. + # - at the end makes screenrecord output to console, if format is h264. + async for data in self._device.streaming_shell( + command="screenrecord --output-format h264 --bit-rate 16M --size 1280x720 -", decode=False + ): + if self._should_stop_screen_recording: + break + + await self.__convert_frame_to_cv2(data) + + async def __screen_record(self): + """ + Start the screen record session. Restarts itself until an external value stops it. + """ + if self._is_screen_recording: + return + + await self._device.exec_out("pkill -2 screenrecord") + logger.debug("Screen record starting.") + + self._is_screen_recording = True + while not self._should_stop_screen_recording: + try: + await self.__write_frame_data() + except TcpTimeoutException: + logger.warning("Timed out while re-/starting screen record, waiting 5 seconds.") + await asyncio.sleep(5) + + logger.debug("Screen record stopped.") + await self._device.exec_out("pkill -2 screenrecord") + self._is_screen_recording = False diff --git a/main.py b/main.py index 1361067..f79f57f 100644 --- a/main.py +++ b/main.py @@ -548,6 +548,12 @@ async def main(): logger.debug("ADB is connected, checking phone and app details") await check_phone_preconditions(adb_instance) + + while await adb_instance.get_screen() is None: + logger.debug("Waiting for frame data to become available...") + await asyncio.sleep(0.5) + logger.debug("Frames are now available.") + logger.info("Connected to ADB and device is set up correctly, starting main loop.") if config.should_surrender(): From 8b6676986c73e7911d6376da1dc087e4707d24fd Mon Sep 17 00:00:00 2001 From: Deko Date: Tue, 3 Dec 2024 23:36:01 +0100 Subject: [PATCH 3/7] make screen recording configurable --- alune/adb.py | 24 +++++++++++++++--------- alune/config.py | 9 +++++++++ alune/resources/config.yaml | 10 +++++++++- main.py | 16 +++++++++------- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/alune/adb.py b/alune/adb.py index 629d0bd..d1fac39 100644 --- a/alune/adb.py +++ b/alune/adb.py @@ -13,6 +13,7 @@ from adb_shell.auth.sign_pythonrsa import PythonRSASigner from adb_shell.exceptions import TcpTimeoutException import av +from av.error import InvalidDataError # pylint: disable=no-name-in-module import cv2 from loguru import logger import numpy @@ -20,6 +21,7 @@ import psutil from alune import helpers +from alune.config import AluneConfig from alune.images import ClickButton from alune.images import ImageButton from alune.screen import BoundingBox @@ -28,13 +30,13 @@ # The amount of attributes is fine in my opinion. # We could split off screen recording into its own class, but I don't see the need to. -class ADB: # pylint-disable: too-many-instance-attributes +class ADB: # pylint: disable=too-many-instance-attributes """ Class to hold the connection to an ADB connection via TCP. USB connection is possible, but not supported at the moment. """ - def __init__(self): + def __init__(self, config: AluneConfig): """ Initiates base values for the ADB instance. """ @@ -43,21 +45,23 @@ def __init__(self): self._random = random.Random() self._rsa_signer = None self._device = None + self._config = config + self._default_port = config.get_adb_port() + + if not config.should_use_screen_record(): + return self._video_codec = av.codec.CodecContext.create("h264", "r") self._is_screen_recording = False self._should_stop_screen_recording = False self._latest_frame = None - async def load(self, port: int): + async def load(self): """ Load the RSA signer and attempt to connect to a device via ADB TCP. - - Args: - port: The port to attempt to connect to. """ await self._load_rsa_signer() - await self._connect_to_device(port) + await self._connect_to_device(self._default_port) async def _load_rsa_signer(self): """ @@ -208,7 +212,9 @@ async def get_screen(self) -> ndarray | None: Returns: The ndarray containing the gray-scaled pixels. Is None until the first screen record frame is processed. """ - return self._latest_frame + if self._config.should_use_screen_record(): + return self._latest_frame + return await self.get_screen_capture() async def get_screen_capture(self) -> ndarray | None: """ @@ -351,7 +357,7 @@ async def __convert_frame_to_cv2(self, frame_bytes: bytes): try: frames = self._video_codec.decode(packets[0]) - except av.error.InvalidDataError: + except InvalidDataError: return if not frames: return diff --git a/alune/config.py b/alune/config.py index a60e2e7..cd789f4 100644 --- a/alune/config.py +++ b/alune/config.py @@ -212,3 +212,12 @@ def get_queue_timeout(self) -> int: The queue timeout in seconds. """ return self._config["queue_timeout"] + + def should_use_screen_record(self) -> bool: + """ + Get the screen record setting the user wants. + + Returns: + Whether we should use screen recording. + """ + return self._config["screen_record"]["enabled"] diff --git a/alune/resources/config.yaml b/alune/resources/config.yaml index 9e6ae4f..e6a40db 100644 --- a/alune/resources/config.yaml +++ b/alune/resources/config.yaml @@ -31,6 +31,14 @@ surrender_early: false # Default value : 0 (disabled = surrender as fast as possible) surrender_random_delay: 0 +# Screen recording settings. +screen_record: + # Whether we should use screen recording. + # If false, the bot will fall back to screen capture. + # Screen recording saves ~600ms per screen check, but may occasionally cause a timeout. + # The timeout will be caught and handled. + enabled: true + # Configuration for chance events in the bot, in percent. chances: # The chance % to buy experience per check - configuring 50 means it's a 50% chance. @@ -44,6 +52,6 @@ adb_port: 5555 # Changing these below values manually can potentially break the bot, so don't! # Version of the YAML. -version: 8 +version: 9 # Version of the TFT set. set: 13 diff --git a/main.py b/main.py index f79f57f..bd493b3 100644 --- a/main.py +++ b/main.py @@ -291,7 +291,7 @@ async def loop_disconnect_wrapper(adb_instance: ADB, config: AluneConfig): await loop(adb_instance, config) except TcpTimeoutException: logger.warning("ADB device was disconnected, attempting one reconnect...") - await adb_instance.load(config.get_adb_port()) + await adb_instance.load() if not adb_instance.is_connected(): raise_and_exit("Could not reconnect. Please check your emulator for any errors. Exiting.") logger.info("Reconnected to device, continuing main loop.") @@ -539,9 +539,9 @@ async def main(): await check_version() - adb_instance = ADB() + adb_instance = ADB(config) - await adb_instance.load(config.get_adb_port()) + await adb_instance.load() if not adb_instance.is_connected(): logger.error("There is no ADB device ready. Exiting.") return @@ -549,10 +549,12 @@ async def main(): logger.debug("ADB is connected, checking phone and app details") await check_phone_preconditions(adb_instance) - while await adb_instance.get_screen() is None: - logger.debug("Waiting for frame data to become available...") - await asyncio.sleep(0.5) - logger.debug("Frames are now available.") + if config.should_use_screen_record(): + logger.info("The bot will use live screen recording for image searches.") + while await adb_instance.get_screen() is None: + logger.debug("Waiting for frame data to become available...") + await asyncio.sleep(0.5) + logger.debug("Frames are now available.") logger.info("Connected to ADB and device is set up correctly, starting main loop.") From 3add6d4157046e6832f70e3d14984d29555be6c1 Mon Sep 17 00:00:00 2001 From: Deko Date: Tue, 3 Dec 2024 23:42:27 +0100 Subject: [PATCH 4/7] move creation/stopping of screen records --- alune/adb.py | 7 +++---- main.py | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/alune/adb.py b/alune/adb.py index d1fac39..fbf6183 100644 --- a/alune/adb.py +++ b/alune/adb.py @@ -119,7 +119,7 @@ def mark_screen_record_for_close(self): if self._is_screen_recording: self._should_stop_screen_recording = True - def _create_screen_record_task(self): + def create_screen_record_task(self): """ Create the screen recording task. Will not start recording if there's already a recording. """ @@ -139,7 +139,6 @@ async def _connect_to_device(self, port: int, retry_with_scan: bool = True): connection = await device.connect(rsa_keys=[self._rsa_signer], auth_timeout_s=1) if connection: self._device = device - self._create_screen_record_task() return except OSError: self._device = None @@ -214,9 +213,9 @@ async def get_screen(self) -> ndarray | None: """ if self._config.should_use_screen_record(): return self._latest_frame - return await self.get_screen_capture() + return await self._get_screen_capture() - async def get_screen_capture(self) -> ndarray | None: + async def _get_screen_capture(self) -> ndarray | None: """ Gets a ndarray which contains the values of the gray-scaled pixels currently on the screen. Uses screencap, so will take some processing time. diff --git a/main.py b/main.py index bd493b3..1253cb9 100644 --- a/main.py +++ b/main.py @@ -291,7 +291,9 @@ async def loop_disconnect_wrapper(adb_instance: ADB, config: AluneConfig): await loop(adb_instance, config) except TcpTimeoutException: logger.warning("ADB device was disconnected, attempting one reconnect...") + adb_instance.mark_screen_record_for_close() await adb_instance.load() + adb_instance.create_screen_record_task() if not adb_instance.is_connected(): raise_and_exit("Could not reconnect. Please check your emulator for any errors. Exiting.") logger.info("Reconnected to device, continuing main loop.") @@ -551,6 +553,7 @@ async def main(): if config.should_use_screen_record(): logger.info("The bot will use live screen recording for image searches.") + adb_instance.create_screen_record_task() while await adb_instance.get_screen() is None: logger.debug("Waiting for frame data to become available...") await asyncio.sleep(0.5) From e8019b1f33238c0ba3dd792873027f0a1e5f35be Mon Sep 17 00:00:00 2001 From: Deko Date: Tue, 3 Dec 2024 23:51:09 +0100 Subject: [PATCH 5/7] add av to dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 5b68539..f289e49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "psutil==6.0.0", "keyboard==0.13.5", "imutils==0.5.4", + "av==14.0.0", ] [project.optional-dependencies] From 1bac2a804272c5409a0b285b8b7896b6798f034c Mon Sep 17 00:00:00 2001 From: Deko Date: Wed, 4 Dec 2024 00:10:27 +0100 Subject: [PATCH 6/7] fix setting screen record variables --- alune/adb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/alune/adb.py b/alune/adb.py index fbf6183..84add24 100644 --- a/alune/adb.py +++ b/alune/adb.py @@ -118,6 +118,7 @@ def mark_screen_record_for_close(self): """ if self._is_screen_recording: self._should_stop_screen_recording = True + self._is_screen_recording = False def create_screen_record_task(self): """ @@ -126,6 +127,7 @@ def create_screen_record_task(self): if self._is_screen_recording: return + self._should_stop_screen_recording = False asyncio.create_task(self.__screen_record()) atexit.register(self.mark_screen_record_for_close) @@ -368,10 +370,11 @@ async def __write_frame_data(self): Start a streaming shell that outputs screenrecord frame bytes and store it as a cv2 compatible image. """ # output-format h264 > H264 is the only format that outputs to console which we can work with. + # time-limit 10 > Restarts screen recording every 10 seconds instead of every 180. Fixes compression artifacts. # bit-rate 16M > 16_000_000 Mbps, could probably be lowered or made configurable, but works well. # - at the end makes screenrecord output to console, if format is h264. async for data in self._device.streaming_shell( - command="screenrecord --output-format h264 --bit-rate 16M --size 1280x720 -", decode=False + command="screenrecord --time-limit 10 --output-format h264 --bit-rate 16M --size 1280x720 -", decode=False ): if self._should_stop_screen_recording: break From 7aa916c161d0defc6b7c010e8538b74983286be5 Mon Sep 17 00:00:00 2001 From: Deko Date: Sat, 7 Dec 2024 11:52:10 +0100 Subject: [PATCH 7/7] wrap shell calls to catch more tcp timeouts --- alune/adb.py | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/alune/adb.py b/alune/adb.py index 84add24..e583808 100644 --- a/alune/adb.py +++ b/alune/adb.py @@ -170,7 +170,7 @@ async def get_screen_size(self) -> str: Returns: A string containing 'WIDTHxHEIGHT'. """ - shell_output = await self._device.exec_out("wm size | awk 'END{print $3}'") + shell_output = await self._wrap_shell_call("wm size | awk 'END{print $3}'") return shell_output.replace("\n", "") async def get_screen_density(self) -> str: @@ -180,20 +180,20 @@ async def get_screen_density(self) -> str: Returns: A string containing the pixel density. """ - shell_output = await self._device.exec_out("wm density | awk 'END{print $3}'") + shell_output = await self._wrap_shell_call("wm density | awk 'END{print $3}'") return shell_output.replace("\n", "") async def set_screen_size(self): """ Set the screen size to 1280x720. """ - await self._device.exec_out("wm size 1280x720") + await self._wrap_shell_call("wm size 1280x720") async def set_screen_density(self): """ Set the screen pixel density to 240. """ - await self._device.exec_out("wm density 240") + await self._wrap_shell_call("wm density 240") async def get_memory(self) -> int: """ @@ -202,7 +202,7 @@ async def get_memory(self) -> int: Returns: The memory of the device in kB. """ - shell_output = await self._device.exec_out("grep MemTotal /proc/meminfo | awk '{print $2}'") + shell_output = await self._wrap_shell_call("grep MemTotal /proc/meminfo | awk '{print $2}'") return int(shell_output) async def get_screen(self) -> ndarray | None: @@ -283,7 +283,7 @@ async def click(self, x: int, y: int): """ # input tap x y comes with the downtime of tapping too fast for the game sometimes, # so we swipe on the same coordinate to simulate a longer press with a random duration. - await self._device.exec_out(f"input swipe {x} {y} {x} {y} {self._random.randint(60, 120)}") + await self._wrap_shell_call(f"input swipe {x} {y} {x} {y} {self._random.randint(60, 120)}") async def is_tft_installed(self) -> bool: """ @@ -292,7 +292,7 @@ async def is_tft_installed(self) -> bool: Returns: Whether the TFT package is in the list of the installed packages. """ - shell_output = await self._device.exec_out(f"pm list packages | grep {self.tft_package_name}") + shell_output = await self._wrap_shell_call(f"pm list packages | grep {self.tft_package_name}") if not shell_output: return False @@ -319,14 +319,14 @@ async def is_tft_active(self) -> bool: Returns: Whether TFT is the currently active window. """ - shell_output = await self._device.exec_out("dumpsys window | grep -E 'mCurrentFocus' | awk '{print $3}'") + shell_output = await self._wrap_shell_call("dumpsys window | grep -E 'mCurrentFocus' | awk '{print $3}'") return shell_output.split("/")[0].replace("\n", "") == self.tft_package_name async def start_tft_app(self): """ Start TFT using the activity manager (am). """ - await self._device.exec_out(f"am start -n {self.tft_package_name}/{self._tft_activity_name}") + await self._wrap_shell_call(f"am start -n {self.tft_package_name}/{self._tft_activity_name}") async def get_tft_version(self) -> str: """ @@ -335,7 +335,7 @@ async def get_tft_version(self) -> str: Returns: The versionName of the tft package. """ - return await self._device.exec_out( + return await self._wrap_shell_call( f"dumpsys package {self.tft_package_name} | grep versionName | sed s/[[:space:]]*versionName=//g" ) @@ -343,7 +343,28 @@ async def go_back(self): """ Send a back key press event to the device. """ - await self._device.exec_out("input keyevent 4") + await self._wrap_shell_call("input keyevent 4") + + async def _wrap_shell_call(self, shell_command: str, retries: int = 0): + """ + Wrapper for shell commands to catch timeout exceptions. + Retries 3 times with incremental backoff. + + Args: + shell_command: The shell command to call. + retries: Optional, the amount of attempted retries so far. + + Returns: + The output of the shell command. + """ + try: + return await self._device.exec_out(shell_command) + except TcpTimeoutException: + if retries == 3: + raise + logger.debug(f"Timed out while calling '{shell_command}', retrying {3 - retries} times.") + await asyncio.sleep(1 + (1 * retries)) + return await self._wrap_shell_call(shell_command, retries=retries + 1) async def __convert_frame_to_cv2(self, frame_bytes: bytes): """ @@ -374,7 +395,7 @@ async def __write_frame_data(self): # bit-rate 16M > 16_000_000 Mbps, could probably be lowered or made configurable, but works well. # - at the end makes screenrecord output to console, if format is h264. async for data in self._device.streaming_shell( - command="screenrecord --time-limit 10 --output-format h264 --bit-rate 16M --size 1280x720 -", decode=False + command="screenrecord --time-limit 8 --output-format h264 --bit-rate 16M --size 1280x720 -", decode=False ): if self._should_stop_screen_recording: break