uiautodev 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

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.

Potentially problematic release.


This version of uiautodev might be problematic. Click here for more details.

uiautodev/model.py CHANGED
@@ -33,13 +33,17 @@ class Rect(BaseModel):
33
33
 
34
34
  class Node(BaseModel):
35
35
  key: str
36
- name: str
36
+ name: str # can be seen as description
37
37
  bounds: Optional[Tuple[float, float, float, float]] = None
38
38
  rect: Optional[Rect] = None
39
- properties: Dict[str, Union[str, bool]] = []
39
+ properties: Dict[str, Union[str, bool]] = {}
40
40
  children: List[Node] = []
41
41
 
42
42
 
43
+ class OCRNode(Node):
44
+ confidence: float
45
+
46
+
43
47
  class WindowSize(typing.NamedTuple):
44
48
  width: int
45
49
  height: int
uiautodev/provider.py CHANGED
@@ -12,6 +12,7 @@ import adbutils
12
12
 
13
13
  from uiautodev.driver.android import AndroidDriver
14
14
  from uiautodev.driver.base_driver import BaseDriver
15
+ from uiautodev.driver.harmony import HDC, HarmonyDriver
15
16
  from uiautodev.driver.ios import IOSDriver
16
17
  from uiautodev.driver.mock import MockDriver
17
18
  from uiautodev.exceptions import UiautoException
@@ -27,7 +28,7 @@ class BaseProvider(abc.ABC):
27
28
  @abc.abstractmethod
28
29
  def get_device_driver(self, serial: str) -> BaseDriver:
29
30
  raise NotImplementedError()
30
-
31
+
31
32
  def get_single_device_driver(self) -> BaseDriver:
32
33
  """ debug use """
33
34
  devs = self.list_devices()
@@ -66,11 +67,25 @@ class IOSProvider(BaseProvider):
66
67
  @lru_cache
67
68
  def get_device_driver(self, serial: str) -> BaseDriver:
68
69
  return IOSDriver(serial)
69
-
70
+
71
+
72
+ class HarmonyProvider(BaseProvider):
73
+ def __init__(self):
74
+ super().__init__()
75
+ self.hdc = HDC()
76
+
77
+ def list_devices(self) -> list[DeviceInfo]:
78
+ devices = self.hdc.list_device()
79
+ return [DeviceInfo(serial=d, model=self.hdc.get_model(d), name=self.hdc.get_model(d)) for d in devices]
80
+
81
+ @lru_cache
82
+ def get_device_driver(self, serial: str) -> HarmonyDriver:
83
+ return HarmonyDriver(self.hdc, serial)
84
+
70
85
 
71
86
  class MockProvider(BaseProvider):
72
87
  def list_devices(self) -> list[DeviceInfo]:
73
88
  return [DeviceInfo(serial="mock-serial", model="mock-model", name="mock-name")]
74
89
 
75
90
  def get_device_driver(self, serial: str) -> BaseDriver:
76
- return MockDriver(serial)
91
+ return MockDriver(serial)
@@ -0,0 +1,74 @@
1
+ # Ref
2
+ # https://github.com/Genymobile/scrcpy/blob/master/app/src/android/input.h
3
+ from enum import IntEnum
4
+
5
+
6
+ class MetaState(IntEnum):
7
+ """Android meta state flags ported from Android's KeyEvent class
8
+
9
+ These flags represent the state of meta keys such as ALT, SHIFT, CTRL, etc.
10
+ They can be combined using bitwise OR operations to represent multiple
11
+ meta keys being pressed simultaneously.
12
+
13
+ The values and comments are taken directly from the Android source code
14
+ to maintain compatibility and provide accurate descriptions.
15
+ """
16
+ # No meta keys are pressed
17
+ NONE = 0x0
18
+
19
+ # This mask is used to check whether one of the SHIFT meta keys is pressed
20
+ SHIFT_ON = 0x1
21
+
22
+ # This mask is used to check whether one of the ALT meta keys is pressed
23
+ ALT_ON = 0x2
24
+
25
+ # This mask is used to check whether the SYM meta key is pressed
26
+ SYM_ON = 0x4
27
+
28
+ # This mask is used to check whether the FUNCTION meta key is pressed
29
+ FUNCTION_ON = 0x8
30
+
31
+ # This mask is used to check whether the left ALT meta key is pressed
32
+ ALT_LEFT_ON = 0x10
33
+
34
+ # This mask is used to check whether the right ALT meta key is pressed
35
+ ALT_RIGHT_ON = 0x20
36
+
37
+ # This mask is used to check whether the left SHIFT meta key is pressed
38
+ SHIFT_LEFT_ON = 0x40
39
+
40
+ # This mask is used to check whether the right SHIFT meta key is pressed
41
+ SHIFT_RIGHT_ON = 0x80
42
+
43
+ # This mask is used to check whether the CAPS LOCK meta key is on
44
+ CAPS_LOCK_ON = 0x100000
45
+
46
+ # This mask is used to check whether the NUM LOCK meta key is on
47
+ NUM_LOCK_ON = 0x200000
48
+
49
+ # This mask is used to check whether the SCROLL LOCK meta key is on
50
+ SCROLL_LOCK_ON = 0x400000
51
+
52
+ # This mask is used to check whether one of the CTRL meta keys is pressed
53
+ CTRL_ON = 0x1000
54
+
55
+ # This mask is used to check whether the left CTRL meta key is pressed
56
+ CTRL_LEFT_ON = 0x2000
57
+
58
+ # This mask is used to check whether the right CTRL meta key is pressed
59
+ CTRL_RIGHT_ON = 0x4000
60
+
61
+ # This mask is used to check whether one of the META meta keys is pressed
62
+ META_ON = 0x10000
63
+
64
+ # This mask is used to check whether the left META meta key is pressed
65
+ META_LEFT_ON = 0x20000
66
+
67
+ # This mask is used to check whether the right META meta key is pressed
68
+ META_RIGHT_ON = 0x40000
69
+
70
+
71
+ class KeyeventAction(IntEnum):
72
+ DOWN = 0
73
+ UP = 1
74
+ MULTIPLE = 2
@@ -0,0 +1,350 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class KeyCode(IntEnum):
5
+ """Android key codes ported from Android's KeyEvent class
6
+
7
+ This enum contains all the key codes defined in Android's KeyEvent class,
8
+ which are used for sending key events to Android devices through scrcpy.
9
+
10
+ The comments for each key code are taken directly from the Android source code
11
+ to maintain compatibility and provide accurate descriptions.
12
+ """
13
+ # Unknown key code
14
+ UNKNOWN = 0
15
+ # Soft Left key - Usually situated below the display on phones
16
+ SOFT_LEFT = 1
17
+ # Soft Right key - Usually situated below the display on phones
18
+ SOFT_RIGHT = 2
19
+ # Home key - This key is handled by the framework and is never delivered to applications
20
+ HOME = 3
21
+ # Back key
22
+ BACK = 4
23
+ # Call key
24
+ CALL = 5
25
+ # End Call key
26
+ ENDCALL = 6
27
+ # '0' key
28
+ KEY_0 = 7
29
+ # '1' key
30
+ KEY_1 = 8
31
+ # '2' key
32
+ KEY_2 = 9
33
+ # '3' key
34
+ KEY_3 = 10
35
+ # '4' key
36
+ KEY_4 = 11
37
+ # '5' key
38
+ KEY_5 = 12
39
+ # '6' key
40
+ KEY_6 = 13
41
+ # '7' key
42
+ KEY_7 = 14
43
+ # '8' key
44
+ KEY_8 = 15
45
+ # '9' key
46
+ KEY_9 = 16
47
+ # '*' key
48
+ STAR = 17
49
+ # '#' key
50
+ POUND = 18
51
+ # Directional Pad Up key - May also be synthesized from trackball motions
52
+ DPAD_UP = 19
53
+ # Directional Pad Down key - May also be synthesized from trackball motions
54
+ DPAD_DOWN = 20
55
+ # Directional Pad Left key - May also be synthesized from trackball motions
56
+ DPAD_LEFT = 21
57
+ # Directional Pad Right key - May also be synthesized from trackball motions
58
+ DPAD_RIGHT = 22
59
+ # Directional Pad Center key - May also be synthesized from trackball motions
60
+ DPAD_CENTER = 23
61
+ # Volume Up key - Adjusts the speaker volume up
62
+ VOLUME_UP = 24
63
+ # Volume Down key - Adjusts the speaker volume down
64
+ VOLUME_DOWN = 25
65
+ # Power key
66
+ POWER = 26
67
+ # Camera key - Used to launch a camera application or take pictures
68
+ CAMERA = 27
69
+ # Clear key
70
+ CLEAR = 28
71
+ A = 29
72
+ B = 30
73
+ C = 31
74
+ D = 32
75
+ E = 33
76
+ F = 34
77
+ G = 35
78
+ H = 36
79
+ I = 37
80
+ J = 38
81
+ K = 39
82
+ L = 40
83
+ M = 41
84
+ N = 42
85
+ O = 43
86
+ P = 44
87
+ Q = 45
88
+ R = 46
89
+ S = 47
90
+ T = 48
91
+ U = 49
92
+ V = 50
93
+ W = 51
94
+ X = 52
95
+ Y = 53
96
+ Z = 54
97
+ COMMA = 55
98
+ PERIOD = 56
99
+ ALT_LEFT = 57
100
+ ALT_RIGHT = 58
101
+ SHIFT_LEFT = 59
102
+ SHIFT_RIGHT = 60
103
+ TAB = 61
104
+ SPACE = 62
105
+ SYM = 63
106
+ EXPLORER = 64
107
+ ENVELOPE = 65
108
+ # Enter key
109
+ ENTER = 66
110
+ # Backspace key - Deletes characters before the insertion point
111
+ DEL = 67
112
+ GRAVE = 68
113
+ MINUS = 69
114
+ EQUALS = 70
115
+ LEFT_BRACKET = 71
116
+ RIGHT_BRACKET = 72
117
+ BACKSLASH = 73
118
+ SEMICOLON = 74
119
+ APOSTROPHE = 75
120
+ SLASH = 76
121
+ AT = 77
122
+ NUM = 78
123
+ HEADSETHOOK = 79
124
+ FOCUS = 80
125
+ PLUS = 81
126
+ # Menu key
127
+ MENU = 82
128
+ NOTIFICATION = 83
129
+ SEARCH = 84
130
+ MEDIA_PLAY_PAUSE = 85
131
+ MEDIA_STOP = 86
132
+ MEDIA_NEXT = 87
133
+ MEDIA_PREVIOUS = 88
134
+ MEDIA_REWIND = 89
135
+ MEDIA_FAST_FORWARD = 90
136
+ MUTE = 91
137
+ PAGE_UP = 92
138
+ PAGE_DOWN = 93
139
+ PICTSYMBOLS = 94
140
+ SWITCH_CHARSET = 95
141
+ BUTTON_A = 96
142
+ BUTTON_B = 97
143
+ BUTTON_C = 98
144
+ BUTTON_X = 99
145
+ BUTTON_Y = 100
146
+ BUTTON_Z = 101
147
+ BUTTON_L1 = 102
148
+ BUTTON_R1 = 103
149
+ BUTTON_L2 = 104
150
+ BUTTON_R2 = 105
151
+ BUTTON_THUMBL = 106
152
+ BUTTON_THUMBR = 107
153
+ BUTTON_START = 108
154
+ BUTTON_SELECT = 109
155
+ BUTTON_MODE = 110
156
+ ESCAPE = 111
157
+ FORWARD_DEL = 112
158
+ CTRL_LEFT = 113
159
+ CTRL_RIGHT = 114
160
+ CAPS_LOCK = 115
161
+ SCROLL_LOCK = 116
162
+ META_LEFT = 117
163
+ META_RIGHT = 118
164
+ FUNCTION = 119
165
+ SYSRQ = 120
166
+ BREAK = 121
167
+ MOVE_HOME = 122
168
+ MOVE_END = 123
169
+ INSERT = 124
170
+ FORWARD = 125
171
+ MEDIA_PLAY = 126
172
+ MEDIA_PAUSE = 127
173
+ MEDIA_CLOSE = 128
174
+ MEDIA_EJECT = 129
175
+ MEDIA_RECORD = 130
176
+ F1 = 131
177
+ F2 = 132
178
+ F3 = 133
179
+ F4 = 134
180
+ F5 = 135
181
+ F6 = 136
182
+ F7 = 137
183
+ F8 = 138
184
+ F9 = 139
185
+ F10 = 140
186
+ F11 = 141
187
+ F12 = 142
188
+ NUM_LOCK = 143
189
+ NUMPAD_0 = 144
190
+ NUMPAD_1 = 145
191
+ NUMPAD_2 = 146
192
+ NUMPAD_3 = 147
193
+ NUMPAD_4 = 148
194
+ NUMPAD_5 = 149
195
+ NUMPAD_6 = 150
196
+ NUMPAD_7 = 151
197
+ NUMPAD_8 = 152
198
+ NUMPAD_9 = 153
199
+ NUMPAD_DIVIDE = 154
200
+ NUMPAD_MULTIPLY = 155
201
+ NUMPAD_SUBTRACT = 156
202
+ NUMPAD_ADD = 157
203
+ NUMPAD_DOT = 158
204
+ NUMPAD_COMMA = 159
205
+ NUMPAD_ENTER = 160
206
+ NUMPAD_EQUALS = 161
207
+ NUMPAD_LEFT_PAREN = 162
208
+ NUMPAD_RIGHT_PAREN = 163
209
+ VOLUME_MUTE = 164
210
+ INFO = 165
211
+ CHANNEL_UP = 166
212
+ CHANNEL_DOWN = 167
213
+ ZOOM_IN = 168
214
+ ZOOM_OUT = 169
215
+ TV = 170
216
+ WINDOW = 171
217
+ GUIDE = 172
218
+ DVR = 173
219
+ BOOKMARK = 174
220
+ CAPTIONS = 175
221
+ SETTINGS = 176
222
+ TV_POWER = 177
223
+ TV_INPUT = 178
224
+ STB_POWER = 179
225
+ STB_INPUT = 180
226
+ AVR_POWER = 181
227
+ AVR_INPUT = 182
228
+ PROG_RED = 183
229
+ PROG_GREEN = 184
230
+ PROG_YELLOW = 185
231
+ PROG_BLUE = 186
232
+ APP_SWITCH = 187
233
+ BUTTON_1 = 188
234
+ BUTTON_2 = 189
235
+ BUTTON_3 = 190
236
+ BUTTON_4 = 191
237
+ BUTTON_5 = 192
238
+ BUTTON_6 = 193
239
+ BUTTON_7 = 194
240
+ BUTTON_8 = 195
241
+ BUTTON_9 = 196
242
+ BUTTON_10 = 197
243
+ BUTTON_11 = 198
244
+ BUTTON_12 = 199
245
+ BUTTON_13 = 200
246
+ BUTTON_14 = 201
247
+ BUTTON_15 = 202
248
+ BUTTON_16 = 203
249
+ LANGUAGE_SWITCH = 204
250
+ MANNER_MODE = 205
251
+ MODE_3D = 206
252
+ CONTACTS = 207
253
+ CALENDAR = 208
254
+ MUSIC = 209
255
+ CALCULATOR = 210
256
+ ZENKAKU_HANKAKU = 211
257
+ EISU = 212
258
+ MUHENKAN = 213
259
+ HENKAN = 214
260
+ KATAKANA_HIRAGANA = 215
261
+ YEN = 216
262
+ RO = 217
263
+ KANA = 218
264
+ ASSIST = 219
265
+ BRIGHTNESS_DOWN = 220
266
+ BRIGHTNESS_UP = 221
267
+ MEDIA_AUDIO_TRACK = 222
268
+ SLEEP = 223
269
+ WAKEUP = 224
270
+ PAIRING = 225
271
+ MEDIA_TOP_MENU = 226
272
+ KEY_11 = 227
273
+ KEY_12 = 228
274
+ LAST_CHANNEL = 229
275
+ TV_DATA_SERVICE = 230
276
+ VOICE_ASSIST = 231
277
+ TV_RADIO_SERVICE = 232
278
+ TV_TELETEXT = 233
279
+ TV_NUMBER_ENTRY = 234
280
+ TV_TERRESTRIAL_ANALOG = 235
281
+ TV_TERRESTRIAL_DIGITAL = 236
282
+ TV_SATELLITE = 237
283
+ TV_SATELLITE_BS = 238
284
+ TV_SATELLITE_CS = 239
285
+ TV_SATELLITE_SERVICE = 240
286
+ TV_NETWORK = 241
287
+ TV_ANTENNA_CABLE = 242
288
+ TV_INPUT_HDMI_1 = 243
289
+ TV_INPUT_HDMI_2 = 244
290
+ TV_INPUT_HDMI_3 = 245
291
+ TV_INPUT_HDMI_4 = 246
292
+ TV_INPUT_COMPOSITE_1 = 247
293
+ TV_INPUT_COMPOSITE_2 = 248
294
+ TV_INPUT_COMPONENT_1 = 249
295
+ TV_INPUT_COMPONENT_2 = 250
296
+ TV_INPUT_VGA_1 = 251
297
+ TV_AUDIO_DESCRIPTION = 252
298
+ TV_AUDIO_DESCRIPTION_MIX_UP = 253
299
+ TV_AUDIO_DESCRIPTION_MIX_DOWN = 254
300
+ TV_ZOOM_MODE = 255
301
+ TV_CONTENTS_MENU = 256
302
+ TV_MEDIA_CONTEXT_MENU = 257
303
+ TV_TIMER_PROGRAMMING = 258
304
+ HELP = 259
305
+ NAVIGATE_PREVIOUS = 260
306
+ NAVIGATE_NEXT = 261
307
+ NAVIGATE_IN = 262
308
+ NAVIGATE_OUT = 263
309
+ STEM_PRIMARY = 264
310
+ STEM_1 = 265
311
+ STEM_2 = 266
312
+ STEM_3 = 267
313
+ DPAD_UP_LEFT = 268
314
+ DPAD_DOWN_LEFT = 269
315
+ DPAD_UP_RIGHT = 270
316
+ DPAD_DOWN_RIGHT = 271
317
+ MEDIA_SKIP_FORWARD = 272
318
+ MEDIA_SKIP_BACKWARD = 273
319
+ MEDIA_STEP_FORWARD = 274
320
+ MEDIA_STEP_BACKWARD = 275
321
+ SOFT_SLEEP = 276
322
+ CUT = 277
323
+ COPY = 278
324
+ PASTE = 279
325
+ SYSTEM_NAVIGATION_UP = 280
326
+ SYSTEM_NAVIGATION_DOWN = 281
327
+ SYSTEM_NAVIGATION_LEFT = 282
328
+ SYSTEM_NAVIGATION_RIGHT = 283
329
+ ALL_APPS = 284
330
+
331
+ # =========================================================================
332
+ # Aliases for original Android KeyEvent names
333
+ # =========================================================================
334
+ # These aliases are provided to maintain compatibility with the original
335
+ # Android KeyEvent naming convention (AKEYCODE_*). This makes it easier
336
+ # to reference keys using the same names as in Android documentation.
337
+
338
+ # Numeric key aliases
339
+ KEYCODE_0 = KEY_0 # '0' key
340
+ KEYCODE_1 = KEY_1 # '1' key
341
+ KEYCODE_2 = KEY_2 # '2' key
342
+ KEYCODE_3 = KEY_3 # '3' key
343
+ KEYCODE_4 = KEY_4 # '4' key
344
+ KEYCODE_5 = KEY_5 # '5' key
345
+ KEYCODE_6 = KEY_6 # '6' key
346
+ KEYCODE_7 = KEY_7 # '7' key
347
+ KEYCODE_8 = KEY_8 # '8' key
348
+ KEYCODE_9 = KEY_9 # '9' key
349
+ KEYCODE_11 = KEY_11 # '11' key
350
+ KEYCODE_12 = KEY_12 # '12' key
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import socket
6
+ import struct
7
+ from typing import Optional
8
+
9
+ import retry
10
+ from adbutils import AdbError, Network, adb
11
+ from adbutils._adb import AdbConnection
12
+ from adbutils._device import AdbDevice
13
+ from starlette.websockets import WebSocket, WebSocketDisconnect
14
+
15
+ from uiautodev.remote.touch_controller import ScrcpyTouchController
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class ScrcpyServer:
20
+ """
21
+ ScrcpyServer class is responsible for managing the scrcpy server on Android devices.
22
+ It handles the initialization, communication, and control of the scrcpy server,
23
+ including video streaming and touch control.
24
+ """
25
+
26
+ def __init__(self, device: AdbDevice, scrcpy_jar_path: Optional[str] = None):
27
+ """
28
+ Initializes the ScrcpyServer instance.
29
+
30
+ Args:
31
+ scrcpy_jar_path (str, optional): Path to the scrcpy server JAR file. Defaults to None.
32
+ """
33
+ self.scrcpy_jar_path = scrcpy_jar_path or os.path.join(os.path.dirname(__file__), '../binaries/scrcpy_server.jar')
34
+ self.device = device
35
+ self.resolution_width = 0 # scrcpy 投屏转换宽度
36
+ self.resolution_height = 0 # scrcpy 投屏转换高度
37
+
38
+ self._shell_conn: AdbConnection
39
+ self._video_conn: socket.socket
40
+ self._control_conn: socket.socket
41
+
42
+ self._setup_connection()
43
+
44
+ def _setup_connection(self):
45
+ self._shell_conn = self._start_scrcpy_server(control=True)
46
+ self._video_conn = self._connect_scrcpy(self.device)
47
+ self._control_conn = self._connect_scrcpy(self.device)
48
+ self._parse_scrcpy_info(self._video_conn)
49
+ self.controller = ScrcpyTouchController(self._control_conn)
50
+
51
+ @retry.retry(exceptions=AdbError, tries=20, delay=0.1)
52
+ def _connect_scrcpy(self, device: AdbDevice) -> socket.socket:
53
+ return device.create_connection(Network.LOCAL_ABSTRACT, 'scrcpy')
54
+
55
+ def _parse_scrcpy_info(self, conn: socket.socket):
56
+ dummy_byte = conn.recv(1)
57
+ if not dummy_byte or dummy_byte != b"\x00":
58
+ raise ConnectionError("Did not receive Dummy Byte!")
59
+ logger.debug('Received Dummy Byte!')
60
+ device_name = conn.recv(64).decode('utf-8').rstrip('\x00')
61
+ logger.debug(f'Device name: {device_name}')
62
+ codec = conn.recv(4)
63
+ logger.debug(f'resolution_data: {codec}')
64
+ resolution_data = conn.recv(8)
65
+ logger.debug(f'resolution_data: {resolution_data}')
66
+ self.resolution_width, self.resolution_height = struct.unpack(">II", resolution_data)
67
+ logger.debug(f'Resolution: {self.resolution_width}x{self.resolution_height}')
68
+
69
+ def close(self):
70
+ try:
71
+ self._control_conn.close()
72
+ self._video_conn.close()
73
+ self._shell_conn.close()
74
+ except:
75
+ pass
76
+
77
+ def __del__(self):
78
+ self.close()
79
+
80
+ def _start_scrcpy_server(self, control: bool = True) -> AdbConnection:
81
+ """
82
+ Pushes the scrcpy server JAR file to the Android device and starts the scrcpy server.
83
+
84
+ Args:
85
+ control (bool, optional): Whether to enable touch control. Defaults to True.
86
+
87
+ Returns:
88
+ AdbConnection
89
+ """
90
+ # 获取设备对象
91
+ device = self.device
92
+
93
+ # 推送 scrcpy 服务器到设备
94
+ device.sync.push(self.scrcpy_jar_path, '/data/local/tmp/scrcpy_server.jar', check=True)
95
+ logger.info('scrcpy server JAR pushed to device')
96
+
97
+ # 构建启动 scrcpy 服务器的命令
98
+ start_command = (
99
+ 'CLASSPATH=/data/local/tmp/scrcpy_server.jar '
100
+ 'app_process / '
101
+ 'com.genymobile.scrcpy.Server 2.7 '
102
+ 'log_level=info max_size=1024 max_fps=30 '
103
+ 'video_bit_rate=8000000 tunnel_forward=true '
104
+ 'send_frame_meta=false '
105
+ f'control={"true" if control else "false"} '
106
+ 'audio=false show_touches=false stay_awake=false '
107
+ 'power_off_on_close=false clipboard_autosync=false'
108
+ )
109
+ conn = device.shell(start_command, stream=True)
110
+ logger.debug("scrcpy output: %s", conn.conn.recv(100))
111
+ return conn # type: ignore
112
+
113
+ async def handle_unified_websocket(self, websocket: WebSocket, serial=''):
114
+ logger.info(f"[Unified] WebSocket connection from {websocket} for serial: {serial}")
115
+
116
+ video_task = asyncio.create_task(self._stream_video_to_websocket(self._video_conn, websocket))
117
+ control_task = asyncio.create_task(self._handle_control_websocket(websocket))
118
+
119
+ try:
120
+ # 不使用 return_exceptions=True,让异常能够正确传播
121
+ await asyncio.gather(video_task, control_task)
122
+ finally:
123
+ # 取消任务
124
+ for task in (video_task, control_task):
125
+ if not task.done():
126
+ task.cancel()
127
+ logger.info(f"[Unified] WebSocket closed for serial={serial}")
128
+
129
+ async def _stream_video_to_websocket(self, conn: socket.socket, ws: WebSocket):
130
+ # Set socket to non-blocking mode
131
+ conn.setblocking(False)
132
+
133
+ while True:
134
+ # check if ws closed
135
+ if ws.client_state.name != "CONNECTED":
136
+ logger.info('WebSocket no longer connected. Exiting video stream.')
137
+ break
138
+ # Use asyncio to read data asynchronously
139
+ data = await asyncio.get_event_loop().sock_recv(conn, 1024*1024)
140
+ if not data:
141
+ logger.warning('No data received, connection may be closed.')
142
+ raise ConnectionError("Video stream ended unexpectedly")
143
+ # send data to ws
144
+ await ws.send_bytes(data)
145
+
146
+ async def _handle_control_websocket(self, ws: WebSocket):
147
+ while True:
148
+ try:
149
+ message = await ws.receive_text()
150
+ logger.debug(f"[Unified] Received message: {message}")
151
+ message = json.loads(message)
152
+
153
+ width, height = self.resolution_width, self.resolution_height
154
+ message_type = message.get('type')
155
+ if message_type == 'touchMove':
156
+ xP = message['xP']
157
+ yP = message['yP']
158
+ self.controller.move(int(xP * width), int(yP * height), width, height)
159
+ elif message_type == 'touchDown':
160
+ xP = message['xP']
161
+ yP = message['yP']
162
+ self.controller.down(int(xP * width), int(yP * height), width, height)
163
+ elif message_type == 'touchUp':
164
+ xP = message['xP']
165
+ yP = message['yP']
166
+ self.controller.up(int(xP * width), int(yP * height), width, height)
167
+ elif message_type == 'keyEvent':
168
+ event_number = message['data']['eventNumber']
169
+ self.device.shell(f'input keyevent {event_number}')
170
+ elif message_type == 'text':
171
+ text = message['detail']
172
+ self.device.shell(f'am broadcast -a SONIC_KEYBOARD --es msg \'{text}\'')
173
+ elif message_type == 'ping':
174
+ await ws.send_text(json.dumps({"type": "pong"}))
175
+ except json.JSONDecodeError as e:
176
+ logger.error(f"Invalid JSON message: {e}")
177
+ continue