StarRailCopilot/module/device/method/scrcpy/core.py
2023-05-16 23:39:18 +08:00

217 lines
7.5 KiB
Python

import socket
import struct
import threading
import time
import typing as t
from time import sleep
import numpy as np
from adbutils import AdbError, Network
from module.base.decorator import cached_property
from module.base.timer import Timer
from module.device.connection import Connection
from module.device.method.scrcpy.control import ControlSender
from module.device.method.scrcpy.options import ScrcpyOptions
from module.device.method.utils import AdbConnection, recv_all
from module.exception import RequestHumanTakeover
from module.logger import logger
class ScrcpyError(Exception):
pass
class ScrcpyCore(Connection):
"""
Scrcpy: https://github.com/Genymobile/scrcpy
Module from https://github.com/leng-yue/py-scrcpy-client
"""
_scrcpy_last_frame: t.Optional[np.ndarray] = None
_scrcpy_last_frame_time: float = 0.
_scrcpy_alive = False
_scrcpy_server_stream: t.Optional[AdbConnection] = None
_scrcpy_video_socket: t.Optional[socket.socket] = None
_scrcpy_control_socket: t.Optional[socket.socket] = None
_scrcpy_control_socket_lock = threading.Lock()
_scrcpy_stream_loop_thread = None
_scrcpy_resolution: t.Tuple[int, int] = (1280, 720)
@cached_property
def _scrcpy_control(self) -> ControlSender:
return ControlSender(self)
def scrcpy_init(self):
self._scrcpy_server_stop()
logger.hr('Scrcpy init')
logger.info(f'pushing {self.config.SCRCPY_FILEPATH_LOCAL}')
self.adb_push(self.config.SCRCPY_FILEPATH_LOCAL, self.config.SCRCPY_FILEPATH_REMOTE)
self._scrcpy_alive = False
self.scrcpy_ensure_running()
def scrcpy_ensure_running(self):
if not self._scrcpy_alive:
with self._scrcpy_control_socket_lock:
self._scrcpy_server_start()
def _scrcpy_server_start(self):
"""
Connect to scrcpy server, there will be two sockets, video and control socket.
Raises:
ScrcpyError:
"""
logger.hr('Scrcpy server start')
commands = ScrcpyOptions.command_v120(jar_path=self.config.SCRCPY_FILEPATH_REMOTE)
self._scrcpy_server_stream: AdbConnection = self.adb.shell(
commands,
stream=True,
)
logger.info('Create server stream')
ret = self._scrcpy_server_stream.read(10)
# b'Aborted \r\n'
# Probably because file not exists
if b'Aborted' in ret:
raise ScrcpyError('Aborted')
if ret == b'[server] E':
# [server] ERROR: ...
ret += recv_all(self._scrcpy_server_stream)
logger.error(ret)
# java.lang.IllegalArgumentException: The server version (1.25) does not match the client (...)
if b'does not match the client' in ret:
raise ScrcpyError('Server version does not match the client')
else:
raise ScrcpyError('Unknown scrcpy error')
else:
# [server] INFO: Device: ...
ret += self._scrcpy_receive_from_server_stream()
logger.info(ret)
pass
logger.info('Create video socket')
timeout = Timer(3).start()
while 1:
if timeout.reached():
raise ScrcpyError('Connect scrcpy-server timeout')
try:
self._scrcpy_video_socket = self.adb.create_connection(
Network.LOCAL_ABSTRACT, "scrcpy"
)
break
except AdbError:
sleep(0.1)
dummy_byte = self._scrcpy_video_socket.recv(1)
if not len(dummy_byte) or dummy_byte != b"\x00":
raise ScrcpyError('Did not receive Dummy Byte from video stream')
logger.info('Create control socket')
self._scrcpy_control_socket = self.adb.create_connection(
Network.LOCAL_ABSTRACT, "scrcpy"
)
logger.info('Fetch device info')
device_name = self._scrcpy_video_socket.recv(64).decode("utf-8").rstrip("\x00")
if len(device_name):
logger.attr('Scrcpy Device', device_name)
else:
raise ScrcpyError('Did not receive Device Name')
ret = self._scrcpy_video_socket.recv(4)
self._scrcpy_resolution = struct.unpack(">HH", ret)
logger.attr('Scrcpy Resolution', self._scrcpy_resolution)
self._scrcpy_video_socket.setblocking(False)
self._scrcpy_alive = True
logger.info('Start video stream loop thread')
self._scrcpy_stream_loop_thread = threading.Thread(
target=self._scrcpy_stream_loop, daemon=True
)
self._scrcpy_stream_loop_thread.start()
while 1:
if self._scrcpy_stream_loop_thread is not None and self._scrcpy_stream_loop_thread.is_alive():
break
self.sleep(0.001)
logger.info('Scrcpy server is up')
def _scrcpy_server_stop(self):
"""
Stop listening (both threaded and blocked)
"""
logger.hr('Scrcpy server stop')
# err = self._scrcpy_receive_from_server_stream()
# if err:
# logger.error(err)
self._scrcpy_alive = False
if self._scrcpy_server_stream is not None:
try:
self._scrcpy_server_stream.close()
except Exception:
pass
if self._scrcpy_control_socket is not None:
try:
self._scrcpy_control_socket.close()
except Exception:
pass
if self._scrcpy_video_socket is not None:
try:
self._scrcpy_video_socket.close()
except Exception:
pass
logger.info('Scrcpy server stopped')
def _scrcpy_receive_from_server_stream(self):
if self._scrcpy_server_stream is not None:
try:
return self._scrcpy_server_stream.conn.recv(4096)
except Exception:
pass
def _scrcpy_stream_loop(self) -> None:
"""
Core loop for video parsing
"""
try:
from av.codec import CodecContext
from av.error import InvalidDataError
except ImportError as e:
logger.error(e)
logger.error('You must have `av` installed to use scrcpy screenshot, please update dependencies')
raise RequestHumanTakeover
codec = CodecContext.create("h264", "r")
while self._scrcpy_alive:
try:
raw_h264 = self._scrcpy_video_socket.recv(0x10000)
if raw_h264 == b"":
raise ScrcpyError("Video stream is disconnected")
packets = codec.parse(raw_h264)
for packet in packets:
frames = codec.decode(packet)
for frame in frames:
# logger.info('frame received')
frame = frame.to_ndarray(format="rgb24")
self._scrcpy_last_frame = frame
self._scrcpy_last_frame_time = time.time()
self._scrcpy_resolution = (frame.shape[1], frame.shape[0])
except (BlockingIOError, InvalidDataError):
# only return nonempty frames, may block cv2 render thread
time.sleep(0.001)
except (ConnectionError, OSError) as e: # Socket Closed
if self._scrcpy_alive:
logger.error(f'_scrcpy_stream_loop_thread: {repr(e)}')
raise
raise ScrcpyError('_scrcpy_stream_loop stopped')