autoxkit 2.1.1__tar.gz → 2.2.1__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.
Files changed (35) hide show
  1. {autoxkit-2.1.1 → autoxkit-2.2.1}/PKG-INFO +1 -1
  2. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/__init__.py +34 -35
  3. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/adb.py +13 -13
  4. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/binary.py +6 -6
  5. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/client.py +66 -70
  6. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/control.py +13 -13
  7. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/control_backup.py +3 -3
  8. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/models.py +1 -1
  9. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/android/streams.py +16 -17
  10. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/hook/hook_listener.py +19 -9
  11. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit.egg-info/PKG-INFO +1 -1
  12. {autoxkit-2.1.1 → autoxkit-2.2.1}/pyproject.toml +1 -1
  13. {autoxkit-2.1.1 → autoxkit-2.2.1}/LICENSE.txt +0 -0
  14. {autoxkit-2.1.1 → autoxkit-2.2.1}/README.md +0 -0
  15. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/__init__.py +0 -0
  16. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/constants.py +0 -0
  17. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/hook/__init__.py +0 -0
  18. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/hook/event.py +0 -0
  19. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/hook/hotkey_listener.py +0 -0
  20. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/match/__init__.py +0 -0
  21. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/match/match.py +0 -0
  22. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/mousekey/__init__.py +0 -0
  23. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/mousekey/input.py +0 -0
  24. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/mousekey/keyboard.py +0 -0
  25. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/mousekey/mouse.py +0 -0
  26. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/utils.py +0 -0
  27. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/window/__init__.py +0 -0
  28. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/window/window.py +0 -0
  29. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/window/window_action.py +0 -0
  30. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit/window/window_match.py +0 -0
  31. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit.egg-info/SOURCES.txt +0 -0
  32. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit.egg-info/dependency_links.txt +0 -0
  33. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit.egg-info/requires.txt +0 -0
  34. {autoxkit-2.1.1 → autoxkit-2.2.1}/autoxkit.egg-info/top_level.txt +0 -0
  35. {autoxkit-2.1.1 → autoxkit-2.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autoxkit
3
- Version: 2.1.1
3
+ Version: 2.2.1
4
4
  Summary: Python library for Windows automation and Android device screen casting and control
5
5
  Author-email: YorickFin <1582456060@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -1,4 +1,4 @@
1
- """Android device control module."""
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
- """Synchronous Android device controller.
26
+ """同步 Android 设备控制器。
27
27
 
28
- Wraps the async ``ScrcpyClient`` in a background thread so all public
29
- methods are regular synchronous calls, suitable for use inside hook
30
- callbacks (``key_down``, ``key_up``, etc.).
28
+ 将异步的 ``ScrcpyClient`` 封装在后台线程中,使所有公共方法都成为
29
+ 普通的同步调用,适合在钩子回调中使用(``key_down``、``key_up`` 等)。
31
30
 
32
- Usage::
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
- # ── Connection management ────────────────────────────────
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
- """Connect to an Android device via scrcpy-server.
68
+ """通过 scrcpy-server 连接到 Android 设备。
70
69
 
71
- Parameters are forwarded to :class:`ScrcpyOptions`. Common extras:
70
+ 参数将传递给 :class:`ScrcpyOptions`。常用额外参数:
72
71
 
73
- - ``tunnel_forward`` (bool): use ``adb forward`` instead of
74
- ``adb reverse``.
75
- - ``tcpip`` (bool): enable ADB-over-TCP/IP before starting.
76
- - ``tcpip_dst`` (str): known wireless ADB address.
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
- """Disconnect from the device and stop the scrcpy-server."""
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
- # ── Touch ────────────────────────────────────────────────
89
+ # ── 触摸 ────────────────────────────────────────────────
91
90
 
92
91
  def touch(self, x: int, y: int, action: str) -> None:
93
- """Send a touch event.
92
+ """发送触摸事件。
94
93
 
95
94
  Args:
96
- x: Screen X coordinate.
97
- y: Screen Y coordinate.
98
- action: ``"down"``, ``"up"``, or ``"move"``.
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"invalid touch action: {action!r} (use down/up/move)")
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
- """Perform a tap (touch down then up) at screen coordinates."""
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
- """Perform a swipe gesture from (x1, y1) to (x2, y2)."""
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
- # ── Keycode ──────────────────────────────────────────────
120
+ # ── 按键码 ──────────────────────────────────────────────
122
121
 
