autoglm-gui 0.4.7__tar.gz → 0.4.9__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.
- autoglm_gui-0.4.9/AutoGLM_GUI/__init__.py +52 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/api/media.py +58 -44
- autoglm_gui-0.4.9/AutoGLM_GUI/platform_utils.py +37 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/scrcpy_stream.py +117 -143
- autoglm_gui-0.4.7/AutoGLM_GUI/static/assets/about-DIdU3ZqP.js → autoglm_gui-0.4.9/AutoGLM_GUI/static/assets/about-BI6OV6gm.js +1 -1
- autoglm_gui-0.4.9/AutoGLM_GUI/static/assets/chat-C_2Cot0q.js +25 -0
- autoglm_gui-0.4.7/AutoGLM_GUI/static/assets/index--ElIPD22.js → autoglm_gui-0.4.9/AutoGLM_GUI/static/assets/index-Dn3vR6uV.js +1 -1
- autoglm_gui-0.4.7/AutoGLM_GUI/static/assets/index-BuFMN8G5.js → autoglm_gui-0.4.9/AutoGLM_GUI/static/assets/index-Do7ha9Kf.js +1 -1
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/static/index.html +1 -1
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/PKG-INFO +4 -1
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/README.md +3 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/pyproject.toml +1 -1
- autoglm_gui-0.4.7/AutoGLM_GUI/__init__.py +0 -9
- autoglm_gui-0.4.7/AutoGLM_GUI/static/assets/chat-_-u1G4Ee.js +0 -25
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/.gitignore +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/__main__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/adb_plus/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/adb_plus/touch.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/api/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/api/agents.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/api/control.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/api/devices.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/config.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/schemas.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/server.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/state.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/static/assets/index-DCrxTz-A.css +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/AutoGLM_GUI/version.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/LICENSE +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/actions/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/actions/handler.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/adb/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/adb/connection.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/adb/device.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/adb/input.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/adb/screenshot.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/agent.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/config/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/config/apps.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/config/i18n.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/config/prompts.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/config/prompts_en.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/config/prompts_zh.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/model/__init__.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/phone_agent/model/client.py +0 -0
- {autoglm_gui-0.4.7 → autoglm_gui-0.4.9}/scrcpy-server-v3.3.3 +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""AutoGLM-GUI package metadata."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Fix Windows encoding issue: Force UTF-8 for all subprocess calls
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# On Windows, subprocess defaults to GBK encoding which fails when ADB/scrcpy
|
|
12
|
+
# output UTF-8 characters. This monkey patch ensures all subprocess calls
|
|
13
|
+
# use UTF-8 encoding by default.
|
|
14
|
+
|
|
15
|
+
_original_run = subprocess.run
|
|
16
|
+
_original_popen = subprocess.Popen
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@wraps(_original_run)
|
|
20
|
+
def _patched_run(*args, **kwargs):
|
|
21
|
+
"""Patched subprocess.run that defaults to UTF-8 encoding on Windows."""
|
|
22
|
+
if sys.platform == "win32":
|
|
23
|
+
# Add encoding='utf-8' if text=True is set but encoding is not specified
|
|
24
|
+
if kwargs.get("text") or kwargs.get("universal_newlines"):
|
|
25
|
+
if "encoding" not in kwargs:
|
|
26
|
+
kwargs["encoding"] = "utf-8"
|
|
27
|
+
return _original_run(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _PatchedPopen(_original_popen):
|
|
31
|
+
"""Patched subprocess.Popen that defaults to UTF-8 encoding on Windows."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args, **kwargs):
|
|
34
|
+
if sys.platform == "win32":
|
|
35
|
+
# Add encoding='utf-8' if text=True is set but encoding is not specified
|
|
36
|
+
if kwargs.get("text") or kwargs.get("universal_newlines"):
|
|
37
|
+
if "encoding" not in kwargs:
|
|
38
|
+
kwargs["encoding"] = "utf-8"
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Apply the patches globally
|
|
43
|
+
subprocess.run = _patched_run
|
|
44
|
+
subprocess.Popen = _PatchedPopen
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
|
|
48
|
+
# Expose package version at runtime; fall back to "unknown" during editable/dev runs
|
|
49
|
+
try:
|
|
50
|
+
__version__ = metadata.version("autoglm-gui")
|
|
51
|
+
except metadata.PackageNotFoundError:
|
|
52
|
+
__version__ = "unknown"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Media routes: screenshot, video stream, stream reset."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
6
8
|
|
|
@@ -11,6 +13,9 @@ from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
|
|
|
11
13
|
|
|
12
14
|
router = APIRouter()
|
|
13
15
|
|
|
16
|
+
# Debug configuration: Set DEBUG_SAVE_VIDEO_STREAM=1 to save streams to debug_streams/
|
|
17
|
+
DEBUG_SAVE_STREAM = os.getenv("DEBUG_SAVE_VIDEO_STREAM", "0") == "1"
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
@router.post("/api/video/reset")
|
|
16
21
|
async def reset_video_stream(device_id: str | None = None) -> dict:
|
|
@@ -81,17 +86,14 @@ async def video_stream_ws(
|
|
|
81
86
|
|
|
82
87
|
print(f"[video/stream] WebSocket connection for device {device_id}")
|
|
83
88
|
|
|
84
|
-
# Debug: Save stream to file for analysis
|
|
85
|
-
# Set to True for debugging (default: False)
|
|
86
|
-
debug_save = False
|
|
89
|
+
# Debug: Save stream to file for analysis (controlled by DEBUG_SAVE_VIDEO_STREAM env var)
|
|
87
90
|
debug_file = None
|
|
88
|
-
if
|
|
89
|
-
import os
|
|
90
|
-
from pathlib import Path
|
|
91
|
-
|
|
91
|
+
if DEBUG_SAVE_STREAM:
|
|
92
92
|
debug_dir = Path("debug_streams")
|
|
93
93
|
debug_dir.mkdir(exist_ok=True)
|
|
94
|
-
debug_file_path =
|
|
94
|
+
debug_file_path = (
|
|
95
|
+
debug_dir / f"{device_id}_{int(__import__('time').time())}.h264"
|
|
96
|
+
)
|
|
95
97
|
debug_file = open(debug_file_path, "wb")
|
|
96
98
|
print(f"[video/stream] DEBUG: Saving stream to {debug_file_path}")
|
|
97
99
|
|
|
@@ -110,46 +112,45 @@ async def video_stream_ws(
|
|
|
110
112
|
await scrcpy_streamers[device_id].start()
|
|
111
113
|
print(f"[video/stream] Scrcpy server started for device {device_id}")
|
|
112
114
|
|
|
113
|
-
# Read
|
|
114
|
-
# Then parse the entire buffer to find complete NAL units
|
|
115
|
+
# Read NAL units until we have SPS, PPS, and IDR
|
|
115
116
|
streamer = scrcpy_streamers[device_id]
|
|
116
|
-
accumulated_buffer = bytearray()
|
|
117
|
-
target_size = 50 * 1024 # Accumulate at least 50KB
|
|
118
117
|
|
|
119
|
-
print(
|
|
120
|
-
for attempt in range(
|
|
118
|
+
print("[video/stream] Reading NAL units for initialization...")
|
|
119
|
+
for attempt in range(20): # Max 20 NAL units for initialization
|
|
121
120
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
122
|
+
nal_type = nal_unit[4] & 0x1F if len(nal_unit) > 4 else -1
|
|
123
|
+
nal_type_names = {5: "IDR", 7: "SPS", 8: "PPS"}
|
|
125
124
|
print(
|
|
126
|
-
f"[video/stream] Read
|
|
125
|
+
f"[video/stream] Read NAL unit: type={nal_type_names.get(nal_type, nal_type)}, size={len(nal_unit)} bytes"
|
|
127
126
|
)
|
|
127
|
+
|
|
128
|
+
# Check if we have all required parameter sets
|
|
129
|
+
if (
|
|
130
|
+
streamer.cached_sps
|
|
131
|
+
and streamer.cached_pps
|
|
132
|
+
and streamer.cached_idr
|
|
133
|
+
):
|
|
134
|
+
print(
|
|
135
|
+
f"[video/stream] ✓ Initialization complete: SPS={len(streamer.cached_sps)}B, PPS={len(streamer.cached_pps)}B, IDR={len(streamer.cached_idr)}B"
|
|
136
|
+
)
|
|
137
|
+
break
|
|
128
138
|
except Exception as e:
|
|
129
|
-
print(f"[video/stream] Failed to read
|
|
139
|
+
print(f"[video/stream] Failed to read NAL unit: {e}")
|
|
130
140
|
await asyncio.sleep(0.5)
|
|
131
141
|
continue
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
if len(accumulated_buffer) >= target_size:
|
|
135
|
-
break
|
|
136
|
-
|
|
137
|
-
# Now parse the entire accumulated buffer at once
|
|
138
|
-
# This ensures NAL units spanning multiple chunks are detected as complete
|
|
139
|
-
print(f"[video/stream] Parsing accumulated buffer ({len(accumulated_buffer)} bytes)...")
|
|
140
|
-
streamer._cache_nal_units(bytes(accumulated_buffer))
|
|
141
|
-
|
|
142
|
-
# Get initialization data
|
|
143
|
+
# Get initialization data (SPS + PPS + IDR)
|
|
143
144
|
init_data = streamer.get_initialization_data()
|
|
144
145
|
if not init_data:
|
|
145
146
|
raise RuntimeError(
|
|
146
|
-
|
|
147
|
+
"Failed to get initialization data (missing SPS/PPS/IDR)"
|
|
147
148
|
)
|
|
148
149
|
|
|
149
|
-
# Send initialization data
|
|
150
|
+
# Send initialization data as ONE message (SPS+PPS+IDR combined)
|
|
150
151
|
await websocket.send_bytes(init_data)
|
|
151
152
|
print(
|
|
152
|
-
f"[video/stream] Sent
|
|
153
|
+
f"[video/stream] ✓ Sent initialization data to first client: {len(init_data)} bytes total"
|
|
153
154
|
)
|
|
154
155
|
|
|
155
156
|
# Debug: Save to file
|
|
@@ -188,10 +189,23 @@ async def video_stream_ws(
|
|
|
188
189
|
await asyncio.sleep(0.5)
|
|
189
190
|
|
|
190
191
|
if init_data:
|
|
191
|
-
|
|
192
|
+
# Log what we're sending
|
|
192
193
|
print(
|
|
193
|
-
f"[video/stream]
|
|
194
|
+
f"[video/stream] ✓ Sending cached initialization data for device {device_id}:"
|
|
194
195
|
)
|
|
196
|
+
print(
|
|
197
|
+
f" - SPS: {len(streamer.cached_sps) if streamer.cached_sps else 0}B"
|
|
198
|
+
)
|
|
199
|
+
print(
|
|
200
|
+
f" - PPS: {len(streamer.cached_pps) if streamer.cached_pps else 0}B"
|
|
201
|
+
)
|
|
202
|
+
print(
|
|
203
|
+
f" - IDR: {len(streamer.cached_idr) if streamer.cached_idr else 0}B"
|
|
204
|
+
)
|
|
205
|
+
print(f" - Total: {len(init_data)} bytes")
|
|
206
|
+
|
|
207
|
+
await websocket.send_bytes(init_data)
|
|
208
|
+
print("[video/stream] ✓ Initialization data sent successfully")
|
|
195
209
|
|
|
196
210
|
# Debug: Save to file
|
|
197
211
|
if debug_file:
|
|
@@ -210,23 +224,23 @@ async def video_stream_ws(
|
|
|
210
224
|
|
|
211
225
|
stream_failed = False
|
|
212
226
|
try:
|
|
213
|
-
|
|
227
|
+
nal_count = 0
|
|
214
228
|
while True:
|
|
215
229
|
try:
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
await websocket.send_bytes(
|
|
230
|
+
# Read one complete NAL unit
|
|
231
|
+
# Each WebSocket message = one complete NAL unit (clear semantic boundary)
|
|
232
|
+
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
233
|
+
await websocket.send_bytes(nal_unit)
|
|
220
234
|
|
|
221
235
|
# Debug: Save to file
|
|
222
236
|
if debug_file:
|
|
223
|
-
debug_file.write(
|
|
237
|
+
debug_file.write(nal_unit)
|
|
224
238
|
debug_file.flush()
|
|
225
239
|
|
|
226
|
-
|
|
227
|
-
if
|
|
240
|
+
nal_count += 1
|
|
241
|
+
if nal_count % 100 == 0:
|
|
228
242
|
print(
|
|
229
|
-
f"[video/stream] Device {device_id}: Sent {
|
|
243
|
+
f"[video/stream] Device {device_id}: Sent {nal_count} NAL units"
|
|
230
244
|
)
|
|
231
245
|
except ConnectionError as e:
|
|
232
246
|
print(f"[video/stream] Device {device_id}: Connection error: {e}")
|
|
@@ -260,6 +274,6 @@ async def video_stream_ws(
|
|
|
260
274
|
# Debug: Close file
|
|
261
275
|
if debug_file:
|
|
262
276
|
debug_file.close()
|
|
263
|
-
print(
|
|
277
|
+
print("[video/stream] DEBUG: Closed debug file")
|
|
264
278
|
|
|
265
279
|
print(f"[video/stream] Device {device_id}: Stream ended")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Platform-aware subprocess helpers to avoid duplicated Windows branches."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_windows() -> bool:
|
|
10
|
+
"""Return True if running on Windows."""
|
|
11
|
+
return platform.system() == "Windows"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def run_cmd_silently(cmd: Sequence[str]) -> subprocess.CompletedProcess:
|
|
15
|
+
"""Run a command, suppressing output; safe for async contexts on all platforms."""
|
|
16
|
+
if is_windows():
|
|
17
|
+
# Avoid blocking the event loop with a blocking subprocess call on Windows.
|
|
18
|
+
return await asyncio.to_thread(
|
|
19
|
+
subprocess.run, cmd, capture_output=True, check=False
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
process = await asyncio.create_subprocess_exec(
|
|
23
|
+
*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
24
|
+
)
|
|
25
|
+
await process.wait()
|
|
26
|
+
return subprocess.CompletedProcess(cmd, process.returncode, None, None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def spawn_process(cmd: Sequence[str], *, capture_output: bool = False) -> Any:
|
|
30
|
+
"""Start a long-running process with optional stdio capture."""
|
|
31
|
+
stdout = subprocess.PIPE if capture_output else None
|
|
32
|
+
stderr = subprocess.PIPE if capture_output else None
|
|
33
|
+
|
|
34
|
+
if is_windows():
|
|
35
|
+
return subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
|
|
36
|
+
|
|
37
|
+
return await asyncio.create_subprocess_exec(*cmd, stdout=stdout, stderr=stderr)
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
|
-
import platform
|
|
6
5
|
import socket
|
|
7
6
|
import subprocess
|
|
8
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from AutoGLM_GUI.platform_utils import is_windows, run_cmd_silently, spawn_process
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class ScrcpyStreamer:
|
|
@@ -34,7 +36,7 @@ class ScrcpyStreamer:
|
|
|
34
36
|
self.port = port
|
|
35
37
|
self.idr_interval_s = idr_interval_s
|
|
36
38
|
|
|
37
|
-
self.scrcpy_process:
|
|
39
|
+
self.scrcpy_process: Any | None = None
|
|
38
40
|
self.tcp_socket: socket.socket | None = None
|
|
39
41
|
self.forward_cleanup_needed = False
|
|
40
42
|
|
|
@@ -47,6 +49,9 @@ class ScrcpyStreamer:
|
|
|
47
49
|
self.sps_pps_locked = False # Lock SPS/PPS after initial complete capture
|
|
48
50
|
# Note: IDR is NOT locked - we keep updating to the latest frame
|
|
49
51
|
|
|
52
|
+
# NAL unit reading buffer (for read_nal_unit method)
|
|
53
|
+
self._nal_read_buffer = bytearray()
|
|
54
|
+
|
|
50
55
|
# Find scrcpy-server location
|
|
51
56
|
self.scrcpy_server_path = self._find_scrcpy_server()
|
|
52
57
|
|
|
@@ -83,6 +88,10 @@ class ScrcpyStreamer:
|
|
|
83
88
|
|
|
84
89
|
async def start(self) -> None:
|
|
85
90
|
"""Start scrcpy server and establish connection."""
|
|
91
|
+
# Clear NAL reading buffer to ensure clean state
|
|
92
|
+
self._nal_read_buffer.clear()
|
|
93
|
+
print("[ScrcpyStreamer] Cleared NAL read buffer")
|
|
94
|
+
|
|
86
95
|
try:
|
|
87
96
|
# 0. Kill existing scrcpy server processes on device
|
|
88
97
|
print("[ScrcpyStreamer] Cleaning up existing scrcpy processes...")
|
|
@@ -119,50 +128,20 @@ class ScrcpyStreamer:
|
|
|
119
128
|
if self.device_id:
|
|
120
129
|
cmd_base.extend(["-s", self.device_id])
|
|
121
130
|
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Method 1: Try pkill
|
|
126
|
-
cmd = cmd_base + ["shell", "pkill", "-9", "-f", "app_process.*scrcpy"]
|
|
127
|
-
subprocess.run(cmd, capture_output=True, check=False)
|
|
131
|
+
# Method 1: Try pkill
|
|
132
|
+
cmd = cmd_base + ["shell", "pkill", "-9", "-f", "app_process.*scrcpy"]
|
|
133
|
+
await run_cmd_silently(cmd)
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Method 3: Remove port forward if exists
|
|
137
|
-
cmd_remove_forward = cmd_base + ["forward", "--remove", f"tcp:{self.port}"]
|
|
138
|
-
subprocess.run(cmd_remove_forward, capture_output=True, check=False)
|
|
139
|
-
else:
|
|
140
|
-
# Original asyncio-based implementation for Unix systems
|
|
141
|
-
# Method 1: Try pkill
|
|
142
|
-
cmd = cmd_base + ["shell", "pkill", "-9", "-f", "app_process.*scrcpy"]
|
|
143
|
-
process = await asyncio.create_subprocess_exec(
|
|
144
|
-
*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
145
|
-
)
|
|
146
|
-
await process.wait()
|
|
135
|
+
# Method 2: Find and kill by PID (more reliable)
|
|
136
|
+
cmd = cmd_base + [
|
|
137
|
+
"shell",
|
|
138
|
+
"ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9",
|
|
139
|
+
]
|
|
140
|
+
await run_cmd_silently(cmd)
|
|
147
141
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9",
|
|
152
|
-
]
|
|
153
|
-
process = await asyncio.create_subprocess_exec(
|
|
154
|
-
*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
155
|
-
)
|
|
156
|
-
await process.wait()
|
|
157
|
-
|
|
158
|
-
# Method 3: Remove port forward if exists
|
|
159
|
-
cmd_remove_forward = cmd_base + ["forward", "--remove", f"tcp:{self.port}"]
|
|
160
|
-
process = await asyncio.create_subprocess_exec(
|
|
161
|
-
*cmd_remove_forward,
|
|
162
|
-
stdout=subprocess.DEVNULL,
|
|
163
|
-
stderr=subprocess.DEVNULL,
|
|
164
|
-
)
|
|
165
|
-
await process.wait()
|
|
142
|
+
# Method 3: Remove port forward if exists
|
|
143
|
+
cmd_remove_forward = cmd_base + ["forward", "--remove", f"tcp:{self.port}"]
|
|
144
|
+
await run_cmd_silently(cmd_remove_forward)
|
|
166
145
|
|
|
167
146
|
# Wait longer for resources to be released
|
|
168
147
|
print("[ScrcpyStreamer] Waiting for cleanup to complete...")
|
|
@@ -175,13 +154,7 @@ class ScrcpyStreamer:
|
|
|
175
154
|
cmd.extend(["-s", self.device_id])
|
|
176
155
|
cmd.extend(["push", self.scrcpy_server_path, "/data/local/tmp/scrcpy-server"])
|
|
177
156
|
|
|
178
|
-
|
|
179
|
-
subprocess.run(cmd, capture_output=True, check=False)
|
|
180
|
-
else:
|
|
181
|
-
process = await asyncio.create_subprocess_exec(
|
|
182
|
-
*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
183
|
-
)
|
|
184
|
-
await process.wait()
|
|
157
|
+
await run_cmd_silently(cmd)
|
|
185
158
|
|
|
186
159
|
async def _setup_port_forward(self) -> None:
|
|
187
160
|
"""Setup ADB port forwarding."""
|
|
@@ -190,13 +163,7 @@ class ScrcpyStreamer:
|
|
|
190
163
|
cmd.extend(["-s", self.device_id])
|
|
191
164
|
cmd.extend(["forward", f"tcp:{self.port}", "localabstract:scrcpy"])
|
|
192
165
|
|
|
193
|
-
|
|
194
|
-
subprocess.run(cmd, capture_output=True, check=False)
|
|
195
|
-
else:
|
|
196
|
-
process = await asyncio.create_subprocess_exec(
|
|
197
|
-
*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
198
|
-
)
|
|
199
|
-
await process.wait()
|
|
166
|
+
await run_cmd_silently(cmd)
|
|
200
167
|
self.forward_cleanup_needed = True
|
|
201
168
|
|
|
202
169
|
async def _start_server(self) -> None:
|
|
@@ -231,22 +198,14 @@ class ScrcpyStreamer:
|
|
|
231
198
|
cmd.extend(server_args)
|
|
232
199
|
|
|
233
200
|
# Capture stderr to see error messages
|
|
234
|
-
|
|
235
|
-
# On Windows, use subprocess.Popen for async-like behavior
|
|
236
|
-
self.scrcpy_process = subprocess.Popen(
|
|
237
|
-
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
238
|
-
)
|
|
239
|
-
else:
|
|
240
|
-
self.scrcpy_process = await asyncio.create_subprocess_exec(
|
|
241
|
-
*cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
242
|
-
)
|
|
201
|
+
self.scrcpy_process = await spawn_process(cmd, capture_output=True)
|
|
243
202
|
|
|
244
203
|
# Wait for server to start
|
|
245
204
|
await asyncio.sleep(2)
|
|
246
205
|
|
|
247
206
|
# Check if process is still running
|
|
248
207
|
error_msg = None
|
|
249
|
-
if
|
|
208
|
+
if is_windows():
|
|
250
209
|
# For Windows Popen, check returncode directly
|
|
251
210
|
if self.scrcpy_process.poll() is not None:
|
|
252
211
|
# Process has exited
|
|
@@ -307,9 +266,7 @@ class ScrcpyStreamer:
|
|
|
307
266
|
|
|
308
267
|
raise ConnectionError("Failed to connect to scrcpy server")
|
|
309
268
|
|
|
310
|
-
def _find_nal_units(
|
|
311
|
-
self, data: bytes
|
|
312
|
-
) -> list[tuple[int, int, int, bool]]:
|
|
269
|
+
def _find_nal_units(self, data: bytes) -> list[tuple[int, int, int, bool]]:
|
|
313
270
|
"""Find NAL units in H.264 data.
|
|
314
271
|
|
|
315
272
|
Returns:
|
|
@@ -385,9 +342,7 @@ class ScrcpyStreamer:
|
|
|
385
342
|
f"[ScrcpyStreamer] ✓ Cached SPS ({size} bytes, complete={is_complete}): {hex_preview}..."
|
|
386
343
|
)
|
|
387
344
|
elif size < 10:
|
|
388
|
-
print(
|
|
389
|
-
f"[ScrcpyStreamer] ✗ Skipped short SPS ({size} bytes)"
|
|
390
|
-
)
|
|
345
|
+
print(f"[ScrcpyStreamer] ✗ Skipped short SPS ({size} bytes)")
|
|
391
346
|
|
|
392
347
|
elif nal_type == 8: # PPS
|
|
393
348
|
# Only cache PPS if not yet locked
|
|
@@ -402,29 +357,22 @@ class ScrcpyStreamer:
|
|
|
402
357
|
f"[ScrcpyStreamer] ✓ Cached PPS ({size} bytes, complete={is_complete}): {hex_preview}..."
|
|
403
358
|
)
|
|
404
359
|
elif size < 6:
|
|
405
|
-
print(
|
|
406
|
-
f"[ScrcpyStreamer] ✗ Skipped short PPS ({size} bytes)"
|
|
407
|
-
)
|
|
360
|
+
print(f"[ScrcpyStreamer] ✗ Skipped short PPS ({size} bytes)")
|
|
408
361
|
|
|
409
362
|
elif nal_type == 5: # IDR frame
|
|
410
|
-
#
|
|
411
|
-
#
|
|
412
|
-
|
|
363
|
+
# Cache IDR if it's large enough (size check is sufficient)
|
|
364
|
+
# Note: When called from read_nal_unit(), the NAL is guaranteed complete
|
|
365
|
+
# because we extract it between two start codes. The is_complete flag
|
|
366
|
+
# is only False because the NAL is isolated (no next start code in buffer).
|
|
367
|
+
if self.cached_sps and self.cached_pps and size >= 1024:
|
|
413
368
|
is_first = self.cached_idr is None
|
|
414
369
|
self.cached_idr = nal_data
|
|
415
370
|
if is_first:
|
|
416
|
-
print(
|
|
417
|
-
f"[ScrcpyStreamer] ✓ Cached COMPLETE IDR frame ({size} bytes)"
|
|
418
|
-
)
|
|
371
|
+
print(f"[ScrcpyStreamer] ✓ Cached IDR frame ({size} bytes)")
|
|
419
372
|
# Don't log every IDR update (too verbose)
|
|
420
|
-
elif not is_complete:
|
|
421
|
-
if size > 1024: # Only log if it's a large incomplete IDR
|
|
422
|
-
print(
|
|
423
|
-
f"[ScrcpyStreamer] ⚠ Skipped INCOMPLETE IDR ({size} bytes, extends to chunk boundary)"
|
|
424
|
-
)
|
|
425
373
|
elif size < 1024:
|
|
426
374
|
print(
|
|
427
|
-
f"[ScrcpyStreamer] ✗ Skipped small IDR ({size} bytes)"
|
|
375
|
+
f"[ScrcpyStreamer] ✗ Skipped small IDR ({size} bytes, likely incomplete)"
|
|
428
376
|
)
|
|
429
377
|
|
|
430
378
|
# Lock SPS/PPS once we have complete initial parameters
|
|
@@ -432,60 +380,6 @@ class ScrcpyStreamer:
|
|
|
432
380
|
self.sps_pps_locked = True
|
|
433
381
|
print("[ScrcpyStreamer] 🔒 SPS/PPS locked (IDR will continue updating)")
|
|
434
382
|
|
|
435
|
-
def _prepend_sps_pps_to_idr(self, data: bytes) -> bytes:
|
|
436
|
-
"""Prepend SPS/PPS before EVERY IDR frame unconditionally.
|
|
437
|
-
|
|
438
|
-
This ensures that clients can start decoding from any IDR frame,
|
|
439
|
-
even if they join mid-stream. We always prepend to guarantee
|
|
440
|
-
that every IDR is self-contained.
|
|
441
|
-
|
|
442
|
-
Returns:
|
|
443
|
-
Modified data with SPS/PPS prepended to all IDR frames
|
|
444
|
-
"""
|
|
445
|
-
if not self.cached_sps or not self.cached_pps:
|
|
446
|
-
return data
|
|
447
|
-
|
|
448
|
-
nal_units = self._find_nal_units(data)
|
|
449
|
-
if not nal_units:
|
|
450
|
-
return data
|
|
451
|
-
|
|
452
|
-
# Find all IDR frames
|
|
453
|
-
idr_positions = [
|
|
454
|
-
(start, size)
|
|
455
|
-
for start, nal_type, size, _ in nal_units
|
|
456
|
-
if nal_type == 5
|
|
457
|
-
]
|
|
458
|
-
|
|
459
|
-
if not idr_positions:
|
|
460
|
-
return data
|
|
461
|
-
|
|
462
|
-
# Build modified data by prepending SPS/PPS before each IDR
|
|
463
|
-
result = bytearray()
|
|
464
|
-
last_pos = 0
|
|
465
|
-
sps_pps = self.cached_sps + self.cached_pps
|
|
466
|
-
|
|
467
|
-
for idr_start, idr_size in idr_positions:
|
|
468
|
-
# Add data before this IDR
|
|
469
|
-
result.extend(data[last_pos:idr_start])
|
|
470
|
-
|
|
471
|
-
# Check if SPS/PPS already exists right before this IDR
|
|
472
|
-
# (to avoid duplicating if scrcpy already sent them)
|
|
473
|
-
prepend_offset = max(0, idr_start - len(sps_pps))
|
|
474
|
-
if data[prepend_offset:idr_start] != sps_pps:
|
|
475
|
-
# Prepend SPS/PPS before this IDR
|
|
476
|
-
result.extend(sps_pps)
|
|
477
|
-
print(
|
|
478
|
-
f"[ScrcpyStreamer] Prepended SPS/PPS before IDR at position {idr_start}"
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
# Update position to start of IDR
|
|
482
|
-
last_pos = idr_start
|
|
483
|
-
|
|
484
|
-
# Add remaining data (including all IDR frames and data after)
|
|
485
|
-
result.extend(data[last_pos:])
|
|
486
|
-
|
|
487
|
-
return bytes(result)
|
|
488
|
-
|
|
489
383
|
def get_initialization_data(self) -> bytes | None:
|
|
490
384
|
"""Get cached SPS/PPS/IDR for initializing new connections.
|
|
491
385
|
|
|
@@ -563,6 +457,86 @@ class ScrcpyStreamer:
|
|
|
563
457
|
)
|
|
564
458
|
raise ConnectionError(f"Failed to read from socket: {e}") from e
|
|
565
459
|
|
|
460
|
+
async def read_nal_unit(self, auto_cache: bool = True) -> bytes:
|
|
461
|
+
"""Read one complete NAL unit from socket.
|
|
462
|
+
|
|
463
|
+
This method ensures each returned chunk is a complete, self-contained NAL unit.
|
|
464
|
+
WebSocket messages will have clear semantic boundaries (one message = one NAL unit).
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
auto_cache: If True, automatically cache SPS/PPS/IDR from this NAL unit
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
bytes: Complete NAL unit (including start code)
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
ConnectionError: If socket is closed or error occurs
|
|
474
|
+
"""
|
|
475
|
+
if not self.tcp_socket:
|
|
476
|
+
raise ConnectionError("Socket not connected")
|
|
477
|
+
|
|
478
|
+
while True:
|
|
479
|
+
# Look for start codes in buffer
|
|
480
|
+
buffer = bytes(self._nal_read_buffer)
|
|
481
|
+
start_positions = []
|
|
482
|
+
|
|
483
|
+
# Find all start codes (0x00 0x00 0x00 0x01 or 0x00 0x00 0x01)
|
|
484
|
+
i = 0
|
|
485
|
+
while i < len(buffer) - 3:
|
|
486
|
+
if buffer[i] == 0x00 and buffer[i + 1] == 0x00:
|
|
487
|
+
if buffer[i + 2] == 0x00 and buffer[i + 3] == 0x01:
|
|
488
|
+
start_positions.append(i)
|
|
489
|
+
i += 4
|
|
490
|
+
elif buffer[i + 2] == 0x01:
|
|
491
|
+
start_positions.append(i)
|
|
492
|
+
i += 3
|
|
493
|
+
else:
|
|
494
|
+
i += 1
|
|
495
|
+
else:
|
|
496
|
+
i += 1
|
|
497
|
+
|
|
498
|
+
# If we have at least 2 start codes, we can extract the first NAL unit
|
|
499
|
+
if len(start_positions) >= 2:
|
|
500
|
+
# Extract first complete NAL unit (from first start code to second start code)
|
|
501
|
+
nal_unit = buffer[start_positions[0] : start_positions[1]]
|
|
502
|
+
|
|
503
|
+
# Remove extracted NAL unit from buffer
|
|
504
|
+
self._nal_read_buffer = bytearray(buffer[start_positions[1] :])
|
|
505
|
+
|
|
506
|
+
# Cache parameter sets if enabled
|
|
507
|
+
if auto_cache:
|
|
508
|
+
self._cache_nal_units(nal_unit)
|
|
509
|
+
|
|
510
|
+
return nal_unit
|
|
511
|
+
|
|
512
|
+
# Need more data - read from socket
|
|
513
|
+
try:
|
|
514
|
+
loop = asyncio.get_event_loop()
|
|
515
|
+
chunk = await loop.run_in_executor(
|
|
516
|
+
None, self.tcp_socket.recv, 512 * 1024
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if not chunk:
|
|
520
|
+
# Socket closed - return any remaining buffered data as final NAL unit
|
|
521
|
+
if len(self._nal_read_buffer) > 0:
|
|
522
|
+
final_nal = bytes(self._nal_read_buffer)
|
|
523
|
+
self._nal_read_buffer.clear()
|
|
524
|
+
if auto_cache:
|
|
525
|
+
self._cache_nal_units(final_nal)
|
|
526
|
+
return final_nal
|
|
527
|
+
raise ConnectionError("Socket closed by remote")
|
|
528
|
+
|
|
529
|
+
# Append new data to buffer
|
|
530
|
+
self._nal_read_buffer.extend(chunk)
|
|
531
|
+
|
|
532
|
+
except ConnectionError:
|
|
533
|
+
raise
|
|
534
|
+
except Exception as e:
|
|
535
|
+
print(
|
|
536
|
+
f"[ScrcpyStreamer] Unexpected error in read_nal_unit: {type(e).__name__}: {e}"
|
|
537
|
+
)
|
|
538
|
+
raise ConnectionError(f"Failed to read from socket: {e}") from e
|
|
539
|
+
|
|
566
540
|
def stop(self) -> None:
|
|
567
541
|
"""Stop scrcpy server and cleanup resources."""
|
|
568
542
|
# Close socket
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as o}from"./index
|
|
1
|
+
import{j as o}from"./index-Dn3vR6uV.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
|