autoxkit 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {autoxkit-2.1.0 → autoxkit-2.2.0}/PKG-INFO +1 -1
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/__init__.py +34 -35
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/adb.py +13 -13
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/binary.py +6 -6
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/client.py +66 -70
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/control.py +13 -13
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/control_backup.py +3 -3
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/models.py +1 -1
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/android/streams.py +16 -17
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/hook/hotkey_listener.py +0 -3
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit.egg-info/PKG-INFO +1 -1
- {autoxkit-2.1.0 → autoxkit-2.2.0}/pyproject.toml +1 -1
- {autoxkit-2.1.0 → autoxkit-2.2.0}/LICENSE.txt +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/README.md +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/__init__.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/constants.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/hook/__init__.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/hook/event.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/hook/hook_listener.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/match/__init__.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/match/match.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/mousekey/__init__.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/mousekey/input.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/mousekey/keyboard.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/mousekey/mouse.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/utils.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/window/__init__.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/window/window.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/window/window_action.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit/window/window_match.py +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit.egg-info/SOURCES.txt +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit.egg-info/dependency_links.txt +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit.egg-info/requires.txt +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/autoxkit.egg-info/top_level.txt +0 -0
- {autoxkit-2.1.0 → autoxkit-2.2.0}/setup.cfg +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Android
|
|
1
|
+
"""Android 设备控制模块。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -23,13 +23,12 @@ __all__ = [
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class AndroidDevice:
|
|
26
|
-
"""
|
|
26
|
+
"""同步 Android 设备控制器。
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
callbacks (``key_down``, ``key_up``, etc.).
|
|
28
|
+
将异步的 ``ScrcpyClient`` 封装在后台线程中,使所有公共方法都成为
|
|
29
|
+
普通的同步调用,适合在钩子回调中使用(``key_down``、``key_up`` 等)。
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
用法::
|
|
33
32
|
|
|
34
33
|
from autoxkit.android import AndroidDevice
|
|
35
34
|
|
|
@@ -55,7 +54,7 @@ class AndroidDevice:
|
|
|
55
54
|
self._client: ScrcpyClient | None = None
|
|
56
55
|
self._session_size: tuple[int, int] | None = None
|
|
57
56
|
|
|
58
|
-
# ──
|
|
57
|
+
# ── 连接管理 ────────────────────────────────
|
|
59
58
|
|
|
60
59
|
def connect(
|
|
61
60
|
self,
|
|
@@ -66,50 +65,50 @@ class AndroidDevice:
|
|
|
66
65
|
control: bool = True,
|
|
67
66
|
**kwargs: Any,
|
|
68
67
|
) -> None:
|
|
69
|
-
"""
|
|
68
|
+
"""通过 scrcpy-server 连接到 Android 设备。
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
参数将传递给 :class:`ScrcpyOptions`。常用额外参数:
|
|
72
71
|
|
|
73
|
-
- ``tunnel_forward`` (bool):
|
|
74
|
-
``adb reverse
|
|
75
|
-
- ``tcpip`` (bool):
|
|
76
|
-
- ``tcpip_dst`` (str):
|
|
72
|
+
- ``tunnel_forward`` (bool): 使用 ``adb forward`` 而不是
|
|
73
|
+
``adb reverse``。
|
|
74
|
+
- ``tcpip`` (bool): 启动前启用 ADB over TCP/IP。
|
|
75
|
+
- ``tcpip_dst`` (str): 已知的无线 ADB 地址。
|
|
77
76
|
"""
|
|
78
77
|
self._submit(
|
|
79
78
|
self._connect_async(serial, video=video, audio=audio, control=control, **kwargs)
|
|
80
79
|
)
|
|
81
80
|
|
|
82
81
|
def disconnect(self) -> None:
|
|
83
|
-
"""
|
|
82
|
+
"""断开与设备的连接并停止 scrcpy-server。"""
|
|
84
83
|
self._submit(self._disconnect_async())
|
|
85
84
|
|
|
86
85
|
@property
|
|
87
86
|
def connected(self) -> bool:
|
|
88
87
|
return self._client is not None
|
|
89
88
|
|
|
90
|
-
# ──
|
|
89
|
+
# ── 触摸 ────────────────────────────────────────────────
|
|
91
90
|
|
|
92
91
|
def touch(self, x: int, y: int, action: str) -> None:
|
|
93
|
-
"""
|
|
92
|
+
"""发送触摸事件。
|
|
94
93
|
|
|
95
94
|
Args:
|
|
96
|
-
x:
|
|
97
|
-
y:
|
|
98
|
-
action: ``"down"
|
|
95
|
+
x: 屏幕 X 坐标。
|
|
96
|
+
y: 屏幕 Y 坐标。
|
|
97
|
+
action: ``"down"``、``"up"`` 或 ``"move"``。
|
|
99
98
|
"""
|
|
100
99
|
action_code = {"down": 0, "up": 1, "move": 2}.get(action)
|
|
101
100
|
if action_code is None:
|
|
102
|
-
raise ValueError(f"
|
|
101
|
+
raise ValueError(f"无效的触摸动作:{action!r}(请使用 down/up/move)")
|
|
103
102
|
self._submit(self._touch_async(action_code, x, y))
|
|
104
103
|
|
|
105
104
|
def tap(self, x: int, y: int, delay: float = 0.05) -> None:
|
|
106
|
-
"""
|
|
105
|
+
"""在屏幕坐标上执行点击(触摸按下然后抬起)。"""
|
|
107
106
|
self.touch(x, y, "down")
|
|
108
107
|
time.sleep(delay)
|
|
109
108
|
self.touch(x, y, "up")
|
|
110
109
|
|
|
111
110
|
def swipe(self, x1: int, y1: int, x2: int, y2: int, steps: int = 10) -> None:
|
|
112
|
-
"""
|
|
111
|
+
"""执行从 (x1, y1) 到 (x2, y2) 的滑动手势。"""
|
|
113
112
|
self.touch(x1, y1, "down")
|
|
114
113
|
for i in range(1, steps + 1):
|
|
115
114
|
t = i / steps
|
|
@@ -118,33 +117,33 @@ class AndroidDevice:
|
|
|
118
117
|
self.touch(cx, cy, "move")
|
|
119
118
|
self.touch(x2, y2, "up")
|
|
120
119
|
|
|
121
|
-
# ──
|
|
120
|
+
# ── 按键码 ──────────────────────────────────────────────
|
|
122
121
|
|
|
123
122
|
def keycode(self, keycode: int, action: str = "down") -> None:
|
|
124
|
-
"""
|
|
123
|
+
"""发送 Android 按键码事件。
|
|
125
124
|
|
|
126
125
|
Args:
|
|
127
|
-
keycode: Android
|
|
128
|
-
action: ``"down"``
|
|
126
|
+
keycode: Android 按键码(例如 4 = 返回,3 = 主页)。
|
|
127
|
+
action: ``"down"`` 或 ``"up"``。
|
|
129
128
|
"""
|
|
130
129
|
action_code = {"down": 0, "up": 1}.get(action)
|
|
131
130
|
if action_code is None:
|
|
132
|
-
raise ValueError(f"
|
|
131
|
+
raise ValueError(f"无效的按键动作:{action!r}(请使用 down/up)")
|
|
133
132
|
self._submit(self._keycode_async(action_code, keycode))
|
|
134
133
|
|
|
135
134
|
def key_press(self, keycode: int) -> None:
|
|
136
|
-
"""
|
|
135
|
+
"""按下并释放按键(模拟单次点击)。"""
|
|
137
136
|
self.keycode(keycode, "down")
|
|
138
137
|
time.sleep(0.02)
|
|
139
138
|
self.keycode(keycode, "up")
|
|
140
139
|
|
|
141
|
-
# ──
|
|
140
|
+
# ── 剪贴板 ────────────────────────────────────────────
|
|
142
141
|
|
|
143
142
|
def set_clipboard(self, text: str, paste: bool = False) -> None:
|
|
144
|
-
"""
|
|
143
|
+
"""设置设备剪贴板(可选同时粘贴)。"""
|
|
145
144
|
self._submit(self._clipboard_async(text, paste))
|
|
146
145
|
|
|
147
|
-
# ──
|
|
146
|
+
# ── 内部异步辅助方法 ───────────────────────────────
|
|
148
147
|
|
|
149
148
|
async def _connect_async(
|
|
150
149
|
self,
|
|
@@ -178,21 +177,21 @@ class AndroidDevice:
|
|
|
178
177
|
|
|
179
178
|
async def _touch_async(self, action: int, x: int, y: int) -> None:
|
|
180
179
|
if self._client is None or self._client.control is None:
|
|
181
|
-
raise RuntimeError("
|
|
180
|
+
raise RuntimeError("未连接到设备")
|
|
182
181
|
width, height = self._session_size or (1, 1)
|
|
183
182
|
await self._client.control.send_touch(action, x, y, width, height)
|
|
184
183
|
|
|
185
184
|
async def _keycode_async(self, action: int, keycode: int) -> None:
|
|
186
185
|
if self._client is None or self._client.control is None:
|
|
187
|
-
raise RuntimeError("
|
|
186
|
+
raise RuntimeError("未连接到设备")
|
|
188
187
|
await self._client.control.send_keycode(action, keycode)
|
|
189
188
|
|
|
190
189
|
async def _clipboard_async(self, text: str, paste: bool) -> None:
|
|
191
190
|
if self._client is None or self._client.control is None:
|
|
192
|
-
raise RuntimeError("
|
|
191
|
+
raise RuntimeError("未连接到设备")
|
|
193
192
|
await self._client.control.set_clipboard(text, sequence=1, paste=paste)
|
|
194
193
|
|
|
195
|
-
# ──
|
|
194
|
+
# ── 异步循环桥接 ─────────────────────────────────────
|
|
196
195
|
|
|
197
196
|
def _submit(self, coro: Any) -> Any:
|
|
198
197
|
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""ADB
|
|
1
|
+
"""ADB 启动器,用于 scrcpy-server v4.0."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -40,33 +40,33 @@ class AdbServerLauncher:
|
|
|
40
40
|
await self._adb("tcpip", str(port))
|
|
41
41
|
|
|
42
42
|
async def get_device_ip(self) -> str:
|
|
43
|
-
"""
|
|
43
|
+
"""从 `adb shell ip route` 解析 Wi-Fi IP 地址。
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
仅考虑接口名称以 ``wlan`` 开头的行,
|
|
46
|
+
与官方 scrcpy 行为一致,跳过非 Wi-Fi 接口,
|
|
47
|
+
例如 rmnet_data(移动数据)或 usb0(USB 共享网络)。
|
|
48
48
|
"""
|
|
49
49
|
route = await self.shell_output("ip", "route")
|
|
50
50
|
for line in route.splitlines():
|
|
51
|
-
#
|
|
51
|
+
# 查找类似如下的行:
|
|
52
52
|
# 192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x
|
|
53
53
|
fields = line.split()
|
|
54
54
|
for i, field in enumerate(fields):
|
|
55
55
|
if field == "dev" and i + 1 < len(fields) and fields[i + 1].startswith("wlan"):
|
|
56
|
-
#
|
|
56
|
+
# 找到 wlan 行,提取源 IP
|
|
57
57
|
for j, f in enumerate(fields):
|
|
58
58
|
if f == "src" and j + 1 < len(fields):
|
|
59
59
|
ip = fields[j + 1]
|
|
60
60
|
if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", ip):
|
|
61
61
|
return ip
|
|
62
62
|
raise AdbError(
|
|
63
|
-
f"
|
|
63
|
+
f"在 `adb shell ip route` 中找不到 Wi-Fi(wlan)设备的 IP: {route.strip()}"
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
async def discover_usb_serial(self) -> str | None:
|
|
67
|
-
"""
|
|
67
|
+
"""发现单个 USB 连接设备的序列号。
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
如果没有 USB 设备或有多个设备,则返回 None。
|
|
70
70
|
"""
|
|
71
71
|
proc = await asyncio.create_subprocess_exec(
|
|
72
72
|
str(self.adb_path),
|
|
@@ -80,7 +80,7 @@ class AdbServerLauncher:
|
|
|
80
80
|
|
|
81
81
|
usb_serials = []
|
|
82
82
|
for line in out.splitlines():
|
|
83
|
-
#
|
|
83
|
+
# 跳过空行和 "List of devices attached" 标题行
|
|
84
84
|
if not line.strip() or not line[0].isalnum():
|
|
85
85
|
continue
|
|
86
86
|
parts = line.split()
|
|
@@ -90,8 +90,8 @@ class AdbServerLauncher:
|
|
|
90
90
|
state = parts[1]
|
|
91
91
|
if state != "device":
|
|
92
92
|
continue
|
|
93
|
-
# USB
|
|
94
|
-
# TCP/IP
|
|
93
|
+
# USB 设备的序列号不包含 ":"
|
|
94
|
+
# TCP/IP 设备的格式为 ip:port
|
|
95
95
|
if ":" not in serial:
|
|
96
96
|
usb_serials.append(serial)
|
|
97
97
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""scrcpy v4.0 协议的二进制辅助函数。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -10,15 +10,15 @@ ReadExact = Callable[[int], Awaitable[bytes]]
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ProtocolError(Exception):
|
|
13
|
-
"""
|
|
13
|
+
"""当网络上的字节不符合预期协议时抛出。"""
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class StreamDisabledError(ProtocolError):
|
|
17
|
-
"""
|
|
17
|
+
"""当 scrcpy-server 显式禁用媒体流时抛出。"""
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class StreamConfigurationError(ProtocolError):
|
|
21
|
-
"""
|
|
21
|
+
"""当 scrcpy-server 报告媒体流配置错误时抛出。"""
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def u16be(value: int) -> bytes:
|
|
@@ -65,7 +65,7 @@ async def read_exact(reader: asyncio.StreamReader, size: int) -> bytes:
|
|
|
65
65
|
try:
|
|
66
66
|
return await reader.readexactly(size)
|
|
67
67
|
except asyncio.IncompleteReadError as exc:
|
|
68
|
-
raise EOFError(f"
|
|
68
|
+
raise EOFError(f"期望 {size} 字节,实际获取 {len(exc.partial)} 字节") from exc
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def utf8_truncate(text: str, max_bytes: int) -> bytes:
|
|
@@ -88,7 +88,7 @@ def string32(text: str, max_payload_len: int) -> bytes:
|
|
|
88
88
|
|
|
89
89
|
def string8(text: str, max_payload_len: int = 255) -> bytes:
|
|
90
90
|
if max_payload_len > 255:
|
|
91
|
-
raise ValueError("1
|
|
91
|
+
raise ValueError("1字节字符串不能超过255字节")
|
|
92
92
|
raw = utf8_truncate(text, max_payload_len)
|
|
93
93
|
return bytes([len(raw)]) + raw
|
|
94
94
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""高级 scrcpy-server v4.0 客户端外观类。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ from .streams import AudioStream, AudioVideoCombinedStream, ControlStream, DEVIC
|
|
|
15
15
|
|
|
16
16
|
@dataclass(slots=True)
|
|
17
17
|
class ScrcpyOptions:
|
|
18
|
-
"""
|
|
18
|
+
"""用于启动和连接 scrcpy-server v4.0 的选项。"""
|
|
19
19
|
|
|
20
20
|
serial: str | None = None
|
|
21
21
|
adb_path: Path = Path("scrcpy-win64-v4.0") / "adb.exe"
|
|
@@ -32,87 +32,83 @@ class ScrcpyOptions:
|
|
|
32
32
|
push_server: bool = True
|
|
33
33
|
merge_video_config_packets: bool = True
|
|
34
34
|
tcpip: bool = False
|
|
35
|
-
"""
|
|
35
|
+
"""在启动 scrcpy-server 之前启用 ADB-over-TCP/IP。
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
``<device-ip>:<tcpip_port>``.
|
|
37
|
+
如果设置了 ``tcpip_dst``,客户端运行 ``adb connect`` 连接到该地址,
|
|
38
|
+
并将其用作设备序列号。如果未设置 ``tcpip_dst``,客户端
|
|
39
|
+
假设当前选择了一个 USB 设备,从 ``adb shell ip route`` 读取其 WLAN IP 地址,
|
|
40
|
+
运行 ``adb tcpip <tcpip_port>``,然后连接到 ``<device-ip>:<tcpip_port>``。
|
|
42
41
|
"""
|
|
43
42
|
|
|
44
43
|
tcpip_dst: str | None = None
|
|
45
|
-
"""
|
|
44
|
+
"""已知的无线 ADB 地址。
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
接受的格式为 ``"192.168.1.23"`` 和 ``"192.168.1.23:5555"``。如果省略端口,
|
|
47
|
+
则会附加 ``tcpip_port``。
|
|
49
48
|
"""
|
|
50
49
|
|
|
51
50
|
tcpip_port: int = 5555
|
|
52
|
-
"""
|
|
51
|
+
"""用于 ``adb tcpip`` 和默认地址端口的无线 ADB TCP 端口。"""
|
|
53
52
|
|
|
54
53
|
tcpip_disconnect_existing: bool = False
|
|
55
|
-
"""
|
|
54
|
+
"""在 ``adb connect <address>`` 之前运行 ``adb disconnect <address>``。"""
|
|
56
55
|
|
|
57
56
|
server_args: list[str] = field(default_factory=list)
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
``video
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- ``
|
|
68
|
-
- ``
|
|
69
|
-
- ``
|
|
70
|
-
|
|
71
|
-
``mic-
|
|
72
|
-
``
|
|
73
|
-
|
|
74
|
-
- ``
|
|
75
|
-
- ``
|
|
76
|
-
- ``
|
|
77
|
-
- ``
|
|
78
|
-
- ``
|
|
79
|
-
- ``crop``: ``width:height:x:y``.
|
|
57
|
+
"""传递给 ``scrcpy-server`` 的额外原始 ``key=value`` 参数。
|
|
58
|
+
|
|
59
|
+
这些值会附加在 ``ScrcpyClient`` 直接管理的参数之后。除非你也相应地更新了
|
|
60
|
+
Python 端的连接逻辑,否则避免覆盖已管理的键(``scid``、``log_level``、
|
|
61
|
+
``video``、``audio``、``control`` 和 ``tunnel_forward``)。
|
|
62
|
+
|
|
63
|
+
scrcpy-server v4.0 的常见值:
|
|
64
|
+
|
|
65
|
+
- ``video_codec``: ``h264``、``h265`` 或 ``av1``。
|
|
66
|
+
- ``audio_codec``: ``opus``、``aac``、``flac`` 或 ``raw``。
|
|
67
|
+
- ``video_source``: ``display`` 或 ``camera``。
|
|
68
|
+
- ``audio_source``: ``output``、``mic``、``playback``、
|
|
69
|
+
``mic-unprocessed``、``mic-camcorder``、``mic-voice-recognition``、
|
|
70
|
+
``mic-voice-communication``、``voice-call``、``voice-call-uplink``、
|
|
71
|
+
``voice-call-downlink`` 或 ``voice-performance``。
|
|
72
|
+
- ``video_bit_rate`` / ``audio_bit_rate``: 整数比特率(单位:比特/秒)。
|
|
73
|
+
- ``max_size``: 整数最大视频尺寸,``0`` 表示无限制。
|
|
74
|
+
- ``max_fps``: 浮点数/整数 FPS 字符串,例如 ``30`` 或 ``60``。
|
|
75
|
+
- ``min_size_alignment``: ``1``、``2``、``4``、``8`` 或 ``16``。
|
|
76
|
+
- ``angle``: 浮点度数字符串。
|
|
77
|
+
- ``crop``: ``width:height:x:y``。
|
|
80
78
|
- ``video_codec_options`` / ``audio_codec_options``:
|
|
81
|
-
|
|
82
|
-
- ``video_encoder`` / ``audio_encoder``: Android
|
|
83
|
-
- ``display_id``:
|
|
84
|
-
- ``new_display``:
|
|
85
|
-
``widthxheight/dpi
|
|
86
|
-
- ``flex_display``:
|
|
87
|
-
- ``display_ime_policy``: ``local
|
|
88
|
-
- ``vd_destroy_content`` / ``vd_system_decorations``:
|
|
89
|
-
- ``capture_orientation``:
|
|
90
|
-
|
|
91
|
-
- ``
|
|
92
|
-
- ``
|
|
93
|
-
- ``
|
|
94
|
-
- ``
|
|
95
|
-
- ``camera_fps``: integer FPS.
|
|
79
|
+
逗号分隔的 MediaCodec 选项,``key[:type]=value``。
|
|
80
|
+
- ``video_encoder`` / ``audio_encoder``: Android 编码器名称。
|
|
81
|
+
- ``display_id``: 整数显示 ID。
|
|
82
|
+
- ``new_display``: 空字符串、``widthxheight``、``/dpi`` 或
|
|
83
|
+
``widthxheight/dpi``。
|
|
84
|
+
- ``flex_display``: 布尔值。
|
|
85
|
+
- ``display_ime_policy``: ``local``、``fallback`` 或 ``hide``。
|
|
86
|
+
- ``vd_destroy_content`` / ``vd_system_decorations``: 布尔值。
|
|
87
|
+
- ``capture_orientation``: 方向名称,可选择以 ``@`` 前缀锁定;``@`` 单独使用锁定初始方向。
|
|
88
|
+
- ``camera_id``: 相机 ID 字符串。
|
|
89
|
+
- ``camera_size``: ``widthxheight``。
|
|
90
|
+
- ``camera_facing``: ``front``、``back`` 或 ``external``。
|
|
91
|
+
- ``camera_ar``: ``sensor``、``width:height`` 或浮点比率。
|
|
92
|
+
- ``camera_fps``: 整数 FPS。
|
|
96
93
|
- ``camera_high_speed`` / ``camera_torch`` / ``audio_dup`` /
|
|
97
94
|
``show_touches`` / ``stay_awake`` / ``power_off_on_close`` /
|
|
98
95
|
``clipboard_autosync`` / ``downsize_on_error`` / ``cleanup`` /
|
|
99
|
-
``power_on`` / ``keep_active``:
|
|
100
|
-
- ``screen_off_timeout``:
|
|
101
|
-
- ``list_encoders
|
|
102
|
-
``list_camera_sizes``
|
|
103
|
-
- ``send_device_meta
|
|
104
|
-
``send_stream_meta``
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
escaping is performed by this client.
|
|
96
|
+
``power_on`` / ``keep_active``: 布尔值。
|
|
97
|
+
- ``screen_off_timeout``: 整数毫秒,或 ``-1`` 表示不变。
|
|
98
|
+
- ``list_encoders``、``list_displays``、``list_cameras``、
|
|
99
|
+
``list_camera_sizes`` 和 ``list_apps``: 布尔值列表模式。
|
|
100
|
+
- ``send_device_meta``、``send_frame_meta``、``send_dummy_byte``、
|
|
101
|
+
``send_stream_meta`` 和 ``raw_stream``: 协议/测试布尔值。
|
|
102
|
+
|
|
103
|
+
布尔值传递为 ``true`` 或 ``false``。值必须使用 scrcpy-server v4.0 的选项名称和
|
|
104
|
+
有线格式;此客户端不执行任何验证或 shell 转义。
|
|
109
105
|
"""
|
|
110
106
|
|
|
111
107
|
|
|
112
108
|
class ScrcpyClient:
|
|
113
109
|
def __init__(self, options: ScrcpyOptions) -> None:
|
|
114
110
|
if not (options.video or options.audio or options.control):
|
|
115
|
-
raise ValueError("
|
|
111
|
+
raise ValueError("必须至少启用 video、audio 或 control 中的一项")
|
|
116
112
|
self.options = options
|
|
117
113
|
self.scid = options.scid if options.scid is not None else random.getrandbits(31)
|
|
118
114
|
self.socket_name = f"scrcpy_{self.scid:08x}"
|
|
@@ -132,10 +128,10 @@ class ScrcpyClient:
|
|
|
132
128
|
if self.options.push_server:
|
|
133
129
|
await self.launcher.push_server()
|
|
134
130
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
131
|
+
# 自动检测 TCP/IP(无线)序列号 — ADB reverse 在无线 ADB 上不工作,
|
|
132
|
+
# 因此自动回退到正向隧道。
|
|
137
133
|
if self._is_tcpip_address(self.launcher.serial) and not self.options.tunnel_forward:
|
|
138
|
-
print("TCP/IP
|
|
134
|
+
print("检测到 TCP/IP 设备,使用 adb forward 隧道")
|
|
139
135
|
await self._start_forward()
|
|
140
136
|
elif self.options.tunnel_forward:
|
|
141
137
|
await self._start_forward()
|
|
@@ -193,14 +189,14 @@ class ScrcpyClient:
|
|
|
193
189
|
|
|
194
190
|
@staticmethod
|
|
195
191
|
def _is_tcpip_address(serial: str | None) -> bool:
|
|
196
|
-
"""
|
|
192
|
+
"""如果序列号看起来像 TCP/IP 地址(ip:port),返回 True。"""
|
|
197
193
|
return serial is not None and ":" in serial
|
|
198
194
|
|
|
199
195
|
async def _start_reverse(self) -> None:
|
|
200
196
|
self._tcp_server = await asyncio.start_server(self._on_accept, self.options.host, self.options.port)
|
|
201
197
|
sockets = self._tcp_server.sockets or []
|
|
202
198
|
if not sockets:
|
|
203
|
-
raise RuntimeError("
|
|
199
|
+
raise RuntimeError("无法创建本地服务器套接字")
|
|
204
200
|
self._port = int(sockets[0].getsockname()[1])
|
|
205
201
|
await self.launcher.reverse(self.socket_name, self._port)
|
|
206
202
|
self._server_process = await self._start_server_process(tunnel_forward=False)
|
|
@@ -208,11 +204,11 @@ class ScrcpyClient:
|
|
|
208
204
|
|
|
209
205
|
async def _start_forward(self) -> None:
|
|
210
206
|
self._port = self.options.port or 27183
|
|
211
|
-
#
|
|
207
|
+
# 首先移除任何陈旧的正向映射以避免冲突
|
|
212
208
|
await self.launcher.remove_forward(self._port)
|
|
213
209
|
await self.launcher.forward(self.socket_name, self._port)
|
|
214
210
|
self._server_process = await self._start_server_process(tunnel_forward=True)
|
|
215
|
-
#
|
|
211
|
+
# 在连接之前给服务器时间创建 LocalServerSocket
|
|
216
212
|
await asyncio.sleep(1)
|
|
217
213
|
await self._connect_streams(read_dummy_byte=True)
|
|
218
214
|
|
|
@@ -251,7 +247,7 @@ class ScrcpyClient:
|
|
|
251
247
|
except OSError as exc:
|
|
252
248
|
last_error = exc
|
|
253
249
|
await asyncio.sleep(0.1)
|
|
254
|
-
raise ConnectionError(f"
|
|
250
|
+
raise ConnectionError(f"无法连接到转发的 scrcpy 套接字: {last_error}")
|
|
255
251
|
|
|
256
252
|
async def _assign_streams(
|
|
257
253
|
self,
|
|
@@ -288,4 +284,4 @@ class ScrcpyClient:
|
|
|
288
284
|
return self
|
|
289
285
|
|
|
290
286
|
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
291
|
-
await self.stop()
|
|
287
|
+
await self.stop()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""scrcpy-server v4.0 的控制消息和设备消息序列化器。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -33,10 +33,10 @@ MAX_TOUCH_POINTERS = 10
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class PointerManager:
|
|
36
|
-
"""
|
|
36
|
+
"""管理多点触控的唯一指针 ID 池。
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
从 0 开始分配指针 ID,最多支持 `MAX_TOUCH_POINTERS` 个并发触摸。
|
|
39
|
+
释放后 ID 可被重用。
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
42
|
def __init__(self) -> None:
|
|
@@ -45,10 +45,10 @@ class PointerManager:
|
|
|
45
45
|
self._active: set[int] = set()
|
|
46
46
|
|
|
47
47
|
def allocate(self) -> int | None:
|
|
48
|
-
"""
|
|
48
|
+
"""分配一个唯一的指针 ID。
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
返回一个整数指针 ID,如果已达到最大并发触摸数 (`MAX_TOUCH_POINTERS`),
|
|
51
|
+
则返回 `None`。
|
|
52
52
|
"""
|
|
53
53
|
if len(self._active) >= MAX_TOUCH_POINTERS:
|
|
54
54
|
return None
|
|
@@ -61,23 +61,23 @@ class PointerManager:
|
|
|
61
61
|
return pid
|
|
62
62
|
|
|
63
63
|
def release(self, pointer_id: int) -> None:
|
|
64
|
-
"""
|
|
64
|
+
"""将之前分配的指针 ID 释放回池中。"""
|
|
65
65
|
if pointer_id in self._active:
|
|
66
66
|
self._active.discard(pointer_id)
|
|
67
67
|
self._freed.append(pointer_id)
|
|
68
68
|
|
|
69
69
|
def reset(self) -> None:
|
|
70
|
-
"""
|
|
70
|
+
"""释放所有活跃的指针 ID。"""
|
|
71
71
|
self._active.clear()
|
|
72
72
|
self._freed.clear()
|
|
73
73
|
self._next_id = 0
|
|
74
74
|
|
|
75
75
|
def active_count(self) -> int:
|
|
76
|
-
"""
|
|
76
|
+
"""返回当前活跃的指针 ID 数量。"""
|
|
77
77
|
return len(self._active)
|
|
78
78
|
|
|
79
79
|
def active_ids(self) -> set[int]:
|
|
80
|
-
"""
|
|
80
|
+
"""返回当前活跃的指针 ID 集合。"""
|
|
81
81
|
return set(self._active)
|
|
82
82
|
|
|
83
83
|
|
|
@@ -172,7 +172,7 @@ def empty(message_type: ControlMessageType) -> ControlMessage:
|
|
|
172
172
|
ControlMessageType.CAMERA_ZOOM_IN,
|
|
173
173
|
ControlMessageType.CAMERA_ZOOM_OUT,
|
|
174
174
|
}:
|
|
175
|
-
raise ValueError(f"{message_type.name}
|
|
175
|
+
raise ValueError(f"{message_type.name} 不是一个空控制消息")
|
|
176
176
|
return ControlMessage(message_type, bytes([message_type]))
|
|
177
177
|
|
|
178
178
|
|
|
@@ -218,7 +218,7 @@ def deserialize_device_message(buffer: bytes) -> tuple[DeviceMessage | None, int
|
|
|
218
218
|
try:
|
|
219
219
|
message_type = DeviceMessageType(buffer[0])
|
|
220
220
|
except ValueError as exc:
|
|
221
|
-
raise ProtocolError(f"
|
|
221
|
+
raise ProtocolError(f"未知的设备消息类型: {buffer[0]}") from exc
|
|
222
222
|
|
|
223
223
|
if message_type is DeviceMessageType.CLIPBOARD:
|
|
224
224
|
if len(buffer) < 5:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""scrcpy-server v4.0 的控制消息和设备消息序列化器。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -120,7 +120,7 @@ def empty(message_type: ControlMessageType) -> ControlMessage:
|
|
|
120
120
|
ControlMessageType.CAMERA_ZOOM_IN,
|
|
121
121
|
ControlMessageType.CAMERA_ZOOM_OUT,
|
|
122
122
|
}:
|
|
123
|
-
raise ValueError(f"{message_type.name}
|
|
123
|
+
raise ValueError(f"{message_type.name} 不是空控制消息")
|
|
124
124
|
return ControlMessage(message_type, bytes([message_type]))
|
|
125
125
|
|
|
126
126
|
|
|
@@ -166,7 +166,7 @@ def deserialize_device_message(buffer: bytes) -> tuple[DeviceMessage | None, int
|
|
|
166
166
|
try:
|
|
167
167
|
message_type = DeviceMessageType(buffer[0])
|
|
168
168
|
except ValueError as exc:
|
|
169
|
-
raise ProtocolError(f"
|
|
169
|
+
raise ProtocolError(f"未知的设备消息类型: {buffer[0]}") from exc
|
|
170
170
|
|
|
171
171
|
if message_type is DeviceMessageType.CLIPBOARD:
|
|
172
172
|
if len(buffer) < 5:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""scrcpy-server v4.0 的异步流读取器和写入器。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -34,13 +34,13 @@ class BaseMediaStream:
|
|
|
34
34
|
async def read_codec(self) -> CodecId:
|
|
35
35
|
raw = read_u32be(await read_exact(self.reader, 4))
|
|
36
36
|
if raw == 0:
|
|
37
|
-
raise StreamDisabledError(f"{self.kind.value}
|
|
37
|
+
raise StreamDisabledError(f"设备禁用了 {self.kind.value} 流")
|
|
38
38
|
if raw == 1:
|
|
39
|
-
raise StreamConfigurationError(f"{self.kind.value}
|
|
39
|
+
raise StreamConfigurationError(f"设备上 {self.kind.value} 流配置错误")
|
|
40
40
|
try:
|
|
41
41
|
codec = CodecId(raw)
|
|
42
42
|
except ValueError as exc:
|
|
43
|
-
raise ProtocolError(f"
|
|
43
|
+
raise ProtocolError(f"未知的编解码器ID: 0x{raw:08x}") from exc
|
|
44
44
|
self.codec = codec
|
|
45
45
|
return codec
|
|
46
46
|
|
|
@@ -54,11 +54,11 @@ class BaseMediaStream:
|
|
|
54
54
|
assert self.codec is not None
|
|
55
55
|
header = await read_exact(self.reader, PACKET_HEADER_SIZE)
|
|
56
56
|
if header[0] & 0x80:
|
|
57
|
-
raise ProtocolError("
|
|
57
|
+
raise ProtocolError("媒体流上出现意外的视频会话数据包")
|
|
58
58
|
pts_flags = read_u64be(header, 0)
|
|
59
59
|
size = read_u32be(header, 8)
|
|
60
60
|
if size <= 0:
|
|
61
|
-
raise ProtocolError("
|
|
61
|
+
raise ProtocolError("无效的媒体数据包大小: 0")
|
|
62
62
|
payload = await read_exact(self.reader, size)
|
|
63
63
|
config = bool(pts_flags & PACKET_FLAG_CONFIG)
|
|
64
64
|
key_frame = bool(pts_flags & PACKET_FLAG_KEY_FRAME)
|
|
@@ -83,7 +83,7 @@ class VideoStream(BaseMediaStream):
|
|
|
83
83
|
await self.read_codec()
|
|
84
84
|
header = await read_exact(self.reader, PACKET_HEADER_SIZE)
|
|
85
85
|
if not header[0] & 0x80:
|
|
86
|
-
raise ProtocolError("
|
|
86
|
+
raise ProtocolError("期望视频会话数据包")
|
|
87
87
|
session = VideoSession(
|
|
88
88
|
width=read_u32be(header, 4),
|
|
89
89
|
height=read_u32be(header, 8),
|
|
@@ -137,7 +137,7 @@ class VideoStream(BaseMediaStream):
|
|
|
137
137
|
pts_flags = read_u64be(header, 0)
|
|
138
138
|
size = read_u32be(header, 8)
|
|
139
139
|
if size <= 0:
|
|
140
|
-
raise ProtocolError("
|
|
140
|
+
raise ProtocolError("无效的视频数据包大小: 0")
|
|
141
141
|
payload = await read_exact(self.reader, size)
|
|
142
142
|
config = bool(pts_flags & PACKET_FLAG_CONFIG)
|
|
143
143
|
key_frame = bool(pts_flags & PACKET_FLAG_KEY_FRAME)
|
|
@@ -226,17 +226,16 @@ class ControlStream:
|
|
|
226
226
|
action_button: int = 0,
|
|
227
227
|
buttons: int = 0,
|
|
228
228
|
) -> int | None:
|
|
229
|
-
"""
|
|
229
|
+
"""发送带有自动指针ID管理的触摸事件。
|
|
230
230
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
在 ``ACTION_DOWN`` 时:如果 *pointer_id* 是 ``None``,则从
|
|
232
|
+
``self.pointer_manager`` 分配一个新ID。返回最终的 pointer_id
|
|
233
|
+
(如果分配失败则返回 ``None``)。
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
the pool after sending.
|
|
235
|
+
在 ``ACTION_UP`` 时:发送后将给定的 *pointer_id* 释放回池。
|
|
237
236
|
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
当 ``self.pointer_manager`` 为 ``None`` 时,回退到默认的
|
|
238
|
+
``POINTER_ID_GENERIC_FINGER``。
|
|
240
239
|
"""
|
|
241
240
|
pm = self.pointer_manager
|
|
242
241
|
|
|
@@ -288,7 +287,7 @@ class ControlStream:
|
|
|
288
287
|
|
|
289
288
|
|
|
290
289
|
class AudioVideoCombinedStream:
|
|
291
|
-
"""
|
|
290
|
+
"""从官方套接字合并视频和音频事件的本地虚拟流。"""
|
|
292
291
|
|
|
293
292
|
def __init__(self, video: VideoStream | None, audio: AudioStream | None) -> None:
|
|
294
293
|
self.video = video
|
|
@@ -98,8 +98,6 @@ class HotkeyListener:
|
|
|
98
98
|
if self.current_keys == hotkey["keys"]:
|
|
99
99
|
hotkey["func"]()
|
|
100
100
|
self.start_time = now
|
|
101
|
-
return True
|
|
102
|
-
return False
|
|
103
101
|
|
|
104
102
|
def _on_hot_key_up(self, event: KeyEvent):
|
|
105
103
|
vk_code = event.key_code
|
|
@@ -109,7 +107,6 @@ class HotkeyListener:
|
|
|
109
107
|
# 如果所有键都释放了,重置时间
|
|
110
108
|
if not self.current_keys:
|
|
111
109
|
self.start_time = None
|
|
112
|
-
return False
|
|
113
110
|
|
|
114
111
|
def start(self):
|
|
115
112
|
self.hook_listener.start()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "autoxkit"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Python library for Windows automation and Android device screen casting and control"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|