123
122
  def keycode(self, keycode: int, action: str = "down") -> None:
124
- """Send an Android keycode event.
123
+ """发送 Android 按键码事件。
125
124
 
126
125
  Args:
127
- keycode: Android keycode (e.g. 4 = BACK, 3 = HOME).
128
- action: ``"down"`` or ``"up"``.
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"invalid key action: {action!r} (use down/up)")
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
- """Press and release a key (simulates a single tap)."""
135
+ """按下并释放按键(模拟单次点击)。"""
137
136
  self.keycode(keycode, "down")
138
137
  time.sleep(0.02)
139
138
  self.keycode(keycode, "up")
140
139
 
141
- # ── Clipboard ────────────────────────────────────────────
140
+ # ── 剪贴板 ────────────────────────────────────────────
142
141
 
143
142
  def set_clipboard(self, text: str, paste: bool = False) -> None:
144
- """Set the device clipboard (optionally also paste)."""
143
+ """设置设备剪贴板(可选同时粘贴)。"""
145
144
  self._submit(self._clipboard_async(text, paste))
146
145
 
147
- # ── Internal async helpers ───────────────────────────────
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("device not connected")
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("device not connected")
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("device not connected")
191
+ raise RuntimeError("未连接到设备")
193
192
  await self._client.control.set_clipboard(text, sequence=1, paste=paste)
194
193
 
195
- # ── Async loop bridge ────────────────────────────────────
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 launcher for scrcpy-server v4.0."""
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
- """Parse the Wi-Fi IP address from `adb shell ip route`.
43
+ """ `adb shell ip route` 解析 Wi-Fi IP 地址。
44
44
 
45
- Only considers lines where the interface name starts with ``wlan``,
46
- matching the official scrcpy behavior to skip non-Wi-Fi interfaces
47
- such as rmnet_data (mobile data) or usb0 (USB tethering).
45
+ 仅考虑接口名称以 ``wlan`` 开头的行,
46
+ 与官方 scrcpy 行为一致,跳过非 Wi-Fi 接口,
47
+ 例如 rmnet_data(移动数据)或 usb0USB 共享网络)。
48
48
  """
49
49
  route = await self.shell_output("ip", "route")
50
50
  for line in route.splitlines():
51
- # Look for lines like:
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
- # Found a wlan line, extract the src IP
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"could not find Wi-Fi (wlan) device ip in `adb shell ip route`: {route.strip()}"
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
- """Discover the serial of a single USB-connected device.
67
+ """发现单个 USB 连接设备的序列号。
68
68
 
69
- Returns None if there is no USB device, or if there are multiple.
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
- # Skip empty lines and the "List of devices attached" header
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 devices have serials without ":"
94
- # TCP/IP devices have format ip:port
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
- """Binary helpers for the scrcpy v4.0 protocol."""
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
- """Raised when bytes on the wire do not match the expected protocol."""
13
+ """当网络上的字节不符合预期协议时抛出。"""
14
14
 
15
15
 
16
16
  class StreamDisabledError(ProtocolError):
17
- """Raised when scrcpy-server explicitly disables a media stream."""
17
+ """ scrcpy-server 显式禁用媒体流时抛出。"""
18
18
 
19
19
 
20
20
  class StreamConfigurationError(ProtocolError):
21
- """Raised when scrcpy-server reports a media stream configuration error."""
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"expected {size} bytes, got {len(exc.partial)}") from exc
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-byte strings may not exceed 255 bytes")
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
- """High-level scrcpy-server v4.0 client facade."""
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
- """Options used to launch and connect to scrcpy-server v4.0."""
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
- """Enable ADB-over-TCP/IP before starting scrcpy-server.
35
+ """在启动 scrcpy-server 之前启用 ADB-over-TCP/IP
36
36
 
37
- If ``tcpip_dst`` is set, the client runs ``adb connect`` to that address
38
- and uses it as the device serial. If ``tcpip_dst`` is not set, the client
39
- assumes a USB device is currently selected, reads its WLAN IP address from
40
- ``adb shell ip route``, runs ``adb tcpip <tcpip_port>``, then connects to
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
- """Known wireless ADB address.
44
+ """已知的无线 ADB 地址。
46
45
 
