autoglm-gui 1.4.1__py3-none-any.whl → 1.5.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.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -4
- AutoGLM_GUI/actions/__init__.py +6 -0
- AutoGLM_GUI/actions/handler.py +196 -0
- AutoGLM_GUI/actions/types.py +15 -0
- AutoGLM_GUI/adb/__init__.py +53 -0
- AutoGLM_GUI/adb/apps.py +227 -0
- AutoGLM_GUI/adb/connection.py +323 -0
- AutoGLM_GUI/adb/device.py +171 -0
- AutoGLM_GUI/adb/input.py +67 -0
- AutoGLM_GUI/adb/screenshot.py +11 -0
- AutoGLM_GUI/adb/timing.py +167 -0
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +43 -12
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +31 -38
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +292 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
- AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +405 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +12 -8
- AutoGLM_GUI/agents/stream_runner.py +188 -0
- AutoGLM_GUI/api/__init__.py +40 -21
- AutoGLM_GUI/api/agents.py +157 -240
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +102 -12
- AutoGLM_GUI/api/history.py +78 -0
- AutoGLM_GUI/api/layered_agent.py +67 -15
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/config.py +81 -0
- AutoGLM_GUI/config_manager.py +68 -51
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +1 -1
- AutoGLM_GUI/devices/adb_device.py +5 -10
- AutoGLM_GUI/devices/mock_device.py +4 -2
- AutoGLM_GUI/devices/remote_device.py +8 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/i18n.py +81 -0
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +96 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +62 -396
- AutoGLM_GUI/platform_utils.py +26 -0
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +304 -0
- AutoGLM_GUI/schemas.py +234 -72
- AutoGLM_GUI/scrcpy_stream.py +142 -24
- AutoGLM_GUI/socketio_server.py +100 -27
- AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
- AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
- AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
- AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
- AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
- AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
- AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +17 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- AutoGLM_GUI/agents/mai_adapter.py +0 -627
- AutoGLM_GUI/api/dual_model.py +0 -317
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -147
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
- AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
- AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
- AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
- autoglm_gui-1.4.1.dist-info/RECORD +0 -117
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/scrcpy_stream.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import socket
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
|
+
import time
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from asyncio.subprocess import Process as AsyncProcess
|
|
@@ -23,6 +24,74 @@ from AutoGLM_GUI.scrcpy_protocol import (
|
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
async def is_port_available(port: int, host: str = "127.0.0.1") -> bool:
|
|
28
|
+
"""Test if TCP port is available for binding.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
port: TCP port number
|
|
32
|
+
host: Host address to test
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if port can be bound (available), False otherwise
|
|
36
|
+
"""
|
|
37
|
+
sock = None
|
|
38
|
+
try:
|
|
39
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
40
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
41
|
+
sock.setblocking(False)
|
|
42
|
+
sock.bind((host, port))
|
|
43
|
+
logger.debug(f"Port {port} is available for binding")
|
|
44
|
+
return True
|
|
45
|
+
except OSError as e:
|
|
46
|
+
# Handle cross-platform errno for "Address already in use"
|
|
47
|
+
# macOS: 48, Linux: 98, Windows: 10048
|
|
48
|
+
logger.debug(f"Port {port} is occupied: {e}")
|
|
49
|
+
return False
|
|
50
|
+
finally:
|
|
51
|
+
if sock:
|
|
52
|
+
sock.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def wait_for_port_release(
|
|
56
|
+
port: int,
|
|
57
|
+
timeout: float = 5.0,
|
|
58
|
+
poll_interval: float = 0.2,
|
|
59
|
+
host: str = "127.0.0.1",
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Wait for TCP port to become available with polling.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
port: TCP port to wait for
|
|
65
|
+
timeout: Maximum wait time in seconds (default: 5.0)
|
|
66
|
+
poll_interval: Check interval in seconds (default: 0.2)
|
|
67
|
+
host: Host address
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if port became available, False if timeout
|
|
71
|
+
"""
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
attempt = 0
|
|
74
|
+
|
|
75
|
+
while time.time() - start_time < timeout:
|
|
76
|
+
attempt += 1
|
|
77
|
+
if await is_port_available(port, host):
|
|
78
|
+
elapsed = time.time() - start_time
|
|
79
|
+
logger.info(
|
|
80
|
+
f"Port {port} became available after {elapsed:.2f}s ({attempt} checks)"
|
|
81
|
+
)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Log progress every second for debugging
|
|
85
|
+
if attempt % 5 == 0: # Every 1 second (5 * 0.2s)
|
|
86
|
+
elapsed = time.time() - start_time
|
|
87
|
+
logger.debug(f"Still waiting for port {port}... ({elapsed:.1f}s elapsed)")
|
|
88
|
+
|
|
89
|
+
await asyncio.sleep(poll_interval)
|
|
90
|
+
|
|
91
|
+
logger.warning(f"Port {port} did not release within {timeout}s timeout")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
26
95
|
@dataclass
|
|
27
96
|
class ScrcpyServerOptions:
|
|
28
97
|
max_size: int
|
|
@@ -159,29 +228,44 @@ class ScrcpyStreamer:
|
|
|
159
228
|
raise RuntimeError(f"Failed to start scrcpy server: {e}") from e
|
|
160
229
|
|
|
161
230
|
async def _cleanup_existing_server(self) -> None:
|
|
162
|
-
"""Kill existing scrcpy server processes
|
|
231
|
+
"""Kill existing scrcpy server processes and wait for port release."""
|
|
163
232
|
cmd_base = ["adb"]
|
|
164
233
|
if self.device_id:
|
|
165
234
|
cmd_base.extend(["-s", self.device_id])
|
|
166
235
|
|
|
167
236
|
# Method 1: Try pkill
|
|
237
|
+
logger.debug("Killing scrcpy processes via pkill...")
|
|
168
238
|
cmd = cmd_base + ["shell", "pkill", "-9", "-f", "app_process.*scrcpy"]
|
|
169
239
|
await run_cmd_silently(cmd)
|
|
170
240
|
|
|
171
241
|
# Method 2: Find and kill by PID (more reliable)
|
|
242
|
+
logger.debug("Killing scrcpy processes via PID...")
|
|
172
243
|
cmd = cmd_base + [
|
|
173
244
|
"shell",
|
|
174
245
|
"ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9",
|
|
175
246
|
]
|
|
176
247
|
await run_cmd_silently(cmd)
|
|
177
248
|
|
|
178
|
-
# Method 3: Remove port forward
|
|
249
|
+
# Method 3: Remove port forward
|
|
250
|
+
logger.debug(f"Removing ADB port forward on port {self.port}...")
|
|
179
251
|
cmd_remove_forward = cmd_base + ["forward", "--remove", f"tcp:{self.port}"]
|
|
180
252
|
await run_cmd_silently(cmd_remove_forward)
|
|
181
253
|
|
|
182
|
-
# Wait for
|
|
183
|
-
logger.
|
|
184
|
-
await
|
|
254
|
+
# Wait for port to be truly available (instead of fixed sleep)
|
|
255
|
+
logger.info(f"Waiting for port {self.port} to be released...")
|
|
256
|
+
port_released = await wait_for_port_release(
|
|
257
|
+
self.port,
|
|
258
|
+
timeout=5.0, # Max 5 seconds (vs old fixed 2s)
|
|
259
|
+
poll_interval=0.2, # Check every 200ms
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if not port_released:
|
|
263
|
+
logger.warning(
|
|
264
|
+
f"Port {self.port} still occupied after cleanup. "
|
|
265
|
+
"Will attempt to start anyway (may fail)."
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
logger.info(f"Port {self.port} successfully released and ready")
|
|
185
269
|
|
|
186
270
|
async def _push_server(self) -> None:
|
|
187
271
|
"""Push scrcpy-server to device."""
|
|
@@ -221,9 +305,9 @@ class ScrcpyStreamer:
|
|
|
221
305
|
)
|
|
222
306
|
|
|
223
307
|
async def _start_server(self) -> None:
|
|
224
|
-
"""Start scrcpy server on device with retry
|
|
308
|
+
"""Start scrcpy server on device with intelligent retry."""
|
|
225
309
|
max_retries = 3
|
|
226
|
-
retry_delay =
|
|
310
|
+
retry_delay = 1.0 # Reduced from 2s (cleanup handles waiting now)
|
|
227
311
|
|
|
228
312
|
options = self._build_server_options()
|
|
229
313
|
|
|
@@ -275,43 +359,77 @@ class ScrcpyStreamer:
|
|
|
275
359
|
error_msg = stderr.decode() if stderr else stdout.decode()
|
|
276
360
|
|
|
277
361
|
if error_msg is not None:
|
|
362
|
+
# Detailed error classification
|
|
278
363
|
if "Address already in use" in error_msg:
|
|
364
|
+
logger.error(
|
|
365
|
+
f"Port {self.port} conflict detected (attempt {attempt + 1}/{max_retries}). "
|
|
366
|
+
f"Error: {error_msg[:200]}"
|
|
367
|
+
)
|
|
279
368
|
if attempt < max_retries - 1:
|
|
280
369
|
logger.warning(
|
|
281
|
-
f"
|
|
370
|
+
f"Retrying with aggressive cleanup in {retry_delay}s..."
|
|
282
371
|
)
|
|
283
372
|
await self._cleanup_existing_server()
|
|
284
373
|
await asyncio.sleep(retry_delay)
|
|
285
374
|
continue
|
|
375
|
+
# Specific error for port conflicts
|
|
286
376
|
raise RuntimeError(
|
|
287
|
-
f"
|
|
377
|
+
f"Port {self.port} persistently occupied after {max_retries} attempts. "
|
|
378
|
+
"Please check if another scrcpy instance is running."
|
|
288
379
|
)
|
|
289
|
-
|
|
380
|
+
else:
|
|
381
|
+
# Non-port errors fail immediately (no retry)
|
|
382
|
+
logger.error(f"Scrcpy server startup failed: {error_msg[:200]}")
|
|
383
|
+
raise RuntimeError(f"Scrcpy server failed to start: {error_msg}")
|
|
290
384
|
|
|
385
|
+
logger.info("Scrcpy server started successfully")
|
|
291
386
|
return
|
|
292
387
|
|
|
293
388
|
raise RuntimeError("Failed to start scrcpy server after maximum retries")
|
|
294
389
|
|
|
295
390
|
async def _connect_socket(self) -> None:
|
|
296
391
|
"""Connect to scrcpy TCP socket."""
|
|
297
|
-
|
|
298
|
-
|
|
392
|
+
# Retry connection with exponential backoff (max ~6 seconds total)
|
|
393
|
+
max_attempts = 10
|
|
394
|
+
retry_delay = 0.3
|
|
299
395
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
396
|
+
for attempt in range(max_attempts):
|
|
397
|
+
# Create a fresh socket for each attempt to avoid "Invalid argument" error
|
|
398
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
399
|
+
sock.settimeout(5)
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024)
|
|
403
|
+
except OSError as e:
|
|
404
|
+
logger.debug(f"Failed to set socket buffer size: {e}")
|
|
307
405
|
|
|
308
|
-
for _ in range(5):
|
|
309
406
|
try:
|
|
310
|
-
|
|
311
|
-
|
|
407
|
+
sock.connect(("localhost", self.port))
|
|
408
|
+
sock.settimeout(None)
|
|
409
|
+
self.tcp_socket = sock # Only assign on success
|
|
410
|
+
logger.debug(f"Connected to scrcpy server on attempt {attempt + 1}")
|
|
312
411
|
return
|
|
313
|
-
except (ConnectionRefusedError, OSError):
|
|
314
|
-
|
|
412
|
+
except (ConnectionRefusedError, OSError) as e:
|
|
413
|
+
# Close the failed socket
|
|
414
|
+
try:
|
|
415
|
+
sock.close()
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
if attempt < max_attempts - 1:
|
|
420
|
+
logger.debug(
|
|
421
|
+
f"Connection attempt {attempt + 1}/{max_attempts} failed: {e}. "
|
|
422
|
+
f"Retrying in {retry_delay}s..."
|
|
423
|
+
)
|
|
424
|
+
await asyncio.sleep(retry_delay)
|
|
425
|
+
# Gradually increase delay for later attempts
|
|
426
|
+
if attempt >= 3:
|
|
427
|
+
retry_delay = 0.5
|
|
428
|
+
else:
|
|
429
|
+
logger.error(
|
|
430
|
+
f"Failed to connect after {max_attempts} attempts. "
|
|
431
|
+
f"Last error: {e}"
|
|
432
|
+
)
|
|
315
433
|
|
|
316
434
|
raise ConnectionError("Failed to connect to scrcpy server")
|
|
317
435
|
|
AutoGLM_GUI/socketio_server.py
CHANGED
|
@@ -4,9 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import time
|
|
7
|
-
from typing import NotRequired
|
|
8
7
|
|
|
9
|
-
from typing_extensions import TypedDict
|
|
8
|
+
from typing_extensions import NotRequired, TypedDict
|
|
10
9
|
|
|
11
10
|
import socketio
|
|
12
11
|
|
|
@@ -31,6 +30,9 @@ sio = socketio.AsyncServer(
|
|
|
31
30
|
|
|
32
31
|
_socket_streamers: dict[str, ScrcpyStreamer] = {}
|
|
33
32
|
_stream_tasks: dict[str, asyncio.Task] = {}
|
|
33
|
+
_device_locks: dict[
|
|
34
|
+
str, asyncio.Lock
|
|
35
|
+
] = {} # Lock per device to prevent concurrent connections
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
async def _stop_stream_for_sid(sid: str) -> None:
|
|
@@ -43,6 +45,46 @@ async def _stop_stream_for_sid(sid: str) -> None:
|
|
|
43
45
|
streamer.stop()
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def _classify_error(exc: Exception) -> dict:
|
|
49
|
+
"""Classify error and return user-friendly message."""
|
|
50
|
+
error_str = str(exc)
|
|
51
|
+
|
|
52
|
+
if "Address already in use" in error_str or (
|
|
53
|
+
"Port" in error_str and "occupied" in error_str
|
|
54
|
+
):
|
|
55
|
+
return {
|
|
56
|
+
"message": "端口冲突,视频流端口仍被占用。通常会自动解决,如果持续出现请重启应用。",
|
|
57
|
+
"type": "port_conflict",
|
|
58
|
+
"technical_details": error_str,
|
|
59
|
+
}
|
|
60
|
+
elif "Device" in error_str and (
|
|
61
|
+
"not available" in error_str or "not found" in error_str
|
|
62
|
+
):
|
|
63
|
+
return {
|
|
64
|
+
"message": "设备无响应,请检查 USB/WiFi 连接。",
|
|
65
|
+
"type": "device_offline",
|
|
66
|
+
"technical_details": error_str,
|
|
67
|
+
}
|
|
68
|
+
elif "timeout" in error_str.lower() or "timed out" in error_str.lower():
|
|
69
|
+
return {
|
|
70
|
+
"message": "连接超时,请检查设备连接后重试。",
|
|
71
|
+
"type": "timeout",
|
|
72
|
+
"technical_details": error_str,
|
|
73
|
+
}
|
|
74
|
+
elif "Failed to connect" in error_str:
|
|
75
|
+
return {
|
|
76
|
+
"message": "无法连接到 scrcpy 服务器,请检查设备连接。",
|
|
77
|
+
"type": "connection_failed",
|
|
78
|
+
"technical_details": error_str,
|
|
79
|
+
}
|
|
80
|
+
else:
|
|
81
|
+
return {
|
|
82
|
+
"message": error_str,
|
|
83
|
+
"type": "unknown",
|
|
84
|
+
"technical_details": error_str,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
46
88
|
def stop_streamers(device_id: str | None = None) -> None:
|
|
47
89
|
"""Stop active scrcpy streamers (all or by device)."""
|
|
48
90
|
sids = list(_socket_streamers.keys())
|
|
@@ -103,35 +145,66 @@ async def disconnect(sid: str) -> None:
|
|
|
103
145
|
async def connect_device(sid: str, data: dict | None) -> None:
|
|
104
146
|
payload = data or {}
|
|
105
147
|
device_id = payload.get("device_id") or payload.get("deviceId")
|
|
148
|
+
if not device_id:
|
|
149
|
+
await sio.emit(
|
|
150
|
+
"error",
|
|
151
|
+
{"message": "Device ID is required", "type": "invalid_request"},
|
|
152
|
+
to=sid,
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
106
156
|
max_size = int(payload.get("maxSize") or 1280)
|
|
107
157
|
bit_rate = int(payload.get("bitRate") or 4_000_000)
|
|
108
158
|
|
|
159
|
+
# Stop any existing stream for this sid
|
|
109
160
|
await _stop_stream_for_sid(sid)
|
|
110
161
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
# Get or create a lock for this device
|
|
163
|
+
if device_id not in _device_locks:
|
|
164
|
+
_device_locks[device_id] = asyncio.Lock()
|
|
165
|
+
|
|
166
|
+
device_lock = _device_locks[device_id]
|
|
167
|
+
|
|
168
|
+
# Acquire lock to prevent concurrent connections to the same device
|
|
169
|
+
async with device_lock:
|
|
170
|
+
logger.debug(f"Acquired device lock for {device_id}, sid: {sid}")
|
|
171
|
+
|
|
172
|
+
# Stop any existing streams for the same device (from other sids)
|
|
173
|
+
sids_to_stop = [
|
|
174
|
+
s
|
|
175
|
+
for s, streamer in _socket_streamers.items()
|
|
176
|
+
if s != sid and streamer.device_id == device_id
|
|
177
|
+
]
|
|
178
|
+
for s in sids_to_stop:
|
|
179
|
+
logger.info(f"Stopping existing stream for device {device_id} from sid {s}")
|
|
180
|
+
await _stop_stream_for_sid(s)
|
|
181
|
+
|
|
182
|
+
streamer = ScrcpyStreamer(
|
|
183
|
+
device_id=device_id,
|
|
184
|
+
max_size=max_size,
|
|
185
|
+
bit_rate=bit_rate,
|
|
129
186
|
)
|
|
130
|
-
except Exception as exc:
|
|
131
|
-
streamer.stop()
|
|
132
|
-
logger.exception("Failed to start scrcpy stream: %s", exc)
|
|
133
|
-
await sio.emit("error", {"message": str(exc)}, to=sid)
|
|
134
|
-
return
|
|
135
187
|
|
|
136
|
-
|
|
137
|
-
|
|
188
|
+
try:
|
|
189
|
+
await streamer.start() # ScrcpyStreamer has built-in retry logic
|
|
190
|
+
metadata = await streamer.read_video_metadata()
|
|
191
|
+
await sio.emit(
|
|
192
|
+
"video-metadata",
|
|
193
|
+
{
|
|
194
|
+
"deviceName": metadata.device_name,
|
|
195
|
+
"width": metadata.width,
|
|
196
|
+
"height": metadata.height,
|
|
197
|
+
"codec": metadata.codec,
|
|
198
|
+
},
|
|
199
|
+
to=sid,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
_socket_streamers[sid] = streamer
|
|
203
|
+
_stream_tasks[sid] = asyncio.create_task(_stream_packets(sid, streamer))
|
|
204
|
+
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
streamer.stop()
|
|
207
|
+
logger.exception("Failed to start scrcpy stream: %s", exc)
|
|
208
|
+
# Use unified error classification
|
|
209
|
+
error_info = _classify_error(exc)
|
|
210
|
+
await sio.emit("error", error_info, to=sid)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as o}from"./index-
|
|
1
|
+
import{j as o}from"./index-CssG-3TH.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as u,r as o,j as a,b as r,B as d}from"./index-CssG-3TH.js";import{P as g,b as x,c as f,d as m}from"./popover-DLsuV5Sx.js";import{D as p,d as h,e as w,f as j,g as D}from"./dialog-DZ78cEcj.js";const N=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],b=u("check",N),c=o.createContext(void 0),P=({value:t="",onValueChange:e,children:s})=>{const[n,l]=o.useState(!1);return a.jsx(c.Provider,{value:{value:t,onValueChange:e||(()=>{}),open:n,setOpen:l},children:a.jsx(g,{open:n,onOpenChange:l,children:s})})},C=o.forwardRef(({className:t,children:e,...s},n)=>{if(!o.useContext(c))throw new Error("SelectTrigger must be used within Select");return a.jsx(x,{asChild:!0,children:a.jsxs("button",{ref:n,className:r("flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300",t),...s,children:[e,a.jsx(f,{className:"h-4 w-4 opacity-50"})]})})});C.displayName="SelectTrigger";const V=({placeholder:t})=>{const e=o.useContext(c);if(!e)throw new Error("SelectValue must be used within Select");return a.jsx("span",{className:e.value?"":"text-slate-500",children:e.value||t})},I=({children:t,className:e})=>a.jsx(m,{className:r("w-[var(--radix-popover-trigger-width)] p-1",e),children:t}),O=({value:t,children:e,disabled:s,className:n})=>{const l=o.useContext(c);if(!l)throw new Error("SelectItem must be used within Select");const i=l.value===t;return a.jsxs("div",{role:"option","aria-selected":i,className:r("relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-slate-100 focus:bg-slate-100 dark:hover:bg-slate-800 dark:focus:bg-slate-800",s&&"pointer-events-none opacity-50",n),onClick:()=>{s||(l.onValueChange(t),l.setOpen(!1))},children:[a.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:i&&a.jsx(b,{className:"h-4 w-4"})}),e]})},B=({open:t,onOpenChange:e,children:s})=>a.jsx(p,{open:t,onOpenChange:e,children:s}),v=o.forwardRef(({className:t,...e},s)=>a.jsx(h,{ref:s,className:r("sm:max-w-[425px]",t),...e}));v.displayName="AlertDialogContent";const F=({className:t,...e})=>a.jsx(w,{className:r(t),...e}),H=({className:t,...e})=>a.jsx(D,{className:r(t),...e}),A=o.forwardRef(({className:t,...e},s)=>a.jsx(j,{ref:s,className:r(t),...e}));A.displayName="AlertDialogTitle";const S=o.forwardRef(({className:t,...e},s)=>a.jsx("p",{ref:s,className:r("text-sm text-slate-500 dark:text-slate-400",t),...e}));S.displayName="AlertDialogDescription";const y=o.forwardRef(({className:t,...e},s)=>a.jsx(d,{ref:s,className:r(t),...e}));y.displayName="AlertDialogAction";const k=o.forwardRef(({className:t,...e},s)=>a.jsx(d,{ref:s,variant:"outline",className:r(t),...e}));k.displayName="AlertDialogCancel";export{B as A,P as S,C as a,V as b,I as c,O as d,v as e,F as f,A as g,S as h,H as i,k as j,y as k};
|