47
- Accepted forms are ``"192.168.1.23"`` and ``"192.168.1.23:5555"``. If the
48
- port is omitted, ``tcpip_port`` is appended.
46
+ 接受的格式为 ``"192.168.1.23"`` ``"192.168.1.23:5555"``。如果省略端口,
47
+ 则会附加 ``tcpip_port``。
49
48
  """
50
49
 
51
50
  tcpip_port: int = 5555
52
- """Wireless ADB TCP port used for ``adb tcpip`` and default address ports."""
51
+ """用于 ``adb tcpip`` 和默认地址端口的无线 ADB TCP 端口。"""
53
52
 
54
53
  tcpip_disconnect_existing: bool = False
55
- """Run ``adb disconnect <address>`` before ``adb connect <address>``."""
54
+ """ ``adb connect <address>`` 之前运行 ``adb disconnect <address>``。"""
56
55
 
57
56
  server_args: list[str] = field(default_factory=list)
58
- """Additional raw ``key=value`` arguments passed to ``scrcpy-server``.
59
-
60
- These values are appended after the arguments managed directly by
61
- ``ScrcpyClient``. Avoid overriding managed keys (``scid``, ``log_level``,
62
- ``video``, ``audio``, ``control`` and ``tunnel_forward``) unless you also
63
- update the Python-side connection logic accordingly.
64
-
65
- Common scrcpy-server v4.0 values:
66
-
67
- - ``video_codec``: ``h264``, ``h265`` or ``av1``.
68
- - ``audio_codec``: ``opus``, ``aac``, ``flac`` or ``raw``.
69
- - ``video_source``: ``display`` or ``camera``.
70
- - ``audio_source``: ``output``, ``mic``, ``playback``,
71
- ``mic-unprocessed``, ``mic-camcorder``, ``mic-voice-recognition``,
72
- ``mic-voice-communication``, ``voice-call``, ``voice-call-uplink``,
73
- ``voice-call-downlink`` or ``voice-performance``.
74
- - ``video_bit_rate`` / ``audio_bit_rate``: integer bit rate in bits/s.
75
- - ``max_size``: integer max video dimension, ``0`` for no limit.
76
- - ``max_fps``: float/integer FPS string, for example ``30`` or ``60``.
77
- - ``min_size_alignment``: ``1``, ``2``, ``4``, ``8`` or ``16``.
78
- - ``angle``: float degrees string.
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
- comma-separated MediaCodec options, ``key[:type]=value``.
82
- - ``video_encoder`` / ``audio_encoder``: Android encoder name.
83
- - ``display_id``: integer display id.
84
- - ``new_display``: empty string, ``widthxheight``, ``/dpi`` or
85
- ``widthxheight/dpi``.
86
- - ``flex_display``: boolean.
87
- - ``display_ime_policy``: ``local``, ``fallback`` or ``hide``.
88
- - ``vd_destroy_content`` / ``vd_system_decorations``: boolean.
89
- - ``capture_orientation``: orientation name, optionally prefixed with
90
- ``@`` to lock it; ``@`` alone locks the initial orientation.
91
- - ``camera_id``: camera id string.
92
- - ``camera_size``: ``widthxheight``.
93
- - ``camera_facing``: ``front``, ``back`` or ``external``.
94
- - ``camera_ar``: ``sensor``, ``width:height`` or a float ratio.
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``: boolean.
100
- - ``screen_off_timeout``: integer milliseconds, or ``-1`` for unchanged.
101
- - ``list_encoders``, ``list_displays``, ``list_cameras``,
102
- ``list_camera_sizes`` and ``list_apps``: boolean listing modes.
103
- - ``send_device_meta``, ``send_frame_meta``, ``send_dummy_byte``,
104
- ``send_stream_meta`` and ``raw_stream``: protocol/testing booleans.
105
-
106
- Booleans are passed as ``true`` or ``false``. Values must use
107
- scrcpy-server v4.0 option names and wire formats; no validation or shell
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("at least one of video, audio or control must be enabled")
111
+ raise ValueError("必须至少启用 videoaudio 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
- # Auto-detect TCP/IP (wireless) serials — ADB reverse does not
136
- # work over wireless ADB, so fall back to forward tunnel automatically.
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 device detected, using adb forward tunnel")
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
- """Return True if the serial looks like a TCP/IP address (ip:port)."""
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("failed to create local server socket")
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
- # Remove any stale forward mapping first to avoid conflicts
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
- # Give the server time to create the LocalServerSocket before connecting
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"could not connect to forwarded scrcpy socket: {last_error}")
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
- """Control and device-message serializers for scrcpy-server v4.0."""
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
- """Manages a pool of unique pointer IDs for multi-touch.
36
+ """管理多点触控的唯一指针 ID 池。
37
37
 
38
- Allocates pointer IDs from 0 upward, up to `MAX_TOUCH_POINTERS`
39
- concurrent touches. IDs are reused when released.
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
- """Allocate a unique pointer ID.
48
+ """分配一个唯一的指针 ID
49
49
 
50
- Returns an integer pointer ID, or `None` if the maximum number
51
- of concurrent touches (`MAX_TOUCH_POINTERS`) has been reached.
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
- """Release a previously allocated pointer ID back to the pool."""
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
- """Release all active pointer IDs."""
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
- """Return the number of currently active pointer IDs."""
76
+ """返回当前活跃的指针 ID 数量。"""
77
77
  return len(self._active)
78
78
 
79
79
  def active_ids(self) -> set[int]:
80
- """Return the set of currently active pointer IDs."""
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} is not an empty control message")
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"unknown device message type: {buffer[0]}") from exc
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
- """Control and device-message serializers for scrcpy-server v4.0."""
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} is not an empty control message")
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"unknown device message type: {buffer[0]}") from exc
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
- """Protocol data models for scrcpy-server v4.0."""
1
+ """scrcpy-server v4.0 的协议数据模型。"""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -1,4 +1,4 @@
1
- """Async stream readers and writers for scrcpy-server v4.0."""
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} stream disabled by device")
37
+ raise StreamDisabledError(f"设备禁用了 {self.kind.value} ")
38
38
  if raw == 1:
39
- raise StreamConfigurationError(f"{self.kind.value} stream configuration error on device")
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"unknown codec id: 0x{raw:08x}") from exc
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("unexpected video session packet on media stream")
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("invalid media packet size: 0")
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("expected video session packet")
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("invalid video packet size: 0")
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
- """Send a touch event with automatic pointer ID management.
229
+ """发送带有自动指针ID管理的触摸事件。
230
230
 
231
- On ``ACTION_DOWN``: if *pointer_id* is ``None``, a new ID is
232
- allocated from ``self.pointer_manager``. The final pointer_id
233
- is returned (or ``None`` if allocation failed).
231
+ ``ACTION_DOWN`` 时:如果 *pointer_id* ``None``,则从
232
+ ``self.pointer_manager`` 分配一个新ID。返回最终的 pointer_id
233
+ (如果分配失败则返回 ``None``)。
234
234
 
235
- On ``ACTION_UP``: the given *pointer_id* is released back to
236
- the pool after sending.
235
+ ``ACTION_UP`` 时:发送后将给定的 *pointer_id* 释放回池。
237
236
 
238
- When ``self.pointer_manager`` is ``None``, falls back to the
239
- default ``POINTER_ID_GENERIC_FINGER``.
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
- """A local virtual stream merging video and audio events from official sockets."""
290
+ """从官方套接字合并视频和音频事件的本地虚拟流。"""
292
291
 
293
292
  def __init__(self, video: VideoStream | None, audio: AudioStream | None) -> None:
294
293
  self.video = video
@@ -1,13 +1,16 @@
1
-
2
1
  # hook_listener.py
3
2
  import ctypes
4
3
  from ctypes import wintypes, Structure, POINTER, CFUNCTYPE, byref
5
4
  import threading
5
+ import time
6
6
  from .event import KeyEvent, MouseEvent
7
7
  from ..constants import Hex_Hook_Code
8
8
 
9
9
  HHC = Hex_Hook_Code
10
10
 
11
+ # 消息泵空闲轮询间隔(秒),无消息时休眠此时间以释放 CPU
12
+ MSG_POLL_INTERVAL = 0.001
13
+
11
14
  # ---------- 结构体定义 ----------
12
15
  class KBDLLHOOKSTRUCT(Structure):
13
16
  _fields_ = [
@@ -30,7 +33,7 @@ class MSLLHOOKSTRUCT(Structure):
30
33
  # ---------- 回调类型 ----------
31
34
  HOOKPROC = CFUNCTYPE(ctypes.c_long, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM)
32
35
 
33
- # ---------- 加载 DLL 并声明 API 签名(你指出必须有的部分) ----------
36
+ # ---------- 加载 DLL 并声明 API 签名 ----------
34
37
  user32 = ctypes.WinDLL('user32', use_last_error=True)
35
38
  kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
36
39
 
@@ -163,7 +166,9 @@ class HookListener:
163
166
  if result is True:
164
167
  return 1 # 截断事件传播
165
168
  except ValueError as e:
166
- raise e
169
+ print(f"[hook_listener] ValueError in keydown callback: {e}", file=__import__('sys').stderr)
170
+ except Exception as e:
171
+ print(f"[hook_listener] Exception in keydown callback: {e}", file=__import__('sys').stderr)
167
172
  elif wParam in (HHC["KeyUp"], HHC["SysKeyUp"]):
168
173
  event = KeyEvent('KeyUp', kbd.vkCode)
169
174
  for cb in self._on_keyup:
@@ -172,9 +177,11 @@ class HookListener:
172
177
  if result is True:
173
178
  return 1 # 截断事件传播
174
179
  except ValueError as e:
175
- raise e
180
+ print(f"[hook_listener] ValueError in keyup callback: {e}", file=__import__('sys').stderr)
181
+ except Exception as e:
182
+ print(f"[hook_listener] Exception in keyup callback: {e}", file=__import__('sys').stderr)
176
183
  except Exception as e:
177
- raise e
184
+ print(f"[hook_listener] Exception in _keyboard_proc: {e}", file=__import__('sys').stderr)
178
185
 
179
186
  return user32.CallNextHookEx(self.keyboard_hook, nCode, wParam, lParam)
180
187
 
@@ -194,7 +201,7 @@ class HookListener:
194
201
  if result is True:
195
202
  return 1 # 截断事件传播
196
203
  except Exception as e:
197
- raise e
204
+ print(f"[hook_listener] Exception in mousedown callback: {e}", file=__import__('sys').stderr)
198
205
 
199
206
  elif wParam in (HHC["MLeftUp"], HHC["MRightUp"], HHC["MiddleUp"], HHC["XUp"]):
200
207
  button = self._get_mouse_button(wParam, ms.mouseData)
@@ -205,9 +212,9 @@ class HookListener:
205
212
  if result is True:
206
213
  return 1 # 截断事件传播
207
214
  except Exception as e:
208
- raise e
215
+ print(f"[hook_listener] Exception in mouseup callback: {e}", file=__import__('sys').stderr)
209
216
  except Exception as e:
210
- raise e
217
+ print(f"[hook_listener] Exception in _mouse_proc: {e}", file=__import__('sys').stderr)
211
218
 
212
219
  return user32.CallNextHookEx(self.mouse_hook, nCode, wParam, lParam)
213
220
 
@@ -276,9 +283,12 @@ class HookListener:
276
283
  if user32.PeekMessageW(byref(msg), 0, 0, 0, 1): # PM_REMOVE = 1
277
284
  user32.TranslateMessage(byref(msg))
278
285
  user32.DispatchMessageW(byref(msg))
286
+ else:
287
+ # 无消息时休眠,避免 CPU 忙等
288
+ time.sleep(MSG_POLL_INTERVAL)
279
289
  except Exception:
280
290
  # 记录异常但不退出循环,确保钩子持续工作
281
- # 回调中的异常应由上层自行处理
291
+ print("[hook_listener] Exception in message pump", file=__import__('sys').stderr)
282
292
  continue
283
293
 
284
294
  # 离开循环之前确保取消钩子
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autoxkit
3
- Version: 2.1.1
3
+ Version: 2.2.1
4
4
  Summary: Python library for Windows automation and Android device screen casting and control
5
5
  Author-email: YorickFin <1582456060@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autoxkit"
7
- version = "2.1.1"
7
+ version = "2.2.1"
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